feat: create ClickUp task after confirming accommodation booking

This commit is contained in:
Björn Fromme committed 2026-08-20 17:56:29 +02:00
1 parent 0a1683667f
commit 7c5f6dd5ef
18 files changed
+535 -48

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\ClickUp;
use App\ClickUp\Exception\ClickUpException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiClient
{
private const string DEFAULT_BASE_URL = 'https://api.clickup.com/api/v2';
// Creating a task from a template regularly takes several seconds on ClickUp's side, so these
// ceilings sit well above the ones the BpnConnect client uses. They exist to stop a hung
// upstream from pinning a worker forever, not to enforce a latency budget.
private const int IDLE_TIMEOUT_SECONDS = 30;
private const int MAX_DURATION_SECONDS = 120;
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly LoggerInterface $logger,
private readonly ?string $apiToken = null,
private readonly ?string $listId = null,
private readonly ?string $templateId = null,
private readonly ?string $baseUrl = null,
) {
}
/**
* Environments without ClickUp credentials (dev, test, staging) must not fail — callers check
* this instead of letting assertConfigured() blow up.
*/
public function isConfigured(): bool
{
return null !== $this->apiToken && '' !== $this->apiToken
&& null !== $this->listId && '' !== $this->listId
&& null !== $this->templateId && '' !== $this->templateId;
}
/**
* Creates a task from the configured template and returns the new task id.
*/
public function createTaskFromTemplate(string $name): string
{
$data = $this->request('POST', sprintf('/list/%s/taskTemplate/%s', $this->listId, $this->templateId), [
'name' => $name,
]);
$id = $data['id'] ?? null;
if (!is_string($id) || '' === $id) {
throw new ClickUpException('ClickUp did not return a task id for the created task.');
}
return $id;
}
public function updateTaskStatus(string $taskId, string $status): void
{
$this->request('PUT', '/task/'.$taskId, ['status' => $status]);
}
/**
* @param array<string, mixed> $payload
*
* @return array<string, mixed>
*/
private function request(string $method, string $path, array $payload): array
{
$this->assertConfigured();
try {
$response = $this->httpClient->request($method, ($this->baseUrl ?? self::DEFAULT_BASE_URL).$path, [
'headers' => [
// ClickUp deviates from the usual scheme: the raw token goes into the
// Authorization header, without a "Bearer " prefix.
'Authorization' => $this->apiToken,
],
'json' => $payload,
'timeout' => self::IDLE_TIMEOUT_SECONDS,
'max_duration' => self::MAX_DURATION_SECONDS,
]);
return $response->toArray();
} catch (ExceptionInterface $e) {
$this->logger->error('ClickUp request failed', [
'method' => $method,
'path' => $path,
'error' => $e->getMessage(),
]);
throw new ClickUpException('ClickUp request failed: '.$e->getMessage(), 0, $e);
}
}
private function assertConfigured(): void
{
if (!$this->isConfigured()) {
throw new ClickUpException('ClickUp client is not configured (missing CLICKUP_API_TOKEN, CLICKUP_LIST_ID or CLICKUP_TEMPLATE_ID).');
}
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\ClickUp\Exception;
class ClickUpException extends \RuntimeException
{
}