400 lines
13 KiB
PHP
400 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Tickets;
|
|
|
|
use App\Actions\Tickets\Support\TicketAcceptanceCriteriaData;
|
|
use App\Enums\TicketWorklogEntryType;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketFieldDefinition;
|
|
use App\Models\TicketFieldValue;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class UpdateProjectTicket
|
|
{
|
|
public function __construct(
|
|
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
|
|
private readonly SyncProjectTicketFieldValues $syncProjectTicketFieldValues,
|
|
) {}
|
|
|
|
/**
|
|
* @param array{
|
|
* acceptance_criteria: array{
|
|
* summary: string|null,
|
|
* items: array<int, array{
|
|
* checked: bool,
|
|
* indent: int,
|
|
* text: string
|
|
* }>
|
|
* },
|
|
* assignee_user_id?: int|null,
|
|
* author_user_id?: int|null,
|
|
* blocked_comment: string|null,
|
|
* description: string|null,
|
|
* field_values?: array<int, array{
|
|
* ticket_field_definition_id: int,
|
|
* value_number?: float|int|string|null,
|
|
* value_user_id?: int|string|null
|
|
* }>,
|
|
* is_blocked: bool,
|
|
* story_points?: int|null,
|
|
* ticket_priority_id: int,
|
|
* ticket_type_id: int,
|
|
* parent_ticket_id: int|null,
|
|
* title: string
|
|
* } $validated
|
|
*/
|
|
public function handle(
|
|
Ticket $ticket,
|
|
User $actor,
|
|
array $validated,
|
|
): bool {
|
|
$fieldValueAttributes = Arr::get($validated, 'field_values', []);
|
|
$ticketAttributes = $this->buildTicketAttributes($ticket, $validated);
|
|
|
|
return DB::transaction(function () use ($actor, $fieldValueAttributes, $ticket, $ticketAttributes): bool {
|
|
if (! $ticketAttributes['is_blocked']) {
|
|
$ticketAttributes['blocked_comment'] = null;
|
|
}
|
|
|
|
$ticket->loadMissing([
|
|
'assignee',
|
|
'author',
|
|
'fieldValues.ticketFieldDefinition',
|
|
'fieldValues.userValue',
|
|
'parentTicket',
|
|
'project',
|
|
'ticketPriority',
|
|
'ticketType',
|
|
]);
|
|
|
|
/** @var Collection<int, TicketFieldDefinition> $fieldDefinitions */
|
|
$fieldDefinitions = $ticket->project->ticketFieldDefinitions()
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
$ticketTypeOptions = $ticket->project->ticketTypes()
|
|
->get()
|
|
->keyBy('id');
|
|
$ticketPriorityOptions = $ticket->project->ticketPriorities()
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
/** @var Collection<int, TicketFieldValue> $existingFieldValues */
|
|
$existingFieldValues = $ticket->fieldValues
|
|
->keyBy('ticket_field_definition_id');
|
|
$currentAcceptanceCriteria = TicketAcceptanceCriteriaData::fromStored(
|
|
$ticket->acceptance_criteria_data,
|
|
$ticket->acceptance_criteria,
|
|
);
|
|
$nextAcceptanceCriteria = TicketAcceptanceCriteriaData::fromStored(
|
|
$ticketAttributes['acceptance_criteria_data'],
|
|
$ticketAttributes['acceptance_criteria'],
|
|
);
|
|
|
|
$changes = [];
|
|
$summaryLabels = [];
|
|
|
|
$this->trackScalarChange(
|
|
$changes,
|
|
$summaryLabels,
|
|
'title',
|
|
'title',
|
|
$ticket->title,
|
|
$ticketAttributes['title'],
|
|
);
|
|
$this->trackScalarChange(
|
|
$changes,
|
|
$summaryLabels,
|
|
'description',
|
|
'description',
|
|
$ticket->description,
|
|
$ticketAttributes['description'],
|
|
);
|
|
$this->trackAcceptanceCriteriaChange(
|
|
$changes,
|
|
$summaryLabels,
|
|
$currentAcceptanceCriteria,
|
|
$nextAcceptanceCriteria,
|
|
);
|
|
$this->trackScalarChange(
|
|
$changes,
|
|
$summaryLabels,
|
|
'blocked_comment',
|
|
'blocker comment',
|
|
$ticket->blocked_comment,
|
|
$ticketAttributes['blocked_comment'],
|
|
);
|
|
$this->trackNumberChange(
|
|
$changes,
|
|
$summaryLabels,
|
|
'story_points',
|
|
'story points',
|
|
$ticket->story_points,
|
|
$ticketAttributes['story_points'],
|
|
);
|
|
|
|
if ($ticket->ticket_type_id !== $ticketAttributes['ticket_type_id']) {
|
|
$changes['ticket_type'] = [
|
|
'from' => $ticket->ticketType?->name,
|
|
'to' => $ticketTypeOptions->get($ticketAttributes['ticket_type_id'])?->name,
|
|
];
|
|
$summaryLabels[] = 'type';
|
|
}
|
|
|
|
if ($ticket->ticket_priority_id !== $ticketAttributes['ticket_priority_id']) {
|
|
$changes['ticket_priority'] = [
|
|
'from' => $ticket->ticketPriority?->name,
|
|
'to' => $ticketPriorityOptions->get($ticketAttributes['ticket_priority_id'])?->name,
|
|
];
|
|
$summaryLabels[] = 'priority';
|
|
}
|
|
|
|
if ($ticket->is_blocked !== $ticketAttributes['is_blocked']) {
|
|
$changes['is_blocked'] = [
|
|
'from' => $ticket->is_blocked,
|
|
'to' => $ticketAttributes['is_blocked'],
|
|
];
|
|
$summaryLabels[] = 'blocked state';
|
|
}
|
|
|
|
if ($ticket->parent_ticket_id !== $ticketAttributes['parent_ticket_id']) {
|
|
$newParentTicket = $ticketAttributes['parent_ticket_id'] === null
|
|
? null
|
|
: Ticket::query()->find($ticketAttributes['parent_ticket_id']);
|
|
|
|
$changes['parent_ticket'] = [
|
|
'from' => $ticket->parentTicket?->ticket_key,
|
|
'to' => $newParentTicket?->ticket_key,
|
|
];
|
|
$summaryLabels[] = 'parent ticket';
|
|
}
|
|
|
|
if ($ticket->assignee_user_id !== $ticketAttributes['assignee_user_id']) {
|
|
$changes['assignee'] = [
|
|
'from' => $ticket->assignee?->name,
|
|
'to' => $ticketAttributes['assignee_user_id'] === null
|
|
? null
|
|
: User::query()->find($ticketAttributes['assignee_user_id'])?->name,
|
|
];
|
|
$summaryLabels[] = 'assignee';
|
|
}
|
|
|
|
if ($ticket->author_user_id !== $ticketAttributes['author_user_id']) {
|
|
$changes['author'] = [
|
|
'from' => $ticket->author?->name,
|
|
'to' => $ticketAttributes['author_user_id'] === null
|
|
? null
|
|
: User::query()->find($ticketAttributes['author_user_id'])?->name,
|
|
];
|
|
$summaryLabels[] = 'author';
|
|
}
|
|
|
|
$ticket->fill($ticketAttributes);
|
|
|
|
if ($ticket->isDirty()) {
|
|
$ticket->save();
|
|
}
|
|
|
|
$fieldValueChangeSet = $this->syncProjectTicketFieldValues->handle(
|
|
$ticket,
|
|
$fieldDefinitions,
|
|
$existingFieldValues,
|
|
$fieldValueAttributes,
|
|
);
|
|
|
|
if ($fieldValueChangeSet['changes'] !== []) {
|
|
$changes['field_values'] = $fieldValueChangeSet['changes'];
|
|
$summaryLabels = [
|
|
...$summaryLabels,
|
|
...$fieldValueChangeSet['summary_labels'],
|
|
];
|
|
}
|
|
|
|
if ($summaryLabels === []) {
|
|
return false;
|
|
}
|
|
|
|
$this->createTicketWorklogEntry->handle(
|
|
$ticket->fresh(),
|
|
$actor,
|
|
TicketWorklogEntryType::Updated,
|
|
$this->buildSummary($summaryLabels),
|
|
$changes,
|
|
);
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* acceptance_criteria: array{
|
|
* summary: string|null,
|
|
* items: array<int, array{
|
|
* checked: bool,
|
|
* indent: int,
|
|
* text: string
|
|
* }>
|
|
* },
|
|
* assignee_user_id?: int|null,
|
|
* author_user_id?: int|null,
|
|
* blocked_comment: string|null,
|
|
* description: string|null,
|
|
* is_blocked: bool,
|
|
* parent_ticket_id: int|null,
|
|
* story_points?: int|null,
|
|
* ticket_priority_id: int,
|
|
* ticket_type_id: int,
|
|
* title: string
|
|
* } $validated
|
|
* @return array{
|
|
* acceptance_criteria: string|null,
|
|
* acceptance_criteria_data: array{
|
|
* summary: string|null,
|
|
* items: array<int, array{
|
|
* checked: bool,
|
|
* indent: int,
|
|
* text: string
|
|
* }>
|
|
* }|null,
|
|
* assignee_user_id: int|null,
|
|
* author_user_id: int|null,
|
|
* blocked_comment: string|null,
|
|
* description: string|null,
|
|
* is_blocked: bool,
|
|
* parent_ticket_id: int|null,
|
|
* story_points: int|null,
|
|
* ticket_priority_id: int,
|
|
* ticket_type_id: int,
|
|
* title: string
|
|
* }
|
|
*/
|
|
private function buildTicketAttributes(Ticket $ticket, array $validated): array
|
|
{
|
|
$acceptanceCriteria = TicketAcceptanceCriteriaData::normalize(
|
|
Arr::get($validated, 'acceptance_criteria'),
|
|
);
|
|
|
|
return [
|
|
'acceptance_criteria' => TicketAcceptanceCriteriaData::toLegacyText($acceptanceCriteria),
|
|
'acceptance_criteria_data' => TicketAcceptanceCriteriaData::forStorage($acceptanceCriteria),
|
|
'assignee_user_id' => Arr::exists($validated, 'assignee_user_id')
|
|
? Arr::get($validated, 'assignee_user_id')
|
|
: $ticket->assignee_user_id,
|
|
'author_user_id' => Arr::exists($validated, 'author_user_id')
|
|
? Arr::get($validated, 'author_user_id')
|
|
: $ticket->author_user_id,
|
|
'blocked_comment' => Arr::get($validated, 'blocked_comment'),
|
|
'description' => Arr::get($validated, 'description'),
|
|
'is_blocked' => Arr::get($validated, 'is_blocked', false),
|
|
'parent_ticket_id' => Arr::get($validated, 'parent_ticket_id'),
|
|
'story_points' => Arr::exists($validated, 'story_points')
|
|
? Arr::get($validated, 'story_points')
|
|
: $ticket->story_points,
|
|
'ticket_priority_id' => Arr::get($validated, 'ticket_priority_id'),
|
|
'ticket_type_id' => Arr::get($validated, 'ticket_type_id'),
|
|
'title' => Arr::get($validated, 'title'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $changes
|
|
* @param array<int, string> $summaryLabels
|
|
* @param array{
|
|
* summary: string|null,
|
|
* items: array<int, array{checked: bool, indent: int, text: string}>
|
|
* } $currentValue
|
|
* @param array{
|
|
* summary: string|null,
|
|
* items: array<int, array{checked: bool, indent: int, text: string}>
|
|
* } $newValue
|
|
*/
|
|
private function trackAcceptanceCriteriaChange(
|
|
array &$changes,
|
|
array &$summaryLabels,
|
|
array $currentValue,
|
|
array $newValue,
|
|
): void {
|
|
if ($currentValue === $newValue) {
|
|
return;
|
|
}
|
|
|
|
$changes['acceptance_criteria'] = [
|
|
'from' => $currentValue,
|
|
'to' => $newValue,
|
|
];
|
|
$summaryLabels[] = 'acceptance criteria';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $changes
|
|
* @param array<int, string> $summaryLabels
|
|
*/
|
|
private function trackScalarChange(
|
|
array &$changes,
|
|
array &$summaryLabels,
|
|
string $changeKey,
|
|
string $summaryLabel,
|
|
?string $currentValue,
|
|
?string $newValue,
|
|
): void {
|
|
if ($currentValue === $newValue) {
|
|
return;
|
|
}
|
|
|
|
$changes[$changeKey] = [
|
|
'from' => $currentValue,
|
|
'to' => $newValue,
|
|
];
|
|
$summaryLabels[] = $summaryLabel;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $changes
|
|
* @param array<int, string> $summaryLabels
|
|
*/
|
|
private function trackNumberChange(
|
|
array &$changes,
|
|
array &$summaryLabels,
|
|
string $changeKey,
|
|
string $summaryLabel,
|
|
?int $currentValue,
|
|
?int $newValue,
|
|
): void {
|
|
if ($currentValue === $newValue) {
|
|
return;
|
|
}
|
|
|
|
$changes[$changeKey] = [
|
|
'from' => $currentValue,
|
|
'to' => $newValue,
|
|
];
|
|
$summaryLabels[] = $summaryLabel;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $summaryLabels
|
|
*/
|
|
private function buildSummary(array $summaryLabels): string
|
|
{
|
|
$uniqueLabels = collect($summaryLabels)
|
|
->unique()
|
|
->values();
|
|
|
|
if ($uniqueLabels->count() === 1) {
|
|
return 'Updated '.$uniqueLabels->first().'.';
|
|
}
|
|
|
|
if ($uniqueLabels->count() === 2) {
|
|
return 'Updated '.$uniqueLabels->first().' and '.$uniqueLabels->last().'.';
|
|
}
|
|
|
|
$lastLabel = $uniqueLabels->pop();
|
|
|
|
return 'Updated '.$uniqueLabels->implode(', ').', and '.$lastLabel.'.';
|
|
}
|
|
}
|