, * assignee_user_id?: int|null, * author_user_id?: int|null, * blocked_comment: string|null, * description: string|null, * field_values?: array, * 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 $replaceCriteriaWithIds = false, ): bool { $fieldValueAttributes = Arr::get($validated, 'field_values', []); $nextAcceptanceCriteria = AcceptanceCriteriaChecklistMapper::normalize( Arr::get($validated, 'acceptance_criteria'), ); $ticketAttributes = $this->buildTicketAttributes($ticket, $validated); return DB::transaction(function () use ($actor, $fieldValueAttributes, $nextAcceptanceCriteria, $replaceCriteriaWithIds, $ticket, $ticketAttributes): bool { if (! $ticketAttributes['is_blocked']) { $ticketAttributes['blocked_comment'] = null; } $ticket->loadMissing([ 'assignee', 'author', 'acceptanceCriteria', 'fieldValues.ticketFieldDefinition', 'fieldValues.userValue', 'parentTicket', 'project', 'ticketPriority', 'ticketStatus.boardColumn', 'ticketType', ]); /** @var Collection $fieldDefinitions */ $fieldDefinitions = $ticket->project->ticketFieldDefinitions() ->get() ->keyBy('id'); $ticketTypeOptions = $ticket->project->ticketTypes() ->get() ->keyBy('id'); $ticketPriorityOptions = $ticket->project->ticketPriorities() ->get() ->keyBy('id'); /** @var Collection $existingFieldValues */ $existingFieldValues = $ticket->fieldValues ->keyBy('ticket_field_definition_id'); $currentAcceptanceCriteria = $replaceCriteriaWithIds ? AcceptanceCriteriaChecklistMapper::fromModelsWithIds($ticket->acceptanceCriteria) : AcceptanceCriteriaChecklistMapper::fromModels($ticket->acceptanceCriteria); $changes = []; $summaryLabels = []; $assigneeChange = null; $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']) { $toAssigneeId = $ticketAttributes['assignee_user_id']; $toAssigneeName = $toAssigneeId === null ? null : User::query()->find($toAssigneeId)?->name; $changes['assignee'] = [ 'from' => $ticket->assignee?->name, 'to' => $toAssigneeName, ]; $summaryLabels[] = 'assignee'; $assigneeChange = [ 'from_id' => $ticket->assignee_user_id, 'from_name' => $ticket->assignee?->name, 'to_id' => $toAssigneeId, 'to_name' => $toAssigneeName, ]; } 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(); } if ($currentAcceptanceCriteria !== $nextAcceptanceCriteria) { AcceptanceCriteriaChecklistMapper::syncTicket($ticket, $nextAcceptanceCriteria, ! $replaceCriteriaWithIds); } $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, ); if ($assigneeChange !== null) { TicketAssigneeChanged::dispatch( $this->buildTicketAssigneeChangedPayload->handle( $ticket->project, $ticket, $actor, $assigneeChange['from_id'], $assigneeChange['from_name'], $assigneeChange['to_id'], $assigneeChange['to_name'], CarbonImmutable::now(), ), ); } return true; }); } /** * @param array{ * acceptance_criteria: array, * 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{ * 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 { return [ '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 $changes * @param array $summaryLabels * @param array $currentValue * @param array $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 $changes * @param array $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 $changes * @param array $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 $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.'.'; } }