diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php index b4c2385a..12120e42 100644 --- a/app/Actions/Fortify/PasswordValidationRules.php +++ b/app/Actions/Fortify/PasswordValidationRules.php @@ -2,6 +2,7 @@ namespace App\Actions\Fortify; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Validation\Rules\Password; trait PasswordValidationRules @@ -9,7 +10,7 @@ trait PasswordValidationRules /** * Get the validation rules used to validate passwords. * - * @return array|string> + * @return array|string> */ protected function passwordRules(): array { diff --git a/app/Agents/Logic/HeuristicLogic.php b/app/Agents/Logic/HeuristicLogic.php index 261da1d6..7c23339f 100644 --- a/app/Agents/Logic/HeuristicLogic.php +++ b/app/Agents/Logic/HeuristicLogic.php @@ -2,6 +2,9 @@ namespace App\Agents\Logic; +use App\Games\Checkers\CheckersLogic; +use App\Games\Hearts\HeartsLogic; +use App\Games\ValidateFour\ValidateFourLogic; use App\Interfaces\AgentContract; use App\Models\Games\Game; use Illuminate\Support\Facades\Log; @@ -124,11 +127,11 @@ protected function getGameLogic(Game $game): object return match ($gameTitle) { // @phpstan-ignore class.notFound - 'checkers' => app(\App\Games\Checkers\CheckersLogic::class), + 'checkers' => app(CheckersLogic::class), // @phpstan-ignore class.notFound - 'hearts' => app(\App\Games\Hearts\HeartsLogic::class), + 'hearts' => app(HeartsLogic::class), // @phpstan-ignore class.notFound, match.alwaysFalse - 'validatefour' => app(\App\Games\ValidateFour\ValidateFourLogic::class), + 'validatefour' => app(ValidateFourLogic::class), default => throw new \Exception("Unsupported game type: {$gameTitle}"), }; } diff --git a/app/Agents/Logic/MinimaxLogic.php b/app/Agents/Logic/MinimaxLogic.php index da1c6e5b..e319fe86 100644 --- a/app/Agents/Logic/MinimaxLogic.php +++ b/app/Agents/Logic/MinimaxLogic.php @@ -2,6 +2,9 @@ namespace App\Agents\Logic; +use App\Games\Checkers\CheckersLogic; +use App\Games\Hearts\HeartsLogic; +use App\Games\ValidateFour\ValidateFourLogic; use App\Interfaces\AgentContract; use App\Models\Games\Game; use Illuminate\Support\Facades\Log; @@ -163,11 +166,11 @@ protected function getGameLogic(Game $game): object return match ($gameTitle) { // @phpstan-ignore class.notFound - 'checkers' => app(\App\Games\Checkers\CheckersLogic::class), + 'checkers' => app(CheckersLogic::class), // @phpstan-ignore class.notFound - 'hearts' => app(\App\Games\Hearts\HeartsLogic::class), + 'hearts' => app(HeartsLogic::class), // @phpstan-ignore class.notFound, match.alwaysFalse - 'validatefour' => app(\App\Games\ValidateFour\ValidateFourLogic::class), + 'validatefour' => app(ValidateFourLogic::class), default => throw new \Exception("Unsupported game type: {$gameTitle}"), }; } diff --git a/app/Agents/Logic/RandomLogic.php b/app/Agents/Logic/RandomLogic.php index db3952ae..fae80f32 100644 --- a/app/Agents/Logic/RandomLogic.php +++ b/app/Agents/Logic/RandomLogic.php @@ -2,6 +2,9 @@ namespace App\Agents\Logic; +use App\Games\Checkers\CheckersLogic; +use App\Games\Hearts\HeartsLogic; +use App\Games\ValidateFour\ValidateFourLogic; use App\Interfaces\AgentContract; use App\Models\Games\Game; use Illuminate\Support\Facades\Log; @@ -65,11 +68,11 @@ protected function getGameLogic(Game $game): object return match ($gameTitle) { // @phpstan-ignore class.notFound - 'checkers' => app(\App\Games\Checkers\CheckersLogic::class), + 'checkers' => app(CheckersLogic::class), // @phpstan-ignore class.notFound - 'hearts' => app(\App\Games\Hearts\HeartsLogic::class), + 'hearts' => app(HeartsLogic::class), // @phpstan-ignore class.notFound, match.alwaysFalse - 'validatefour' => app(\App\Games\ValidateFour\ValidateFourLogic::class), + 'validatefour' => app(ValidateFourLogic::class), default => throw new \Exception("Unsupported game type: {$gameTitle}"), }; } diff --git a/app/GameEngine/GameEngine.php b/app/GameEngine/GameEngine.php index cd147940..10b3e182 100644 --- a/app/GameEngine/GameEngine.php +++ b/app/GameEngine/GameEngine.php @@ -136,7 +136,8 @@ public function processPlayerAction( $game, $action, $mode, - $gameState + $gameState, + $player ); // Record the successful action @@ -155,6 +156,10 @@ public function processPlayerAction( $gameState = $coordinationResult->updatedGameState; $game->game_state = $gameState->toArray(); $game->save(); + + // Mark the current action as completed + $actionRecord->coordination_completed_at = now(); + $actionRecord->save(); } // ── 5. GAME LIFECYCLE PROGRESSION ─────────────────────────── diff --git a/app/GameEngine/Interfaces/GameTitleContract.php b/app/GameEngine/Interfaces/GameTitleContract.php index 0286fdd4..8d494ba5 100644 --- a/app/GameEngine/Interfaces/GameTitleContract.php +++ b/app/GameEngine/Interfaces/GameTitleContract.php @@ -15,6 +15,7 @@ use App\Exceptions\Game\TurnTimerExpiredException; use App\GameEngine\GameOutcome; use App\GameEngine\ValidationResult; +use App\Games\ValidateFour\BaseValidateFourMode; use App\Models\Auth\User; use App\Models\Games\Game; use Carbon\Carbon; @@ -55,7 +56,7 @@ * } * ``` * - * @see \App\Games\ValidateFour\BaseValidateFourMode For Connect Four implementation + * @see BaseValidateFourMode For Connect Four implementation */ interface GameTitleContract { diff --git a/app/GameEngine/Lifecycle/Progression/CoordinatedActionProcessor.php b/app/GameEngine/Lifecycle/Progression/CoordinatedActionProcessor.php index 89683704..46f63ad8 100644 --- a/app/GameEngine/Lifecycle/Progression/CoordinatedActionProcessor.php +++ b/app/GameEngine/Lifecycle/Progression/CoordinatedActionProcessor.php @@ -5,9 +5,11 @@ namespace App\GameEngine\Lifecycle\Progression; use App\DataTransferObjects\Games\CoordinatedActionResult; +use App\Enums\ActionType; use App\GameEngine\Interfaces\GameActionContract; use App\Models\Games\Action; use App\Models\Games\Game; +use App\Models\Games\Player; /** * Handles coordinated actions where multiple players must submit actions @@ -22,9 +24,10 @@ class CoordinatedActionProcessor * @param GameActionContract $action The action being processed * @param mixed $mode The game mode handler * @param object $gameState The current game state + * @param Player $player The current player * @return CoordinatedActionResult Result indicating if coordination is needed and status */ - public function process(Game $game, GameActionContract $action, mixed $mode, object $gameState): CoordinatedActionResult + public function process(Game $game, GameActionContract $action, mixed $mode, object $gameState, Player $player): CoordinatedActionResult { // Check if this action requires coordination if (! $this->isCoordinatedAction($action)) { @@ -35,11 +38,19 @@ public function process(Game $game, GameActionContract $action, mixed $mode, obj $coordinationSequence = $this->getSequenceNumber($game, $coordinationGroup); $requiredCount = $this->getRequiredPlayerCount($game, $action, $gameState); - // Check if coordination is complete + // Check if coordination is complete (counting existing actions + current action) $completedCount = $this->getCompletedCount($game, $coordinationGroup); - if ($completedCount >= $requiredCount) { - return $this->completeCoordination($game, $mode, $gameState, $coordinationGroup, $coordinationSequence); + if ($completedCount + 1 >= $requiredCount) { + return $this->completeCoordination( + $game, + $mode, + $gameState, + $coordinationGroup, + $coordinationSequence, + $action, + $player + ); } return CoordinatedActionResult::coordinated( @@ -123,24 +134,38 @@ protected function completeCoordination( mixed $mode, object $gameState, string $coordinationGroup, - int $coordinationSequence + int $coordinationSequence, + GameActionContract $action, + Player $player ): CoordinatedActionResult { - // Retrieve all coordinated actions + // Retrieve all previously coordinated actions $coordinatedActions = Action::where('game_id', $game->id) ->withCoordinationGroup($coordinationGroup) ->pendingCoordination() ->with('player') ->get(); - // Process the coordinated action through the game mode + // Add the current action to the list (as a transient Action model) + $currentActionModel = new Action; + // Since we can't easily hydrate it completely without saving, we'll set what's needed for coordination + $currentActionModel->setRelation('player', $player); + $currentActionModel->action_details = $action->toArray(); + $currentActionModel->action_type = ActionType::from($action->getType()); + + $coordinatedActions->push($currentActionModel); + + // Process the coordinated actions through the game mode $updatedGameState = $this->processCoordinatedActions($mode, $gameState, $coordinatedActions); - // Mark all actions as completed + // Mark all existing actions as completed Action::where('game_id', $game->id) ->withCoordinationGroup($coordinationGroup) ->pendingCoordination() ->update(['coordination_completed_at' => now()]); + // Note: The current action is not yet in the DB, so it won't be updated here. + // The calling component is responsible for saving the current action with proper status. + return CoordinatedActionResult::coordinated( coordinationGroup: $coordinationGroup, coordinationSequence: $coordinationSequence, diff --git a/app/GameEngine/ModeRegistry.php b/app/GameEngine/ModeRegistry.php index f672b88e..47f51d2b 100644 --- a/app/GameEngine/ModeRegistry.php +++ b/app/GameEngine/ModeRegistry.php @@ -6,6 +6,11 @@ use App\Exceptions\GameModeNotFoundException; use App\GameTitles\BaseGameTitle; +use App\GameTitles\Checkers\Modes\StandardMode; +use App\GameTitles\ConnectFour\Modes\EightBySevenMode; +use App\GameTitles\ConnectFour\Modes\FiveMode; +use App\GameTitles\ConnectFour\Modes\NineBySixMode; +use App\GameTitles\ConnectFour\Modes\PopOutMode; use App\Models\Games\Game; /** @@ -22,17 +27,17 @@ class ModeRegistry */ private array $modes = [ 'checkers' => [ - 'standard' => \App\GameTitles\Checkers\Modes\StandardMode::class, + 'standard' => StandardMode::class, ], 'hearts' => [ 'standard' => \App\GameTitles\Hearts\Modes\StandardMode::class, ], 'connect-four' => [ 'standard' => \App\GameTitles\ConnectFour\Modes\StandardMode::class, - 'pop-out' => \App\GameTitles\ConnectFour\Modes\PopOutMode::class, - 'five' => \App\GameTitles\ConnectFour\Modes\FiveMode::class, - 'eight-by-seven' => \App\GameTitles\ConnectFour\Modes\EightBySevenMode::class, - 'nine-by-six' => \App\GameTitles\ConnectFour\Modes\NineBySixMode::class, + 'pop-out' => PopOutMode::class, + 'five' => FiveMode::class, + 'eight-by-seven' => EightBySevenMode::class, + 'nine-by-six' => NineBySixMode::class, ], ]; diff --git a/app/GameTitles/ConnectFour/ConnectFourBoard.php b/app/GameTitles/ConnectFour/ConnectFourBoard.php index 7e552e3a..f089d32c 100644 --- a/app/GameTitles/ConnectFour/ConnectFourBoard.php +++ b/app/GameTitles/ConnectFour/ConnectFourBoard.php @@ -7,6 +7,7 @@ use App\Enums\GamePhase; use App\Enums\GameStatus; use App\GameTitles\BaseGameState; +use App\Interfaces\GameTitleContract; /** * Immutable game state for Connect Four (Connect Four variant). @@ -59,7 +60,7 @@ * $gameState = ConnectFourConnectFourBoard::fromArray($game->game_state); * ``` * - * @see \App\Interfaces\GameTitleContract For the interface all game modes must implement + * @see GameTitleContract For the interface all game modes must implement */ final class ConnectFourBoard extends BaseGameState { diff --git a/app/Http/Controllers/Api/V1/Account/ProgressionController.php b/app/Http/Controllers/Api/V1/Account/ProgressionController.php index 5d1efbe1..8b9f01d9 100644 --- a/app/Http/Controllers/Api/V1/Account/ProgressionController.php +++ b/app/Http/Controllers/Api/V1/Account/ProgressionController.php @@ -7,6 +7,7 @@ use App\Http\Resources\Account\ProgressionResource; use App\Http\Traits\ApiResponses; use App\Models\Gamification\UserTitleLevel; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -27,7 +28,7 @@ public function __invoke(Request $request): JsonResponse { $user = $request->user(); - /** @var \Illuminate\Database\Eloquent\Collection $levelCollection */ + /** @var Collection $levelCollection */ $levelCollection = $user->titleLevels() ->orderByDesc('last_played_at') ->get(); diff --git a/app/Http/Controllers/Api/V1/Auth/RegisterController.php b/app/Http/Controllers/Api/V1/Auth/RegisterController.php index 2f9fb771..e31cfa15 100644 --- a/app/Http/Controllers/Api/V1/Auth/RegisterController.php +++ b/app/Http/Controllers/Api/V1/Auth/RegisterController.php @@ -32,7 +32,7 @@ public function __invoke(RegisterRequest $request): JsonResponse return response()->json([ 'message' => 'Registration successful. Please check your email to verify your account.', - 'registration_id' => $registration->id, + 'registration_id' => $registration->uuid, ], 201); } } diff --git a/app/Http/Controllers/Api/V1/Auth/SocialAuthController.php b/app/Http/Controllers/Api/V1/Auth/SocialAuthController.php index 3db4aea0..ad63452d 100644 --- a/app/Http/Controllers/Api/V1/Auth/SocialAuthController.php +++ b/app/Http/Controllers/Api/V1/Auth/SocialAuthController.php @@ -8,6 +8,7 @@ use App\Services\Auth\AuthService; use Illuminate\Http\JsonResponse; use Laravel\Socialite\Facades\Socialite; +use Laravel\Socialite\Two\AbstractProvider; class SocialAuthController extends Controller { @@ -24,9 +25,10 @@ public function __invoke(SocialLoginRequest $request): JsonResponse { try { // Verify token with provider - /** @phpstan-ignore-next-line */ - $providerUser = Socialite::driver($request->provider) - ->userFromToken($request->access_token); + $driver = Socialite::driver($request->provider); + + /** @var AbstractProvider $driver */ + $providerUser = $driver->userFromToken($request->access_token); // Find or create user and social account $result = $this->authService->handleSocialLogin( diff --git a/app/Http/Controllers/Api/V1/Competitions/StandingsController.php b/app/Http/Controllers/Api/V1/Competitions/StandingsController.php index affd2c1f..3a3e78f2 100644 --- a/app/Http/Controllers/Api/V1/Competitions/StandingsController.php +++ b/app/Http/Controllers/Api/V1/Competitions/StandingsController.php @@ -7,6 +7,7 @@ use App\Http\Traits\ApiResponses; use App\Models\Auth\User; use App\Models\Competitions\Tournament; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -21,7 +22,7 @@ public function show(Request $request, Tournament $tournament): JsonResponse { $tournament->load(['users.avatar.image']); - /** @var \Illuminate\Database\Eloquent\Collection $users */ + /** @var Collection $users */ $users = $tournament->users() ->orderBy('tournament_user.placement', 'asc') ->orderBy('tournament_user.earnings', 'desc') diff --git a/app/Http/Controllers/Api/V1/System/ConfigController.php b/app/Http/Controllers/Api/V1/System/ConfigController.php index c227d54e..21d58b3b 100644 --- a/app/Http/Controllers/Api/V1/System/ConfigController.php +++ b/app/Http/Controllers/Api/V1/System/ConfigController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Api\V1\System; +use App\Enums\GameTitle; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; @@ -24,7 +25,7 @@ public function __invoke(): JsonResponse 'leaderboards' => true, 'sse_feeds' => true, ], - 'supported_games' => collect(\App\Enums\GameTitle::cases())->map(fn ($title) => [ + 'supported_games' => collect(GameTitle::cases())->map(fn ($title) => [ 'key' => $title->value, 'name' => $title->label(), 'min_players' => $title->minPlayers(), diff --git a/app/Http/Controllers/Api/V1/System/FeedbackController.php b/app/Http/Controllers/Api/V1/System/FeedbackController.php index 042fcdd4..598eccc5 100644 --- a/app/Http/Controllers/Api/V1/System/FeedbackController.php +++ b/app/Http/Controllers/Api/V1/System/FeedbackController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Resources\System\FeedbackResource; use App\Models\System\Feedback; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; @@ -13,7 +14,7 @@ class FeedbackController extends Controller { /** - * @return \Illuminate\Http\JsonResponse + * @return JsonResponse */ public function __invoke(Request $request) { diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php new file mode 100644 index 00000000..19c45813 --- /dev/null +++ b/app/Http/Controllers/ApplicationController.php @@ -0,0 +1,50 @@ +errors()->has('_permission_denied')) { - throw new \Illuminate\Validation\ValidationException( + throw new ValidationException( $validator, response()->json([ 'message' => 'You do not have permission to update your username.', diff --git a/app/Http/Requests/Games/ForfeitGameRequest.php b/app/Http/Requests/Games/ForfeitGameRequest.php index fc168226..09a1ce88 100644 --- a/app/Http/Requests/Games/ForfeitGameRequest.php +++ b/app/Http/Requests/Games/ForfeitGameRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\Games; +use Illuminate\Contracts\Validation\ValidationRule; + class ForfeitGameRequest extends BaseGameRequest { /** @@ -15,7 +17,7 @@ public function authorize(): bool /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { diff --git a/app/Http/Requests/Games/RequestRematchRequest.php b/app/Http/Requests/Games/RequestRematchRequest.php index e8484712..7b9383bc 100644 --- a/app/Http/Requests/Games/RequestRematchRequest.php +++ b/app/Http/Requests/Games/RequestRematchRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Games; use App\Enums\GameStatus; +use Illuminate\Contracts\Validation\ValidationRule; class RequestRematchRequest extends BaseGameRequest { @@ -23,7 +24,7 @@ public function authorize(): bool /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { diff --git a/app/Http/Requests/Matchmaking/CancelLobbyRequest.php b/app/Http/Requests/Matchmaking/CancelLobbyRequest.php index 8611e82d..5e8e9a5a 100644 --- a/app/Http/Requests/Matchmaking/CancelLobbyRequest.php +++ b/app/Http/Requests/Matchmaking/CancelLobbyRequest.php @@ -4,6 +4,7 @@ use App\Matchmaking\Enums\LobbyStatus; use App\Models\Matchmaking\Lobby; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; class CancelLobbyRequest extends FormRequest @@ -35,7 +36,7 @@ public function authorize(): bool /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { diff --git a/app/Http/Requests/Matchmaking/InitiateReadyCheckRequest.php b/app/Http/Requests/Matchmaking/InitiateReadyCheckRequest.php index 80733536..a5b2fd57 100644 --- a/app/Http/Requests/Matchmaking/InitiateReadyCheckRequest.php +++ b/app/Http/Requests/Matchmaking/InitiateReadyCheckRequest.php @@ -4,6 +4,7 @@ use App\Matchmaking\Enums\LobbyStatus; use App\Models\Matchmaking\Lobby; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; class InitiateReadyCheckRequest extends FormRequest @@ -35,7 +36,7 @@ public function authorize(): bool /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { diff --git a/app/Http/Requests/StoreApplicationRequest.php b/app/Http/Requests/StoreApplicationRequest.php new file mode 100644 index 00000000..25c0097e --- /dev/null +++ b/app/Http/Requests/StoreApplicationRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/StoreWorkflowRequest.php b/app/Http/Requests/StoreWorkflowRequest.php new file mode 100644 index 00000000..bc7be4b1 --- /dev/null +++ b/app/Http/Requests/StoreWorkflowRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/StoreWorkflowStepRequest.php b/app/Http/Requests/StoreWorkflowStepRequest.php new file mode 100644 index 00000000..4b33a30e --- /dev/null +++ b/app/Http/Requests/StoreWorkflowStepRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/UpdateApplicationRequest.php b/app/Http/Requests/UpdateApplicationRequest.php new file mode 100644 index 00000000..eb52534b --- /dev/null +++ b/app/Http/Requests/UpdateApplicationRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/UpdateWorkflowRequest.php b/app/Http/Requests/UpdateWorkflowRequest.php new file mode 100644 index 00000000..ed12362f --- /dev/null +++ b/app/Http/Requests/UpdateWorkflowRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/UpdateWorkflowStepRequest.php b/app/Http/Requests/UpdateWorkflowStepRequest.php new file mode 100644 index 00000000..7b1811fa --- /dev/null +++ b/app/Http/Requests/UpdateWorkflowStepRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Http/Traits/ApiResponses.php b/app/Http/Traits/ApiResponses.php index f3c45bc8..4730d1b3 100644 --- a/app/Http/Traits/ApiResponses.php +++ b/app/Http/Traits/ApiResponses.php @@ -2,6 +2,8 @@ namespace App\Http\Traits; +use App\Exceptions\InvalidActionDataException; +use App\Exceptions\RematchNotAvailableException; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\JsonResource; @@ -224,10 +226,10 @@ protected function handleServiceCall( ): mixed { try { return $callback(); - } catch (\App\Exceptions\RematchNotAvailableException $e) { + } catch (RematchNotAvailableException $e) { // Re-throw custom exceptions to let the exception handler deal with them throw $e; - } catch (\App\Exceptions\InvalidActionDataException $e) { + } catch (InvalidActionDataException $e) { // Re-throw action data validation exceptions throw $e; } catch (\Exception $e) { diff --git a/app/Jobs/CalculateAgentAction.php b/app/Jobs/CalculateAgentAction.php index e231f824..76536365 100644 --- a/app/Jobs/CalculateAgentAction.php +++ b/app/Jobs/CalculateAgentAction.php @@ -4,6 +4,7 @@ use App\Agents\Orchestrators\AgentService; use App\Agents\Scheduling\AgentSchedulingService; +use App\Games\Checkers\Actions\ActionHandler; use App\Models\Auth\Agent; use App\Models\Auth\User; use App\Models\Games\Game; @@ -158,7 +159,7 @@ protected function applyAction(object $action): void $actionHandler = match ($gameTitle) { // @phpstan-ignore class.notFound - 'checkers' => app(\App\Games\Checkers\Actions\ActionHandler::class), + 'checkers' => app(ActionHandler::class), // @phpstan-ignore class.notFound 'hearts' => app(\App\Games\Hearts\Actions\ActionHandler::class), // @phpstan-ignore class.notFound, match.alwaysFalse diff --git a/app/Matchmaking/Lobby/LobbyValidator.php b/app/Matchmaking/Lobby/LobbyValidator.php index f5a1ecf2..c6d5c9ae 100644 --- a/app/Matchmaking/Lobby/LobbyValidator.php +++ b/app/Matchmaking/Lobby/LobbyValidator.php @@ -2,6 +2,7 @@ namespace App\Matchmaking\Lobby; +use App\Enums\GameTitle; use App\Exceptions\InvalidGameConfigurationException; use App\Exceptions\LobbyStateException; use App\Exceptions\PlayerBusyException; @@ -111,7 +112,7 @@ public function validateGameTitle(?object $gameTitle, string $slug): void throw new InvalidGameConfigurationException( "Game title '{$slug}' is not supported", $slug, - ['available_titles' => array_column(\App\Enums\GameTitle::cases(), 'value')] + ['available_titles' => array_column(GameTitle::cases(), 'value')] ); } } diff --git a/app/Matchmaking/Proposals/RematchHandler.php b/app/Matchmaking/Proposals/RematchHandler.php index 1c27f026..cf9546ba 100644 --- a/app/Matchmaking/Proposals/RematchHandler.php +++ b/app/Matchmaking/Proposals/RematchHandler.php @@ -6,6 +6,7 @@ use App\Enums\GameStatus; use App\Exceptions\RematchNotAvailableException; +use App\GameEngine\Player\PlayerActivityManager; use App\Jobs\AgentAutoAcceptRematch; use App\Matchmaking\Enums\ProposalStatus; use App\Matchmaking\Enums\ProposalType; @@ -198,7 +199,7 @@ private function scheduleAgentAutoAccept(Proposal $proposal, User $agentUser, Us // Schedule delayed auto-accept (1-7 seconds random) $delay = rand(1, 7); - dispatch(new AgentAutoAcceptRematch($proposal->ulid, $agentUser->id, app(\App\GameEngine\Player\PlayerActivityManager::class))) + dispatch(new AgentAutoAcceptRematch($proposal->ulid, $agentUser->id, app(PlayerActivityManager::class))) ->delay(now()->addSeconds($delay)); Log::info('Scheduled agent auto-accept', [ diff --git a/app/Models/Access/Client.php b/app/Models/Access/Client.php index 7d99665f..e065db4d 100644 --- a/app/Models/Access/Client.php +++ b/app/Models/Access/Client.php @@ -5,6 +5,7 @@ use App\Enums\Platform; use App\Models\Auth\Entry; use App\Models\Auth\User; +use Database\Factories\Access\ClientFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -12,7 +13,7 @@ class Client extends Model { - /** @use HasFactory<\Database\Factories\Access\ClientFactory> */ + /** @use HasFactory */ use HasCreator, HasFactory; protected $fillable = [ diff --git a/app/Models/Account/Alert.php b/app/Models/Account/Alert.php index 1184560a..7b0f32e6 100644 --- a/app/Models/Account/Alert.php +++ b/app/Models/Account/Alert.php @@ -3,25 +3,27 @@ namespace App\Models\Account; use App\Models\Auth\User; +use Database\Factories\Account\AlertFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * @property string $id * @property int $user_id * @property string $type * @property array $data - * @property \Illuminate\Support\Carbon|null $read_at - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon|null $read_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property User $user */ class Alert extends Model { - /** @use HasFactory<\Database\Factories\Account\AlertFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids; protected $fillable = [ diff --git a/app/Models/Auth/Agent.php b/app/Models/Auth/Agent.php index eae56f80..0201d510 100644 --- a/app/Models/Auth/Agent.php +++ b/app/Models/Auth/Agent.php @@ -2,12 +2,14 @@ namespace App\Models\Auth; +use Database\Factories\Auth\AgentFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasOne; class Agent extends Model { - /** @use HasFactory<\Database\Factories\Auth\AgentFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ @@ -50,7 +52,7 @@ protected static function boot() // Relationships /** - * @return \Illuminate\Database\Eloquent\Relations\HasOne + * @return HasOne */ public function user() { diff --git a/app/Models/Auth/Application.php b/app/Models/Auth/Application.php new file mode 100644 index 00000000..0f2af87f --- /dev/null +++ b/app/Models/Auth/Application.php @@ -0,0 +1,13 @@ + */ + use HasFactory; +} diff --git a/app/Models/Auth/Entry.php b/app/Models/Auth/Entry.php index 3eb0c67a..8b465efe 100644 --- a/app/Models/Auth/Entry.php +++ b/app/Models/Auth/Entry.php @@ -3,13 +3,14 @@ namespace App\Models\Auth; use App\Models\Access\Client; +use Database\Factories\Auth\EntryFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Entry extends Model { - /** @use HasFactory<\Database\Factories\Auth\EntryFactory> */ + /** @use HasFactory */ use HasFactory; public $timestamps = false; diff --git a/app/Models/Auth/Registration.php b/app/Models/Auth/Registration.php index 519f8c4c..be2a4752 100644 --- a/app/Models/Auth/Registration.php +++ b/app/Models/Auth/Registration.php @@ -3,14 +3,27 @@ namespace App\Models\Auth; use App\Models\Access\Client; +use Database\Factories\Auth\RegistrationFactory; +use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property string|null $password + * @property string|null $verification_token + */ class Registration extends Model { - /** @use HasFactory<\Database\Factories\Auth\RegistrationFactory> */ - use HasFactory; + /** @use HasFactory */ + use HasFactory, HasUuids; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = 'uuid'; /** * The attributes that are mass assignable. @@ -18,11 +31,20 @@ class Registration extends Model * @var list */ protected $fillable = [ + 'uuid', 'client_id', + 'workflow_id', + 'current_step_id', 'email', 'password', 'verification_token', - 'user_id', + 'form_data', + 'step_timings', + 'status', + 'intended_role', + 'parent_registration_uuid', + 'approved_by', + 'expires_at', ]; /** @@ -33,8 +55,38 @@ class Registration extends Model protected $hidden = [ 'password', 'verification_token', + 'form_data', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'form_data' => 'encrypted:array', + 'step_timings' => 'array', + 'expires_at' => 'datetime', ]; + /** + * Virtual attribute for backward compatibility. + */ + public function getPasswordAttribute(): ?string + { + return $this->form_data['password'] ?? null; + } + + /** + * Virtual attribute for backward compatibility. + */ + public function setPasswordAttribute(string $value): void + { + $data = $this->form_data ?? []; + $data['password'] = $value; + $this->form_data = $data; + } + /** * Get the client that originated the registration. * diff --git a/app/Models/Auth/SocialAccount.php b/app/Models/Auth/SocialAccount.php index 5bb893d8..a36a1f31 100644 --- a/app/Models/Auth/SocialAccount.php +++ b/app/Models/Auth/SocialAccount.php @@ -2,13 +2,14 @@ namespace App\Models\Auth; +use Database\Factories\Auth\SocialAccountFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class SocialAccount extends Model { - /** @use HasFactory<\Database\Factories\Auth\SocialAccountFactory> */ + /** @use HasFactory */ use HasFactory; /** diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index dbde7e40..2debdf29 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -14,6 +14,7 @@ use App\Models\Gamification\GlobalRank; use App\Models\Gamification\Point; use App\Models\Gamification\UserTitleLevel; +use Database\Factories\Auth\UserFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -30,7 +31,7 @@ class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\Auth\UserFactory> */ + /** @use HasFactory */ use Billable, HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable; protected $fillable = [ diff --git a/app/Models/Auth/Workflow.php b/app/Models/Auth/Workflow.php new file mode 100644 index 00000000..f173ecd5 --- /dev/null +++ b/app/Models/Auth/Workflow.php @@ -0,0 +1,13 @@ + */ + use HasFactory; +} diff --git a/app/Models/Auth/WorkflowStep.php b/app/Models/Auth/WorkflowStep.php new file mode 100644 index 00000000..91e9fb19 --- /dev/null +++ b/app/Models/Auth/WorkflowStep.php @@ -0,0 +1,13 @@ + */ + use HasFactory; +} diff --git a/app/Models/Competitions/Tournament.php b/app/Models/Competitions/Tournament.php index 2915a910..d05fba3c 100644 --- a/app/Models/Competitions/Tournament.php +++ b/app/Models/Competitions/Tournament.php @@ -5,10 +5,12 @@ use App\Models\Auth\User; use App\Models\Games\Game; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -23,12 +25,12 @@ * @property int|null $prize_pool * @property array|null $bracket_data * @property array|null $rules - * @property \Illuminate\Support\Carbon|null $starts_at - * @property \Illuminate\Support\Carbon|null $ends_at - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at - * @property-read \Illuminate\Database\Eloquent\Collection $users - * @property-read \Illuminate\Database\Eloquent\Collection $games + * @property Carbon|null $starts_at + * @property Carbon|null $ends_at + * @property Carbon $created_at + * @property Carbon $updated_at + * @property-read Collection $users + * @property-read Collection $games */ class Tournament extends Model { diff --git a/app/Models/Content/Avatar.php b/app/Models/Content/Avatar.php index 6d6095e9..f87dcde4 100644 --- a/app/Models/Content/Avatar.php +++ b/app/Models/Content/Avatar.php @@ -4,6 +4,7 @@ use App\Enums\AvatarType; use App\Models\Auth\User; +use Database\Factories\Content\AvatarFactory; use DrewRoberts\Media\Models\Image; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -13,7 +14,7 @@ class Avatar extends Model { - /** @use HasFactory<\Database\Factories\Content\AvatarFactory> */ + /** @use HasFactory */ use HasCreator, HasFactory; protected $fillable = [ diff --git a/app/Models/Economy/Balance.php b/app/Models/Economy/Balance.php index e6575342..17c93f47 100644 --- a/app/Models/Economy/Balance.php +++ b/app/Models/Economy/Balance.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * @property int $id @@ -13,8 +14,8 @@ * @property string $currency_type * @property int $amount * @property int $reserved_amount - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read int $availableBalance * @property-read User $user */ diff --git a/app/Models/Economy/Quota.php b/app/Models/Economy/Quota.php index 2fdd0c36..4c46681d 100644 --- a/app/Models/Economy/Quota.php +++ b/app/Models/Economy/Quota.php @@ -3,13 +3,14 @@ namespace App\Models\Economy; use App\Models\Auth\User; +use Database\Factories\Economy\QuotaFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Quota extends Model { - /** @use HasFactory<\Database\Factories\Economy\QuotaFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ diff --git a/app/Models/Economy/Strike.php b/app/Models/Economy/Strike.php index 439b1d89..647ff11f 100644 --- a/app/Models/Economy/Strike.php +++ b/app/Models/Economy/Strike.php @@ -3,13 +3,14 @@ namespace App\Models\Economy; use App\Models\Auth\User; +use Database\Factories\Economy\StrikeFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Strike extends Model { - /** @use HasFactory<\Database\Factories\Economy\StrikeFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ diff --git a/app/Models/Economy/Transaction.php b/app/Models/Economy/Transaction.php index 3649e3d4..a06f5cb0 100644 --- a/app/Models/Economy/Transaction.php +++ b/app/Models/Economy/Transaction.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Support\Carbon; /** * @property int $id @@ -20,8 +21,8 @@ * @property string|null $reference_type * @property int|null $reference_id * @property array|null $metadata - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read User $user * @property-read Model|null $reference */ diff --git a/app/Models/Games/Action.php b/app/Models/Games/Action.php index 6455a588..8bf8bfab 100644 --- a/app/Models/Games/Action.php +++ b/app/Models/Games/Action.php @@ -3,12 +3,14 @@ namespace App\Models\Games; use App\Enums\ActionType; +use Database\Factories\Games\ActionFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; /** * @property int $id @@ -22,12 +24,12 @@ * @property array|null $resulting_state * @property Player $player * @property Game $game - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at + * @property Carbon|null $created_at + * @property Carbon|null $updated_at */ class Action extends Model { - /** @use HasFactory<\Database\Factories\Games\ActionFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids, SoftDeletes; protected $fillable = [ diff --git a/app/Models/Games/Game.php b/app/Models/Games/Game.php index decff867..b67b45e2 100644 --- a/app/Models/Games/Game.php +++ b/app/Models/Games/Game.php @@ -6,20 +6,23 @@ use App\Enums\GameTitle; use App\Enums\OutcomeType; use App\Models\Auth\User; +use Database\Factories\Games\GameFactory; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; /** * @property int $id * @property string $ulid * @property Mode $mode - * @property \Illuminate\Database\Eloquent\Collection $players - * @property \Illuminate\Database\Eloquent\Collection $actions + * @property Collection $players + * @property Collection $actions * @property Player|null $winner * @property GameStatus $status * @property int|null $mode_id @@ -29,17 +32,17 @@ * @property int|null $turn_number * @property array|null $game_state * @property array|null $game_settings - * @property \Illuminate\Support\Carbon|null $started_at - * @property \Illuminate\Support\Carbon|null $completed_at - * @property \Illuminate\Support\Carbon|null $expires_at - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at + * @property Carbon|null $started_at + * @property Carbon|null $completed_at + * @property Carbon|null $expires_at + * @property Carbon|null $created_at + * @property Carbon|null $updated_at * @property OutcomeType|null $outcome_type * @property array|null $outcome_details * @property GameTitle $title_slug * @property string $game_title * @property int|null $max_players - * @property \Illuminate\Support\Carbon|null $turn_ends_at + * @property Carbon|null $turn_ends_at * @property int|null $current_player_id * @property array|null $final_scores * @property array|null $xp_awarded @@ -47,7 +50,7 @@ */ class Game extends Model { - /** @use HasFactory<\Database\Factories\Games\GameFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids, SoftDeletes; protected $fillable = [ @@ -225,7 +228,7 @@ public function isActive(): bool return $this->status === GameStatus::ACTIVE; } - public function getRecentActionTime(): \Illuminate\Support\Carbon + public function getRecentActionTime(): Carbon { /** @var Action|null $lastAction */ $lastAction = $this->actions()->latest()->first(); diff --git a/app/Models/Games/Mode.php b/app/Models/Games/Mode.php index 109b4471..4edf46b9 100644 --- a/app/Models/Games/Mode.php +++ b/app/Models/Games/Mode.php @@ -5,10 +5,13 @@ use App\Enums\GameTitle; use App\GameEngine\ModeRegistry; use App\GameTitles\BaseGameTitle; +use Database\Factories\Games\ModeFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -17,12 +20,12 @@ * @property string $name * @property bool $is_active * @property int|null $turn_time_limit_seconds - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at */ class Mode extends Model { - /** @use HasFactory<\Database\Factories\Games\ModeFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ @@ -98,7 +101,7 @@ public static function findByTitleAndSlug(string $titleSlug, string $modeSlug): * Get seeded mode by title and slug. * Use this in tests after running ModeSeeder. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public static function seeded(string|GameTitle $titleSlug, string $slug = 'standard'): self { @@ -114,7 +117,7 @@ public static function seeded(string|GameTitle $titleSlug, string $slug = 'stand /** * Get seeded Connect Four mode. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public static function connectFour(string $slug = 'standard'): self { @@ -124,7 +127,7 @@ public static function connectFour(string $slug = 'standard'): self /** * Get seeded Checkers mode. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public static function checkers(string $slug = 'standard'): self { @@ -134,7 +137,7 @@ public static function checkers(string $slug = 'standard'): self /** * Get seeded Hearts mode. * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public static function hearts(string $slug = 'standard'): self { diff --git a/app/Models/Games/Player.php b/app/Models/Games/Player.php index ba5945a0..b1823582 100644 --- a/app/Models/Games/Player.php +++ b/app/Models/Games/Player.php @@ -4,6 +4,7 @@ use App\Models\Access\Client; use App\Models\Auth\User; +use Database\Factories\Games\PlayerFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -27,7 +28,7 @@ */ class Player extends Model { - /** @use HasFactory<\Database\Factories\Games\PlayerFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids; protected $fillable = [ diff --git a/app/Models/Gamification/Badge.php b/app/Models/Gamification/Badge.php index 6bb8e64b..02886b64 100644 --- a/app/Models/Gamification/Badge.php +++ b/app/Models/Gamification/Badge.php @@ -3,6 +3,7 @@ namespace App\Models\Gamification; use App\Models\Auth\User; +use Database\Factories\Gamification\BadgeFactory; use DrewRoberts\Media\Models\Image; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -12,7 +13,7 @@ class Badge extends Model { - /** @use HasFactory<\Database\Factories\Gamification\BadgeFactory> */ + /** @use HasFactory */ use HasCreator, HasFactory; protected $fillable = [ diff --git a/app/Models/Gamification/GlobalRank.php b/app/Models/Gamification/GlobalRank.php index f496b044..1b3dd0d0 100644 --- a/app/Models/Gamification/GlobalRank.php +++ b/app/Models/Gamification/GlobalRank.php @@ -3,13 +3,14 @@ namespace App\Models\Gamification; use App\Models\Auth\User; +use Database\Factories\Gamification\GlobalRankFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class GlobalRank extends Model { - /** @use HasFactory<\Database\Factories\Gamification\GlobalRankFactory> */ + /** @use HasFactory */ use HasFactory; protected $primaryKey = 'user_id'; diff --git a/app/Models/Gamification/Point.php b/app/Models/Gamification/Point.php index a4f42eb5..e0f2c5bb 100644 --- a/app/Models/Gamification/Point.php +++ b/app/Models/Gamification/Point.php @@ -3,6 +3,7 @@ namespace App\Models\Gamification; use App\Models\Auth\User; +use Database\Factories\Gamification\PointFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -10,7 +11,7 @@ class Point extends Model { - /** @use HasFactory<\Database\Factories\Gamification\PointFactory> */ + /** @use HasFactory */ use HasFactory; protected $table = 'points'; diff --git a/app/Models/Gamification/UserTitleLevel.php b/app/Models/Gamification/UserTitleLevel.php index 33010ae2..4bae69a4 100644 --- a/app/Models/Gamification/UserTitleLevel.php +++ b/app/Models/Gamification/UserTitleLevel.php @@ -3,9 +3,11 @@ namespace App\Models\Gamification; use App\Models\Auth\User; +use Database\Factories\Gamification\UserTitleLevelFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * User progress tracking for game titles. @@ -18,12 +20,12 @@ * @property string $title_slug * @property int $level * @property int $xp_current - * @property \Illuminate\Support\Carbon|null $last_played_at + * @property Carbon|null $last_played_at * @property User $user */ class UserTitleLevel extends Model { - /** @use HasFactory<\Database\Factories\Gamification\UserTitleLevelFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ diff --git a/app/Models/Matchmaking/Lobby.php b/app/Models/Matchmaking/Lobby.php index 2816dc66..fcbabe5c 100644 --- a/app/Models/Matchmaking/Lobby.php +++ b/app/Models/Matchmaking/Lobby.php @@ -8,12 +8,15 @@ use App\Models\Auth\User; use App\Models\Games\Game; use App\Models\Games\Mode; +use Database\Factories\Matchmaking\LobbyFactory; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -25,15 +28,15 @@ * @property LobbyStatus $status * @property bool $is_public * @property int $min_players - * @property \Illuminate\Support\Carbon|null $scheduled_at - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at - * @property-read \Illuminate\Database\Eloquent\Collection $players + * @property Carbon|null $scheduled_at + * @property Carbon $created_at + * @property Carbon $updated_at + * @property-read Collection $players * @property-read User $host */ class Lobby extends Model { - /** @use HasFactory<\Database\Factories\Matchmaking\LobbyFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids; protected $fillable = [ diff --git a/app/Models/Matchmaking/LobbyPlayer.php b/app/Models/Matchmaking/LobbyPlayer.php index 6e91bfc5..73ea27b3 100644 --- a/app/Models/Matchmaking/LobbyPlayer.php +++ b/app/Models/Matchmaking/LobbyPlayer.php @@ -5,10 +5,12 @@ use App\Matchmaking\Enums\LobbyPlayerSource; use App\Matchmaking\Enums\LobbyPlayerStatus; use App\Models\Auth\User; +use Database\Factories\Matchmaking\LobbyPlayerFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * @property int $id @@ -17,12 +19,12 @@ * @property int $client_id * @property LobbyPlayerStatus $status * @property LobbyPlayerSource $source - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at */ class LobbyPlayer extends Model { - /** @use HasFactory<\Database\Factories\Matchmaking\LobbyPlayerFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ diff --git a/app/Models/Matchmaking/Proposal.php b/app/Models/Matchmaking/Proposal.php index 9f0a8564..28901d51 100644 --- a/app/Models/Matchmaking/Proposal.php +++ b/app/Models/Matchmaking/Proposal.php @@ -7,11 +7,13 @@ use App\Models\Auth\User; use App\Models\Games\Game; use App\Models\Games\Mode; +use Database\Factories\Matchmaking\ProposalFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * @property int $id @@ -24,15 +26,15 @@ * @property int|null $original_game_id * @property int|null $game_id * @property array|null $game_settings - * @property \Illuminate\Support\Carbon|null $responded_at + * @property Carbon|null $responded_at * @property ProposalStatus $status - * @property \Illuminate\Support\Carbon|null $expires_at - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon|null $expires_at + * @property Carbon $created_at + * @property Carbon $updated_at */ class Proposal extends Model { - /** @use HasFactory<\Database\Factories\Matchmaking\ProposalFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids; protected $fillable = [ diff --git a/app/Models/Matchmaking/QueueSlot.php b/app/Models/Matchmaking/QueueSlot.php index 6f3dcbb2..a4f442cb 100644 --- a/app/Models/Matchmaking/QueueSlot.php +++ b/app/Models/Matchmaking/QueueSlot.php @@ -4,6 +4,7 @@ use App\Matchmaking\Enums\QueueSlotStatus; use App\Models\Auth\User; +use Database\Factories\Matchmaking\QueueSlotFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -12,7 +13,7 @@ class QueueSlot extends Model { - /** @use HasFactory<\Database\Factories\Matchmaking\QueueSlotFactory> */ + /** @use HasFactory */ use HasFactory, HasUlids; protected $table = 'queue_slots'; diff --git a/app/Policies/ApplicationPolicy.php b/app/Policies/ApplicationPolicy.php new file mode 100644 index 00000000..eda1f7d1 --- /dev/null +++ b/app/Policies/ApplicationPolicy.php @@ -0,0 +1,65 @@ + $participants + * @param Collection $participants * @return array */ protected function generateBracket(Tournament $tournament, $participants): array diff --git a/app/Services/Economy/AppleReceiptValidator.php b/app/Services/Economy/AppleReceiptValidator.php index e5970282..c324eed4 100644 --- a/app/Services/Economy/AppleReceiptValidator.php +++ b/app/Services/Economy/AppleReceiptValidator.php @@ -93,7 +93,6 @@ private function callAppleAPI(string $jwt, string $transactionId): array $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); if ($httpCode !== 200) { throw new PaymentValidationException( diff --git a/app/Services/Games/GameResponseEnhancementService.php b/app/Services/Games/GameResponseEnhancementService.php index 391f3150..0479f44a 100644 --- a/app/Services/Games/GameResponseEnhancementService.php +++ b/app/Services/Games/GameResponseEnhancementService.php @@ -10,6 +10,7 @@ use App\Models\Games\Action; use App\Models\Games\Game; use App\Models\Games\Player; +use Illuminate\Database\Eloquent\Collection; /** * Service to enhance game responses with rich context. @@ -137,7 +138,7 @@ protected function calculateGameStats(Game $game): array } /** - * @param \Illuminate\Database\Eloquent\Collection $actions + * @param Collection $actions */ protected function calculateAverageActionTime($actions): float { @@ -159,7 +160,7 @@ protected function calculateAverageActionTime($actions): float } /** - * @param \Illuminate\Database\Eloquent\Collection $actions + * @param Collection $actions */ protected function calculateAverageResponseTime($actions): float { diff --git a/app/Services/System/SystemHealthService.php b/app/Services/System/SystemHealthService.php index 985aea4e..0694c938 100644 --- a/app/Services/System/SystemHealthService.php +++ b/app/Services/System/SystemHealthService.php @@ -2,6 +2,7 @@ namespace App\Services\System; +use App\Enums\GameTitle; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; @@ -113,7 +114,7 @@ private function checkGameEngine(): array { try { // Test that game modes are configured - $availableGames = collect(\App\Enums\GameTitle::cases())->count(); + $availableGames = collect(GameTitle::cases())->count(); return [ 'status' => 'healthy', diff --git a/bootstrap/app.php b/bootstrap/app.php index 3bdb38ea..fc276439 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -39,7 +39,7 @@ }) ->withExceptions(function (Exceptions $exceptions): void { // Global API error handler with correlation IDs - $exceptions->render(function (\Throwable $e, $request) { + $exceptions->render(function (Throwable $e, $request) { // Only format JSON for API requests if (! $request->is('api/*')) { return null; // Let Laravel handle non-API exceptions normally @@ -48,7 +48,7 @@ $correlationId = (string) Str::uuid(); // Log all API errors with correlation ID - \Log::error('API Error', [ + Log::error('API Error', [ 'correlation_id' => $correlationId, 'exception' => get_class($e), 'message' => $e->getMessage(), diff --git a/bootstrap/providers.php b/bootstrap/providers.php index ea71344f..8bed96a1 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,7 +1,11 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\Auth\User::class), + 'model' => env('AUTH_MODEL', User::class), ], // 'users' => [ diff --git a/config/database.php b/config/database.php index d9477bef..4b45bdba 100644 --- a/config/database.php +++ b/config/database.php @@ -1,6 +1,7 @@ true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -79,7 +80,7 @@ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], diff --git a/config/permission.php b/config/permission.php index f39f6b5b..85b88328 100644 --- a/config/permission.php +++ b/config/permission.php @@ -1,5 +1,9 @@ [ @@ -13,7 +17,7 @@ * `Spatie\Permission\Contracts\Permission` contract. */ - 'permission' => Spatie\Permission\Models\Permission::class, + 'permission' => Permission::class, /* * When using the "HasRoles" trait from this package, we need to know which @@ -24,7 +28,7 @@ * `Spatie\Permission\Contracts\Role` contract. */ - 'role' => Spatie\Permission\Models\Role::class, + 'role' => Role::class, ], @@ -136,7 +140,7 @@ /* * The class to use to resolve the permissions team id */ - 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + 'team_resolver' => DefaultTeamResolver::class, /* * Passport Client Credentials Grant @@ -183,7 +187,7 @@ * When permissions or roles are updated the cache is flushed automatically. */ - 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + 'expiration_time' => DateInterval::createFromDateString('24 hours'), /* * The cache key used to store all permissions. diff --git a/database/factories/Access/ClientFactory.php b/database/factories/Access/ClientFactory.php index f2dc65c3..e2e83a5a 100644 --- a/database/factories/Access/ClientFactory.php +++ b/database/factories/Access/ClientFactory.php @@ -3,10 +3,12 @@ namespace Database\Factories\Access; use App\Enums\Platform; +use App\Models\Access\Client; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Access\Client> + * @extends Factory */ class ClientFactory extends Factory { @@ -19,7 +21,7 @@ public function definition(): array { return [ 'name' => $this->faker->company, - 'api_key' => \Illuminate\Support\Str::random(32), + 'api_key' => Str::random(32), 'website' => $this->faker->domainName, 'platform' => $this->faker->randomElement(Platform::cases()), 'is_active' => true, diff --git a/database/factories/Account/AlertFactory.php b/database/factories/Account/AlertFactory.php index d89c365e..73c6d4db 100644 --- a/database/factories/Account/AlertFactory.php +++ b/database/factories/Account/AlertFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Account\Alert> + * @extends Factory */ class AlertFactory extends Factory { diff --git a/database/factories/Auth/ApplicationFactory.php b/database/factories/Auth/ApplicationFactory.php new file mode 100644 index 00000000..47b58f2b --- /dev/null +++ b/database/factories/Auth/ApplicationFactory.php @@ -0,0 +1,33 @@ + + */ +class ApplicationFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Application::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/Auth/EntryFactory.php b/database/factories/Auth/EntryFactory.php index 75c08c82..54b445d4 100644 --- a/database/factories/Auth/EntryFactory.php +++ b/database/factories/Auth/EntryFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Auth\Entry> + * @extends Factory */ class EntryFactory extends Factory { diff --git a/database/factories/Auth/RegistrationFactory.php b/database/factories/Auth/RegistrationFactory.php index 0193b5e2..db47d7bd 100644 --- a/database/factories/Auth/RegistrationFactory.php +++ b/database/factories/Auth/RegistrationFactory.php @@ -10,7 +10,7 @@ use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Auth\Registration> + * @extends Factory */ class RegistrationFactory extends Factory { diff --git a/database/factories/Auth/SocialAccountFactory.php b/database/factories/Auth/SocialAccountFactory.php index b17918ed..4beadbc0 100644 --- a/database/factories/Auth/SocialAccountFactory.php +++ b/database/factories/Auth/SocialAccountFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Auth\SocialAccount> + * @extends Factory */ class SocialAccountFactory extends Factory { diff --git a/database/factories/Auth/UserFactory.php b/database/factories/Auth/UserFactory.php index 9de63e54..b57a3b07 100644 --- a/database/factories/Auth/UserFactory.php +++ b/database/factories/Auth/UserFactory.php @@ -3,12 +3,13 @@ namespace Database\Factories\Auth; use App\Models\Access\Client; +use App\Models\Auth\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Auth\User> + * @extends Factory */ class UserFactory extends Factory { diff --git a/database/factories/Auth/WorkflowFactory.php b/database/factories/Auth/WorkflowFactory.php new file mode 100644 index 00000000..8be286ea --- /dev/null +++ b/database/factories/Auth/WorkflowFactory.php @@ -0,0 +1,33 @@ + + */ +class WorkflowFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Workflow::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/Auth/WorkflowStepFactory.php b/database/factories/Auth/WorkflowStepFactory.php new file mode 100644 index 00000000..3828c97b --- /dev/null +++ b/database/factories/Auth/WorkflowStepFactory.php @@ -0,0 +1,33 @@ + + */ +class WorkflowStepFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = WorkflowStep::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/Content/AvatarFactory.php b/database/factories/Content/AvatarFactory.php index b1bb0a0e..ebee841b 100644 --- a/database/factories/Content/AvatarFactory.php +++ b/database/factories/Content/AvatarFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Content\Avatar> + * @extends Factory */ class AvatarFactory extends Factory { diff --git a/database/factories/Economy/QuotaFactory.php b/database/factories/Economy/QuotaFactory.php index dc9a05aa..34b9ab81 100644 --- a/database/factories/Economy/QuotaFactory.php +++ b/database/factories/Economy/QuotaFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Economy\Quota> + * @extends Factory */ class QuotaFactory extends Factory { diff --git a/database/factories/Economy/StrikeFactory.php b/database/factories/Economy/StrikeFactory.php index ce2661be..8eca426c 100644 --- a/database/factories/Economy/StrikeFactory.php +++ b/database/factories/Economy/StrikeFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Economy\Strike> + * @extends Factory */ class StrikeFactory extends Factory { diff --git a/database/factories/Economy/SubscriptionFactory.php b/database/factories/Economy/SubscriptionFactory.php index 4d16adce..350dee0a 100644 --- a/database/factories/Economy/SubscriptionFactory.php +++ b/database/factories/Economy/SubscriptionFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Economy\Subscription> + * @extends Factory */ class SubscriptionFactory extends Factory { diff --git a/database/factories/Games/ActionFactory.php b/database/factories/Games/ActionFactory.php index b663d702..77be27c2 100644 --- a/database/factories/Games/ActionFactory.php +++ b/database/factories/Games/ActionFactory.php @@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Games\Action> + * @extends Factory */ class ActionFactory extends Factory { diff --git a/database/factories/Games/GameFactory.php b/database/factories/Games/GameFactory.php index 276d5b4c..f004f14c 100644 --- a/database/factories/Games/GameFactory.php +++ b/database/factories/Games/GameFactory.php @@ -12,14 +12,14 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Games\Game> + * @extends Factory */ class GameFactory extends Factory { /** * The name of the factory's corresponding model. * - * @var class-string<\App\Models\Games\Game> + * @var class-string */ protected $model = Game::class; diff --git a/database/factories/Games/ModeFactory.php b/database/factories/Games/ModeFactory.php index 3c7f476e..0684e66b 100644 --- a/database/factories/Games/ModeFactory.php +++ b/database/factories/Games/ModeFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Games\Mode> + * @extends Factory */ class ModeFactory extends Factory { diff --git a/database/factories/Games/PlayerFactory.php b/database/factories/Games/PlayerFactory.php index 3d84422d..6717b876 100644 --- a/database/factories/Games/PlayerFactory.php +++ b/database/factories/Games/PlayerFactory.php @@ -9,14 +9,14 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Games\Player> + * @extends Factory */ class PlayerFactory extends Factory { /** * The name of the factory's corresponding model. * - * @var class-string<\App\Models\Games\Player> + * @var class-string */ protected $model = Player::class; diff --git a/database/factories/Gamification/BadgeFactory.php b/database/factories/Gamification/BadgeFactory.php index ec6c9851..6bb2f303 100644 --- a/database/factories/Gamification/BadgeFactory.php +++ b/database/factories/Gamification/BadgeFactory.php @@ -8,7 +8,7 @@ use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Gamification\Badge> + * @extends Factory */ class BadgeFactory extends Factory { diff --git a/database/factories/Gamification/GlobalRankFactory.php b/database/factories/Gamification/GlobalRankFactory.php index 986fd2e1..6881c6ee 100644 --- a/database/factories/Gamification/GlobalRankFactory.php +++ b/database/factories/Gamification/GlobalRankFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Gamification\GlobalRank> + * @extends Factory */ class GlobalRankFactory extends Factory { diff --git a/database/factories/Gamification/PointFactory.php b/database/factories/Gamification/PointFactory.php index 098f562c..fd6fa941 100644 --- a/database/factories/Gamification/PointFactory.php +++ b/database/factories/Gamification/PointFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Gamification\Point> + * @extends Factory */ class PointFactory extends Factory { diff --git a/database/factories/Gamification/UserTitleLevelFactory.php b/database/factories/Gamification/UserTitleLevelFactory.php index 9d3183a6..d33fb423 100644 --- a/database/factories/Gamification/UserTitleLevelFactory.php +++ b/database/factories/Gamification/UserTitleLevelFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Gamification\UserTitleLevel> + * @extends Factory */ class UserTitleLevelFactory extends Factory { diff --git a/database/factories/Matchmaking/ProposalFactory.php b/database/factories/Matchmaking/ProposalFactory.php index ca6abf62..18332f87 100644 --- a/database/factories/Matchmaking/ProposalFactory.php +++ b/database/factories/Matchmaking/ProposalFactory.php @@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Matchmaking\Proposal> + * @extends Factory */ class ProposalFactory extends Factory { diff --git a/database/factories/Matchmaking/QueueSlotFactory.php b/database/factories/Matchmaking/QueueSlotFactory.php index 46dff71d..e5f1b521 100644 --- a/database/factories/Matchmaking/QueueSlotFactory.php +++ b/database/factories/Matchmaking/QueueSlotFactory.php @@ -11,7 +11,7 @@ use Illuminate\Support\Str; /** - * @extends Factory<\App\Models\Matchmaking\QueueSlot> + * @extends Factory */ class QueueSlotFactory extends Factory { diff --git a/database/migrations/0002_01_01_000006_create_workflows_table.php b/database/migrations/0002_01_01_000006_create_workflows_table.php new file mode 100644 index 00000000..2996317b --- /dev/null +++ b/database/migrations/0002_01_01_000006_create_workflows_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('client_id')->constrained('clients'); + $table->string('name'); + $table->string('category')->default('registration'); + $table->boolean('is_active')->default(true); + $table->json('traffic_split')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } +}; diff --git a/database/migrations/0002_01_01_000007_create_workflow_steps_table.php b/database/migrations/0002_01_01_000007_create_workflow_steps_table.php new file mode 100644 index 00000000..535ea6c3 --- /dev/null +++ b/database/migrations/0002_01_01_000007_create_workflow_steps_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('workflow_id')->constrained('workflows'); + $table->string('slug')->index(); + $table->string('type')->default('form'); // e.g. form, kyc, gate, review, payment, info, enrichment, game + $table->string('blade_view'); + $table->json('logic_rule')->nullable(); + $table->json('risk_rule')->nullable(); + $table->boolean('requires_approval')->default(false); + $table->integer('sort_order')->default(0); + $table->timestamps(); + $table->softDeletes(); + }); + } +}; diff --git a/database/migrations/0003_01_01_000001_create_users_table.php b/database/migrations/0003_01_01_000001_create_users_table.php index b4327610..f4d17600 100644 --- a/database/migrations/0003_01_01_000001_create_users_table.php +++ b/database/migrations/0003_01_01_000001_create_users_table.php @@ -15,8 +15,8 @@ public function up(): void $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); - $table->foreignId('registration_client_id')->constrained('clients'); $table->foreignId('agent_id')->nullable()->unique()->index()->constrained('agents'); + $table->foreignId('registration_client_id')->nullable()->constrained('clients'); $table->text('bio')->nullable(); $table->json('social_links')->nullable(); $table->unsignedBigInteger('avatar_id')->nullable()->index(); diff --git a/database/migrations/0003_01_01_000002_create_registrations_table.php b/database/migrations/0003_01_01_000002_create_registrations_table.php index 2ebcde54..20687e35 100644 --- a/database/migrations/0003_01_01_000002_create_registrations_table.php +++ b/database/migrations/0003_01_01_000002_create_registrations_table.php @@ -12,21 +12,23 @@ public function up(): void { Schema::create('registrations', function (Blueprint $table) { - $table->id(); + $table->uuid('uuid')->primary(); // W3RD Context ID $table->foreignId('client_id')->constrained('clients'); - $table->string('email')->unique(); - $table->string('password'); - $table->string('verification_token')->unique(); - $table->foreignId('user_id')->nullable()->constrained('users'); + $table->foreignId('workflow_id')->nullable()->constrained('workflows'); + $table->foreignId('current_step_id')->nullable()->constrained('workflow_steps')->nullOnDelete(); + $table->string('email')->index(); + $table->string('verification_token')->nullable()->index(); // For email verification + $table->json('form_data')->nullable(); // Encrypted blob of all step inputs + $table->json('step_timings')->nullable(); // Analytics: Time spent per step + $table->string('status')->default('draft'); // draft, pending_review, approved, graduated + $table->string('intended_role')->nullable(); // Target User Role + $table->uuid('parent_registration_uuid')->nullable()->index(); // For Team "Seat Holding" + $table->foreignId('approved_by')->nullable()->constrained('users'); // Admin User ID who authorized graduation + $table->timestamp('expires_at')->nullable(); // TTL for abandoned registrations $table->timestamps(); - }); - } + $table->softDeletes(); - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('registrations'); + $table->unique(['client_id', 'email']); // Scope email uniqueness to client + }); } }; diff --git a/database/migrations/0003_01_01_000005_create_applications_table.php b/database/migrations/0003_01_01_000005_create_applications_table.php new file mode 100644 index 00000000..0e658c32 --- /dev/null +++ b/database/migrations/0003_01_01_000005_create_applications_table.php @@ -0,0 +1,20 @@ +id(); + $table->foreignUuid('registration_uuid')->nullable()->constrained('registrations', 'uuid'); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2025_11_13_000007_create_games_table.php b/database/migrations/2025_11_13_000007_create_games_table.php index fc0b6680..0fa486b1 100644 --- a/database/migrations/2025_11_13_000007_create_games_table.php +++ b/database/migrations/2025_11_13_000007_create_games_table.php @@ -33,6 +33,7 @@ public function up(): void $table->integer('action_count')->default(0); $table->integer('duration_seconds')->nullable(); + $table->timestamp('turn_ends_at')->nullable(); $table->timestamp('started_at')->nullable(); $table->timestamp('completed_at')->nullable(); $table->timestamp('expires_at')->nullable(); diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php new file mode 100644 index 00000000..6aa07b9b --- /dev/null +++ b/database/seeders/ApplicationSeeder.php @@ -0,0 +1,16 @@ + +Response: 200 OK +New-Context-ID: +HTML:
...
+``` + +**2. Submit Info (POST /personal-info)** +```http +POST /htmx/v1/registration/personal-info +Header: X-W3RD-Context: +Body: email=bob@example.com&name=Bob +Response: 200 OK +HX-Push-Url: /register/team-invite +HTML:
...
+``` + +### 3.3 W3RD Header Requirements + +| Header | Requirement | Purpose | +| :--- | :--- | :--- | +| `X-W3RD-Client` | **Mandatory** | The `client_uuid` of the requesting site | +| `X-W3RD-Context` | **Mandatory** | The `registration_uuid` (Context ID) | +| `HX-Request` | **Mandatory** | Ensures the request is from HTMX | +| `HX-Trigger` | Optional (Response) | Triggers Alpine.js events on the AHA frontend | + +--- + +## 4. Functional Logic Specifications + +### 4.0 Lifecycle State Machine + +```mermaid +graph TD + A[Draft] -->|Submit Step| A + A -->|Requires Approval| B[Pending Review] + B -->|Admin Approve| C[Approved] + B -->|Admin Reject| A + A -->|All Steps Complete| C + C -->|Graduation Event| D[Graduated] + D -->|User Created| E[User Active] +``` + +### 4.1 Feature #1 & #7: Conditional & Risk-Based Logic +Before serving a fragment, the `WorkflowEngine` evaluates both `logic_rule` and `risk_rule`. + +* **Logic Rule**: `{"field": "intended_role", "operator": "==", "value": "vendor"}` (Skips irrelevant steps) +* **Risk Rule (Feature #7)**: `{"provider": "MaxMind", "score": ">", "threshold": 80}` + * **Behavior**: If the risk score of the incoming IP/Request is high, the engine *dynamically injects* a `verification-gate` step (CAPTCHA/SMS) that is otherwise bypassed for safe traffic. + +### 4.2 Feature #4, #5 & #9: Branching and Team Provisioning +* **Branching**: The workflow can branch based on `intended_role`. +* **Team Seat Holding (Feature #9)**: + * If `form_data.invited_team` is populated, the backend immediately creates "Child Registrations" for each invitee in `draft` status, linked via `parent_registration_uuid`. + * Trigger `registration.team_invite_sent` webhook to dispatch emails. + * Invitees enter their own "Holding Pen" flow, pre-linked to the Organization being created. + +### 4.3 Feature #10: Manual Review (Hypermedia Hold) +If a step is flagged `requires_approval`: +1. The backend updates `registration.status` to `pending_review`. +2. The response is a "Review in Progress" Blade fragment. +3. The fragment uses `hx-get` with a `trigger="every 10s"` to poll the backend. +4. Once an Admin approves, the next poll returns the "Success/Graduation" fragment. + +### 4.4 Feature #4: Data Enrichment (Invisible Step) +* **Step Type**: `enrichment` +* **Behavior**: Asynchronous server-side process triggered after Email capture. +* **Action**: Queries external providers (Clearbit/FullContact). +* **Result**: Merges returned company/profile data into `form_data`. Subsequent steps (e.g., "Company Info") render with fields pre-filled, reducing user friction. + +--- + +## 5. Security, Analytics & Graduation + +### 5.1 feature #2: Step-Level Analytics +* **Tracking**: `registrations.step_timings` stores `{ "step_slug": { "viewed_at": "timestamp", "completed_at": "timestamp" } }`. +* **Insight**: Allows calculation of "Time to Complete" per step and identification of bottleneck steps with high abandonment rates. + +### 5.2 Feature #1: Magic Link Session Restoration +* **Endpoint**: `POST /htmx/v1/registration/magic-link` +* **Action**: Generates a signed URL containing the `registration_uuid`. +* **Behavior**: Clicking the link sets the `X-W3RD-Context` on the frontend (via LocalStorage/Cookie) and redirects the user to their last active `current_step_id`, enabling cross-device completion. + +### 5.3 feature #6: Webhook Pulse Events +The Workflow Engine emits events to the Client's configured Webhook URL: +* `registration.started` (Lead created) +* `registration.step_completed` (Progress update, includes `step_slug`) +* `registration.abandoned` (Trigger re-engagement email) +* `registration.graduated` (Conversion event) + +### 5.4 Feature #8: White-Label Styling +* **Implementation**: All Blade fragments utilize Tailwind CSS utility classes. +* **Customization**: The `Client` model stores a `theme_config` JSON (e.g., `{"primary": "#ff0000", "font": "Inter"}`). +* **Injection**: The API response injects a `