diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45608ef..4fb9fa9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.1.0] - 2026-07-17
+
### Added
- Initial release of the STROMCOM Customer SDK for PHP.
- `Client` covering Project, Users, Threads, Messages and Attachment endpoints.
@@ -27,3 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PHPUnit 11 test suite (78 tests, 210 assertions), PHPStan level 9,
php-cs-fixer config.
- GitHub Actions CI for PHP 8.3 and 8.4.
+- `MessageResource::reactions()` — list emoji reactions on a message,
+ aggregated by code (`MessageReaction`, `MessageReactionUser` models).
+- `ThreadResource::create()` — upsert a thread by code, optionally with an
+ initial message and attachments.
+- `ThreadResource::update()` — update thread `name` / `url` / `attributes`.
+- `MessageResource::create()` — send a message (with attachments) in an
+ existing thread.
diff --git a/README.md b/README.md
index b5ff49c..31acb8c 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,26 @@ $stromcom->users()->update($hash, ['name' => 'Jane Smith']);
$stromcom->users()->delete($hash); // soft delete, idempotent
$stromcom->users()->restore($hash);
+// Create a thread (upsert by code) with an optional initial message.
+// The author must be identified via user_hash/user_code when a message/
+// attachments are present — the request isn't bound to a logged-in user.
+$created = $stromcom->threads()->create([
+ 'code' => 'order-1234',
+ 'name' => 'Order #1234',
+ 'message' => '
Your order has shipped.
',
+ 'user_code' => 'employee-42',
+]);
+$thread = $created['thread']; // Thread
+$message = $created['message']; // ?Message
+
+$stromcom->threads()->update('order-1234', ['name' => 'Order #1234 (shipped)']);
+
+// Send a follow-up message in the same thread
+$stromcom->messages()->create('order-1234', [
+ 'message' => 'Tracking number: 1Z999AA10123456784
',
+ 'user_code' => 'employee-42',
+]);
+
// Thread + messages + attachments in one round-trip
$result = $stromcom->threads()->getWithMessages('order-1234');
$thread = $result['thread']; // Thread
@@ -87,8 +107,10 @@ covering each area:
| [`project_settings.php`](examples/project_settings.php) | Read & PATCH project policy flags. |
| [`users_lifecycle.php`](examples/users_lifecycle.php) | Create → fetch → update → delete → restore. |
| [`thread_with_messages.php`](examples/thread_with_messages.php) | Thread + messages + attachments in one call. |
+| [`create_thread_and_message.php`](examples/create_thread_and_message.php) | Create/upsert a thread with an initial message, update it, send a follow-up message. |
| [`paginate_messages.php`](examples/paginate_messages.php) | Cursor pagination via `lastMessageHash`. |
| [`download_attachment.php`](examples/download_attachment.php) | Resolve and save attachments to disk. |
+| [`message_reactions.php`](examples/message_reactions.php) | List emoji reactions on a message, aggregated by code. |
| [`error_handling.php`](examples/error_handling.php) | Catch the typed exception hierarchy. |
| [`custom_http_client.php`](examples/custom_http_client.php) | Plug a custom `HttpClientInterface`. |
@@ -114,6 +136,7 @@ keys land as `null`.
| `Message`, `MessageList` | `messages()->list()`, `threads()->getWithMessages()` |
| `MessageAttachment` | `messages()->attachments()`, `MessageList::attachmentsFor()` |
| `AttachmentDownload` | `messages()->attachmentDownload()` |
+| `MessageReaction`, `MessageReactionUser` | `messages()->reactions()` |
## Method reference
@@ -131,14 +154,18 @@ void $stromcom->users()->delete(string $hash);
User $stromcom->users()->restore(string $hash);
// Threads
+array{thread: Thread, message: ?Message, attachmentsUploadURL: list} $stromcom->threads()->create(array $payload);
+Thread $stromcom->threads()->update(string $code, array $changes);
Thread $stromcom->threads()->get(string $code, array $expand = []);
array{thread: Thread, messages: MessageList} $stromcom->threads()->getWithMessages(string $code);
// Messages
+array{message: Message, attachmentsUploadURL: list} $stromcom->messages()->create(string $threadCode, array $payload);
MessageList $stromcom->messages()->list(string $threadCode, ?string $lastMessageHash = null);
void $stromcom->messages()->delete(string $threadCode, string $messageHash);
list $stromcom->messages()->attachments(string $threadCode, string $messageHash);
AttachmentDownload $stromcom->messages()->attachmentDownload(string $threadCode, string $messageHash, string $attachmentHash);
+list $stromcom->messages()->reactions(string $threadCode, string $messageHash);
```
## Configuration
diff --git a/examples/README.md b/examples/README.md
index 06ebc33..80adc82 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -18,8 +18,10 @@ php examples/quickstart.php
| [`project_settings.php`](project_settings.php) | Read project settings, then PATCH a couple of policy flags. |
| [`users_lifecycle.php`](users_lifecycle.php) | Full user lifecycle: create → fetch → update → delete → restore. |
| [`thread_with_messages.php`](thread_with_messages.php) | Single-call fetch of a thread, its messages, and grouped attachments. |
+| [`create_thread_and_message.php`](create_thread_and_message.php) | Create/upsert a thread with an initial message, update it, send a follow-up message. |
| [`paginate_messages.php`](paginate_messages.php) | Walk every message in a thread via the `lastMessageHash` cursor. |
| [`download_attachment.php`](download_attachment.php) | Resolve attachment metadata, fetch a signed URL, save to disk. |
+| [`message_reactions.php`](message_reactions.php) | List emoji reactions on a message, aggregated by code. |
| [`error_handling.php`](error_handling.php) | Catch the typed exception hierarchy (`NotFound`, `Validation`, …). |
| [`custom_http_client.php`](custom_http_client.php) | Plug a custom `HttpClientInterface` (logging wrapper here). |
diff --git a/examples/create_thread_and_message.php b/examples/create_thread_and_message.php
new file mode 100644
index 0000000..d66e87d
--- /dev/null
+++ b/examples/create_thread_and_message.php
@@ -0,0 +1,55 @@
+create($payload)
+ * → ['thread' => Thread, 'message' => ?Message, 'attachmentsUploadURL' => list]
+ * threads()->update($code, $changes)
+ * → Thread
+ * messages()->create($threadCode, $payload)
+ * → ['message' => Message, 'attachmentsUploadURL' => list]
+ *
+ * Requests are not bound to a logged-in user — whenever a message or
+ * attachments are present, the author must be identified via `user_hash`
+ * or `user_code`.
+ *
+ * Run with:
+ * STROMCOM_TOKEN=… STROMCOM_THREAD=order-1234 STROMCOM_USER_CODE=employee-42 \
+ * php examples/create_thread_and_message.php
+ */
+
+require __DIR__ . '/../vendor/autoload.php';
+
+use Stromcom\Sdk\Client;
+
+$token = getenv('STROMCOM_TOKEN') ?: '';
+$threadCode = getenv('STROMCOM_THREAD') ?: '';
+$userCode = getenv('STROMCOM_USER_CODE') ?: '';
+if ($token === '' || $threadCode === '' || $userCode === '') {
+ fwrite(STDERR, "Set STROMCOM_TOKEN, STROMCOM_THREAD and STROMCOM_USER_CODE.\n"); exit(1);
+}
+
+$stromcom = new Client($token);
+
+$created = $stromcom->threads()->create([
+ 'code' => $threadCode,
+ 'name' => "Order {$threadCode}",
+ 'message' => 'Your order has shipped.
',
+ 'user_code' => $userCode,
+]);
+printf("Thread created/updated: %s (%s)\n", $created['thread']->name, $created['thread']->hash);
+if ($created['message'] !== null) {
+ printf("Initial message: %s\n", $created['message']->hash);
+}
+
+$updated = $stromcom->threads()->update($threadCode, ['name' => "Order {$threadCode} (shipped)"]);
+printf("Thread renamed to: %s\n", $updated->name);
+
+$sent = $stromcom->messages()->create($threadCode, [
+ 'message' => 'Tracking number: 1Z999AA10123456784
',
+ 'user_code' => $userCode,
+]);
+printf("Follow-up message: %s\n", $sent['message']->hash);
diff --git a/examples/message_reactions.php b/examples/message_reactions.php
new file mode 100644
index 0000000..7a11bb9
--- /dev/null
+++ b/examples/message_reactions.php
@@ -0,0 +1,46 @@
+reactions($threadCode, $messageHash)
+ * → list (code, count, currentUserReacted, users)
+ *
+ * Read-only: reacting to a message is only available to end users via the
+ * App API, not to the customer credential.
+ *
+ * Run with:
+ * STROMCOM_TOKEN=… STROMCOM_THREAD=order-1234 STROMCOM_MESSAGE=eVO6SY5kf5 \
+ * php examples/message_reactions.php
+ */
+
+require __DIR__ . '/../vendor/autoload.php';
+
+use Stromcom\Sdk\Client;
+
+$token = getenv('STROMCOM_TOKEN') ?: '';
+$threadCode = getenv('STROMCOM_THREAD') ?: '';
+$messageHash = getenv('STROMCOM_MESSAGE') ?: '';
+if ($token === '' || $threadCode === '' || $messageHash === '') {
+ fwrite(STDERR, "Set STROMCOM_TOKEN, STROMCOM_THREAD and STROMCOM_MESSAGE.\n"); exit(1);
+}
+
+$stromcom = new Client($token);
+$reactions = $stromcom->messages()->reactions($threadCode, $messageHash);
+
+if ($reactions === []) {
+ echo "Message has no reactions.\n";
+ exit(0);
+}
+
+foreach ($reactions as $reaction) {
+ $userNames = implode(', ', array_map(static fn($user) => $user->name, $reaction->users));
+ printf(
+ "%s x%d%s — %s\n",
+ $reaction->code,
+ $reaction->count,
+ $reaction->currentUserReacted ? ' (you)' : '',
+ $userNames,
+ );
+}
diff --git a/src/Model/MessageReaction.php b/src/Model/MessageReaction.php
new file mode 100644
index 0000000..243fcea
--- /dev/null
+++ b/src/Model/MessageReaction.php
@@ -0,0 +1,43 @@
+ $users */
+ public function __construct(
+ public readonly string $code,
+ public readonly int $count,
+ public readonly bool $currentUserReacted,
+ public readonly array $users,
+ ) {
+ }
+
+ public static function fromArray(array $data): static {
+ /** @var list> $rawUsers */
+ $rawUsers = isset($data['users']) && is_array($data['users']) ? array_values($data['users']) : [];
+
+ return new self(
+ code: self::requireString($data, 'code'),
+ count: self::readInt($data, 'count') ?? 0,
+ currentUserReacted: self::readBool($data, 'currentUserReacted') ?? false,
+ users: MessageReactionUser::listFromArray($rawUsers),
+ );
+ }
+
+ /**
+ * @param list> $items
+ *
+ * @return list
+ */
+ public static function listFromArray(array $items): array {
+ return array_map(static fn(array $row): self => self::fromArray($row), $items);
+ }
+
+}
diff --git a/src/Model/MessageReactionUser.php b/src/Model/MessageReactionUser.php
new file mode 100644
index 0000000..97062cc
--- /dev/null
+++ b/src/Model/MessageReactionUser.php
@@ -0,0 +1,33 @@
+> $items
+ *
+ * @return list
+ */
+ public static function listFromArray(array $items): array {
+ return array_map(static fn(array $row): self => self::fromArray($row), $items);
+ }
+
+}
diff --git a/src/Resource/MessageResource.php b/src/Resource/MessageResource.php
index 1088e08..390d3dd 100644
--- a/src/Resource/MessageResource.php
+++ b/src/Resource/MessageResource.php
@@ -4,14 +4,44 @@
namespace Stromcom\Sdk\Resource;
use Stromcom\Sdk\Model\AttachmentDownload;
+use Stromcom\Sdk\Model\Message;
use Stromcom\Sdk\Model\MessageAttachment;
use Stromcom\Sdk\Model\MessageList;
+use Stromcom\Sdk\Model\MessageReaction;
/**
- * `/threads/{code}/messages/` — read messages and attachments, admin-delete.
+ * `/threads/{code}/messages/` — create, read messages and attachments, admin-delete.
*/
final class MessageResource extends AbstractResource {
+ /**
+ * POST /threads/{code}/messages/ — create a message in an existing thread.
+ * The author is identified via `user_hash` or `user_code` in $payload
+ * (requests are not bound to a logged-in user). Attachments are uploaded
+ * in two steps: pass file names in `attachments`, then PUT each file's
+ * bytes to the matching `attachmentsUploadURL` entry.
+ *
+ * @param array $payload
+ *
+ * @return array{message: Message, attachmentsUploadURL: list}
+ */
+ public function create(string $threadCode, array $payload): array {
+ /** @var array $data */
+ $data = $this->client->request('POST', "threads/{$threadCode}/messages/", body: $payload) ?? [];
+
+ /** @var array $messageData */
+ $messageData = isset($data['message']) && is_array($data['message']) ? $data['message'] : [];
+ /** @var list $uploadURLs */
+ $uploadURLs = isset($data['attachmentsUploadURL']) && is_array($data['attachmentsUploadURL'])
+ ? array_values($data['attachmentsUploadURL'])
+ : [];
+
+ return [
+ 'message' => Message::fromArray($messageData),
+ 'attachmentsUploadURL' => $uploadURLs,
+ ];
+ }
+
/**
* GET /threads/{code}/messages/
*
@@ -57,4 +87,19 @@ public function attachmentDownload(string $threadCode, string $messageHash, stri
return AttachmentDownload::fromArray($data);
}
+ /**
+ * GET /threads/{code}/messages/{messageHash}/reactions/ — emoji reactions
+ * aggregated by code. Read-only: reacting is only available to end users
+ * via the App API, not to the customer credential.
+ *
+ * @return list
+ */
+ public function reactions(string $threadCode, string $messageHash): array {
+ /** @var array $data */
+ $data = $this->client->request('GET', "threads/{$threadCode}/messages/{$messageHash}/reactions/") ?? [];
+ /** @var list> $rawReactions */
+ $rawReactions = isset($data['reactions']) && is_array($data['reactions']) ? array_values($data['reactions']) : [];
+ return MessageReaction::listFromArray($rawReactions);
+ }
+
}
diff --git a/src/Resource/ThreadResource.php b/src/Resource/ThreadResource.php
index 171180e..08ee660 100644
--- a/src/Resource/ThreadResource.php
+++ b/src/Resource/ThreadResource.php
@@ -3,14 +3,58 @@
namespace Stromcom\Sdk\Resource;
+use Stromcom\Sdk\Model\Message;
use Stromcom\Sdk\Model\MessageList;
use Stromcom\Sdk\Model\Thread;
/**
- * `/threads/` — read threads bound to your business entities by `code`.
+ * `/threads/` — create, read and update threads bound to your business
+ * entities by `code`.
*/
final class ThreadResource extends AbstractResource {
+ /**
+ * POST /threads/ — create a thread (upsert by `code` — an existing thread
+ * with the same code is updated). Optionally include an initial `message`;
+ * when `message` or `attachments` are present in $payload, the author must
+ * be identified via `user_hash` or `user_code`.
+ *
+ * @param array $payload
+ *
+ * @return array{thread: Thread, message: ?Message, attachmentsUploadURL: list}
+ */
+ public function create(array $payload): array {
+ /** @var array $data */
+ $data = $this->client->request('POST', 'threads/', body: $payload) ?? [];
+
+ /** @var array $threadData */
+ $threadData = isset($data['thread']) && is_array($data['thread']) ? $data['thread'] : [];
+ /** @var array|null $messageData */
+ $messageData = isset($data['message']) && is_array($data['message']) ? $data['message'] : null;
+ /** @var list $uploadURLs */
+ $uploadURLs = isset($data['attachmentsUploadURL']) && is_array($data['attachmentsUploadURL'])
+ ? array_values($data['attachmentsUploadURL'])
+ : [];
+
+ return [
+ 'thread' => Thread::fromArray($threadData),
+ 'message' => $messageData !== null ? Message::fromArray($messageData) : null,
+ 'attachmentsUploadURL' => $uploadURLs,
+ ];
+ }
+
+ /**
+ * PUT /threads/{code}/ — update thread metadata (`name`, `url`,
+ * `attributes`). Only the keys present in $changes are written.
+ *
+ * @param array $changes
+ */
+ public function update(string $code, array $changes): Thread {
+ /** @var array $data */
+ $data = $this->client->request('PUT', "threads/{$code}/", body: $changes) ?? [];
+ return Thread::fromArray($this->extractThread($data));
+ }
+
/**
* GET /threads/{code}/
*
diff --git a/tests/Model/MessageReactionTest.php b/tests/Model/MessageReactionTest.php
new file mode 100644
index 0000000..00f8419
--- /dev/null
+++ b/tests/Model/MessageReactionTest.php
@@ -0,0 +1,58 @@
+ '👍',
+ 'count' => 2,
+ 'currentUserReacted' => true,
+ 'users' => [
+ ['hash' => 'u1', 'name' => 'Jane'],
+ ['hash' => 'u2', 'name' => 'John'],
+ ],
+ ]);
+
+ $this->assertSame('👍', $reaction->code);
+ $this->assertSame(2, $reaction->count);
+ $this->assertTrue($reaction->currentUserReacted);
+ $this->assertCount(2, $reaction->users);
+ $this->assertSame('u1', $reaction->users[0]->hash);
+ $this->assertSame('Jane', $reaction->users[0]->name);
+ }
+
+ #[Test]
+ public function count_and_current_user_reacted_default_when_missing(): void {
+ $reaction = MessageReaction::fromArray(['code' => '👍']);
+
+ $this->assertSame(0, $reaction->count);
+ $this->assertFalse($reaction->currentUserReacted);
+ $this->assertSame([], $reaction->users);
+ }
+
+ #[Test]
+ public function throws_when_code_is_missing(): void {
+ $this->expectException(\UnexpectedValueException::class);
+ MessageReaction::fromArray(['count' => 1]);
+ }
+
+ #[Test]
+ public function list_from_array_hydrates_each_row(): void {
+ $list = MessageReaction::listFromArray([
+ ['code' => '👍', 'count' => 1, 'currentUserReacted' => false, 'users' => []],
+ ['code' => '🎉', 'count' => 3, 'currentUserReacted' => true, 'users' => []],
+ ]);
+
+ $this->assertCount(2, $list);
+ $this->assertContainsOnlyInstancesOf(MessageReaction::class, $list);
+ }
+
+}
diff --git a/tests/Resource/MessageResourceTest.php b/tests/Resource/MessageResourceTest.php
index 94f6f23..585c76e 100644
--- a/tests/Resource/MessageResourceTest.php
+++ b/tests/Resource/MessageResourceTest.php
@@ -24,6 +24,47 @@ protected function setUp(): void {
);
}
+ #[Test]
+ public function create_sends_payload_and_hydrates_message(): void {
+ $this->http->enqueueJson(201, [
+ 'status' => 'success',
+ 'data' => [
+ 'message' => ['hash' => 'm1', 'message' => 'Hi
', 'userName' => 'Jane'],
+ 'attachmentsUploadURL' => ['https://upload.example/invoice.pdf'],
+ ],
+ ]);
+
+ $result = $this->client->messages()->create('order-1', [
+ 'message' => 'Hi
',
+ 'user_code' => 'u1',
+ 'attachments' => ['invoice.pdf'],
+ ]);
+
+ $this->assertSame('m1', $result['message']->hash);
+ $this->assertSame('Jane', $result['message']->userName);
+ $this->assertSame(['https://upload.example/invoice.pdf'], $result['attachmentsUploadURL']);
+
+ $call = $this->http->lastCall();
+ $this->assertSame('POST', $call['method']);
+ $this->assertSame(self::BASE_URL . 'threads/order-1/messages/', $call['url']);
+ $this->assertSame(
+ ['message' => 'Hi
', 'user_code' => 'u1', 'attachments' => ['invoice.pdf']],
+ json_decode((string) $call['body'], true),
+ );
+ }
+
+ #[Test]
+ public function create_defaults_upload_urls_to_empty_list(): void {
+ $this->http->enqueueJson(201, [
+ 'status' => 'success',
+ 'data' => ['message' => ['hash' => 'm1', 'message' => 'Hi
']],
+ ]);
+
+ $result = $this->client->messages()->create('order-1', ['message' => 'Hi
']);
+
+ $this->assertSame([], $result['attachmentsUploadURL']);
+ }
+
#[Test]
public function list_returns_messages_and_attachments(): void {
$this->http->enqueueJson(200, [
@@ -112,4 +153,48 @@ public function attachment_download_returns_signed_url_payload(): void {
);
}
+ #[Test]
+ public function reactions_returns_aggregated_reactions(): void {
+ $this->http->enqueueJson(200, [
+ 'status' => 'success',
+ 'data' => [
+ 'reactions' => [
+ [
+ 'code' => '👍',
+ 'count' => 2,
+ 'currentUserReacted' => true,
+ 'users' => [
+ ['hash' => 'u1', 'name' => 'Jane'],
+ ['hash' => 'u2', 'name' => 'John'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $reactions = $this->client->messages()->reactions('order-1', 'm1');
+
+ $this->assertCount(1, $reactions);
+ $this->assertSame('👍', $reactions[0]->code);
+ $this->assertSame(2, $reactions[0]->count);
+ $this->assertTrue($reactions[0]->currentUserReacted);
+ $this->assertCount(2, $reactions[0]->users);
+ $this->assertSame('u1', $reactions[0]->users[0]->hash);
+ $this->assertSame('Jane', $reactions[0]->users[0]->name);
+ $this->assertSame(
+ self::BASE_URL . 'threads/order-1/messages/m1/reactions/',
+ $this->http->lastCall()['url'],
+ );
+ }
+
+ #[Test]
+ public function reactions_returns_empty_list_when_message_has_none(): void {
+ $this->http->enqueueJson(200, [
+ 'status' => 'success',
+ 'data' => ['reactions' => []],
+ ]);
+
+ $this->assertSame([], $this->client->messages()->reactions('order-1', 'm1'));
+ }
+
}
diff --git a/tests/Resource/ThreadResourceTest.php b/tests/Resource/ThreadResourceTest.php
index 7bffc1d..055bb85 100644
--- a/tests/Resource/ThreadResourceTest.php
+++ b/tests/Resource/ThreadResourceTest.php
@@ -24,6 +24,68 @@ protected function setUp(): void {
);
}
+ #[Test]
+ public function create_upserts_thread_without_message(): void {
+ $this->http->enqueueJson(201, [
+ 'status' => 'success',
+ 'data' => [
+ 'thread' => ['code' => 'order-1', 'hash' => 'eVO6SY5kf5', 'name' => 'Order 1'],
+ 'message' => null,
+ 'attachmentsUploadURL' => [],
+ ],
+ ]);
+
+ $result = $this->client->threads()->create(['code' => 'order-1', 'name' => 'Order 1']);
+
+ $this->assertSame('order-1', $result['thread']->code);
+ $this->assertNull($result['message']);
+ $this->assertSame([], $result['attachmentsUploadURL']);
+
+ $call = $this->http->lastCall();
+ $this->assertSame('POST', $call['method']);
+ $this->assertSame(self::BASE_URL . 'threads/', $call['url']);
+ $this->assertSame(['code' => 'order-1', 'name' => 'Order 1'], json_decode((string) $call['body'], true));
+ }
+
+ #[Test]
+ public function create_with_initial_message_hydrates_message_and_upload_urls(): void {
+ $this->http->enqueueJson(201, [
+ 'status' => 'success',
+ 'data' => [
+ 'thread' => ['code' => 'order-1', 'hash' => 'eVO6SY5kf5', 'name' => 'Order 1'],
+ 'message' => ['hash' => 'm1', 'message' => 'Hi
', 'userName' => 'Jane'],
+ 'attachmentsUploadURL' => ['https://upload.example/invoice.pdf'],
+ ],
+ ]);
+
+ $result = $this->client->threads()->create([
+ 'code' => 'order-1',
+ 'message' => 'Hi
',
+ 'user_code' => 'u1',
+ 'attachments' => ['invoice.pdf'],
+ ]);
+
+ $this->assertSame('m1', $result['message']?->hash);
+ $this->assertSame(['https://upload.example/invoice.pdf'], $result['attachmentsUploadURL']);
+ }
+
+ #[Test]
+ public function update_sends_changes_and_returns_thread(): void {
+ $this->http->enqueueJson(200, [
+ 'status' => 'success',
+ 'data' => ['code' => 'order-1', 'hash' => 'eVO6SY5kf5', 'name' => 'Renamed'],
+ ]);
+
+ $thread = $this->client->threads()->update('order-1', ['name' => 'Renamed']);
+
+ $this->assertSame('Renamed', $thread->name);
+
+ $call = $this->http->lastCall();
+ $this->assertSame('PUT', $call['method']);
+ $this->assertSame(self::BASE_URL . 'threads/order-1/', $call['url']);
+ $this->assertSame(['name' => 'Renamed'], json_decode((string) $call['body'], true));
+ }
+
#[Test]
public function get_without_expand_omits_query_string(): void {
$this->http->enqueueJson(200, [