diff --git a/app/Community/Actions/CreateGameClaimAction.php b/app/Community/Actions/CreateGameClaimAction.php index 134a9079fb..a2599692cb 100644 --- a/app/Community/Actions/CreateGameClaimAction.php +++ b/app/Community/Actions/CreateGameClaimAction.php @@ -117,7 +117,7 @@ private function maybeSendClaimWithUnresolvedTicketsAlert(User $currentUser, Gam return; } - $ticketCount = Ticket::forDeveloper($currentUser)->awaitingDeveloper()->count(); + $ticketCount = Ticket::forAssignee($currentUser)->awaitingDeveloper()->count(); if ($ticketCount < 2) { // two or more suggests the developer may be ignoring tickets return; } diff --git a/app/Community/Enums/TicketType.php b/app/Community/Enums/TicketType.php index 686610b537..bb4e74ad25 100644 --- a/app/Community/Enums/TicketType.php +++ b/app/Community/Enums/TicketType.php @@ -4,20 +4,38 @@ namespace App\Community\Enums; +use App\Platform\Enums\TicketableType; use InvalidArgumentException; +use LogicException; use Spatie\TypeScriptTransformer\Attributes\TypeScript; #[TypeScript] enum TicketType: string { - case TriggeredAtWrongTime = 'triggered_at_wrong_time'; + case DidNotCancel = 'did_not_cancel'; + case DidNotStart = 'did_not_start'; + case DidNotSubmit = 'did_not_submit'; case DidNotTrigger = 'did_not_trigger'; + case SubmittedWrongValue = 'submitted_wrong_value'; + case TriggeredAtWrongTime = 'triggered_at_wrong_time'; + + public function appliesTo(TicketableType $type): bool + { + return match ($this) { + self::TriggeredAtWrongTime, self::DidNotTrigger => $type === TicketableType::Achievement, + self::DidNotStart, self::DidNotCancel, self::DidNotSubmit, self::SubmittedWrongValue => $type === TicketableType::Leaderboard, + }; + } public function label(): string { return match ($this) { - self::TriggeredAtWrongTime => 'Triggered at the wrong time', + self::DidNotCancel => 'Did not cancel', + self::DidNotStart => 'Did not start', + self::DidNotSubmit => 'Did not submit', self::DidNotTrigger => 'Did not trigger', + self::SubmittedWrongValue => 'Submitted wrong value', + self::TriggeredAtWrongTime => 'Triggered at the wrong time', }; } @@ -31,6 +49,11 @@ public function toLegacyInteger(): int return match ($this) { self::TriggeredAtWrongTime => 1, self::DidNotTrigger => 2, + + self::DidNotStart, + self::DidNotCancel, + self::DidNotSubmit, + self::SubmittedWrongValue => throw new LogicException("TicketType {$this->value} has no legacy integer mapping."), }; } diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index 9cb64ed6b9..8319c8ff26 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -258,7 +258,7 @@ function GetDeveloperStatsFull(int $count, int $offset = 0, int $sortBy = 0, int } elseif ($sortBy == 4) { // TicketsResolvedForOthers DESC $query = "SELECT ua.id, SUM(!ISNULL(ach.id)) as total FROM users as ua - LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id + LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id AND tick.ticketable_type = 'achievement' LEFT JOIN achievements as ach ON ach.id = tick.ticketable_id AND ach.is_promoted = 1 AND ach.user_id != ua.id WHERE $stateCond GROUP BY ua.id diff --git a/app/Models/Achievement.php b/app/Models/Achievement.php index 6a1e096d19..b20db10ba2 100644 --- a/app/Models/Achievement.php +++ b/app/Models/Achievement.php @@ -9,6 +9,7 @@ use App\Community\Enums\CommentableType; use App\Platform\Contracts\HasPermalink; use App\Platform\Contracts\HasVersionedTrigger; +use App\Platform\Contracts\Ticketable; use App\Platform\Enums\AchievementAuthorTask; use App\Platform\Enums\AchievementSetType; use App\Platform\Enums\AchievementType; @@ -22,6 +23,7 @@ use App\Platform\Events\AchievementTypeChanged; use App\Platform\Events\AchievementUnpromoted; use App\Support\Database\Eloquent\BaseModel; +use Carbon\CarbonInterface; use Database\Factories\AchievementFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -51,7 +53,7 @@ /** * @implements HasVersionedTrigger */ -class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger +class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger, Ticketable { /* * Community Traits @@ -260,6 +262,38 @@ public function shouldBeSearchable(): bool return $this->is_promoted; } + // == ticketable + + public function getTicketableType(): TicketableType + { + return TicketableType::Achievement; + } + + public function getTicketableGame(): Game + { + return $this->game; + } + + public function getTicketableAssignee(?CarbonInterface $at = null): ?User + { + return $this->getMaintainerAt($at ?? now()); + } + + public function getTicketableTitle(): string + { + return $this->title; + } + + public function getTicketableUrl(): string + { + return $this->getCanonicalUrlAttribute(); + } + + public function getTicketableBadgeUrl(): ?string + { + return $this->getBadgeUrlAttribute(); + } + // == helpers public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task, ?Carbon $backdate = null): AchievementAuthor @@ -270,7 +304,7 @@ public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task, ); } - public function getMaintainerAt(Carbon $timestamp): ?User + public function getMaintainerAt(CarbonInterface $timestamp): ?User { $maintainer = $this->maintainers() ->where('effective_from', '<=', $timestamp) diff --git a/app/Models/Leaderboard.php b/app/Models/Leaderboard.php index cc4c6b47c7..d79d30f3a8 100644 --- a/app/Models/Leaderboard.php +++ b/app/Models/Leaderboard.php @@ -8,9 +8,12 @@ use App\Platform\Actions\RecalculateLeaderboardTopEntryAction; use App\Platform\Contracts\HasPermalink; use App\Platform\Contracts\HasVersionedTrigger; +use App\Platform\Contracts\Ticketable; use App\Platform\Enums\LeaderboardState; +use App\Platform\Enums\TicketableType; use App\Platform\Enums\ValueFormat; use App\Support\Database\Eloquent\BaseModel; +use Carbon\CarbonInterface; use Database\Factories\LeaderboardFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -30,7 +33,7 @@ /** * @implements HasVersionedTrigger */ -class Leaderboard extends BaseModel implements HasPermalink, HasVersionedTrigger +class Leaderboard extends BaseModel implements HasPermalink, HasVersionedTrigger, Ticketable { /* * Shared Traits @@ -105,6 +108,39 @@ public function getActivitylogOptions(): LogOptions ->dontSubmitEmptyLogs(); } + // == ticketable + + public function getTicketableType(): TicketableType + { + return TicketableType::Leaderboard; + } + + public function getTicketableGame(): Game + { + return $this->game; + } + + public function getTicketableAssignee(?CarbonInterface $at = null): ?User + { + // leaderboards don't have a "maintainer" concept - the assignee is always the author. + return $this->developer; + } + + public function getTicketableTitle(): string + { + return $this->title; + } + + public function getTicketableUrl(): string + { + return $this->getCanonicalUrlAttribute(); + } + + public function getTicketableBadgeUrl(): ?string + { + return null; + } + // == accessors public function getCanonicalUrlAttribute(): string @@ -245,6 +281,14 @@ public function triggers(): MorphMany ->orderBy('version'); } + /** + * @return MorphMany + */ + public function tickets(): MorphMany + { + return $this->morphMany(Ticket::class, 'ticketable'); + } + // == scopes /** @@ -256,6 +300,24 @@ public function scopeVisible(Builder $query): Builder return $query->where('order_column', '>=', 0); } + /** + * @param Builder $query + * @return Builder + */ + public function scopePromoted(Builder $query): Builder + { + return $query->where('state', '!=', LeaderboardState::Unpromoted->value); + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeUnpromoted(Builder $query): Builder + { + return $query->where('state', LeaderboardState::Unpromoted->value); + } + /** * @param Builder $query * @return Builder diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 7a3e6255fa..74995790dd 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -6,6 +6,8 @@ use App\Community\Enums\TicketState; use App\Community\Enums\TicketType; +use App\Platform\Enums\LeaderboardState; +use App\Platform\Enums\TicketableType; use App\Support\Database\Eloquent\BaseModel; use Database\Factories\TicketFactory; use Illuminate\Database\Eloquent\Builder; @@ -14,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; +use LogicException; class Ticket extends BaseModel { @@ -64,6 +67,9 @@ public function ticketable(): MorphTo } /** + * Unsafe without a `ticketable_type = achievement` filter. + * Use `ticketable()` or pair with `scopeForTicketableType`. + * * @return BelongsTo */ public function achievement(): BelongsTo @@ -71,6 +77,17 @@ public function achievement(): BelongsTo return $this->belongsTo(Achievement::class, 'ticketable_id'); } + /** + * Unsafe without a `ticketable_type = leaderboard` filter. + * Use `ticketable()` or pair with `scopeForTicketableType`. + * + * @return BelongsTo + */ + public function leaderboard(): BelongsTo + { + return $this->belongsTo(Leaderboard::class, 'ticketable_id'); + } + /** * @return BelongsTo */ @@ -158,9 +175,11 @@ public function scopeQuarantined(Builder $query): Builder */ public function scopeForGame(Builder $query, Game $game): Builder { - return $query->whereHas('achievement', function ($query) use ($game) { - $query->where('game_id', $game->id); - }); + return $query->whereHasMorph( + 'ticketable', + [Achievement::class, Leaderboard::class], + fn (Builder $q) => $q->where('game_id', $game->id), + ); } /** @@ -170,37 +189,58 @@ public function scopeForGame(Builder $query, Game $game): Builder public function scopeForAchievement(Builder $query, Achievement $achievement): Builder { return $query->where('ticketable_id', $achievement->id) - ->where('ticketable_type', 'achievement'); + ->where('ticketable_type', TicketableType::Achievement->value); } /** * @param Builder $query * @return Builder */ - public function scopeForDeveloper(Builder $query, User $developer): Builder + public function scopeForAssignee(Builder $query, User $user): Builder { - return $query->where('ticketable_author_id', $developer->id); + return $query->where('ticketable_author_id', $user->id); } /** * @param Builder $query * @return Builder */ - public function scopeOfficialCore(Builder $query): Builder + public function scopeForTicketableType(Builder $query, TicketableType $type): Builder { - return $query->whereHas('achievement', function ($query) { - $query->where('is_promoted', true); - }); + return $query->where('ticketable_type', $type->value); } /** * @param Builder $query * @return Builder */ - public function scopeUnofficial(Builder $query): Builder + public function scopePromoted(Builder $query): Builder { - return $query->whereHas('achievement', function ($query) { - $query->where('is_promoted', false); - }); + return $query->whereHasMorph( + 'ticketable', + [Achievement::class, Leaderboard::class], + fn (Builder $q, string $type) => match ($type) { + Achievement::class => $q->where('is_promoted', true), + Leaderboard::class => $q->where('state', '!=', LeaderboardState::Unpromoted->value), + default => throw new LogicException("Unexpected ticketable type: {$type}"), + }, + ); + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeUnpromoted(Builder $query): Builder + { + return $query->whereHasMorph( + 'ticketable', + [Achievement::class, Leaderboard::class], + fn (Builder $q, string $type) => match ($type) { + Achievement::class => $q->where('is_promoted', false), + Leaderboard::class => $q->where('state', LeaderboardState::Unpromoted->value), + default => throw new LogicException("Unexpected ticketable type: {$type}"), + }, + ); } } diff --git a/app/Platform/Actions/BuildGamePageClaimDataAction.php b/app/Platform/Actions/BuildGamePageClaimDataAction.php index a59bdbbb1c..03ebae72ca 100644 --- a/app/Platform/Actions/BuildGamePageClaimDataAction.php +++ b/app/Platform/Actions/BuildGamePageClaimDataAction.php @@ -73,7 +73,7 @@ public function execute(Game $game, ?User $user, Collection $achievementSetClaim isSoleAuthor: $isSoleAuthor, maxClaimCount: $maxClaimCount, numClaimsRemaining: $this->calculateNumClaimsRemaining($user, $maxClaimCount), - numUnresolvedTickets: Ticket::forDeveloper($user)->awaitingDeveloper()->count(), + numUnresolvedTickets: Ticket::forAssignee($user)->awaitingDeveloper()->count(), wouldBeCollaboration: $wouldBeCollaboration, wouldBeRevision: $wouldBeRevision, ); diff --git a/app/Platform/Actions/BuildGameShowPagePropsAction.php b/app/Platform/Actions/BuildGameShowPagePropsAction.php index ba405ec868..f932b97c54 100644 --- a/app/Platform/Actions/BuildGameShowPagePropsAction.php +++ b/app/Platform/Actions/BuildGameShowPagePropsAction.php @@ -352,8 +352,8 @@ public function execute( numLeaderboards: $this->gameLeaderboardService->getCount($backingGame, $isPromoted), numMasters: $numMasters, numOpenTickets: $isPromoted - ? Ticket::forGame($backingGame)->open()->officialCore()->count() - : Ticket::forGame($backingGame)->open()->unofficial()->count(), + ? Ticket::forGame($backingGame)->open()->promoted()->count() + : Ticket::forGame($backingGame)->open()->unpromoted()->count(), numScreenshots: $game->gameScreenshots()->approved()->count(), screenshots: Lazy::inertiaDeferred(fn () => $game->gameScreenshots() diff --git a/app/Platform/Contracts/Ticketable.php b/app/Platform/Contracts/Ticketable.php new file mode 100644 index 0000000000..b15eb96f6d --- /dev/null +++ b/app/Platform/Contracts/Ticketable.php @@ -0,0 +1,25 @@ + 'required|string|in:achievement', // TODO or in:leaderboard 'ticketableId' => 'required|integer|exists:achievements,id', // TODO could also be a leaderboard id 'mode' => 'required|string|in:hardcore,softcore', - 'issue' => ['required', new Enum(TicketType::class)], + // TODO expand or compute this list via TicketType::appliesTo() + 'issue' => ['required', (new Enum(TicketType::class))->only([ + TicketType::TriggeredAtWrongTime, + TicketType::DidNotTrigger, + ])], 'description' => 'required|string|max:2000', 'emulator' => 'required|string', 'emulatorVersion' => 'nullable|string', diff --git a/app/Platform/Services/TicketListService.php b/app/Platform/Services/TicketListService.php index c656a67176..17f279b55f 100644 --- a/app/Platform/Services/TicketListService.php +++ b/app/Platform/Services/TicketListService.php @@ -8,6 +8,7 @@ use App\Enums\Permissions; use App\Models\Emulator; use App\Models\Ticket; +use App\Platform\Enums\TicketableType; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; @@ -217,11 +218,11 @@ public function buildQuery(array $filterOptions, ?Builder $tickets = null): Buil switch ($filterOptions['achievement']) { case 'core': - $tickets->officialCore(); + $tickets->forTicketableType(TicketableType::Achievement)->promoted(); break; case 'unofficial': - $tickets->unofficial(); + $tickets->forTicketableType(TicketableType::Achievement)->unpromoted(); break; } diff --git a/database/factories/TicketFactory.php b/database/factories/TicketFactory.php index e5ed7523bf..f878fa8329 100644 --- a/database/factories/TicketFactory.php +++ b/database/factories/TicketFactory.php @@ -7,6 +7,7 @@ use App\Community\Enums\TicketState; use App\Community\Enums\TicketType; use App\Models\Ticket; +use App\Platform\Enums\TicketableType; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; @@ -20,7 +21,7 @@ class TicketFactory extends Factory public function definition(): array { return [ - 'ticketable_type' => 'achievement', + 'ticketable_type' => TicketableType::Achievement->value, 'body' => $this->faker->sentence(), 'type' => TicketType::DidNotTrigger, 'state' => TicketState::Open, diff --git a/public/API/API_GetTicketData.php b/public/API/API_GetTicketData.php index 6a4847aa05..325930ae34 100644 --- a/public/API/API_GetTicketData.php +++ b/public/API/API_GetTicketData.php @@ -152,6 +152,7 @@ use App\Models\Game; use App\Models\Ticket; use App\Models\User; +use App\Platform\Enums\TicketableType; use Illuminate\Database\Eloquent\Builder; $count = min((int) request()->query('c', '10'), 100); @@ -285,11 +286,11 @@ if ($gameIDGiven > 0) { $game = Game::where('id', $gameIDGiven)->with('system')->first(); if ($game) { - $tickets = Ticket::forGame($game); + $tickets = Ticket::forGame($game)->forTicketableType(TicketableType::Achievement); if ($gamesTableFlag === Achievement::FLAG_UNPROMOTED) { - $tickets->unofficial(); + $tickets->unpromoted(); } else { - $tickets->officialCore(); + $tickets->promoted(); } $ticketData['GameID'] = $game->id; @@ -327,7 +328,7 @@ } // getting the 10 most recent tickets -$tickets = Ticket::officialCore()->open(); +$tickets = Ticket::forTicketableType(TicketableType::Achievement)->promoted()->open(); $ticketData['OpenTickets'] = $tickets->count(); $ticketData['URL'] = route('tickets.index'); $ticketData['RecentTickets'] = $getTicketsInfo($tickets, $offset, $count); diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index e68b0bba26..da5adeca0b 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -202,7 +202,7 @@ export type ModerationReportableType = 'Comment' | 'DirectMessage' | 'ForumTopic export type NewsCategory = 'achievement-set' | 'community' | 'events' | 'guide' | 'media' | 'site-release-notes' | 'technical'; export type SubscriptionSubjectType = 'ForumTopic' | 'UserWall' | 'GameWall' | 'Achievement' | 'Leaderboard' | 'GameTickets' | 'GameAchievements' | 'AchievementTicket' | 'GameScreenshotDecision'; export type TicketState = 'closed' | 'open' | 'resolved' | 'request' | 'quarantined'; -export type TicketType = 'triggered_at_wrong_time' | 'did_not_trigger'; +export type TicketType = 'did_not_cancel' | 'did_not_start' | 'did_not_submit' | 'did_not_trigger' | 'submitted_wrong_value' | 'triggered_at_wrong_time'; export type TrendingReason = 'new-set' | 'revised-set' | 'gaining-traction' | 'renewed-interest' | 'many-more-players' | 'more-players'; export type UserGameListType = 'achievement_set_request' | 'play' | 'develop'; export type UserRelationStatus = 'blocked' | 'not_following' | 'following'; diff --git a/resources/views/components/game/link-buttons/index.blade.php b/resources/views/components/game/link-buttons/index.blade.php index 48d5b7a2c7..2535e15f5b 100644 --- a/resources/views/components/game/link-buttons/index.blade.php +++ b/resources/views/components/game/link-buttons/index.blade.php @@ -27,9 +27,9 @@ if ($canSeeOpenTickets) { $gameTickets = Ticket::forGame($game)->open(); if ($isViewingOfficial) { - $gameTickets->officialCore(); + $gameTickets->promoted(); } else { - $gameTickets->unofficial(); + $gameTickets->unpromoted(); } $numOpenTickets = $gameTickets->count(); } diff --git a/resources/views/livewire/administrative-tools/most-reported-games-table.blade.php b/resources/views/livewire/administrative-tools/most-reported-games-table.blade.php index 8e29774d89..e7e8b116c8 100644 --- a/resources/views/livewire/administrative-tools/most-reported-games-table.blade.php +++ b/resources/views/livewire/administrative-tools/most-reported-games-table.blade.php @@ -2,6 +2,7 @@ use App\Models\Role; use App\Models\Ticket; +use App\Platform\Enums\TicketableType; use Filament\Actions\Concerns\InteractsWithActions; use Filament\Actions\Contracts\HasActions; use Filament\Forms\Concerns\InteractsWithForms; @@ -72,18 +73,21 @@ public function table(Table $table): Table private function buildMostTicketedSetsQuery(): Builder { $oldestTicketSubquery = Ticket::open() - ->officialCore() + ->forTicketableType(TicketableType::Achievement) + ->promoted() ->select('ticketable_id', DB::raw('MIN(created_at) as OldestTicketDate')) ->groupBy('ticketable_id'); $newestTicketSubquery = Ticket::open() - ->officialCore() + ->forTicketableType(TicketableType::Achievement) + ->promoted() ->select('ticketable_id', DB::raw('MAX(created_at) as NewestTicketDate')) ->groupBy('ticketable_id'); return ( Ticket::open() - ->officialCore() + ->forTicketableType(TicketableType::Achievement) + ->promoted() ->join('achievements', 'achievements.id', '=', 'tickets.ticketable_id') ->join('games', 'games.id', '=', 'achievements.game_id') ->join('systems', 'systems.id', '=', 'games.system_id') diff --git a/resources/views/pages/user/[User]/tickets/index.blade.php b/resources/views/pages/user/[User]/tickets/index.blade.php index 9a145ce2db..00f777a9e3 100644 --- a/resources/views/pages/user/[User]/tickets/index.blade.php +++ b/resources/views/pages/user/[User]/tickets/index.blade.php @@ -16,7 +16,7 @@ $ticketListService->perPage = 50; $selectFilters = $ticketListService->getSelectFilters(showDevType: false); $filterOptions = $ticketListService->getFilterOptions(request()); - $tickets = $ticketListService->getTickets($filterOptions, Ticket::forDeveloper($user)); + $tickets = $ticketListService->getTickets($filterOptions, Ticket::forAssignee($user)); return $view->with([ 'user' => $user, diff --git a/tests/Feature/Platform/Controllers/GameControllerShowTest.php b/tests/Feature/Platform/Controllers/GameControllerShowTest.php index 4e94246d61..3296a1e127 100644 --- a/tests/Feature/Platform/Controllers/GameControllerShowTest.php +++ b/tests/Feature/Platform/Controllers/GameControllerShowTest.php @@ -39,6 +39,7 @@ use App\Platform\Enums\GameSetRolePermission; use App\Platform\Enums\GameSetType; use App\Platform\Enums\LeaderboardState; +use App\Platform\Enums\TicketableType; use App\Platform\Services\EventHubIdCacheService; use Database\Seeders\RolesTableSeeder; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; @@ -1803,6 +1804,52 @@ function createLeaderboardWithEntries( ->where('numOpenTickets', 1) ); }); + + it('sums open ticket counts across achievement and leaderboard tickets for the game', function () { + // ARRANGE + $system = System::factory()->create(); + $developer = User::factory()->create(); + $game = Game::factory()->create(['system_id' => $system->id]); + + $achievement = Achievement::factory()->promoted()->create([ + 'game_id' => $game->id, + 'user_id' => $developer->id, + ]); + $leaderboard = Leaderboard::factory()->create([ + 'game_id' => $game->id, + 'author_id' => $developer->id, + 'state' => LeaderboardState::Active, + ]); + + (new UpsertGameCoreAchievementSetFromLegacyFlagsAction())->execute($game); + + $reporter = User::factory()->create(); + + // ... an open achievement ticket ... + Ticket::factory()->create([ + 'ticketable_id' => $achievement->id, + 'reporter_id' => $reporter->id, + 'ticketable_author_id' => $developer->id, + 'state' => TicketState::Open, + ]); + + // ... and an open leaderboard ticket on the same game ... + Ticket::factory()->create([ + 'ticketable_type' => TicketableType::Leaderboard->value, + 'ticketable_id' => $leaderboard->id, + 'ticketable_author_id' => $developer->id, + 'reporter_id' => $reporter->id, + 'state' => TicketState::Open, + ]); + + // ACT + $response = get(route('game.show', ['game' => $game])); + + // ASSERT + $response->assertInertia(fn (Assert $page) => $page + ->where('numOpenTickets', 2) + ); + }); }); describe('Completion Stats Props', function () {