Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
### Added
- Live chat transfer (escalated-laravel `escalated.admin.chat.transfer`): `POST /wp-json/escalated/v1/chat/{session_id}/transfer`, with an API token like the other agent chat endpoints. The body is `{agent_id, department_id}` (either or both); the shared `ChatActionBar`'s `{type: "agent"|"department", id}` is accepted too. With an agent, the session and ticket are reassigned to them. With only a department, the ticket moves there and the chat goes back to the queue (unassigned, waiting), then chat routing runs again for the new department. A missing or non-integer target, an unknown department, an ended chat, or an agent who is not staff or cannot see the ticket is a 422; an unknown session is a 404. New `ChatSessionService::transfer()` and `escalated_chat_transferred` action (`$session, $from_agent_id, $to_agent_id, $to_department_id`).
- `chat.transferred` runs workflows (new trigger, "Chat transferred") and is sent to webhooks subscribed to it; the webhook form offers it. Its payload is the ticket plus `chat {session_id, ticket_id, status, agent_id, department_id, from_agent_id, to_agent_id, to_department_id}`, as escalated-laravel's. Existing webhooks are not subscribed to it automatically.
- Webhooks now receive the other live chat events, as escalated-laravel's `DispatchWebhook`: `chat.started` (a chat begins), `chat.assigned` (an agent accepts it), `chat.message` (an agent or visitor message) and `chat.ended`. Until now only workflows ran on them. The webhook form offers them. Each payload is the ticket plus `chat {session_id, ticket_id, status, agent_id, department_id}`; `chat.message` adds `reply_id` and `author_type` (`agent` or `visitor`) but never the body, and `chat.ended` adds `ended_by` (`agent`, `customer` or `system`). Existing webhooks, including one folded from the old single webhook URL, are not subscribed to them automatically; tick them on the webhook to receive them.
- `escalated_chat_message` passes a third argument, `$author_type` (`agent` or `visitor`), and `escalated_chat_ended` a third, `$ended_by`. `ChatSessionService::end()` takes an optional third `$ended_by` (default `system`); the agent and widget end endpoints pass `agent` and `customer`. Existing listeners registered for fewer arguments are unaffected.

## [1.6.4] - 2026-10-22

Expand Down
4 changes: 3 additions & 1 deletion docs/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ The `escalated_webhook_args` filter can change the request arguments. `escalated

The canonical names from the shared event vocabulary:

`ticket.created`, `ticket.updated`, `ticket.status_changed`, `ticket.resolved`, `ticket.closed`, `ticket.reopened`, `ticket.assigned`, `ticket.unassigned`, `ticket.escalated`, `ticket.priority_changed`, `ticket.department_changed`, `reply.created`, `note.created`, `sla.breached`, `sla.warning`, `ticket.tag_added`, `ticket.tag_removed`, `chat.transferred`.
`ticket.created`, `ticket.updated`, `ticket.status_changed`, `ticket.resolved`, `ticket.closed`, `ticket.reopened`, `ticket.assigned`, `ticket.unassigned`, `ticket.escalated`, `ticket.priority_changed`, `ticket.department_changed`, `reply.created`, `note.created`, `sla.breached`, `sla.warning`, `ticket.tag_added`, `ticket.tag_removed`, `chat.started`, `chat.assigned`, `chat.transferred`, `chat.message`, `chat.ended`.

Live chat events carry the ticket plus a `chat` block with escalated-laravel's fields: `session_id`, `ticket_id`, `status`, `agent_id`, `department_id`; `chat.transferred` adds `from_agent_id`, `to_agent_id`, `to_department_id`; `chat.message` adds `reply_id` and `author_type` (`agent` or `visitor`), never the message body; `chat.ended` adds `ended_by` (`agent`, `customer` or `system`). The visitor's email and chat token are never sent.

A webhook saved with an older name (`ticket.tagged`, `internal_note.added`, ...) receives the event under its current name.

Expand Down
2 changes: 1 addition & 1 deletion includes/Api/class-chat-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public function end_session(WP_REST_Request $request)
$session_id = (int) $request->get_param('session_id');

try {
$session = $service->end($session_id);
$session = $service->end($session_id, null, 'agent');

return $this->success($session);
} catch (\Throwable $e) {
Expand Down
4 changes: 2 additions & 2 deletions includes/Api/class-widget-chat-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ public function end_chat(WP_REST_Request $request)
return $session;
}

$session = $service->end($session_id);
$session = $service->end($session_id, null, 'customer');

return $this->success([
'id' => (int) $session->id,
Expand Down Expand Up @@ -497,7 +497,7 @@ public function end_by_id(WP_REST_Request $request)
return $this->error('escalated_chat_ended', __('Chat has already ended.', 'escalated'), 422);
}

$session = (new ChatSessionService)->end((int) $session->id);
$session = (new ChatSessionService)->end((int) $session->id, null, 'customer');

return $this->success([
'message' => __('Chat ended.', 'escalated'),
Expand Down
13 changes: 9 additions & 4 deletions includes/Services/ChatSessionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ public function transfer(int $session_id, ?int $to_agent_id = null, ?int $to_dep

/**
* Send a message within a chat session. Stored as a reply on the ticket.
* Fires escalated_chat_message ($session, $reply, $author_type), where
* $author_type is 'agent' or 'visitor'.
*/
public function send_message(int $session_id, string $body, ?int $author_id = null, string $author_type = 'visitor'): object
{
Expand All @@ -204,15 +206,18 @@ public function send_message(int $session_id, string $body, ?int $author_id = nu
'last_activity_at' => current_time('mysql'),
]);

do_action('escalated_chat_message', $session, $reply);
do_action('escalated_chat_message', $session, $reply, $author_type === 'agent' ? 'agent' : 'visitor');

return $reply;
}

/**
* End a chat session. The underlying ticket is resolved.
* End a chat session. The underlying ticket is resolved. Fires
* escalated_chat_ended ($session, $causer_id, $ended_by), where $ended_by
* is who ended it, as escalated-laravel's ChatEnded: 'agent', 'customer'
* (the visitor) or 'system' (idle cleanup).
*/
public function end(int $session_id, ?int $causer_id = null): object
public function end(int $session_id, ?int $causer_id = null, string $ended_by = 'system'): object
{
$session = ChatSession::find($session_id);

Expand All @@ -233,7 +238,7 @@ public function end(int $session_id, ?int $causer_id = null): object

$session = ChatSession::find($session_id);

do_action('escalated_chat_ended', $session, $causer_id);
do_action('escalated_chat_ended', $session, $causer_id, $ended_by);

return $session;
}
Expand Down
4 changes: 4 additions & 0 deletions includes/Services/EventVocabulary.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ final class EventVocabulary
'sla.warning',
'ticket.tag_added',
'ticket.tag_removed',
'chat.started',
'chat.assigned',
'chat.transferred',
'chat.message',
'chat.ended',
];

/**
Expand Down
56 changes: 48 additions & 8 deletions includes/Services/NotificationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public function register(): void
add_action('escalated_internal_note_added', [$this, 'on_note_created'], 100, 2);
add_action('escalated_tag_added', [$this, 'on_tag_added'], 100, 2);
add_action('escalated_tag_removed', [$this, 'on_tag_removed'], 100, 2);
add_action('escalated_chat_started', [$this, 'on_chat_started'], 100, 1);
add_action('escalated_chat_accepted', [$this, 'on_chat_assigned'], 100, 2);
add_action('escalated_chat_transferred', [$this, 'on_chat_transferred'], 100, 4);
add_action('escalated_chat_message', [$this, 'on_chat_message'], 100, 3);
add_action('escalated_chat_ended', [$this, 'on_chat_ended'], 100, 3);
}

public function on_ticket_resolved($ticket, $causer_id = null): void
Expand Down Expand Up @@ -88,26 +92,62 @@ public function on_note_created($note, $ticket = null): void
]);
}

public function on_chat_started($session): void
{
$this->dispatch_chat_event('chat.started', $session);
}

public function on_chat_assigned($session, $agent_id = null): void
{
$this->dispatch_chat_event('chat.assigned', $session);
}

public function on_chat_transferred($session, $from_agent_id = null, $to_agent_id = null, $to_department_id = null): void
{
$this->dispatch_chat_event('chat.transferred', $session, [
'from_agent_id' => self::nullable_int($from_agent_id),
'to_agent_id' => self::nullable_int($to_agent_id),
'to_department_id' => self::nullable_int($to_department_id),
]);
}

/**
* chat.transferred, with escalated-laravel's DispatchWebhook chat fields.
* The visitor's email and chat token stay out.
* chat.message carries the reply's id and who wrote it, never the body:
* a subscriber reads that through the API.
*/
public function on_chat_transferred($session, $from_agent_id = null, $to_agent_id = null, $to_department_id = null): void
public function on_chat_message($session, $reply = null, $author_type = 'visitor'): void
{
$this->dispatch_chat_event('chat.message', $session, [
'reply_id' => is_object($reply) && ! empty($reply->id) ? (int) $reply->id : null,
'author_type' => $author_type === 'agent' ? 'agent' : 'visitor',
]);
}

public function on_chat_ended($session, $causer_id = null, $ended_by = 'system'): void
{
$this->dispatch_chat_event('chat.ended', $session, [
'ended_by' => in_array($ended_by, ['agent', 'customer', 'system'], true) ? $ended_by : 'system',
]);
}

/**
* A chat event with escalated-laravel's DispatchWebhook chat fields, plus
* the ticket. The visitor's email, the chat token and message bodies
* stay out.
*/
protected function dispatch_chat_event(string $event, $session, array $extra = []): void
{
if (! is_object($session) || empty($session->ticket_id)) {
return;
}
$this->dispatch_ticket_event('chat.transferred', Ticket::find((int) $session->ticket_id), [
$this->dispatch_ticket_event($event, Ticket::find((int) $session->ticket_id), [
'chat' => [
'session_id' => (int) $session->id,
'ticket_id' => (int) $session->ticket_id,
'status' => $session->status ?? null,
'agent_id' => self::nullable_int($session->agent_id ?? null),
'department_id' => self::nullable_int($session->department_id ?? null),
'from_agent_id' => self::nullable_int($from_agent_id),
'to_agent_id' => self::nullable_int($to_agent_id),
'to_department_id' => self::nullable_int($to_department_id),
],
] + $extra,
]);
}

Expand Down
8 changes: 4 additions & 4 deletions includes/Services/WorkflowListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ public function register(): void
add_action('escalated_chat_started', [$this, 'on_chat_started'], 50, 1);
add_action('escalated_chat_accepted', [$this, 'on_chat_assigned'], 50, 2);
add_action('escalated_chat_transferred', [$this, 'on_chat_transferred'], 50, 4);
add_action('escalated_chat_message', [$this, 'on_chat_message'], 50, 2);
add_action('escalated_chat_ended', [$this, 'on_chat_ended'], 50, 2);
add_action('escalated_chat_message', [$this, 'on_chat_message'], 50, 3);
add_action('escalated_chat_ended', [$this, 'on_chat_ended'], 50, 3);
}

public function on_ticket_created($ticket): void
Expand Down Expand Up @@ -129,12 +129,12 @@ public function on_chat_transferred($session, $from_agent_id = null, $to_agent_i
$this->run_for_chat('chat.transferred', $session);
}

public function on_chat_message($session, $reply = null): void
public function on_chat_message($session, $reply = null, $author_type = 'visitor'): void
{
$this->run_for_chat('chat.message', $session);
}

public function on_chat_ended($session, $causer_id = null): void
public function on_chat_ended($session, $causer_id = null, $ended_by = 'system'): void
{
$this->run_for_chat('chat.ended', $session);
}
Expand Down
Loading
Loading