Refactor acceptance criteria into model
linter / quality (pull_request) Successful in 1m14s
tests / ci (8.4) (pull_request) Successful in 1m28s

This commit is contained in:
2026-05-19 22:20:15 +00:00
parent 3ddfcc89f9
commit 7805bd057e
10 changed files with 217 additions and 72 deletions
@@ -2,6 +2,9 @@
namespace App\Actions\Tickets\Support;
use App\Models\AcceptanceCriterion;
use App\Models\Ticket;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Arr;
class TicketAcceptanceCriteriaData
@@ -9,6 +12,7 @@ class TicketAcceptanceCriteriaData
public const MAX_INDENT_LEVEL = 8;
/**
* @param iterable<AcceptanceCriterion> $criteria
* @return array{
* summary: string|null,
* items: array<int, array{
@@ -18,17 +22,37 @@ class TicketAcceptanceCriteriaData
* }>
* }
*/
public static function fromStored(mixed $storedData, ?string $legacyText): array
public static function fromModels(iterable $criteria, ?string $legacyText): array
{
$normalizedStoredData = is_array($storedData)
? self::normalize($storedData)
: self::empty();
$summary = self::normalize($legacyText)['summary'];
$models = ($criteria instanceof EloquentCollection ? $criteria : collect($criteria))
->sort(function (AcceptanceCriterion $left, AcceptanceCriterion $right): int {
return [
$left->parent_acceptance_criterion_id ?? 0,
$left->position,
$left->id,
] <=> [
$right->parent_acceptance_criterion_id ?? 0,
$right->position,
$right->id,
];
})
->values();
if (self::hasContent($normalizedStoredData)) {
return $normalizedStoredData;
if ($models->isEmpty()) {
return self::normalize($legacyText);
}
return self::normalize($legacyText);
/** @var array<int|null, array<int, AcceptanceCriterion>> $grouped */
$grouped = $models->groupBy('parent_acceptance_criterion_id')->all();
$items = [];
self::flattenItems($items, $grouped, null, 0);
return [
'summary' => $summary,
'items' => $items,
];
}
/**
@@ -71,6 +95,7 @@ class TicketAcceptanceCriteriaData
}
/**
* @param Ticket $ticket
* @param array{
* summary: string|null,
* items: array<int, array{
@@ -79,22 +104,39 @@ class TicketAcceptanceCriteriaData
* text: string
* }>
* } $acceptanceCriteria
* @return array{
* summary: string|null,
* items: array<int, array{
* checked: bool,
* indent: int,
* text: string
* }>
* }|null
*/
public static function forStorage(array $acceptanceCriteria): ?array
public static function syncTicket(Ticket $ticket, array $acceptanceCriteria): void
{
$normalized = self::normalize($acceptanceCriteria);
return self::hasContent($normalized)
? $normalized
: null;
$ticket->acceptanceCriteria()->delete();
if ($normalized['items'] === []) {
return;
}
$parentStack = [];
$positionsByParent = [];
foreach ($normalized['items'] as $item) {
$indent = min($item['indent'], count($parentStack));
$parentId = $indent === 0 ? null : $parentStack[$indent - 1];
$positionKey = $parentId ?? 'root';
$position = $positionsByParent[$positionKey] ?? 0;
$criterion = $ticket->acceptanceCriteria()->create([
'parent_acceptance_criterion_id' => $parentId,
'position' => $position,
'text' => $item['text'],
'is_checked' => $item['checked'],
]);
$positionsByParent[$positionKey] = $position + 1;
$parentStack[$indent] = $criterion->id;
$parentStack = array_slice($parentStack, 0, $indent + 1);
}
$ticket->unsetRelation('acceptanceCriteria');
}
/**
@@ -154,6 +196,23 @@ class TicketAcceptanceCriteriaData
];
}
/**
* @param array<int, array{checked: bool, indent: int, text: string}> $items
* @param array<int|string, iterable<AcceptanceCriterion>> $grouped
*/
private static function flattenItems(array &$items, array $grouped, ?int $parentId, int $indent): void
{
foreach ($grouped[$parentId] ?? [] as $criterion) {
$items[] = [
'checked' => $criterion->is_checked,
'indent' => $indent,
'text' => $criterion->text,
];
self::flattenItems($items, $grouped, $criterion->id, $indent + 1);
}
}
/**
* @param array<string, mixed> $value
* @return array{