111 lines
5.3 KiB
PHP
111 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Mcp\Support;
|
|
|
|
use App\Actions\Tickets\Support\AcceptanceCriteriaChecklistMapper;
|
|
use App\Http\Requests\UpdateProjectTicketRequest;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketFieldDefinition;
|
|
use App\Models\User;
|
|
use App\Rules\VisualTicketDescription;
|
|
use Illuminate\Routing\Route;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Laravel\Mcp\Request;
|
|
|
|
class TicketUpdateInput
|
|
{
|
|
public function __construct(private readonly KanbanGitMcp $kanbanGitMcp) {}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function validate(Request $request, Ticket $ticket, User $user): array
|
|
{
|
|
$form = new UpdateProjectTicketRequest;
|
|
$route = new Route('PUT', '/', fn () => null);
|
|
$route->bind(\Illuminate\Http\Request::create('/', 'PUT'));
|
|
$route->setParameter('project', $ticket->project);
|
|
$route->setParameter('ticket', $ticket);
|
|
$form->setRouteResolver(fn () => $route);
|
|
$form->setUserResolver(fn () => $user);
|
|
$rules = $form->rules();
|
|
foreach ($rules as $key => &$rule) {
|
|
if (! str_contains($key, '.')) {
|
|
$rule = ['sometimes', ...$rule];
|
|
}
|
|
}
|
|
unset($rule);
|
|
$rules['description'][] = new VisualTicketDescription;
|
|
$rules['acceptance_criteria'] = ['sometimes', 'array', 'list'];
|
|
$rules['acceptance_criteria.*'] = ['array:id,text,checked,indent'];
|
|
$rules['acceptance_criteria.*.id'] = ['sometimes', 'integer', 'min:1', 'distinct', Rule::exists('acceptance_criteria', 'id')->where('ticket_id', $ticket->id)];
|
|
$rules['acceptance_criteria.*.checked'] = ['sometimes', 'boolean'];
|
|
$rules['acceptance_criteria.*.indent'] = ['sometimes', 'integer', 'min:0', 'max:'.AcceptanceCriteriaChecklistMapper::MAX_INDENT_LEVEL];
|
|
$rules['field_values'] = ['sometimes', 'array', 'list'];
|
|
$rules['field_values.*'] = ['array:ticket_field_definition_id,value_number,value_user_id'];
|
|
$rules['field_values.*.ticket_field_definition_id'][] = 'distinct';
|
|
$rules['ticket_status_id'] = ['prohibited'];
|
|
$rules['acceptance_criteria.*.text'][] = function (string $attribute, mixed $value, \Closure $fail): void {
|
|
if (is_string($value) && trim($value) === '') {
|
|
$fail('Criterion text must not be blank.');
|
|
}
|
|
};
|
|
$input = $request->validate($rules);
|
|
$previousIndent = -1;
|
|
foreach (Arr::get($input, 'acceptance_criteria', []) as $index => $item) {
|
|
$indent = Arr::get($item, 'indent', 0);
|
|
if ($indent > $previousIndent + 1) {
|
|
throw ValidationException::withMessages(["acceptance_criteria.{$index}.indent" => 'Indentation cannot skip a level; the first item must have indent 0.']);
|
|
}
|
|
$previousIndent = $indent;
|
|
}
|
|
$merged = array_replace($this->kanbanGitMcp->updatePayload($ticket), $input);
|
|
$merged['acceptance_criteria'] = Arr::exists($input, 'acceptance_criteria')
|
|
? AcceptanceCriteriaChecklistMapper::normalize($input['acceptance_criteria'])
|
|
: AcceptanceCriteriaChecklistMapper::fromModelsWithIds($ticket->acceptanceCriteria()->get());
|
|
|
|
$validator = Validator::make($merged, $form->rules());
|
|
$form->setValidator($validator);
|
|
$validator->validate();
|
|
foreach ($form->after() as $callback) {
|
|
$callback($validator);
|
|
}
|
|
|
|
$people = User::query()->visibleToProject($ticket->project)->pluck('id')->all();
|
|
foreach (['assignee_user_id', 'author_user_id'] as $field) {
|
|
if (Arr::exists($input, $field) && $input[$field] !== null && ! in_array((int) $input[$field], $people, true)) {
|
|
$validator->errors()->add($field, 'The person must be able to view this project.');
|
|
}
|
|
}
|
|
$definitions = $ticket->project->ticketFieldDefinitions()->get()->keyBy('id');
|
|
foreach (Arr::get($input, 'field_values', []) as $index => $value) {
|
|
$definition = $definitions->get($value['ticket_field_definition_id']);
|
|
$incompatible = $definition->field_type === 'user' ? 'value_number' : 'value_user_id';
|
|
if (Arr::get($value, $incompatible) !== null) {
|
|
$validator->errors()->add("field_values.{$index}.{$incompatible}", 'The value must match the custom field type.');
|
|
}
|
|
if (Arr::get($value, 'value_user_id') !== null && ! in_array((int) $value['value_user_id'], $people, true)) {
|
|
$validator->errors()->add("field_values.{$index}.value_user_id", 'The person must be able to view this project.');
|
|
}
|
|
}
|
|
if ($validator->errors()->isNotEmpty()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
if (Arr::exists($input, 'field_values')) {
|
|
$replacement = $definitions->map(fn (TicketFieldDefinition $definition): array => [
|
|
'ticket_field_definition_id' => $definition->id,
|
|
'value_number' => null,
|
|
'value_user_id' => null,
|
|
])->all();
|
|
foreach ($input['field_values'] as $value) {
|
|
$replacement[$value['ticket_field_definition_id']] = $value;
|
|
}
|
|
$merged['field_values'] = array_values($replacement);
|
|
}
|
|
|
|
return $merged;
|
|
}
|
|
}
|