The project form gets «Crear en TaskStream» while the project has no board, and `taskstream: true` on a PATCH does the same through the API. A refusal leaves the edit unsaved; a project with a board keeps it, and `taskstream_key` without the flag is still ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
3.9 KiB
PHP
112 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Actions\Projects;
|
|
|
|
use App\Models\Project;
|
|
use App\Models\User;
|
|
use App\Services\TaskStream\TaskStreamClient;
|
|
use App\Services\TaskStream\TaskStreamException;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
/**
|
|
* Opens a board in TaskStream for a project, new or one without a board
|
|
* yet, and points the project at it.
|
|
*
|
|
* It runs before the project is saved: if TaskStream says no, nothing is
|
|
* saved here either, and the caller gets the reason to fix and retry.
|
|
* The board is public, like the project it tracks is meant to be.
|
|
*/
|
|
class OpenInTaskStream
|
|
{
|
|
/** TaskStream's rule for a project key. */
|
|
public const KEY_PATTERN = '/^[A-Z][A-Z0-9]{1,9}$/';
|
|
|
|
public function __construct(protected TaskStreamClient $client) {}
|
|
|
|
/**
|
|
* @param string|null $bearer The token the caller presented here.
|
|
* @param string|null $key The key asked for; derived from the slug when null.
|
|
*
|
|
* @throws ValidationException when the caller can fix the request
|
|
*/
|
|
public function handle(Project $project, User $user, ?string $bearer, ?string $key = null): void
|
|
{
|
|
$locale = (string) config('locales.default');
|
|
$key = $key !== null ? Str::upper($key) : $this->keyFrom((string) $project->getTranslation('slug', $locale, false));
|
|
|
|
if ($key === null) {
|
|
throw ValidationException::withMessages(['taskstream_key' => __('rules.taskstream.key_needed')]);
|
|
}
|
|
|
|
try {
|
|
$created = $this->client->createProject(
|
|
token: $this->token($user, $bearer),
|
|
key: $key,
|
|
name: Str::limit((string) $project->getTranslation('title', $locale, false), 80, '…'),
|
|
description: $project->getTranslation('tagline', $locale, false) ?: null,
|
|
public: true,
|
|
);
|
|
} catch (TaskStreamException $e) {
|
|
throw $this->explain($e);
|
|
}
|
|
|
|
$project->forceFill([
|
|
'taskstream_project_id' => $created['id'],
|
|
'taskstream_key' => $created['key'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* A Pireon token is passed on, so the board belongs to whoever asked. A
|
|
* machine token is this application's alone and means nothing there.
|
|
*/
|
|
protected function token(User $user, ?string $bearer): string
|
|
{
|
|
$token = $user->currentAccessToken() === null ? $bearer : $this->client->serviceToken();
|
|
|
|
if (blank($token)) {
|
|
throw ValidationException::withMessages(['taskstream' => __('rules.taskstream.machine')]);
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* A key in TaskStream's style: one word gives its first ten characters
|
|
* ("catalogo" → CATALOGO), several give their initials ("mi-web-nueva"
|
|
* → MWN). Null when nothing usable is left.
|
|
*/
|
|
public function keyFrom(string $slug): ?string
|
|
{
|
|
$words = array_values(array_filter(preg_split('/[^A-Z0-9]+/', Str::upper(Str::ascii($slug))) ?: []));
|
|
|
|
$key = count($words) === 1
|
|
? $words[0]
|
|
: implode('', array_map(fn (string $word) => $word[0], $words));
|
|
|
|
// It must start with a letter.
|
|
$key = substr((string) preg_replace('/^[0-9]+/', '', $key), 0, 10);
|
|
|
|
return preg_match(self::KEY_PATTERN, $key) === 1 ? $key : null;
|
|
}
|
|
|
|
protected function explain(TaskStreamException $e): ValidationException|HttpException
|
|
{
|
|
if ($e->isRefusal()) {
|
|
return ValidationException::withMessages([
|
|
'taskstream_key' => __('rules.taskstream.refused', ['reason' => $e->getMessage()]),
|
|
]);
|
|
}
|
|
|
|
if ($e->isUnauthorised()) {
|
|
return ValidationException::withMessages(['taskstream' => __('rules.taskstream.unauthorised')]);
|
|
}
|
|
|
|
return new HttpException(502, __('rules.taskstream.unavailable'), $e);
|
|
}
|
|
}
|