From 4a9dd3b21562f1266c70da636eab3405b35e2bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sat, 9 May 2026 21:03:22 +0200 Subject: [PATCH 01/17] add structure file for account, card, flik and transaction. --- backend/.editorconfig | 2 +- backend/.gitignore | 1 + backend/.php-cs-fixer.dist.php | 62 + .../Commands/Generator/ServiceCommand.php | 6 +- backend/app/Enums/Card/CardNetwork.php | 30 + backend/app/Enums/Card/CardType.php | 21 + backend/app/Exceptions/Handler.php | 57 +- .../Controllers/Account/AccountController.php | 9 + .../Account/Card/CardController.php | 9 + .../Account/Flik/FlikCodeController.php | 9 + .../Transaction/TransactionController.php | 9 + .../Auth/ConfirmablePasswordController.php | 6 +- .../Auth/NewPasswordController.php | 10 +- .../Controllers/Auth/PasswordController.php | 2 +- .../Auth/PasswordResetLinkController.php | 4 +- .../Auth/RegisteredUserController.php | 8 +- .../Auth/VerifyEmailController.php | 4 +- backend/app/Http/Controllers/Controller.php | 1 - .../Controllers/User/ProfileController.php | 29 +- .../app/Http/Middleware/ForceJsonResponse.php | 5 +- .../app/Http/Requests/Auth/LoginRequest.php | 15 +- .../Http/Requests/ProfileUpdateRequest.php | 2 +- .../Requests/User/UpdateProfileRequest.php | 14 +- .../Responses/AuthorizationViewResponse.php | 2 +- backend/app/Models/Account/Account.php | 29 + backend/app/Models/Account/Card/Card.php | 28 + backend/app/Models/Account/Flik/FlikCode.php | 27 + .../Account/Transaction/Transaction.php | 27 + backend/app/Models/User.php | 47 +- backend/app/Providers/AppServiceProvider.php | 42 +- .../app/Rules/Password/CurrentPassword.php | 7 +- .../app/Services/Account/AccountService.php | 11 + .../app/Services/Account/Card/CardService.php | 9 + .../Services/Account/Flik/FlikCodeService.php | 9 + .../Transaction/TransactionService.php | 9 + .../AccountNumberGeneratorService.php | 24 +- .../Generator/CardNumberGeneratorService.php | 154 + backend/composer.json | 3 + backend/composer.lock | 2797 ++++++++++++----- backend/config/app.php | 8 +- backend/config/auth.php | 16 +- backend/config/cache.php | 44 +- backend/config/cors.php | 4 +- backend/config/database.php | 162 +- backend/config/filesystems.php | 40 +- backend/config/logging.php | 68 +- backend/config/mail.php | 32 +- backend/config/passport.php | 3 +- backend/config/queue.php | 58 +- backend/config/sanctum.php | 8 +- backend/config/scramble.php | 8 +- backend/config/services.php | 8 +- backend/config/session.php | 6 +- .../factories/Account/AccountFactory.php | 20 + .../factories/Account/Card/CardFactory.php | 23 + .../Account/Flik/FlikCodeFactory.php | 23 + .../Transaction/TransactionFactory.php | 31 + backend/database/factories/UserFactory.php | 8 +- .../0001_01_01_000000_create_users_table.php | 8 +- .../0001_01_01_000001_create_cache_table.php | 4 +- .../0001_01_01_000002_create_jobs_table.php | 4 +- ...05_create_personal_access_tokens_table.php | 4 +- ...5_200011_create_oauth_auth_codes_table.php | 6 +- ...00012_create_oauth_access_tokens_table.php | 6 +- ...0013_create_oauth_refresh_tokens_table.php | 4 +- ...5_05_200014_create_oauth_clients_table.php | 4 +- ...200015_create_oauth_device_codes_table.php | 6 +- .../migrations/2026_05_09_170729_accounts.php | 80 + ...5_09_173714_create_notifications_table.php | 29 + backend/database/seeders/DatabaseSeeder.php | 10 +- .../database/seeders/PassportClientSeeder.php | 55 +- backend/routes/api.php | 6 +- backend/routes/auth.php | 1 - backend/routes/console.php | 1 - backend/routes/web.php | 3 +- .../tests/Feature/Auth/AuthenticationTest.php | 12 +- .../Feature/Auth/EmailVerificationTest.php | 12 +- .../Feature/Auth/PasswordConfirmationTest.php | 6 +- .../tests/Feature/Auth/PasswordResetTest.php | 16 +- .../tests/Feature/Auth/PasswordUpdateTest.php | 12 +- .../tests/Feature/Auth/RegistrationTest.php | 10 +- backend/tests/Feature/ExampleTest.php | 2 +- backend/tests/Feature/ProfileTest.php | 14 +- backend/tests/TestCase.php | 1 - backend/tests/Unit/ExampleTest.php | 2 +- 85 files changed, 3229 insertions(+), 1199 deletions(-) create mode 100644 backend/.php-cs-fixer.dist.php create mode 100644 backend/app/Enums/Card/CardNetwork.php create mode 100644 backend/app/Enums/Card/CardType.php create mode 100644 backend/app/Http/Controllers/Account/AccountController.php create mode 100644 backend/app/Http/Controllers/Account/Card/CardController.php create mode 100644 backend/app/Http/Controllers/Account/Flik/FlikCodeController.php create mode 100644 backend/app/Http/Controllers/Account/Transaction/TransactionController.php create mode 100644 backend/app/Models/Account/Account.php create mode 100644 backend/app/Models/Account/Card/Card.php create mode 100644 backend/app/Models/Account/Flik/FlikCode.php create mode 100644 backend/app/Models/Account/Transaction/Transaction.php create mode 100644 backend/app/Services/Account/AccountService.php create mode 100644 backend/app/Services/Account/Card/CardService.php create mode 100644 backend/app/Services/Account/Flik/FlikCodeService.php create mode 100644 backend/app/Services/Account/Transaction/TransactionService.php rename backend/app/Services/{AccountNumberGenerator => Generator}/AccountNumberGeneratorService.php (65%) create mode 100644 backend/app/Services/Generator/CardNumberGeneratorService.php create mode 100644 backend/database/factories/Account/AccountFactory.php create mode 100644 backend/database/factories/Account/Card/CardFactory.php create mode 100644 backend/database/factories/Account/Flik/FlikCodeFactory.php create mode 100644 backend/database/factories/Account/Transaction/TransactionFactory.php create mode 100644 backend/database/migrations/2026_05_09_170729_accounts.php create mode 100644 backend/database/migrations/2026_05_09_173714_create_notifications_table.php diff --git a/backend/.editorconfig b/backend/.editorconfig index 6df8428..0a2498d 100644 --- a/backend/.editorconfig +++ b/backend/.editorconfig @@ -15,4 +15,4 @@ trim_trailing_whitespace = false indent_size = 2 [{compose,docker-compose}.{yml,yaml}] -indent_size = 4 +indent_size = 4 \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 83d894f..43fab78 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -5,6 +5,7 @@ .env.production .phpactor.json .phpunit.result.cache +.php-cs-fixer.cache /.codex /.cursor/ /.idea diff --git a/backend/.php-cs-fixer.dist.php b/backend/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7af594d --- /dev/null +++ b/backend/.php-cs-fixer.dist.php @@ -0,0 +1,62 @@ +in([ + __DIR__ . '/app', + __DIR__ . '/config', + __DIR__ . '/database', + __DIR__ . '/routes', + __DIR__ . '/tests', + ]) + ->exclude([ + 'bootstrap', + 'storage', + 'vendor', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays', 'arguments', 'parameters'], + ], + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => [ + 'default' => 'align_single_space_minimal', + ], + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + 'declare_strict_types' => false, + 'blank_line_after_opening_tag' => false, + 'strict_comparison' => true, + 'cast_spaces' => true, + 'concat_space' => ['spacing' => 'one'], + 'types_spaces' => true, + 'modernize_strpos' => true, + ]) + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/backend/app/Console/Commands/Generator/ServiceCommand.php b/backend/app/Console/Commands/Generator/ServiceCommand.php index 2708d6d..aaf4936 100644 --- a/backend/app/Console/Commands/Generator/ServiceCommand.php +++ b/backend/app/Console/Commands/Generator/ServiceCommand.php @@ -49,7 +49,7 @@ protected function makeDirectory(string $path): void { $directory = dirname($path); - if (!$this->filesystem->isDirectory($directory)) { + if (! $this->filesystem->isDirectory($directory)) { $this->filesystem->makeDirectory($directory, 0755, true, true); } } @@ -57,7 +57,7 @@ protected function makeDirectory(string $path): void protected function buildClass(string $name): string { $namespace = 'App\\Services'; - $extends = 'Service'; + $extends = 'Service'; $className = class_basename($name); @@ -71,7 +71,7 @@ protected function buildClass(string $name): string " ['4'], + self::MASTERCARD => ['51', '52', '53', '54', '55'], + }; + } + + public function label(): string + { + return match ($this) { + self::VISA => 'Visa', + self::MASTERCARD => 'Mastercard', + }; + } + + public function cvvLength(): int + { + return 3; + } +} diff --git a/backend/app/Enums/Card/CardType.php b/backend/app/Enums/Card/CardType.php new file mode 100644 index 0000000..624ea81 --- /dev/null +++ b/backend/app/Enums/Card/CardType.php @@ -0,0 +1,21 @@ + 'Karta debetowa', + self::CREDIT => 'Karta kredytowa', + self::PREPAID => 'Karta przedpłacona', + self::VIRTUAL => 'Karta wirtualna', + }; + } +} diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php index 28da11c..7c1c025 100644 --- a/backend/app/Exceptions/Handler.php +++ b/backend/app/Exceptions/Handler.php @@ -2,31 +2,28 @@ namespace App\Exceptions; -use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Http\Exceptions\ThrottleRequestsException; +use Illuminate\Http\JsonResponse; use Illuminate\Validation\ValidationException; use Laravel\Passport\Exceptions\MissingScopeException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use Throwable; class Handler extends ExceptionHandler { public function register(): void { - $this->reportable(function (Throwable $e) { - // + $this->reportable(function (\Throwable $e) { }); } - public function render($request, Throwable $e): Response + public function render($request, \Throwable $e): Response { if ($request->is('api*')) { return $this->handleApiException($e); @@ -35,69 +32,69 @@ public function render($request, Throwable $e): Response return parent::render($request, $e); } - private function handleApiException(Throwable $e): JsonResponse + private function handleApiException(\Throwable $e): JsonResponse { // MissingScopeException (Passport) if ($e instanceof MissingScopeException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Brak wymaganych uprawnień.', - 'code' => 403 + 'code' => 403, ], 403); } // AuthenticationException (brak autoryzacji) if ($e instanceof AuthenticationException || $e instanceof UnauthorizedHttpException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Brak autoryzacji.', - 'code' => 401 + 'code' => 401, ], 401); } // AuthorizationException (brak dostępu) if ($e instanceof AuthorizationException || $e instanceof AccessDeniedHttpException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Brak uprawnień do tego zasobu.', - 'code' => 403 + 'code' => 403, ], 403); } // ValidationException (błędy walidacji) if ($e instanceof ValidationException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Błąd walidacji danych.', - 'errors' => $e->errors(), - 'code' => 422 + 'errors' => $e->errors(), + 'code' => 422, ], 422); } // ModelNotFoundException (model nie znaleziony) if ($e instanceof ModelNotFoundException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Żądany zasób nie został znaleziony.', - 'code' => 404 + 'code' => 404, ], 404); } // NotFoundHttpException (endpoint nie istnieje) if ($e instanceof NotFoundHttpException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Endpoint nie istnieje.', - 'code' => 404 + 'code' => 404, ], 404); } // ThrottleRequestsException (zbyt wiele zapytań) if ($e instanceof ThrottleRequestsException) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => 'Zbyt wiele żądań. Spróbuj ponownie później.', - 'code' => 429 + 'code' => 429, ], 429); } @@ -107,19 +104,19 @@ private function handleApiException(Throwable $e): JsonResponse // W trybie debug pokaż więcej informacji if (config('app.debug')) { return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => $e->getMessage(), - 'code' => $statusCode, - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => collect($e->getTrace())->take(5)->toArray() + 'code' => $statusCode, + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => collect($e->getTrace())->take(5)->toArray(), ], $statusCode >= 100 && $statusCode < 600 ? $statusCode : 500); } return response()->json([ - 'status' => 'error', + 'status' => 'error', 'message' => $e->getMessage() ?: 'Wystąpił wewnętrzny błąd serwera.', - 'code' => $statusCode + 'code' => $statusCode, ], $statusCode >= 100 && $statusCode < 600 ? $statusCode : 500); } } diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php new file mode 100644 index 0000000..2c5c5e7 --- /dev/null +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -0,0 +1,9 @@ +validate([ - 'email' => $request->user()->email, + 'email' => $request->user()->email, 'password' => $request->password, ])) { - throw ValidationException::withMessages([ - 'password' => __('auth.password'), - ]); + throw ValidationException::withMessages(['password' => __('auth.password')]); } $request->session()->put('auth.password_confirmed_at', time()); diff --git a/backend/app/Http/Controllers/Auth/NewPasswordController.php b/backend/app/Http/Controllers/Auth/NewPasswordController.php index fc16e14..beee828 100644 --- a/backend/app/Http/Controllers/Auth/NewPasswordController.php +++ b/backend/app/Http/Controllers/Auth/NewPasswordController.php @@ -32,8 +32,8 @@ public function create(Request $request): View public function store(Request $request): RedirectResponse { $request->validate([ - 'token' => ['required'], - 'email' => ['required', 'email'], + 'token' => ['required'], + 'email' => ['required', 'email'], 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); @@ -44,18 +44,18 @@ public function store(Request $request): RedirectResponse $request->only('email', 'password', 'password_confirmation', 'token'), function (User $user) use ($request) { $user->forceFill([ - 'password' => Hash::make($request->password), + 'password' => Hash::make($request->password), 'remember_token' => Str::random(60), ])->save(); event(new PasswordReset($user)); - } + }, ); // If the password was successfully reset, we will redirect the user back to // the application's home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. - return $status == Password::PASSWORD_RESET + return Password::PASSWORD_RESET === $status ? redirect()->route('login')->with('status', __($status)) : back()->withInput($request->only('email')) ->withErrors(['email' => __($status)]); diff --git a/backend/app/Http/Controllers/Auth/PasswordController.php b/backend/app/Http/Controllers/Auth/PasswordController.php index 6916409..4d0db32 100644 --- a/backend/app/Http/Controllers/Auth/PasswordController.php +++ b/backend/app/Http/Controllers/Auth/PasswordController.php @@ -17,7 +17,7 @@ public function update(Request $request): RedirectResponse { $validated = $request->validateWithBag('updatePassword', [ 'current_password' => ['required', 'current_password'], - 'password' => ['required', Password::defaults(), 'confirmed'], + 'password' => ['required', Password::defaults(), 'confirmed'], ]); $request->user()->update([ diff --git a/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php index 1bc9c11..13a4d08 100644 --- a/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -34,10 +34,10 @@ public function store(Request $request): RedirectResponse // to send the link, we will examine the response then see the message we // need to show to the user. Finally, we'll send out a proper response. $status = Password::sendResetLink( - $request->only('email') + $request->only('email'), ); - return $status == Password::RESET_LINK_SENT + return Password::RESET_LINK_SENT === $status ? back()->with('status', __($status)) : back()->withInput($request->only('email')) ->withErrors(['email' => __($status)]); diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php index 44a3930..51e2845 100644 --- a/backend/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -31,14 +31,14 @@ public function create(): View public function store(Request $request): RedirectResponse { $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); $user = User::create([ - 'name' => $request->name, - 'email' => $request->email, + 'name' => $request->name, + 'email' => $request->email, 'password' => Hash::make($request->password), ]); diff --git a/backend/app/Http/Controllers/Auth/VerifyEmailController.php b/backend/app/Http/Controllers/Auth/VerifyEmailController.php index 784765e..aed2efd 100644 --- a/backend/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/backend/app/Http/Controllers/Auth/VerifyEmailController.php @@ -15,13 +15,13 @@ class VerifyEmailController extends Controller public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('dashboard', absolute: false) . '?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('dashboard', absolute: false) . '?verified=1'); } } diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php index 8677cd5..3aa2580 100644 --- a/backend/app/Http/Controllers/Controller.php +++ b/backend/app/Http/Controllers/Controller.php @@ -4,5 +4,4 @@ abstract class Controller { - // } diff --git a/backend/app/Http/Controllers/User/ProfileController.php b/backend/app/Http/Controllers/User/ProfileController.php index 5736efe..59785e1 100644 --- a/backend/app/Http/Controllers/User/ProfileController.php +++ b/backend/app/Http/Controllers/User/ProfileController.php @@ -12,7 +12,6 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; use Laravel\Passport\Attributes\AuthorizeToken; -use Illuminate\Validation\Rules; /** * Zarządzanie profilem użytkownika. @@ -23,12 +22,12 @@ class ProfileController extends Controller { /** - * Informacja o profilu użytkownika. - * - * Zwraca dane aktualnego zalogowanego użytkownika. - * - * @unauthenticated - */ + * Informacja o profilu użytkownika. + * + * Zwraca dane aktualnego zalogowanego użytkownika. + * + * @unauthenticated + */ public function show(Request $request): JsonResponse { return response()->json($request->user()->toArray()); @@ -50,10 +49,10 @@ public function update(UpdateProfileRequest $request): JsonResponse } /** - * Usuń konto. - * - * Usuwa konto zalogowanego użytkownika - na zawsze! - */ + * Usuń konto. + * + * Usuwa konto zalogowanego użytkownika - na zawsze! + */ public function destroy(Request $request): JsonResponse { $request->user()->delete(); @@ -81,8 +80,8 @@ public function revokeToken(Request $request): JsonResponse public function create(Request $request): User { $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], 'password' => [ 'required', 'string', @@ -98,8 +97,8 @@ public function create(Request $request): User ]); $user = User::create([ - 'name' => $request->name, - 'email' => $request->email, + 'name' => $request->name, + 'email' => $request->email, 'password' => Hash::make($request->password), ]); diff --git a/backend/app/Http/Middleware/ForceJsonResponse.php b/backend/app/Http/Middleware/ForceJsonResponse.php index 35ea080..b24679b 100644 --- a/backend/app/Http/Middleware/ForceJsonResponse.php +++ b/backend/app/Http/Middleware/ForceJsonResponse.php @@ -2,7 +2,6 @@ namespace App\Http\Middleware; -use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -11,9 +10,9 @@ class ForceJsonResponse /** * Handle an incoming request. * - * @param Closure(Request): (Response) $next + * @param \Closure(Request): (Response) $next */ - public function handle(Request $request, Closure $next): Response + public function handle(Request $request, \Closure $next): Response { $request->headers->set('Accept', 'application/json'); diff --git a/backend/app/Http/Requests/Auth/LoginRequest.php b/backend/app/Http/Requests/Auth/LoginRequest.php index 711e0a1..0196b8b 100644 --- a/backend/app/Http/Requests/Auth/LoginRequest.php +++ b/backend/app/Http/Requests/Auth/LoginRequest.php @@ -28,7 +28,7 @@ public function authorize(): bool public function rules(): array { return [ - 'email' => ['required', 'string', 'email'], + 'email' => ['required', 'string', 'email'], 'password' => ['required', 'string'], ]; } @@ -45,9 +45,7 @@ public function authenticate(): void if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { RateLimiter::hit($this->throttleKey()); - throw ValidationException::withMessages([ - 'email' => trans('auth.failed'), - ]); + throw ValidationException::withMessages(['email' => trans('auth.failed')]); } RateLimiter::clear($this->throttleKey()); @@ -68,12 +66,7 @@ public function ensureIsNotRateLimited(): void $seconds = RateLimiter::availableIn($this->throttleKey()); - throw ValidationException::withMessages([ - 'email' => trans('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); + throw ValidationException::withMessages(['email' => trans('auth.throttle', ['seconds' => $seconds, 'minutes' => ceil($seconds / 60)])]); } /** @@ -81,6 +74,6 @@ public function ensureIsNotRateLimited(): void */ public function throttleKey(): string { - return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); + return Str::transliterate(Str::lower($this->string('email')) . '|' . $this->ip()); } } diff --git a/backend/app/Http/Requests/ProfileUpdateRequest.php b/backend/app/Http/Requests/ProfileUpdateRequest.php index e2202dd..2986fd0 100644 --- a/backend/app/Http/Requests/ProfileUpdateRequest.php +++ b/backend/app/Http/Requests/ProfileUpdateRequest.php @@ -17,7 +17,7 @@ class ProfileUpdateRequest extends FormRequest public function rules(): array { return [ - 'name' => ['required', 'string', 'max:255'], + 'name' => ['required', 'string', 'max:255'], 'email' => [ 'required', 'string', diff --git a/backend/app/Http/Requests/User/UpdateProfileRequest.php b/backend/app/Http/Requests/User/UpdateProfileRequest.php index b5e86bf..5860103 100644 --- a/backend/app/Http/Requests/User/UpdateProfileRequest.php +++ b/backend/app/Http/Requests/User/UpdateProfileRequest.php @@ -25,8 +25,8 @@ public function authorize(): bool public function rules(): array { return [ - 'name' => ['sometimes', 'string', 'max:255'], - 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $this->user()->id], + 'name' => ['sometimes', 'string', 'max:255'], + 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $this->user()->id], 'password' => [ 'sometimes', 'string', @@ -48,20 +48,20 @@ public function bodyParameters(): array return [ 'name' => [ 'description' => 'Nowa nazwa użytkownika', - 'example' => 'Jan Kowalski', + 'example' => 'Jan Kowalski', ], 'email' => [ 'description' => 'Nowy adres email', - 'example' => 'jan@example.com', + 'example' => 'jan@example.com', ], 'password' => [ 'description' => 'Nowe hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', - 'example' => 'newpassword123', + 'example' => 'newpassword123', ], 'password_confirmation' => [ 'description' => 'Potwierdzenie nowego hasła', - 'example' => 'newpassword123', - ] + 'example' => 'newpassword123', + ], ]; } } diff --git a/backend/app/Http/Responses/AuthorizationViewResponse.php b/backend/app/Http/Responses/AuthorizationViewResponse.php index dbdaece..025c6cc 100644 --- a/backend/app/Http/Responses/AuthorizationViewResponse.php +++ b/backend/app/Http/Responses/AuthorizationViewResponse.php @@ -12,7 +12,7 @@ class AuthorizationViewResponse implements AuthorizationViewResponseContract, Re public function __construct($view = 'passport::authorize', array $parameters = []) { - $this->view = $view; + $this->view = $view; $this->parameters = $parameters; } diff --git a/backend/app/Models/Account/Account.php b/backend/app/Models/Account/Account.php new file mode 100644 index 0000000..f090e9a --- /dev/null +++ b/backend/app/Models/Account/Account.php @@ -0,0 +1,29 @@ + + */ + protected function casts(): array + { + return [ + 'balance' => 'decimal:2', + ]; + } +} diff --git a/backend/app/Models/Account/Card/Card.php b/backend/app/Models/Account/Card/Card.php new file mode 100644 index 0000000..bf0ec13 --- /dev/null +++ b/backend/app/Models/Account/Card/Card.php @@ -0,0 +1,28 @@ + + */ + protected function casts(): array + { + return [ + 'exp_month' => 'integer', + 'exp_year' => 'integer', + ]; + } +} diff --git a/backend/app/Models/Account/Flik/FlikCode.php b/backend/app/Models/Account/Flik/FlikCode.php new file mode 100644 index 0000000..095bd3a --- /dev/null +++ b/backend/app/Models/Account/Flik/FlikCode.php @@ -0,0 +1,27 @@ + + */ + protected function casts(): array + { + return [ + 'expires_at' => 'datetime', + ]; + } +} diff --git a/backend/app/Models/Account/Transaction/Transaction.php b/backend/app/Models/Account/Transaction/Transaction.php new file mode 100644 index 0000000..d5b667b --- /dev/null +++ b/backend/app/Models/Account/Transaction/Transaction.php @@ -0,0 +1,27 @@ + + */ + protected function casts(): array + { + return [ + 'amount' => 'decimal:2', + ]; + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 9b4707e..ac707d8 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -2,23 +2,32 @@ namespace App\Models; - use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Models\Account\Account; +use App\Models\Account\Card\Card; +use App\Models\Account\Flik\FlikCode; +use App\Models\Account\Transaction\Transaction; use Database\Factories\UserFactory; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Passport\HasApiTokens; use Laravel\Passport\Contracts\OAuthenticatable; - +use Laravel\Passport\HasApiTokens; #[Fillable(['name', 'email', 'password'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail { /** @use HasFactory */ - use HasFactory, Notifiable, HasApiTokens; + use HasFactory; + use Notifiable; + use HasApiTokens; + use HasUuids; /** * Get the attributes that should be cast. @@ -32,4 +41,34 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function accounts(): HasMany + { + return $this->hasMany(Account::class); + } + + public function cards(): HasManyThrough + { + return $this->hasManyThrough(Card::class, Account::class); + } + + public function transactions(): HasManyThrough + { + return $this->hasManyThrough(Transaction::class, Account::class); + } + + public function flikCodes(): HasManyThrough + { + return $this->hasManyThrough(FlikCode::class, Card::class, 'account_id', 'card_id', 'id', 'id'); + } + + public function sentTransactions() + { + return $this->hasManyThrough(Transaction::class, Account::class, 'user_id', 'from_account_id'); + } + + public function receivedTransactions() + { + return $this->hasManyThrough(Transaction::class, Account::class, 'user_id', 'to_account_id'); + } } diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 465627e..b0e7e62 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -28,58 +28,58 @@ public function boot(): void 'user-profile' => 'Odczyt podstawowych danych profilu', // Informacje finansowe - 'balance-view' => 'Odczyt bieżącego salda konta', + 'balance-view' => 'Odczyt bieżącego salda konta', 'balance-history' => 'Odczyt historii salda', // Karty - 'cards-view' => 'Odczyt listy kart', - 'cards-details' => 'Odczyt szczegółów karty (ostatnie 4 cyfry, typ, status)', + 'cards-view' => 'Odczyt listy kart', + 'cards-details' => 'Odczyt szczegółów karty (ostatnie 4 cyfry, typ, status)', 'cards-transactions' => 'Odczyt transakcji kartą', - 'cards-manage' => 'Zarządzanie kartami (blokada, odblokowanie, limity)', + 'cards-manage' => 'Zarządzanie kartami (blokada, odblokowanie, limity)', // Transakcje - 'transactions-view' => 'Odczyt historii transakcji', - 'transactions-details' => 'Odczyt szczegółów transakcji', - 'transactions-transfer' => 'Wykonywanie przelewów', + 'transactions-view' => 'Odczyt historii transakcji', + 'transactions-details' => 'Odczyt szczegółów transakcji', + 'transactions-transfer' => 'Wykonywanie przelewów', 'transactions-scheduled' => 'Odczyt zaplanowanych przelewów', - 'transactions-schedule' => 'Planowanie przelewów', + 'transactions-schedule' => 'Planowanie przelewów', // Konta - 'accounts-view' => 'Odczyt listy kont bankowych', - 'accounts-details' => 'Odczyt szczegółów konta (numer, typ, waluta)', + 'accounts-view' => 'Odczyt listy kont bankowych', + 'accounts-details' => 'Odczyt szczegółów konta (numer, typ, waluta)', 'accounts-statement' => 'Pobieranie wyciągów bankowych', // Blik / Płatności mobilne 'blik-generate' => 'Generowanie kodu BLIK', - 'blik-confirm' => 'Potwierdzanie transakcji BLIK', + 'blik-confirm' => 'Potwierdzanie transakcji BLIK', // Przelewy 'transfer-internal' => 'Przelew wewnętrzny (na konto w tym samym banku)', 'transfer-external' => 'Przelew zewnętrzny (na konto w innym banku)', - 'transfer-express' => 'Przelew ekspresowy', - 'transfer-foreign' => 'Przelew zagraniczny', + 'transfer-express' => 'Przelew ekspresowy', + 'transfer-foreign' => 'Przelew zagraniczny', // Ustawienia i bezpieczeństwo - 'settings-view' => 'Odczyt ustawień konta', - 'settings-update' => 'Aktualizacja ustawień konta', - 'notifications-view' => 'Odczyt powiadomień', + 'settings-view' => 'Odczyt ustawień konta', + 'settings-update' => 'Aktualizacja ustawień konta', + 'notifications-view' => 'Odczyt powiadomień', 'notifications-manage' => 'Zarządzanie powiadomieniami', // Limity i uprawnienia - 'limits-view' => 'Odczyt limitów transakcyjnych', + 'limits-view' => 'Odczyt limitów transakcyjnych', 'limits-update' => 'Modyfikacja limitów transakcyjnych', // Beneficjenci (zaufani odbiorcy) - 'beneficiaries-view' => 'Odczyt listy zaufanych odbiorców', + 'beneficiaries-view' => 'Odczyt listy zaufanych odbiorców', 'beneficiaries-manage' => 'Zarządzanie zaufanymi odbiorcami', // Kredyty / Pożyczki - 'loans-view' => 'Odczyt listy kredytów', - 'loans-details' => 'Odczyt szczegółów kredytu', + 'loans-view' => 'Odczyt listy kredytów', + 'loans-details' => 'Odczyt szczegółów kredytu', 'loans-payments' => 'Odczyt harmonogramu spłat', // Produkty inwestycyjne - 'investments-view' => 'Odczyt produktów inwestycyjnych', + 'investments-view' => 'Odczyt produktów inwestycyjnych', 'investments-details' => 'Odczyt szczegółów inwestycji', ]); diff --git a/backend/app/Rules/Password/CurrentPassword.php b/backend/app/Rules/Password/CurrentPassword.php index 31d20dd..875f284 100644 --- a/backend/app/Rules/Password/CurrentPassword.php +++ b/backend/app/Rules/Password/CurrentPassword.php @@ -2,7 +2,6 @@ namespace App\Rules\Password; -use Closure; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Facades\Hash; use Illuminate\Translation\PotentiallyTranslatedString; @@ -12,11 +11,11 @@ class CurrentPassword implements ValidationRule /** * Run the validation rule. * - * @param Closure(string, ?string=): PotentiallyTranslatedString $fail + * @param \Closure(string, ?string=): PotentiallyTranslatedString $fail */ - public function validate(string $attribute, mixed $value, Closure $fail): void + public function validate(string $attribute, mixed $value, \Closure $fail): void { - if (!Hash::check($value, auth()->user()->password)) { + if (! Hash::check($value, auth()->user()->password)) { $fail('The provided password is incorrect.'); } } diff --git a/backend/app/Services/Account/AccountService.php b/backend/app/Services/Account/AccountService.php new file mode 100644 index 0000000..db086af --- /dev/null +++ b/backend/app/Services/Account/AccountService.php @@ -0,0 +1,11 @@ +calculateChecksum($tmpChecksumStr); @@ -30,7 +28,7 @@ public function generate(): string private function generateClientNumber(): string { $random = ''; - for ($i = 0; $i < 16; $i++) { + for ($i = 0; $i < 16; ++$i) { $random .= random_int(0, 9); } @@ -38,24 +36,24 @@ private function generateClientNumber(): string } /** - * Oblicza sumę kontrolną modulo 97 (standard IBAN) + * Oblicza sumę kontrolną modulo 97 (standard IBAN). */ private function calculateChecksum(string $ibanBase): string { $remainder = 0; - $digits = str_split($ibanBase); + $digits = str_split($ibanBase); foreach ($digits as $digit) { - $remainder = (int)(($remainder . $digit) % 97); + $remainder = (int) (($remainder . $digit) % 97); } $checksum = 98 - $remainder; - return str_pad((string)$checksum, 2, '0', STR_PAD_LEFT); + return str_pad((string) $checksum, 2, '0', STR_PAD_LEFT); } /** - * Konwertuj NRB na format IBAN + * Konwertuj NRB na format IBAN. */ public function toIBAN(string $nrb): string { @@ -63,7 +61,7 @@ public function toIBAN(string $nrb): string } /** - * Formatuj numer do wyświetlenia (co 4 cyfry spacja) + * Formatuj numer do wyświetlenia (co 4 cyfry spacja). */ public function format(string $nrb): string { diff --git a/backend/app/Services/Generator/CardNumberGeneratorService.php b/backend/app/Services/Generator/CardNumberGeneratorService.php new file mode 100644 index 0000000..aaae2c3 --- /dev/null +++ b/backend/app/Services/Generator/CardNumberGeneratorService.php @@ -0,0 +1,154 @@ +prefix(); + $prefix = $prefixes[array_rand($prefixes)]; + $prefixLength = strlen($prefix); + + $randomLength = 15 - $prefixLength; + + $number = $prefix; + for ($i = 0; $i < $randomLength; ++$i) { + $number .= random_int(0, 9); + } + + return $number . $this->calculateLuhnChecksum($number); + } + + /** + * Generate CVV for given network. + */ + public function generateCvv(CardNetwork $network = CardNetwork::VISA): string + { + $length = $network->cvvLength(); + + return str_pad((string) random_int(0, 10 ** $length - 1), $length, '0', STR_PAD_LEFT); + } + + /** + * Generate expiry date (4 years from now). + */ + public function generateExpiryDate(): array + { + $now = now(); + $expiryDate = $now->copy()->addYears(4); + + return [ + 'month' => (int) $expiryDate->format('m'), + 'year' => (int) $expiryDate->format('Y'), + ]; + } + + /** + * Generate full card data. + */ + public function generateCardData( + CardNetwork $network = CardNetwork::VISA, + CardType $type = CardType::DEBIT, + ): array { + $expiry = $this->generateExpiryDate(); + + return [ + 'card_number' => $this->generateCardNumber($network), + 'cvv' => $this->generateCvv($network), + 'exp_month' => $expiry['month'], + 'exp_year' => $expiry['year'], + 'network' => $network->value, + 'type' => $type->value, + ]; + } + + /** + * Validate card number using Luhn algorithm. + */ + public function isValidCardNumber(string $number): bool + { + $number = str_replace(' ', '', $number); + + if (16 !== strlen($number) || ! is_numeric($number)) { + return false; + } + + $checksum = (int) $number[15]; + $payload = substr($number, 0, 15); + + return $this->calculateLuhnChecksum($payload) === (string) $checksum; + } + + /** + * Detect card network from number. + */ + public function detectNetwork(string $number): ?CardNetwork + { + $number = str_replace(' ', '', $number); + + foreach (CardNetwork::cases() as $network) { + foreach ($network->prefix() as $prefix) { + if (str_starts_with($number, $prefix)) { + return $network; + } + } + } + + return null; + } + + /** + * Mask card number for display. + */ + public function maskCardNumber(string $number): string + { + $number = str_replace(' ', '', $number); + + return substr($number, 0, 4) . ' **** **** ' . substr($number, -4); + } + + /** + * Format card number (groups of 4). + */ + public function formatCardNumber(string $number): string + { + $number = str_replace(' ', '', $number); + + return trim(chunk_split($number, 4, ' ')); + } + + /** + * Calculate Luhn checksum digit. + */ + private function calculateLuhnChecksum(string $payload): string + { + $digits = str_split($payload); + $sum = 0; + $double = true; + + for ($i = count($digits) - 1; $i >= 0; --$i) { + $digit = (int) $digits[$i]; + + if ($double) { + $digit *= 2; + if ($digit > 9) { + $digit -= 9; + } + } + + $sum += $digit; + $double = ! $double; + } + + $checksum = (10 - ($sum % 10)) % 10; + + return (string) $checksum; + } +} diff --git a/backend/composer.json b/backend/composer.json index 11f6d0d..7107699 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", + "friendsofphp/php-cs-fixer": "^3.95", "laravel/breeze": "^2.4", "laravel/pail": "^1.2.5", "laravel/pao": "^1.0.6", @@ -41,6 +42,8 @@ } }, "scripts": { + "cs:check": "php-cs-fixer fix --dry-run --diff", + "cs:fix": "php-cs-fixer fix", "setup": [ "composer install", "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", diff --git a/backend/composer.lock b/backend/composer.lock index 4d55c55..b3fee59 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e5e1a7abd0d7cc47e14243b34ccc257a", + "content-hash": "b540227e017f2b0979b75c7d66055a41", "packages": [ { "name": "brick/math", @@ -7358,45 +7358,31 @@ ], "packages-dev": [ { - "name": "fakerphp/faker", - "version": "v1.24.1", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" + "php": ">=5.3", + "react/stream": "^1.2" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", "autoload": { "psr-4": { - "Faker\\": "src/Faker/" + "Clue\\React\\NDJson\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7405,57 +7391,75 @@ ], "authors": [ { - "name": "François Zaninotto" + "name": "Christian Lück", + "email": "christian@clue.engineering" } ], - "description": "Faker is a PHP library that generates fake data for you.", + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", "keywords": [ - "data", - "faker", - "fixtures" + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" ], "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, - "time": "2024-11-21T13:46:39+00:00" + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" }, { - "name": "filp/whoops", - "version": "2.18.4", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" + "php": "^7.4 || ^8.0" }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^4.0 || ^5.0" + "conflict": { + "phpstan/phpstan": "<1.11.10" }, - "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { - "dev-master": "2.7-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Whoops\\": "src/Whoops/" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -7464,116 +7468,143 @@ ], "authors": [ { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.4" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { - "url": "https://github.com/denis-sokolov", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2025-08-08T12:00:00+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.1.1", + "name": "composer/semver", + "version": "3.4.4", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { - "php": "^7.4|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-main": "3.x-dev" } }, "autoload": { - "classmap": [ - "hamcrest" - ] + "psr-4": { + "Composer\\Semver\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "This is the PHP port of Hamcrest Matchers", + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", "keywords": [ - "test" + "semantic", + "semver", + "validation", + "versioning" ], "support": { - "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" }, - "time": "2025-04-30T06:54:44+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" }, { - "name": "laravel/agent-detector", - "version": "v2.0.2", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/laravel/agent-detector.git", - "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/agent-detector/zipball/90694b9256099591cf9e55d08c18ba7a00bf099f", - "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^8.2.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "laravel/pint": "^1.24.0", - "pestphp/pest": "^3.8.5|^4.1.0", - "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", - "phpstan/phpstan": "^2.1.26", - "rector/rector": "^2.1.7", - "symfony/var-dumper": "^7.3.3" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Laravel\\AgentDetector\\": "src/" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -7582,66 +7613,83 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Detect if code is running in an AI agent or automated development environment", - "homepage": "https://github.com/laravel/agent-detector", + "description": "Restarts a process without Xdebug.", "keywords": [ - "Agent", - "ai", - "automation", - "claude", - "cursor", - "detection", - "devin", - "php" + "Xdebug", + "performance" ], "support": { - "issues": "https://github.com/laravel/agent-detector/issues", - "source": "https://github.com/laravel/agent-detector" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, - "time": "2026-04-29T18:32:34+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "laravel/breeze", - "version": "v2.4.1", + "name": "ergebnis/agent-detector", + "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/laravel/breeze.git", - "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f" + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/breeze/zipball/28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", - "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", "shasum": "" }, "require": { - "illuminate/console": "^11.0|^12.0|^13.0", - "illuminate/filesystem": "^11.0|^12.0|^13.0", - "illuminate/support": "^11.0|^12.0|^13.0", - "illuminate/validation": "^11.0|^12.0|^13.0", - "php": "^8.2.0", - "symfony/console": "^7.0|^8.0" + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" }, "require-dev": { - "laravel/framework": "^11.0|^12.0|^13.0", - "orchestra/testbench-core": "^9.0|^10.0|^11.0", - "phpstan/phpstan": "^2.0" + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "Laravel\\Breeze\\BreezeServiceProvider" - ] + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" } }, "autoload": { "psr-4": { - "Laravel\\Breeze\\": "src/" + "Ergebnis\\AgentDetector\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7650,70 +7698,44 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" } ], - "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", - "keywords": [ - "auth", - "laravel" - ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", "support": { - "issues": "https://github.com/laravel/breeze/issues", - "source": "https://github.com/laravel/breeze" + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" }, - "time": "2026-03-10T19:59:01+00:00" + "time": "2026-05-07T08:19:07+00:00" }, { - "name": "laravel/pail", - "version": "v1.2.6", + "name": "evenement/evenement", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/laravel/pail.git", - "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", - "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { - "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0|^13.0", - "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", - "illuminate/log": "^10.24|^11.0|^12.0|^13.0", - "illuminate/process": "^10.24|^11.0|^12.0|^13.0", - "illuminate/support": "^10.24|^11.0|^12.0|^13.0", - "nunomaduro/termwind": "^1.15|^2.0", - "php": "^8.2", - "symfony/console": "^6.0|^7.0|^8.0" + "php": ">=7.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0|^13.0", - "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", - "pestphp/pest": "^2.20|^3.0|^4.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", - "phpstan/phpstan": "^1.12.27", - "symfony/var-dumper": "^6.3|^7.0|^8.0", - "symfony/yaml": "^6.3|^7.0|^8.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Pail\\PailServiceProvider" - ] - }, - "branch-alias": { - "dev-main": "1.x-dev" - } - }, "autoload": { "psr-4": { - "Laravel\\Pail\\": "src/" + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7722,83 +7744,61 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" } ], - "description": "Easily delve into your Laravel application's log files directly from the command line.", - "homepage": "https://github.com/laravel/pail", + "description": "Événement is a very simple event dispatching library for PHP", "keywords": [ - "dev", - "laravel", - "logs", - "php", - "tail" + "event-dispatcher", + "event-emitter" ], "support": { - "issues": "https://github.com/laravel/pail/issues", - "source": "https://github.com/laravel/pail" + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2026-02-09T13:44:54+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { - "name": "laravel/pao", - "version": "v1.0.6", + "name": "fakerphp/faker", + "version": "v1.24.1", "source": { "type": "git", - "url": "https://github.com/laravel/pao.git", - "reference": "02f62a64c2b60af44a418ee490fee193590d8269" + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pao/zipball/02f62a64c2b60af44a418ee490fee193590d8269", - "reference": "02f62a64c2b60af44a418ee490fee193590d8269", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", "shasum": "" }, "require": { - "laravel/agent-detector": "^2.0.0", - "php": "^8.3" + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" }, "conflict": { - "laravel/framework": "<12.0.0", - "nunomaduro/collision": "<8.9.3", - "pestphp/pest": "<4.6.3 || >=6.0.0", - "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + "fzaninotto/faker": "*" }, "require-dev": { - "brianium/paratest": "^7.20.0", - "laravel/pint": "^1.29.1", - "orchestra/testbench": "^10.11.0 || ^11.1.0", - "pestphp/pest": "^4.6.3 || ^5.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", - "phpstan/phpstan": "^2.1.51", - "rector/rector": "^2.4.2", - "symfony/process": "^7.4.8 || ^8.1.0", - "symfony/var-dumper": "^7.4.8 || ^8.0.8" + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" }, - "type": "library", - "extra": { - "pest": { - "plugins": [ - "Laravel\\Pao\\Drivers\\Pest\\Plugin" - ] - }, - "laravel": { - "providers": [ - "Laravel\\Pao\\Laravel\\ServiceProvider" - ] - } + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." }, + "type": "library", "autoload": { - "files": [ - "src/Autoload.php" - ], "psr-4": { - "Laravel\\Pao\\": "src/" + "Faker\\": "src/Faker/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7807,68 +7807,53 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "François Zaninotto" } ], - "description": "Agent-optimized output for PHP testing tools", + "description": "Faker is a PHP library that generates fake data for you.", "keywords": [ - "Agent", - "PHPStan", - "ai", - "dev", - "paratest", - "pest", - "php", - "phpunit", - "testing" + "data", + "faker", + "fixtures" ], "support": { - "issues": "https://github.com/laravel/pao/issues", - "source": "https://github.com/laravel/pao" + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" }, - "time": "2026-04-27T22:37:26+00:00" + "time": "2024-11-21T13:46:39+00:00" }, { - "name": "laravel/pint", - "version": "v1.29.1", + "name": "fidry/cpu-core-counter", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/laravel/pint.git", - "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", - "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { - "ext-json": "*", - "ext-mbstring": "*", - "ext-tokenizer": "*", - "ext-xml": "*", - "php": "^8.2.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.1", - "illuminate/view": "^12.56.0", - "larastan/larastan": "^3.9.6", - "laravel-zero/framework": "^12.1.0", - "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.6", - "shipfastlabs/agent-detector": "^1.1.3" + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, - "bin": [ - "builds/pint" - ], - "type": "project", + "type": "library", "autoload": { "psr-4": { - "App\\": "app/", - "Database\\Seeders\\": "database/seeders/", - "Database\\Factories\\": "database/factories/" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7877,234 +7862,1306 @@ ], "authors": [ { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "An opinionated code formatter for PHP.", - "homepage": "https://laravel.com", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "dev", - "format", - "formatter", - "lint", - "linter", - "php" + "CPU", + "core" ], "support": { - "issues": "https://github.com/laravel/pint/issues", - "source": "https://github.com/laravel/pint" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, - "time": "2026-04-20T15:26:14+00:00" + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" }, { - "name": "mockery/mockery", - "version": "1.6.12", + "name": "filp/whoops", + "version": "2.18.4", "source": { "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "^2.0.1", - "lib-pcre": ">=7.0", - "php": ">=7.3" - }, - "conflict": { - "phpunit/phpunit": "<8.0" + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.17", - "symplify/easy-coding-standard": "^12.1.14" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, "autoload": { - "files": [ - "library/helpers.php", - "library/Mockery.php" - ], "psr-4": { - "Mockery\\": "library/Mockery" + "Whoops\\": "src/Whoops/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "https://github.com/padraic", - "role": "Author" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "https://davedevelopment.co.uk", + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", "role": "Developer" - }, - { - "name": "Nathanael Esayeas", - "email": "nathanael.esayeas@protonmail.com", - "homepage": "https://github.com/ghostwriter", - "role": "Lead Developer" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", "keywords": [ - "BDD", - "TDD", + "error", + "exception", + "handling", "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" + "throwable", + "whoops" ], "support": { - "docs": "https://docs.mockery.io/", - "issues": "https://github.com/mockery/mockery/issues", - "rss": "https://github.com/mockery/mockery/releases.atom", - "security": "https://github.com/mockery/mockery/security/advisories", - "source": "https://github.com/mockery/mockery" + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" }, - "time": "2024-05-16T03:13:13+00:00" + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" }, { - "name": "nunomaduro/collision", - "version": "v8.9.4", + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.1", "source": { "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a9727678fbd12997f1d9de8f4a37824ed9df1065", + "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.1.1", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.8.0", + "infection/infection": "^0.32.6", + "justinrainbow/json-schema": "^6.8.0", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.1" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-04-12T17:00:09+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/agent-detector", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/agent-detector.git", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/agent-detector/zipball/90694b9256099591cf9e55d08c18ba7a00bf099f", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f", + "shasum": "" + }, + "require": { + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^3.8.5|^4.1.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7", + "symfony/var-dumper": "^7.3.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Laravel\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Detect if code is running in an AI agent or automated development environment", + "homepage": "https://github.com/laravel/agent-detector", + "keywords": [ + "Agent", + "ai", + "automation", + "claude", + "cursor", + "detection", + "devin", + "php" + ], + "support": { + "issues": "https://github.com/laravel/agent-detector/issues", + "source": "https://github.com/laravel/agent-detector" + }, + "time": "2026-04-29T18:32:34+00:00" + }, + { + "name": "laravel/breeze", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", + "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/filesystem": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "illuminate/validation": "^11.0|^12.0|^13.0", + "php": "^8.2.0", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^11.0|^12.0|^13.0", + "orchestra/testbench-core": "^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2026-03-10T19:59:01+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pao", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pao.git", + "reference": "02f62a64c2b60af44a418ee490fee193590d8269" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pao/zipball/02f62a64c2b60af44a418ee490fee193590d8269", + "reference": "02f62a64c2b60af44a418ee490fee193590d8269", + "shasum": "" + }, + "require": { + "laravel/agent-detector": "^2.0.0", + "php": "^8.3" + }, + "conflict": { + "laravel/framework": "<12.0.0", + "nunomaduro/collision": "<8.9.3", + "pestphp/pest": "<4.6.3 || >=6.0.0", + "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.20.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench": "^10.11.0 || ^11.1.0", + "pestphp/pest": "^4.6.3 || ^5.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", + "phpstan/phpstan": "^2.1.51", + "rector/rector": "^2.4.2", + "symfony/process": "^7.4.8 || ^8.1.0", + "symfony/var-dumper": "^7.4.8 || ^8.0.8" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Laravel\\Pao\\Drivers\\Pest\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Laravel\\Pao\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Laravel\\Pao\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Agent-optimized output for PHP testing tools", + "keywords": [ + "Agent", + "PHPStan", + "ai", + "dev", + "paratest", + "pest", + "php", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/laravel/pao/issues", + "source": "https://github.com/laravel/pao" + }, + "time": "2026-04-27T22:37:26+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-04-20T15:26:14+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "876099a072646c7745f673d7aeab5382c4439691" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-04-15T08:23:17+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "filp/whoops": "^2.18.4", - "nunomaduro/termwind": "^2.4.0", - "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.8" - }, - "conflict": { - "laravel/framework": "<11.48.0 || >=14.0.0", - "phpunit/phpunit": "<11.5.50 || >=14.0.0" + "php": ">=8.3" }, "require-dev": { - "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.6", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", - "laravel/pint": "^1.29.1", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", - "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - }, "branch-alias": { - "dev-8.x": "8.x-dev" + "dev-main": "8.0-dev" } }, "autoload": { - "files": [ - "./src/Adapters/Phpunit/Autoload.php" - ], - "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Cli error handling for console/command-line PHP applications.", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "dev", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" + "timer" ], "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" } ], - "time": "2026-04-21T14:04:20+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "phpunit/phpunit", + "version": "12.5.24", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", "shasum": "" }, "require": { "ext-dom": "*", + "ext-json": "*", "ext-libxml": "*", - "ext-phar": "*", + "ext-mbstring": "*", + "ext-xml": "*", "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.6", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.0", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "12.5-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -8114,520 +9171,557 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", - "role": "Developer" + "role": "lead" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" }, "funding": [ { - "url": "https://github.com/theseer", - "type": "github" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2026-05-01T04:21:04+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Cache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Library for handling version information and constraints", + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, - "time": "2022-02-21T01:04:05+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "12.5.6", + "name": "react/child-process", + "version": "v0.6.7", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "876099a072646c7745f673d7aeab5382c4439691" + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", - "reference": "876099a072646c7745f673d7aeab5382c4439691", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.3", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0.3", - "sebastian/lines-of-code": "^4.0", - "sebastian/version": "^6.0", - "theseer/tokenizer": "^2.0.1" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.1" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "12.5.x-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\ChildProcess\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", - "type": "tidelift" + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2026-04-15T08:23:17+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "name": "react/dns", + "version": "v1.14.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Dns\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "Async DNS resolver for ReactPHP", "keywords": [ - "filesystem", - "iterator" + "async", + "dns", + "dns-resolver", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", - "type": "tidelift" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { - "name": "phpunit/php-invoker", - "version": "6.0.0", + "name": "react/event-loop", + "version": "v1.6.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=5.3.0" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-pcntl": "*" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\EventLoop\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "process" + "asynchronous", + "event-loop" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { - "name": "phpunit/php-text-template", - "version": "5.0.0", + "name": "react/promise", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "template" + "promise", + "promises" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { - "name": "phpunit/php-timer", - "version": "8.0.0", + "name": "react/socket", + "version": "v1.17.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { - "php": ">=8.3" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "8.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Socket\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "timer" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { - "name": "phpunit/phpunit", - "version": "12.5.24", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.6", - "phpunit/php-file-iterator": "^6.0.1", - "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", - "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.6", - "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.0", - "sebastian/exporter": "^7.0.2", - "sebastian/global-state": "^8.0.2", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.3", - "sebastian/version": "^6.0.0", - "staabm/side-effects-detector": "^1.0.5" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "12.5-dev" - } + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, + "type": "library", "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Stream\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": [ - "phpunit", - "testing", - "xunit" + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { - "url": "https://phpunit.de/sponsoring.html", - "type": "other" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2026-05-01T04:21:04+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { "name": "sebastian/cli-parser", @@ -9578,6 +10672,293 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/filesystem", + "version": "v8.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", + "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:51:42+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, { "name": "theseer/tokenizer", "version": "2.0.1", diff --git a/backend/config/app.php b/backend/config/app.php index 423eed5..218c6c3 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -1,7 +1,8 @@ [ ...array_filter( - explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + explode(',', (string) env('APP_PREVIOUS_KEYS', '')), ), ], @@ -120,7 +121,6 @@ 'maintenance' => [ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), - 'store' => env('APP_MAINTENANCE_STORE', 'database'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], - ]; diff --git a/backend/config/auth.php b/backend/config/auth.php index 24d8311..a93af87 100644 --- a/backend/config/auth.php +++ b/backend/config/auth.php @@ -1,9 +1,10 @@ [ - 'guard' => env('AUTH_GUARD', 'web'), + 'guard' => env('AUTH_GUARD', 'web'), 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), ], @@ -39,12 +40,12 @@ 'guards' => [ 'web' => [ - 'driver' => 'session', + 'driver' => 'session', 'provider' => 'users', ], 'api' => [ - 'driver' => 'passport', + 'driver' => 'passport', 'provider' => 'users', ], ], @@ -69,7 +70,7 @@ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', User::class), + 'model' => env('AUTH_MODEL', User::class), ], // 'users' => [ @@ -100,8 +101,8 @@ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), - 'expire' => 60, + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, 'throttle' => 60, ], ], @@ -118,5 +119,4 @@ */ 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), - ]; diff --git a/backend/config/cache.php b/backend/config/cache.php index c68acdf..b775a48 100644 --- a/backend/config/cache.php +++ b/backend/config/cache.php @@ -1,9 +1,10 @@ [ - 'array' => [ - 'driver' => 'array', + 'driver' => 'array', 'serialize' => false, ], 'database' => [ - 'driver' => 'database', - 'connection' => env('DB_CACHE_CONNECTION'), - 'table' => env('DB_CACHE_TABLE', 'cache'), + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), - 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), ], 'file' => [ - 'driver' => 'file', - 'path' => storage_path('framework/cache/data'), + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), 'lock_path' => storage_path('framework/cache/data'), ], 'memcached' => [ - 'driver' => 'memcached', + 'driver' => 'memcached', 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), - 'sasl' => [ + 'sasl' => [ env('MEMCACHED_USERNAME'), env('MEMCACHED_PASSWORD'), ], @@ -65,25 +65,25 @@ ], 'servers' => [ [ - 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => env('MEMCACHED_PORT', 11211), + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), 'weight' => 100, ], ], ], 'redis' => [ - 'driver' => 'redis', - 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), ], 'dynamodb' => [ - 'driver' => 'dynamodb', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 'endpoint' => env('DYNAMODB_ENDPOINT'), ], @@ -98,7 +98,6 @@ 'array', ], ], - ], /* @@ -112,7 +111,7 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-'), /* |-------------------------------------------------------------------------- @@ -126,5 +125,4 @@ */ 'serializable_classes' => false, - ]; diff --git a/backend/config/cors.php b/backend/config/cors.php index 5fc46ad..9b05c6f 100644 --- a/backend/config/cors.php +++ b/backend/config/cors.php @@ -1,7 +1,8 @@ 0, 'supports_credentials' => true, - ]; diff --git a/backend/config/database.php b/backend/config/database.php index 64709ce..55b8124 100644 --- a/backend/config/database.php +++ b/backend/config/database.php @@ -1,10 +1,11 @@ [ - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DB_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - 'busy_timeout' => null, - 'journal_mode' => null, - 'synchronous' => null, - 'transaction_mode' => 'DEFERRED', + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', ], 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), - 'prefix' => '', + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], 'mariadb' => [ - 'driver' => 'mariadb', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), - 'prefix' => '', + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => env('DB_CHARSET', 'utf8'), - 'prefix' => '', + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', 'prefix_indexes' => true, - 'search_path' => 'public', - 'sslmode' => env('DB_SSLMODE', 'prefer'), + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), ], 'sqlsrv' => [ - 'driver' => 'sqlsrv', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => env('DB_CHARSET', 'utf8'), - 'prefix' => '', + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', 'prefix_indexes' => true, // 'encrypt' => env('DB_ENCRYPT', 'yes'), // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], - ], /* @@ -128,7 +127,7 @@ */ 'migrations' => [ - 'table' => 'migrations', + 'table' => 'migrations', 'update_date_on_publish' => true, ], @@ -144,41 +143,38 @@ */ 'redis' => [ - 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ - 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-database-'), 'persistent' => env('REDIS_PERSISTENT', false), ], 'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'username' => env('REDIS_USERNAME'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_DB', '0'), - 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), - 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), - 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], 'cache' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'username' => env('REDIS_USERNAME'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_CACHE_DB', '1'), - 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), - 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), - 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], - ], - ]; diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php index 37d8fca..c12e4f3 100644 --- a/backend/config/filesystems.php +++ b/backend/config/filesystems.php @@ -1,7 +1,8 @@ [ - 'local' => [ 'driver' => 'local', - 'root' => storage_path('app/private'), - 'serve' => true, - 'throw' => false, + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, 'report' => false, ], 'public' => [ - 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/') . '/storage', 'visibility' => 'public', - 'throw' => false, - 'report' => false, + 'throw' => false, + 'report' => false, ], 's3' => [ - 'driver' => 's3', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), - 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - 'endpoint' => env('AWS_ENDPOINT'), + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), - 'throw' => false, - 'report' => false, + 'throw' => false, + 'report' => false, ], - ], /* @@ -76,5 +75,4 @@ 'links' => [ public_path('storage') => storage_path('app/public'), ], - ]; diff --git a/backend/config/logging.php b/backend/config/logging.php index b09cb25..26c7eaa 100644 --- a/backend/config/logging.php +++ b/backend/config/logging.php @@ -1,12 +1,13 @@ [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), - 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], /* @@ -51,82 +52,79 @@ */ 'channels' => [ - 'stack' => [ - 'driver' => 'stack', - 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], 'single' => [ - 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'daily' => [ - 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'days' => env('LOG_DAILY_DAYS', 14), + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, ], 'slack' => [ - 'driver' => 'slack', - 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), - 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), - 'level' => env('LOG_LEVEL', 'critical'), + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), 'replace_placeholders' => true, ], 'papertrail' => [ - 'driver' => 'monolog', - 'level' => env('LOG_LEVEL', 'debug'), - 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 'handler_with' => [ - 'host' => env('PAPERTRAIL_URL'), - 'port' => env('PAPERTRAIL_PORT'), - 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), ], 'processors' => [PsrLogMessageProcessor::class], ], 'stderr' => [ - 'driver' => 'monolog', - 'level' => env('LOG_LEVEL', 'debug'), - 'handler' => StreamHandler::class, + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, 'handler_with' => [ 'stream' => 'php://stderr', ], - 'formatter' => env('LOG_STDERR_FORMATTER'), + 'formatter' => env('LOG_STDERR_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], 'syslog' => [ - 'driver' => 'syslog', - 'level' => env('LOG_LEVEL', 'debug'), - 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 'replace_placeholders' => true, ], 'errorlog' => [ - 'driver' => 'errorlog', - 'level' => env('LOG_LEVEL', 'debug'), + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'null' => [ - 'driver' => 'monolog', + 'driver' => 'monolog', 'handler' => NullHandler::class, ], 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], - ], - ]; diff --git a/backend/config/mail.php b/backend/config/mail.php index e32e88d..f4ff05a 100644 --- a/backend/config/mail.php +++ b/backend/config/mail.php @@ -1,7 +1,8 @@ [ - 'smtp' => [ - 'transport' => 'smtp', - 'scheme' => env('MAIL_SCHEME'), - 'url' => env('MAIL_URL'), - 'host' => env('MAIL_HOST', '127.0.0.1'), - 'port' => env('MAIL_PORT', 2525), - 'username' => env('MAIL_USERNAME'), - 'password' => env('MAIL_PASSWORD'), - 'timeout' => null, + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], @@ -67,12 +67,12 @@ 'sendmail' => [ 'transport' => 'sendmail', - 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), ], 'log' => [ 'transport' => 'log', - 'channel' => env('MAIL_LOG_CHANNEL'), + 'channel' => env('MAIL_LOG_CHANNEL'), ], 'array' => [ @@ -81,7 +81,7 @@ 'failover' => [ 'transport' => 'failover', - 'mailers' => [ + 'mailers' => [ 'smtp', 'log', ], @@ -90,13 +90,12 @@ 'roundrobin' => [ 'transport' => 'roundrobin', - 'mailers' => [ + 'mailers' => [ 'ses', 'postmark', ], 'retry_after' => 60, ], - ], /* @@ -112,7 +111,6 @@ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), ], - ]; diff --git a/backend/config/passport.php b/backend/config/passport.php index 11747e2..0b46d33 100644 --- a/backend/config/passport.php +++ b/backend/config/passport.php @@ -1,7 +1,8 @@ [ - 'sync' => [ 'driver' => 'sync', ], 'database' => [ - 'driver' => 'database', - 'connection' => env('DB_QUEUE_CONNECTION'), - 'table' => env('DB_QUEUE_TABLE', 'jobs'), - 'queue' => env('DB_QUEUE', 'default'), - 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 'after_commit' => false, ], 'beanstalkd' => [ - 'driver' => 'beanstalkd', - 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), - 'queue' => env('BEANSTALKD_QUEUE', 'default'), - 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), - 'block_for' => 0, + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, 'after_commit' => false, ], 'sqs' => [ - 'driver' => 'sqs', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), - 'queue' => env('SQS_QUEUE', 'default'), - 'suffix' => env('SQS_SUFFIX'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'after_commit' => false, ], 'redis' => [ - 'driver' => 'redis', - 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), - 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), - 'block_for' => null, + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, 'after_commit' => false, ], @@ -82,13 +82,12 @@ ], 'failover' => [ - 'driver' => 'failover', + 'driver' => 'failover', 'connections' => [ 'database', 'deferred', ], ], - ], /* @@ -104,7 +103,7 @@ 'batching' => [ 'database' => env('DB_CONNECTION', 'sqlite'), - 'table' => 'job_batches', + 'table' => 'job_batches', ], /* @@ -121,9 +120,8 @@ */ 'failed' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'sqlite'), - 'table' => 'failed_jobs', + 'table' => 'failed_jobs', ], - ]; diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php index 04a819e..442acc4 100644 --- a/backend/config/sanctum.php +++ b/backend/config/sanctum.php @@ -1,9 +1,10 @@ [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], - ]; diff --git a/backend/config/scramble.php b/backend/config/scramble.php index f7fdc9d..9190d42 100644 --- a/backend/config/scramble.php +++ b/backend/config/scramble.php @@ -1,5 +1,7 @@ null, - /** + /* * Determines how Scramble stores the descriptions of enum cases. * Available options: * - 'description' – Case descriptions are stored as the enum schema's description using table formatting. @@ -104,7 +106,7 @@ */ 'enum_cases_description_strategy' => 'description', - /** + /* * Determines how Scramble stores the names of enum cases. * Available options: * - 'names' – Case names are stored in the `x-enumNames` enum schema extension. @@ -113,7 +115,7 @@ */ 'enum_cases_names_strategy' => false, - /** + /* * When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated * OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until * OpenAPI 3.2 is released and query string structure can be described properly. diff --git a/backend/config/services.php b/backend/config/services.php index 6a90eb8..3a8d891 100644 --- a/backend/config/services.php +++ b/backend/config/services.php @@ -1,7 +1,8 @@ [ - 'key' => env('AWS_ACCESS_KEY_ID'), + 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], @@ -31,8 +32,7 @@ 'slack' => [ 'notifications' => [ 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), - 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], - ]; diff --git a/backend/config/session.php b/backend/config/session.php index f574482..575c4bd 100644 --- a/backend/config/session.php +++ b/backend/config/session.php @@ -1,9 +1,10 @@ env( 'SESSION_COOKIE', - Str::slug((string) env('APP_NAME', 'laravel')).'-session' + Str::slug((string) env('APP_NAME', 'laravel')) . '-session', ), /* @@ -229,5 +230,4 @@ */ 'serialization' => 'json', - ]; diff --git a/backend/database/factories/Account/AccountFactory.php b/backend/database/factories/Account/AccountFactory.php new file mode 100644 index 0000000..0b13d68 --- /dev/null +++ b/backend/database/factories/Account/AccountFactory.php @@ -0,0 +1,20 @@ + User::factory(), + 'account_number' => $this->faker->unique()->iban(), + 'balance' => $this->faker->randomFloat(2, 0, 10000), + 'currency' => 'PLN', + 'type' => 'current', + ]; + } +} diff --git a/backend/database/factories/Account/Card/CardFactory.php b/backend/database/factories/Account/Card/CardFactory.php new file mode 100644 index 0000000..1b4a821 --- /dev/null +++ b/backend/database/factories/Account/Card/CardFactory.php @@ -0,0 +1,23 @@ + + */ +class CardFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + ]; + } +} diff --git a/backend/database/factories/Account/Flik/FlikCodeFactory.php b/backend/database/factories/Account/Flik/FlikCodeFactory.php new file mode 100644 index 0000000..ca796ec --- /dev/null +++ b/backend/database/factories/Account/Flik/FlikCodeFactory.php @@ -0,0 +1,23 @@ + + */ +class FlikCodeFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + ]; + } +} diff --git a/backend/database/factories/Account/Transaction/TransactionFactory.php b/backend/database/factories/Account/Transaction/TransactionFactory.php new file mode 100644 index 0000000..b5c34c5 --- /dev/null +++ b/backend/database/factories/Account/Transaction/TransactionFactory.php @@ -0,0 +1,31 @@ + + */ +class TransactionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'from_account_id' => null, + 'to_account_id' => null, + 'from_card_id' => null, + 'amount' => $this->faker->randomFloat(2, 1, 1000), + 'description' => $this->faker->sentence(), + 'type' => $this->faker->randomElement(['transfer', 'payment', 'withdrawal']), + 'status' => $this->faker->randomElement(['pending', 'completed', 'failed']), + 'reference' => $this->faker->uuid(), + ]; + } +} diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index 1a5bb59..f4ebfc4 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -26,11 +26,11 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), - 'email' => static::$email ??= fake()->unique()->safeEmail(), + 'name' => fake()->name(), + 'email' => static::$email ??= fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), ]; } diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..e7e014f 100644 --- a/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -1,18 +1,16 @@ id(); + $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); @@ -29,7 +27,7 @@ public function up(): void Schema::create('sessions', function (Blueprint $table) { $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('user_id')->nullable()->index(); $table->string('ip_address', 45)->nullable(); $table->text('user_agent')->nullable(); $table->longText('payload'); diff --git a/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php index 06dc7a5..676d272 100644 --- a/backend/database/migrations/0001_01_01_000001_create_cache_table.php +++ b/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -1,11 +1,9 @@ char('id', 80)->primary(); - $table->foreignId('user_id')->index(); + $table->foreignUuid('user_id')->index(); $table->foreignUuid('client_id'); $table->text('scopes')->nullable(); $table->boolean('revoked'); diff --git a/backend/database/migrations/2026_05_05_200012_create_oauth_access_tokens_table.php b/backend/database/migrations/2026_05_05_200012_create_oauth_access_tokens_table.php index 3e50f7f..15cea58 100644 --- a/backend/database/migrations/2026_05_05_200012_create_oauth_access_tokens_table.php +++ b/backend/database/migrations/2026_05_05_200012_create_oauth_access_tokens_table.php @@ -1,11 +1,9 @@ char('id', 80)->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('user_id')->nullable()->index(); $table->foreignUuid('client_id'); $table->string('name')->nullable(); $table->text('scopes')->nullable(); diff --git a/backend/database/migrations/2026_05_05_200013_create_oauth_refresh_tokens_table.php b/backend/database/migrations/2026_05_05_200013_create_oauth_refresh_tokens_table.php index afb3c55..521e251 100644 --- a/backend/database/migrations/2026_05_05_200013_create_oauth_refresh_tokens_table.php +++ b/backend/database/migrations/2026_05_05_200013_create_oauth_refresh_tokens_table.php @@ -1,11 +1,9 @@ char('id', 80)->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('user_id')->nullable()->index(); $table->foreignUuid('client_id')->index(); $table->char('user_code', 8)->unique(); $table->text('scopes'); diff --git a/backend/database/migrations/2026_05_09_170729_accounts.php b/backend/database/migrations/2026_05_09_170729_accounts.php new file mode 100644 index 0000000..103b24d --- /dev/null +++ b/backend/database/migrations/2026_05_09_170729_accounts.php @@ -0,0 +1,80 @@ +uuid('id')->primary(); + $table->uuid('user_id'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->string('name'); + $table->string('account_number', 32)->unique(); + $table->bigInteger('balance')->default(0); + $table->string('currency', 3)->default('PLN'); + $table->string('type', 20)->default('current'); + $table->timestamps(); + }); + + Schema::create('cards', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('account_id')->constrained()->onDelete('cascade'); + $table->string('card_number', 16)->unique(); + $table->unsignedTinyInteger('exp_month'); + $table->unsignedSmallInteger('exp_year'); + $table->string('cvv', 4); + $table->string('status', 20)->default('active'); + $table->string('type', 20)->default('debit'); + $table->timestamps(); + + $table->index('account_id'); + }); + + Schema::create('transactions', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('from_account_id')->nullable()->constrained('accounts')->onDelete('set null'); + $table->foreignUuid('to_account_id')->nullable()->constrained('accounts')->onDelete('set null'); + $table->foreignUuid('from_card_id')->nullable()->constrained('cards')->onDelete('set null'); + $table->bigInteger('amount'); + $table->string('description')->nullable(); + $table->string('type', 30); + $table->string('status', 20)->default('pending'); + $table->string('reference', 64)->nullable(); + $table->timestamps(); + + $table->index('from_account_id'); + $table->index('to_account_id'); + $table->index('from_card_id'); + $table->index('reference'); + }); + + Schema::create('flik_codes', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('card_id')->constrained()->onDelete('cascade'); + $table->string('code', 6); + $table->timestamp('expires_at'); + $table->string('status', 20)->default('active'); + $table->foreignUuid('used_in_transaction_id')->nullable()->constrained('transactions')->onDelete('set null'); + $table->timestamps(); + + $table->index('code'); + $table->index('card_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('flik_codes'); + Schema::dropIfExists('transactions'); + Schema::dropIfExists('cards'); + Schema::dropIfExists('accounts'); + } +}; diff --git a/backend/database/migrations/2026_05_09_173714_create_notifications_table.php b/backend/database/migrations/2026_05_09_173714_create_notifications_table.php new file mode 100644 index 0000000..b51ae15 --- /dev/null +++ b/backend/database/migrations/2026_05_09_173714_create_notifications_table.php @@ -0,0 +1,29 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index e0615ce..76dd6d5 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -17,11 +17,11 @@ public function run(): void { // User::factory(10)->create(); -// User::factory()->create([ -// 'name' => 'Test User', -// 'email' => 'test@example.com', -// 'password' => 'password', -// ]); + // User::factory()->create([ + // 'name' => 'Test User', + // 'email' => 'test@example.com', + // 'password' => 'password', + // ]); $this->call([ PassportClientSeeder::class, diff --git a/backend/database/seeders/PassportClientSeeder.php b/backend/database/seeders/PassportClientSeeder.php index f86aebe..5df221b 100644 --- a/backend/database/seeders/PassportClientSeeder.php +++ b/backend/database/seeders/PassportClientSeeder.php @@ -6,35 +6,36 @@ use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; -use Laravel\Passport\Client; -use Laravel\Passport\ClientRepository; class PassportClientSeeder extends Seeder { public function run(): void { - $clientId = config('passport.client_id', 'bank-client-id'); + $clientId = config('passport.client_id', 'bank-client-id'); $clientSecret = config('passport.client_secret', 'bank-client-secret'); - $nameApp = config('app.name', 'Bank Online System'); - $redirectUri = config('passport.redirect_uri', ''); - $grantTypes = ['authorization_code', 'refresh_token', 'password', 'client_credentials']; + $nameApp = config('app.name', 'Bank Online System'); + $redirectUri = config('passport.redirect_uri', ''); + $grantTypes = ['authorization_code', 'refresh_token', 'password', 'client_credentials']; - if (!$users = User::query()->where('email', 'test@example.com')->get()) { - $users = User::factory()->create([ + $user = User::where('email', 'test@example.com')->first(); + + if (! $user) { + $user = User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', 'password' => Hash::make('password'), - 'email' => 'test@example.com' ]); - } - foreach ($users as $user) { - $this->command->info(' User created: ' . $user->email); + $this->command->info('✅ User created: ' . $user->email); $this->command->info(' Password: password'); + } else { + $this->command->info('⚠️ User already exists: ' . $user->email); } $exists = DB::table('oauth_clients') - ->where('id', $clientId) - ->orWhere('name', $nameApp) - ->exists(); + ->where('id', $clientId) + ->orWhere('name', $nameApp) + ->exists(); if ($exists) { $this->command->info('⚠️ Passport client already exists. Updating...'); @@ -43,10 +44,10 @@ public function run(): void ->where('id', $clientId) ->orWhere('name', $nameApp) ->update([ - 'secret' => Hash::make($clientSecret), + 'secret' => Hash::make($clientSecret), 'redirect_uris' => json_encode([$redirectUri]), - 'updated_at' => now(), - 'grant_types' => json_encode($grantTypes), + 'updated_at' => now(), + 'grant_types' => json_encode($grantTypes), ]); $this->command->info('✅ Passport client updating!'); @@ -58,15 +59,15 @@ public function run(): void } DB::table('oauth_clients')->insert([ - 'id' => $clientId, - 'name' => $nameApp, - 'secret' => Hash::make($clientSecret), - 'provider' => 'users', - 'redirect_uris' => json_encode([$redirectUri]), - 'grant_types' => json_encode($grantTypes), - 'revoked' => 0, - 'created_at' => now(), - 'updated_at' => now(), + 'id' => $clientId, + 'name' => $nameApp, + 'secret' => Hash::make($clientSecret), + 'provider' => 'users', + 'redirect_uris' => json_encode([$redirectUri]), + 'grant_types' => json_encode($grantTypes), + 'revoked' => 0, + 'created_at' => now(), + 'updated_at' => now(), ]); $this->command->info('✅ Passport client created!'); diff --git a/backend/routes/api.php b/backend/routes/api.php index 22d15f7..e9ed4b0 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,13 +1,11 @@ group(function () { - Route::prefix('/user')->group(function() { + Route::prefix('/user')->group(function () { Route::middleware('guest')->withoutMiddleware(['auth:api'])->group(function () { Route::post('/register', [ProfileController::class, 'create']); }); @@ -22,7 +20,7 @@ Route::middleware([CheckToken::using('user-balance')])->get('/balance', function (Request $request) { return [ - 'balance' => 0.00, + 'balance' => 0.00, 'currency' => 'PLN', ]; }); diff --git a/backend/routes/auth.php b/backend/routes/auth.php index 3926ecf..56dc91f 100644 --- a/backend/routes/auth.php +++ b/backend/routes/auth.php @@ -1,5 +1,4 @@ name('profile.destroy'); }); -require __DIR__.'/auth.php'; +require __DIR__ . '/auth.php'; diff --git a/backend/tests/Feature/Auth/AuthenticationTest.php b/backend/tests/Feature/Auth/AuthenticationTest.php index 13dcb7c..cc7df8c 100644 --- a/backend/tests/Feature/Auth/AuthenticationTest.php +++ b/backend/tests/Feature/Auth/AuthenticationTest.php @@ -10,19 +10,19 @@ class AuthenticationTest extends TestCase { use RefreshDatabase; - public function test_login_screen_can_be_rendered(): void + public function testLoginScreenCanBeRendered(): void { $response = $this->get('/login'); $response->assertStatus(200); } - public function test_users_can_authenticate_using_the_login_screen(): void + public function testUsersCanAuthenticateUsingTheLoginScreen(): void { $user = User::factory()->create(); $response = $this->post('/login', [ - 'email' => $user->email, + 'email' => $user->email, 'password' => 'password', ]); @@ -30,19 +30,19 @@ public function test_users_can_authenticate_using_the_login_screen(): void $response->assertRedirect(route('dashboard', absolute: false)); } - public function test_users_can_not_authenticate_with_invalid_password(): void + public function testUsersCanNotAuthenticateWithInvalidPassword(): void { $user = User::factory()->create(); $this->post('/login', [ - 'email' => $user->email, + 'email' => $user->email, 'password' => 'wrong-password', ]); $this->assertGuest(); } - public function test_users_can_logout(): void + public function testUsersCanLogout(): void { $user = User::factory()->create(); diff --git a/backend/tests/Feature/Auth/EmailVerificationTest.php b/backend/tests/Feature/Auth/EmailVerificationTest.php index 705570b..688423f 100644 --- a/backend/tests/Feature/Auth/EmailVerificationTest.php +++ b/backend/tests/Feature/Auth/EmailVerificationTest.php @@ -13,7 +13,7 @@ class EmailVerificationTest extends TestCase { use RefreshDatabase; - public function test_email_verification_screen_can_be_rendered(): void + public function testEmailVerificationScreenCanBeRendered(): void { $user = User::factory()->unverified()->create(); @@ -22,7 +22,7 @@ public function test_email_verification_screen_can_be_rendered(): void $response->assertStatus(200); } - public function test_email_can_be_verified(): void + public function testEmailCanBeVerified(): void { $user = User::factory()->unverified()->create(); @@ -31,24 +31,24 @@ public function test_email_can_be_verified(): void $verificationUrl = URL::temporarySignedRoute( 'verification.verify', now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1($user->email)] + ['id' => $user->id, 'hash' => sha1($user->email)], ); $response = $this->actingAs($user)->get($verificationUrl); Event::assertDispatched(Verified::class); $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + $response->assertRedirect(route('dashboard', absolute: false) . '?verified=1'); } - public function test_email_is_not_verified_with_invalid_hash(): void + public function testEmailIsNotVerifiedWithInvalidHash(): void { $user = User::factory()->unverified()->create(); $verificationUrl = URL::temporarySignedRoute( 'verification.verify', now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1('wrong-email')] + ['id' => $user->id, 'hash' => sha1('wrong-email')], ); $this->actingAs($user)->get($verificationUrl); diff --git a/backend/tests/Feature/Auth/PasswordConfirmationTest.php b/backend/tests/Feature/Auth/PasswordConfirmationTest.php index ff85721..322c2cc 100644 --- a/backend/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/backend/tests/Feature/Auth/PasswordConfirmationTest.php @@ -10,7 +10,7 @@ class PasswordConfirmationTest extends TestCase { use RefreshDatabase; - public function test_confirm_password_screen_can_be_rendered(): void + public function testConfirmPasswordScreenCanBeRendered(): void { $user = User::factory()->create(); @@ -19,7 +19,7 @@ public function test_confirm_password_screen_can_be_rendered(): void $response->assertStatus(200); } - public function test_password_can_be_confirmed(): void + public function testPasswordCanBeConfirmed(): void { $user = User::factory()->create(); @@ -31,7 +31,7 @@ public function test_password_can_be_confirmed(): void $response->assertSessionHasNoErrors(); } - public function test_password_is_not_confirmed_with_invalid_password(): void + public function testPasswordIsNotConfirmedWithInvalidPassword(): void { $user = User::factory()->create(); diff --git a/backend/tests/Feature/Auth/PasswordResetTest.php b/backend/tests/Feature/Auth/PasswordResetTest.php index aa50350..1355b6b 100644 --- a/backend/tests/Feature/Auth/PasswordResetTest.php +++ b/backend/tests/Feature/Auth/PasswordResetTest.php @@ -12,14 +12,14 @@ class PasswordResetTest extends TestCase { use RefreshDatabase; - public function test_reset_password_link_screen_can_be_rendered(): void + public function testResetPasswordLinkScreenCanBeRendered(): void { $response = $this->get('/forgot-password'); $response->assertStatus(200); } - public function test_reset_password_link_can_be_requested(): void + public function testResetPasswordLinkCanBeRequested(): void { Notification::fake(); @@ -30,7 +30,7 @@ public function test_reset_password_link_can_be_requested(): void Notification::assertSentTo($user, ResetPassword::class); } - public function test_reset_password_screen_can_be_rendered(): void + public function testResetPasswordScreenCanBeRendered(): void { Notification::fake(); @@ -39,7 +39,7 @@ public function test_reset_password_screen_can_be_rendered(): void $this->post('/forgot-password', ['email' => $user->email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification) { - $response = $this->get('/reset-password/'.$notification->token); + $response = $this->get('/reset-password/' . $notification->token); $response->assertStatus(200); @@ -47,7 +47,7 @@ public function test_reset_password_screen_can_be_rendered(): void }); } - public function test_password_can_be_reset_with_valid_token(): void + public function testPasswordCanBeResetWithValidToken(): void { Notification::fake(); @@ -57,9 +57,9 @@ public function test_password_can_be_reset_with_valid_token(): void Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { $response = $this->post('/reset-password', [ - 'token' => $notification->token, - 'email' => $user->email, - 'password' => 'password', + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', 'password_confirmation' => 'password', ]); diff --git a/backend/tests/Feature/Auth/PasswordUpdateTest.php b/backend/tests/Feature/Auth/PasswordUpdateTest.php index ca28c6c..fa92f00 100644 --- a/backend/tests/Feature/Auth/PasswordUpdateTest.php +++ b/backend/tests/Feature/Auth/PasswordUpdateTest.php @@ -11,7 +11,7 @@ class PasswordUpdateTest extends TestCase { use RefreshDatabase; - public function test_password_can_be_updated(): void + public function testPasswordCanBeUpdated(): void { $user = User::factory()->create(); @@ -19,8 +19,8 @@ public function test_password_can_be_updated(): void ->actingAs($user) ->from('/profile') ->put('/password', [ - 'current_password' => 'password', - 'password' => 'new-password', + 'current_password' => 'password', + 'password' => 'new-password', 'password_confirmation' => 'new-password', ]); @@ -31,7 +31,7 @@ public function test_password_can_be_updated(): void $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); } - public function test_correct_password_must_be_provided_to_update_password(): void + public function testCorrectPasswordMustBeProvidedToUpdatePassword(): void { $user = User::factory()->create(); @@ -39,8 +39,8 @@ public function test_correct_password_must_be_provided_to_update_password(): voi ->actingAs($user) ->from('/profile') ->put('/password', [ - 'current_password' => 'wrong-password', - 'password' => 'new-password', + 'current_password' => 'wrong-password', + 'password' => 'new-password', 'password_confirmation' => 'new-password', ]); diff --git a/backend/tests/Feature/Auth/RegistrationTest.php b/backend/tests/Feature/Auth/RegistrationTest.php index 1489d0e..c6a4676 100644 --- a/backend/tests/Feature/Auth/RegistrationTest.php +++ b/backend/tests/Feature/Auth/RegistrationTest.php @@ -9,19 +9,19 @@ class RegistrationTest extends TestCase { use RefreshDatabase; - public function test_registration_screen_can_be_rendered(): void + public function testRegistrationScreenCanBeRendered(): void { $response = $this->get('/register'); $response->assertStatus(200); } - public function test_new_users_can_register(): void + public function testNewUsersCanRegister(): void { $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password', + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', 'password_confirmation' => 'password', ]); diff --git a/backend/tests/Feature/ExampleTest.php b/backend/tests/Feature/ExampleTest.php index 8364a84..b8ab24a 100644 --- a/backend/tests/Feature/ExampleTest.php +++ b/backend/tests/Feature/ExampleTest.php @@ -10,7 +10,7 @@ class ExampleTest extends TestCase /** * A basic test example. */ - public function test_the_application_returns_a_successful_response(): void + public function testTheApplicationReturnsASuccessfulResponse(): void { $response = $this->get('/'); diff --git a/backend/tests/Feature/ProfileTest.php b/backend/tests/Feature/ProfileTest.php index 252fdcc..0aa655a 100644 --- a/backend/tests/Feature/ProfileTest.php +++ b/backend/tests/Feature/ProfileTest.php @@ -10,7 +10,7 @@ class ProfileTest extends TestCase { use RefreshDatabase; - public function test_profile_page_is_displayed(): void + public function testProfilePageIsDisplayed(): void { $user = User::factory()->create(); @@ -21,14 +21,14 @@ public function test_profile_page_is_displayed(): void $response->assertOk(); } - public function test_profile_information_can_be_updated(): void + public function testProfileInformationCanBeUpdated(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->patch('/profile', [ - 'name' => 'Test User', + 'name' => 'Test User', 'email' => 'test@example.com', ]); @@ -43,14 +43,14 @@ public function test_profile_information_can_be_updated(): void $this->assertNull($user->email_verified_at); } - public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + public function testEmailVerificationStatusIsUnchangedWhenTheEmailAddressIsUnchanged(): void { $user = User::factory()->create(); $response = $this ->actingAs($user) ->patch('/profile', [ - 'name' => 'Test User', + 'name' => 'Test User', 'email' => $user->email, ]); @@ -61,7 +61,7 @@ public function test_email_verification_status_is_unchanged_when_the_email_addre $this->assertNotNull($user->refresh()->email_verified_at); } - public function test_user_can_delete_their_account(): void + public function testUserCanDeleteTheirAccount(): void { $user = User::factory()->create(); @@ -79,7 +79,7 @@ public function test_user_can_delete_their_account(): void $this->assertNull($user->fresh()); } - public function test_correct_password_must_be_provided_to_delete_account(): void + public function testCorrectPasswordMustBeProvidedToDeleteAccount(): void { $user = User::factory()->create(); diff --git a/backend/tests/TestCase.php b/backend/tests/TestCase.php index fe1ffc2..539c7dd 100644 --- a/backend/tests/TestCase.php +++ b/backend/tests/TestCase.php @@ -6,5 +6,4 @@ abstract class TestCase extends BaseTestCase { - // } diff --git a/backend/tests/Unit/ExampleTest.php b/backend/tests/Unit/ExampleTest.php index 5773b0c..b5da561 100644 --- a/backend/tests/Unit/ExampleTest.php +++ b/backend/tests/Unit/ExampleTest.php @@ -9,7 +9,7 @@ class ExampleTest extends TestCase /** * A basic test example. */ - public function test_that_true_is_true(): void + public function testThatTrueIsTrue(): void { $this->assertTrue(true); } From 07daec23d58998ff39bad5b4a78e00e397d1c25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sat, 9 May 2026 23:18:01 +0200 Subject: [PATCH 02/17] add accounts, card view. --- .php-cs-fixer.cache | 1 + .../Controllers/Account/AccountController.php | 75 ++++++++++++++++++ .../Account/Card/CardController.php | 64 +++++++++++++++ .../Account/Flik/FlikCodeController.php | 5 ++ .../Transaction/TransactionController.php | 5 ++ .../Requests/Account/StoreAccountRequest.php | 51 ++++++++++++ .../Requests/Account/UpdateAccountRequest.php | 29 +++++++ .../Resources/Account/AccountResource.php | 37 +++++++++ .../Resources/Account/Card/CardResource.php | 48 ++++++++++++ backend/app/Models/Account/Account.php | 38 ++++++++- backend/app/Models/Account/Card/Card.php | 22 +++++- backend/app/Models/Account/Flik/FlikCode.php | 23 ++++++ .../Account/Transaction/Transaction.php | 25 ++++++ backend/app/Models/User.php | 18 ++++- .../app/Services/Account/AccountService.php | 77 +++++++++++++++++++ .../app/Services/Account/Card/CardService.php | 51 ++++++++++++ backend/config/scramble.php | 10 ++- .../migrations/2026_05_09_170729_accounts.php | 1 + backend/routes/api.php | 9 +++ backend/routes/web.php | 6 +- 20 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 .php-cs-fixer.cache create mode 100644 backend/app/Http/Requests/Account/StoreAccountRequest.php create mode 100644 backend/app/Http/Requests/Account/UpdateAccountRequest.php create mode 100644 backend/app/Http/Resources/Account/AccountResource.php create mode 100644 backend/app/Http/Resources/Account/Card/CardResource.php diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..3146062 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.5.5","version":"3.95.1:v3.95.1#a9727678fbd12997f1d9de8f4a37824ed9df1065","indent":" ","lineEnding":"\n","rules":{"blank_line_after_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false},"class_definition":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"modifier_keywords":{"elements":["method","property"]},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","do","else","elseif","final","for","foreach","function","if","interface","namespace","private","protected","public","static","switch","trait","try","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder9\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder8\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder5\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder6\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder11\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder7\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder10\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"dc362f0253581e37d2aba7e2ec5227e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5fe53d843532d65587cd57f1e371a1f1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder591\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder860\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"b2842f3f7da1c1c1feb8a225d061855f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1208\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"b2842f3f7da1c1c1feb8a225d061855f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder216\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"83c4c8599e71925257fe5f758892b55c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder180\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"28307ea66893c602afb196ae97681c34","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1138\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"24dc2545f75300877b99c7ac953d5601","backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1387\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"24dc2545f75300877b99c7ac953d5601","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder423\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"7ce89c2650ae3a5cd7b1290a2d1880ee","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder570\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6efc939b213ff78e16ece89452f9ad2d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1027\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder579\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder100\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder560\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"99922283f76a2023bd0e5fcaa8680ed5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1549\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"c9b991e7847a9fb4aaf364d8f871c231","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder638\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1021\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2120\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder995\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder732\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2073\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder549\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder339\/backend\/app\/Models\/Account\/Card\/Card.php":"6be2e8a3d128da1635b7c3401a19b59d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder696\/backend\/app\/Models\/Account\/Card\/Card.php":"6be2e8a3d128da1635b7c3401a19b59d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder31\/backend\/app\/Models\/Account\/Card\/Card.php":"61807e366df622d5ac6fecdc17cf0cfa","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2644\/backend\/app\/Models\/Account\/Card\/Card.php":"dea21ac314f3bcbc3373fe2b1541b1d4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1984\/backend\/app\/Models\/Account\/Card\/Card.php":"64699651a90bbb6fbe204f971be7c1b5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1264\/backend\/app\/Models\/Account\/Card\/Card.php":"ff4f4be81d82c6791c044643df4ab2ce","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1204\/backend\/app\/Models\/Account\/Card\/Card.php":"208512c9d21fe3b14f411b8bb2edc554","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder481\/backend\/app\/Models\/Account\/Account.php":"9c535d780fd773d8fa1706417cf320ac","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2116\/backend\/app\/Models\/Account\/Account.php":"d5defd4b60f6eb09f72cba9e19ac8904","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1999\/backend\/app\/Models\/Account\/Account.php":"b8e95eb4e4d249ea432e1220e33c7a1f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1025\/backend\/app\/Models\/Account\/Account.php":"b8e95eb4e4d249ea432e1220e33c7a1f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1482\/backend\/app\/Models\/Account\/Account.php":"8e4c6fbee31eef8072f17bd199bb632c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1890\/backend\/app\/Models\/Account\/Account.php":"0045aa576bf98104a475a867d8a2b3a5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1427\/backend\/app\/Models\/Account\/Account.php":"b1fbe69d14a4acc0fa98b3a2cf612cb1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2570\/backend\/app\/Models\/Account\/Account.php":"b1fbe69d14a4acc0fa98b3a2cf612cb1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1143\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"c9b991e7847a9fb4aaf364d8f871c231","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2468\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"5e5c15abf16bc2da3a78e6c8f85dfec5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1759\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"8707f66dc897df908db1f92ec5ca5260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2311\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"a42ec42284da0919720626e0a25ed72a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2070\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"2c497d1c0b78d8f83e88e53746a4e3e9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2485\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"52d240a79a4ca04cdb20e81cb197ed76","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder882\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"84bea80b29f59b1b3fbb6563c2d6969b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2390\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"3212973a8071f0d97f55f536c5149f71","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder473\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"6e0491adbfe87505fc10f70bc7df8776","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1092\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"78dbe1585499a463e838521dd2c0e00c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2582\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"0e8dbbde0091d27d3f48c75d0293ea3b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2837\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder714\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2134\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2268\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1060\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder483\/backend\/routes\/api.php":"8ddf74442937a375883b1def8e83af17","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder958\/backend\/routes\/api.php":"fcf6c54273513719ec1a2c9528cd9365","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder818\/backend\/routes\/api.php":"321d677374ea9c0b7ccb19504fb0abfd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder416\/backend\/routes\/api.php":"a94f8b1dd09b0e4c17e960198042c1ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2853\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2944\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder519\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder792\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder639\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2064\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"47b8f4e9f3e78e4048989d526eb5fd7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1187\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"80b2270e693e4d46033208add7a29c24","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1243\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"0f6a68e4ce583fad192d4cf88ce5d4af","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1413\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder871\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2045\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b818dd6b3694b1fbbac63a3791957b60","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1559\/backend\/routes\/api.php":"a94f8b1dd09b0e4c17e960198042c1ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder750\/backend\/routes\/api.php":"462c91c38b315e453411f055656bc3d7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2719\/backend\/routes\/api.php":"5dac08c78294d433be2d3614ebbaa0fe","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3178\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2676\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2155\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b818dd6b3694b1fbbac63a3791957b60","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2054\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b7baacac71378ab31a93a7383cdefa0f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2675\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"815c68cdc951bb95be3d97edea1c2c80","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2838\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"0036caab3f9af2f639c42998272395e2","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1952\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"145cdf76713b4b00e29de0cece3286a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1017\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1813\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"47b8f4e9f3e78e4048989d526eb5fd7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1075\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"64bb91bc15afa9bf6ae0ced4d9cb6ebe","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder422\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"aae55ef417ebbf1458595792b9a6d5dc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2909\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"b113891fd2b44879140caf3325843dec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1706\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6c1e93318686e4eeb7f8240f84375292","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1280\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"17af69348e302077aa1c4ad97c81df2c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder701\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6799613d64b0f337eb5ae2293fd4b228","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2487\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder813\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1776\/backend\/config\/scramble.php":"1d747a46a21e92b994c83be0be8cbb92","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2214\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder955\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"af9384a0eec52154de700cb98f1ac05f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1314\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2753\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder614\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1793\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder611\/backend\/config\/scramble.php":"e84126627478323cb38261af8c685870","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1070\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder266\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder427\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1939\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder297\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1542\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2325\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3562\/backend\/config\/scramble.php":"bdd391ea9705d0049cbf18a5ba9f92cc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2848\/backend\/config\/scramble.php":"bdd391ea9705d0049cbf18a5ba9f92cc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3584\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2072\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1285\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2633\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1955\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder854\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder358\/backend\/app\/Http\/Controllers\/User\/ProfileController.php":"db1f4bb9b0d9508dcc7083af68aa90e1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder791\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder767\/backend\/app\/Http\/Controllers\/User\/ProfileController.php":"db1f4bb9b0d9508dcc7083af68aa90e1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder674\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"145cdf76713b4b00e29de0cece3286a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1273\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"5b769c509ddd35649bd91975bc1f0acb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3692\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"9059d344f61d0e35cfa0cc6912b5a5e0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2129\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"480750318cc36e64323727b64b7e1fff","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3326\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"5cd0137c0d92795885b8c672db6368b3","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1763\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1562\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1771\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"076669e341349694fb6289b2ae083de1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder121\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ef1ba6b72df08f0c914d0aaa24a45a90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1543\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"c6ec5374ea2aacac3fcb66abe6f8666a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3149\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2588\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"7b8a41d6fd00edc9af718e2d8a26deb8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder629\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2575\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3822\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2384\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2506\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3760\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"0bca438377ca43f7f68b20343343848a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2824\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3202\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3280\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"0bca438377ca43f7f68b20343343848a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder544\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6799613d64b0f337eb5ae2293fd4b228","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1590\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"7346350b708bded8ba1258219088dfdf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2201\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"4af95ebe33d38d637822a760ef15ebb6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2004\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"830dfcf6b18dd07521ff7a57e3f4b883","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3402\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"90f0e8ca8d1b8f057e031ada871c0708","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1201\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"fc087e3cf7d5631f3247dc45e2a775a8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder265\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"994f998f861609d20b1c072fe9778343","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3411\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"15abac2a06e954ccec9ce004a304eec3","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1261\/backend\/app\/Services\/Account\/Card\/CardService.php":"44615867cece43ec8286e4c1ba7e05ab","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1462\/backend\/app\/Services\/Account\/Card\/CardService.php":"d7478b83cb0fe2221cdfc7648211ff1c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2788\/backend\/app\/Services\/Account\/Card\/CardService.php":"4cc9e391418b1ecbf644ed2155cd6f7c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1055\/backend\/app\/Services\/Account\/Card\/CardService.php":"d7478b83cb0fe2221cdfc7648211ff1c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2522\/backend\/app\/Services\/Account\/Card\/CardService.php":"ab76fcda845a0285ea1610bbdb36dfb5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder424\/backend\/app\/Services\/Account\/Card\/CardService.php":"ab76fcda845a0285ea1610bbdb36dfb5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder106\/backend\/app\/Http\/Controllers\/Account\/Flik\/FlikCodeController.php":"8ad3e9a150bdc1eb69b6f4ad117b0c09","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3512\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b2be2f1521048ce79b4b25b090beb6e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder930\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6e76805a3e038e5b644afc21e7856fad","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3448\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6e76805a3e038e5b644afc21e7856fad","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2714\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"264e7416358bc63d2799573b0fc346f0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2184\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1405\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3487\/backend\/app\/Services\/Generator\/CardNumberGeneratorService.php":"c526571f16da20d2cf8aea279d5b4833","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2253\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1378\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"90b1a20346f340a05b4961521da432f7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2939\/backend\/routes\/web.php":"2ce84fe1dbaae1c9e349ab7236cb8efd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4017\/backend\/routes\/web.php":"2bfbce0f0e3b61306c9195825dc17e02","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1705\/backend\/app\/Services\/Generator\/CardNumberGeneratorService.php":"c526571f16da20d2cf8aea279d5b4833","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3738\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder500\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3197\/backend\/config\/scramble.php":"5fa999b0ac7434966d1a28872aef3f3a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1881\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder470\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3308\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b2be2f1521048ce79b4b25b090beb6e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1131\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"093262d6c5b1e09eda854b7f2a0f45bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1307\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1010\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4242\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder906\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"32429f990c3081b7e0b7e2d39d7af5b9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2118\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"99753aab64a2e430c897b07b5179c1bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3259\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder837\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"372a5547df5fd7674a896859c79b9cac","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3301\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b9297acb3787efc71f3ac56334736d90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4228\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ca6e6203ad0fdcded926c5ff66bd2e35","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3835\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b9297acb3787efc71f3ac56334736d90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4222\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"d79d9d1cb6706a4cc56e19d0a6ff95ab","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3291\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"4bed7d8f4b8b6bc958b228478d84170b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder496\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"a64f568af83992202fb26753128d6074","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1485\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ef994721ec0d5f0e197bb772899ae45c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1627\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3724\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4338\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"99753aab64a2e430c897b07b5179c1bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder211\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"022b9a8b8370c1d2a87c2d3fdf131259","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder347\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"652280c001ef31dabd642b167621f632","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2330\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"079a0bbdc030de2af0c271fc0c3bf810","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3222\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"215a802c6ed87a4fa9640ab35485e795","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2218\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"83deb506ce4c3f79920d2f06558ae6bb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3547\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2796\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1722\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder178\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3866\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"703169c34e030930a07559bf4ace7f19","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3837\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"88a5c0334604823baf8ce575a1e8e505","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder145\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6c6ed95a3c2e67dd88cc8604940a9fca","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder323\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"214d5590b4fe7945d2268e31c3e92aae","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1830\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2442\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1022\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"17e03ee4fddf64286f60fc08aef128a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3118\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2897\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1279\/backend\/app\/Http\/Requests\/ProfileUpdateRequest.php":"79d84e54ad077cfd520561f6919d222f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3960\/backend\/app\/Http\/Requests\/Auth\/LoginRequest.php":"0c4f93db7c6fb8bc7e3ebfea7ad8b8d4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1841\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1802\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"12f324864df690fba73b4bd02fcb1f17","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder873\/backend\/app\/Http\/Requests\/Account\/UpdateAccountRequest.php":"23739c3c1e6bccc34fa0b41daf7e056b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1180\/backend\/app\/Http\/Requests\/Account\/UpdateAccountRequest.php":"23739c3c1e6bccc34fa0b41daf7e056b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3598\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder335\/backend\/app\/Models\/User.php":"abb18c8613e708dd0c0e11ab3d20936d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3493\/backend\/app\/Models\/User.php":"abb18c8613e708dd0c0e11ab3d20936d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3016\/backend\/app\/Models\/User.php":"82ede587854c091141477e643858d250","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2923\/backend\/app\/Models\/User.php":"56d1b3d9d88c683d134b95c417a43a3e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1738\/backend\/app\/Models\/User.php":"00c18b2cce2605626ad90af043251bfc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder430\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder687\/backend\/app\/Models\/User.php":"e7b08031a8647c98cef5272a2641ccd0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4128\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3106\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1714\/backend\/app\/Models\/User.php":"2fe9d0d48946f6f94a2341964aa325c1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4393\/backend\/app\/Models\/User.php":"464d077029c9f6500c4f5c83379c51f4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4038\/backend\/app\/Models\/User.php":"7260eeecdb2c7466ab8554bfe2a9ff88","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1544\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3965\/backend\/config\/scramble.php":"a15904163009a526d31ef6b74c488599","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4735\/backend\/config\/scramble.php":"96a3543348f52df4233467a3eb45c84d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3164\/backend\/app\/Models\/User.php":"7260eeecdb2c7466ab8554bfe2a9ff88","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3709\/backend\/app\/Models\/User.php":"f4f2774cdaa264991686dcf55cdbbab7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder963\/backend\/app\/Models\/User.php":"84c2922abe3928a1714dbbbf6650a0dd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2111\/backend\/app\/Models\/User.php":"ee9fcb0c7af65f97e976a88ceac10d79","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3522\/backend\/app\/Models\/User.php":"42a183c1c63364c0d5508a303d4fdc08","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder842\/backend\/database\/factories\/UserFactory.php":"dfc794c51c5878ff543951aab3f398fb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2648\/backend\/app\/Models\/User.php":"42a183c1c63364c0d5508a303d4fdc08","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3779\/backend\/app\/Models\/User.php":"25d1536d93ead48fa532cabd127ddd0a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2629\/backend\/app\/Models\/User.php":"b698a2255a8b84f5ab051500a07611e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2018\/backend\/app\/Models\/User.php":"60d58a42a6dcb01d4c6510dec475fb07","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1863\/backend\/app\/Models\/User.php":"60d58a42a6dcb01d4c6510dec475fb07","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3352\/backend\/routes\/web.php":"79443b2738e96a9411c83e35646041c8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3828\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3883\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"29c56f3565b5380742d2d217beb41547","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder362\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1439\/backend\/routes\/api.php":"0a59815d30fd104f65c44feb76d7e27e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2692\/backend\/routes\/api.php":"26220132f5d6afc9a1cf2097c854065a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3231\/backend\/routes\/web.php":"79443b2738e96a9411c83e35646041c8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1500\/backend\/config\/scramble.php":"92e71ea2416ddca8ec6e625f1315456b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2089\/backend\/config\/scramble.php":"fd1e67e7a43734c3a1aaee74e54df206","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1540\/backend\/config\/scramble.php":"fd1e67e7a43734c3a1aaee74e54df206","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1563\/backend\/config\/scramble.php":"96a3543348f52df4233467a3eb45c84d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder247\/backend\/config\/scramble.php":"ef5ad7668d2ebc132141a58076a9625e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2105\/backend\/routes\/api.php":"26220132f5d6afc9a1cf2097c854065a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1354\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"29c56f3565b5380742d2d217beb41547","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1630\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4797\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1081\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4385\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675"}} \ No newline at end of file diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index 2c5c5e7..28d3f82 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -3,7 +3,82 @@ namespace App\Http\Controllers\Account; use App\Http\Controllers\Controller; +use App\Http\Requests\Account\StoreAccountRequest; +use App\Http\Requests\Account\UpdateAccountRequest; +use App\Http\Resources\Account\AccountResource; +use App\Models\Account\Account; +use App\Services\Account\AccountService; +use Illuminate\Http\JsonResponse; +/** + * Zarządzanie kontem bankowym. + * + * @tags Account + */ class AccountController extends Controller { + public function __construct( + private readonly AccountService $accountService, + ) { + } + + /** + * Wyświetla listę kont bankowych użytkownika. + * + * @return AccountResource::collection(Account::where('user_id', auth()->id())->with('cards')->paginate(20)) + */ + public function index() + { + return AccountResource::collection( + Account::where('user_id', auth()->id()) + ->with('cards') + ->paginate(20), + ); + } + + /** + * Tworzy nowe konto bankowe dla użytkownika. + */ + public function store(StoreAccountRequest $request): JsonResponse + { + $account = $this->accountService->createAccount( + userId: auth()->id(), + name: $request->name, + currency: $request->currency ?? 'PLN', + type: $request->type ?? 'current', + withCard: $request->with_card ?? true, + ); + + return new AccountResource($account->load('cards')) + ->response() + ->setStatusCode(201); + } + + /** + * Wyświetla szczegóły konta bankowego, w tym powiązane karty. + */ + public function show(Account $account): AccountResource + { + return new AccountResource($account->load('cards')); + } + + /** + * Aktualizuje nazwę konta bankowego. + */ + public function update(UpdateAccountRequest $request, Account $account): AccountResource + { + $account = $this->accountService->updateName($account, $request->name); + + return new AccountResource($account->load('cards')); + } + + /** + * Usuwa konto bankowe, zamykając je. + */ + public function destroy(Account $account): JsonResponse + { + $this->accountService->closeAccount($account); + + return response()->json(['message' => 'Account closed.'], 200); + } } diff --git a/backend/app/Http/Controllers/Account/Card/CardController.php b/backend/app/Http/Controllers/Account/Card/CardController.php index dca7ced..42028e3 100644 --- a/backend/app/Http/Controllers/Account/Card/CardController.php +++ b/backend/app/Http/Controllers/Account/Card/CardController.php @@ -3,7 +3,71 @@ namespace App\Http\Controllers\Account\Card; use App\Http\Controllers\Controller; +use App\Http\Resources\Account\Card\CardResource; +use App\Models\Account\Account; +use App\Models\Account\Card\Card; +use App\Services\Account\Card\CardService; +use Illuminate\Http\JsonResponse; +/** + * Zarządzanie kartami płatniczymi. + * + * @tags Card + */ class CardController extends Controller { + public function __construct( + private readonly CardService $cardService, + ) { + } + + /** + * Wyświetla listę kart płatniczych przypisanych do konta. + */ + public function index(Account $account) + { + return CardResource::collection( + $account->cards()->paginate(20), + ); + } + + /** + * Tworzy nową kartę płatniczą dla konta. + */ + public function store(Account $account): JsonResponse + { + $card = $this->cardService->createCard($account->id); + + return new CardResource($card) + ->response() + ->setStatusCode(201); + } + + /** + * Wyświetla szczegóły karty płatniczej. + */ + public function show(Card $card): CardResource + { + return new CardResource($card); + } + + /** + * Blokuje kartę płatniczą. + */ + public function block(Card $card): CardResource + { + $card = $this->cardService->blockCard($card); + + return new CardResource($card); + } + + /** + * Odblokowuje kartę płatniczą. + */ + public function unblock(Card $card): CardResource + { + $card = $this->cardService->unblockCard($card); + + return new CardResource($card); + } } diff --git a/backend/app/Http/Controllers/Account/Flik/FlikCodeController.php b/backend/app/Http/Controllers/Account/Flik/FlikCodeController.php index b639b4e..6522413 100644 --- a/backend/app/Http/Controllers/Account/Flik/FlikCodeController.php +++ b/backend/app/Http/Controllers/Account/Flik/FlikCodeController.php @@ -4,6 +4,11 @@ use App\Http\Controllers\Controller; +/** + * Zarządzanie kodami Flik. + * + * @tags Flik + */ class FlikCodeController extends Controller { } diff --git a/backend/app/Http/Controllers/Account/Transaction/TransactionController.php b/backend/app/Http/Controllers/Account/Transaction/TransactionController.php index d56f76d..7f7f4b3 100644 --- a/backend/app/Http/Controllers/Account/Transaction/TransactionController.php +++ b/backend/app/Http/Controllers/Account/Transaction/TransactionController.php @@ -4,6 +4,11 @@ use App\Http\Controllers\Controller; +/** + * Zarządzanie transakcjami bankowymi. + * + * @tags Transaction + */ class TransactionController extends Controller { } diff --git a/backend/app/Http/Requests/Account/StoreAccountRequest.php b/backend/app/Http/Requests/Account/StoreAccountRequest.php new file mode 100644 index 0000000..d39d10c --- /dev/null +++ b/backend/app/Http/Requests/Account/StoreAccountRequest.php @@ -0,0 +1,51 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'currency' => ['sometimes', 'string', 'in:PLN'], + 'type' => ['sometimes', 'string', 'in:current,savings'], + 'with_card' => ['sometimes', 'boolean'], + ]; + } + + public function bodyParameters(): array + { + return [ + 'name' => [ + 'description' => 'Nazwa konta', + 'example' => 'Konto oszczędnościowe', + ], + 'currency' => [ + 'example' => 'PLN', + ], + 'type' => [ + 'example' => 'savings', + ], + 'with_card' => [ + 'example' => true, + ], + ]; + } +} diff --git a/backend/app/Http/Requests/Account/UpdateAccountRequest.php b/backend/app/Http/Requests/Account/UpdateAccountRequest.php new file mode 100644 index 0000000..fe3659c --- /dev/null +++ b/backend/app/Http/Requests/Account/UpdateAccountRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + ]; + } +} diff --git a/backend/app/Http/Resources/Account/AccountResource.php b/backend/app/Http/Resources/Account/AccountResource.php new file mode 100644 index 0000000..a50a753 --- /dev/null +++ b/backend/app/Http/Resources/Account/AccountResource.php @@ -0,0 +1,37 @@ + $this->id, + /* @example "Konto oszczędnościowe" */ + 'name' => $this->name, + /* @example "98123456780000000012345678" */ + 'account_number' => $this->account_number, + /* @example "2500.00" */ + 'balance' => number_format($this->balance / 100, 2, '.', ''), + /* @example "PLN" */ + 'currency' => $this->currency, + /* @example "savings" */ + 'type' => $this->type, + 'cards' => CardResource::collection($this->whenLoaded('cards')), + /* @example "2024-06-01T12:00:00.000Z" */ + 'created_at' => $this->created_at?->toISOString(), + /* @example "2024-06-01T12:00:00.000Z" */ + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/backend/app/Http/Resources/Account/Card/CardResource.php b/backend/app/Http/Resources/Account/Card/CardResource.php new file mode 100644 index 0000000..9f0fe96 --- /dev/null +++ b/backend/app/Http/Resources/Account/Card/CardResource.php @@ -0,0 +1,48 @@ + $this->id, + /* @example "**** **** **** 1234" */ + 'card_number' => $this->when( + $this->card_number, + $this->maskCardNumber($this->card_number), + ), + /* @example "1234" */ + 'card_last_four' => $this->when( + $this->card_number, + substr($this->card_number, -4), + ), + /* @example 12 */ + 'exp_month' => $this->exp_month, + /* @example 2024 */ + 'exp_year' => $this->exp_year, + /* @example "Visa" */ + 'network' => $this->network, + /* @example "credit" */ + 'type' => $this->type, + /* @example "active" */ + 'status' => $this->status, + /* @example "2024-06-01T12:00:00.000Z" */ + 'created_at' => $this->created_at?->toISOString(), + /* @example "2024-06-01T12:00:00.000Z" */ + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/backend/app/Models/Account/Account.php b/backend/app/Models/Account/Account.php index f090e9a..b6a40d2 100644 --- a/backend/app/Models/Account/Account.php +++ b/backend/app/Models/Account/Account.php @@ -2,13 +2,18 @@ namespace App\Models\Account; +use App\Models\Account\Card\Card; +use App\Models\Account\Transaction\Transaction; +use App\Models\User; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; -#[Fillable(['user_id', 'account_number', 'balance', 'account_number'])] +#[Fillable(['user_id', 'name', 'account_number', 'balance', 'currency', 'type'])] // #[Hidden(['account_number'])] class Account extends Model { @@ -26,4 +31,35 @@ protected function casts(): array 'balance' => 'decimal:2', ]; } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function cards(): HasMany + { + return $this->hasMany(Card::class); + } + + public function sentTransactions(): HasMany + { + return $this->hasMany(Transaction::class, 'from_account_id'); + } + + public function receivedTransactions(): HasMany + { + return $this->hasMany(Transaction::class, 'to_account_id'); + } + + public function transactions() + { + return Transaction::where('from_account_id', $this->id) + ->orWhere('to_account_id', $this->id); + } + + public function getFormattedBalanceAttribute(): string + { + return number_format($this->balance / 100, 2, '.', '') . ' ' . $this->currency; + } } diff --git a/backend/app/Models/Account/Card/Card.php b/backend/app/Models/Account/Card/Card.php index bf0ec13..9b8676e 100644 --- a/backend/app/Models/Account/Card/Card.php +++ b/backend/app/Models/Account/Card/Card.php @@ -2,12 +2,17 @@ namespace App\Models\Account\Card; +use App\Models\Account\Account; +use App\Models\Account\Flik\FlikCode; +use App\Models\Account\Transaction\Transaction; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; -#[Fillable(['account_id', 'card_number', 'exp_month', 'exp_year', 'cvv', 'status', 'type'])] +#[Fillable(['account_id', 'card_number', 'exp_month', 'exp_year', 'cvv', 'status', 'type', 'network'])] class Card extends Model { use HasFactory; @@ -25,4 +30,19 @@ protected function casts(): array 'exp_year' => 'integer', ]; } + + public function account(): BelongsTo + { + return $this->belongsTo(Account::class); + } + + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class, 'from_card_id'); + } + + public function flikCodes(): HasMany + { + return $this->hasMany(FlikCode::class); + } } diff --git a/backend/app/Models/Account/Flik/FlikCode.php b/backend/app/Models/Account/Flik/FlikCode.php index 095bd3a..5e57e71 100644 --- a/backend/app/Models/Account/Flik/FlikCode.php +++ b/backend/app/Models/Account/Flik/FlikCode.php @@ -2,10 +2,13 @@ namespace App\Models\Account\Flik; +use App\Models\Account\Card\Card; +use App\Models\Account\Transaction\Transaction; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Fillable(['card_id', 'code', 'expires_at', 'status', 'used_in_transaction_id'])] class FlikCode extends Model @@ -24,4 +27,24 @@ protected function casts(): array 'expires_at' => 'datetime', ]; } + + public function card(): BelongsTo + { + return $this->belongsTo(Card::class); + } + + public function transaction(): BelongsTo + { + return $this->belongsTo(Transaction::class, 'used_in_transaction_id'); + } + + public function isExpired(): bool + { + return $this->expires_at->isPast(); + } + + public function isActive(): bool + { + return 'active' === $this->status && ! $this->isExpired(); + } } diff --git a/backend/app/Models/Account/Transaction/Transaction.php b/backend/app/Models/Account/Transaction/Transaction.php index d5b667b..d06deb6 100644 --- a/backend/app/Models/Account/Transaction/Transaction.php +++ b/backend/app/Models/Account/Transaction/Transaction.php @@ -2,10 +2,15 @@ namespace App\Models\Account\Transaction; +use App\Models\Account\Account; +use App\Models\Account\Card\Card; +use App\Models\Account\Flik\FlikCode; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOne; #[Fillable(['from_account_id', 'to_account_id', 'from_card_id', 'amount', 'description', 'type', 'status', 'reference'])] class Transaction extends Model @@ -24,4 +29,24 @@ protected function casts(): array 'amount' => 'decimal:2', ]; } + + public function fromAccount(): BelongsTo + { + return $this->belongsTo(Account::class, 'from_account_id'); + } + + public function toAccount(): BelongsTo + { + return $this->belongsTo(Account::class, 'to_account_id'); + } + + public function card(): BelongsTo + { + return $this->belongsTo(Card::class, 'from_card_id'); + } + + public function flikCode(): HasOne + { + return $this->hasOne(FlikCode::class, 'used_in_transaction_id'); + } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index ac707d8..55f3e71 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -19,7 +19,7 @@ use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; -#[Fillable(['name', 'email', 'password'])] +#[Fillable(['id', 'name', 'email'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail { @@ -37,6 +37,9 @@ class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail protected function casts(): array { return [ + 'id' => 'string', + 'name' => 'string', + 'email' => 'string', 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; @@ -52,9 +55,18 @@ public function cards(): HasManyThrough return $this->hasManyThrough(Card::class, Account::class); } - public function transactions(): HasManyThrough + // public function transactions(): HasManyThrough + // { + // return $this->hasManyThrough(Transaction::class, Account::class); + // } + + public function transactions() { - return $this->hasManyThrough(Transaction::class, Account::class); + return Transaction::whereHas('fromAccount', function ($q) { + $q->where('user_id', $this->id); + })->orWhereHas('toAccount', function ($q) { + $q->where('user_id', $this->id); + }); } public function flikCodes(): HasManyThrough diff --git a/backend/app/Services/Account/AccountService.php b/backend/app/Services/Account/AccountService.php index db086af..bccc584 100644 --- a/backend/app/Services/Account/AccountService.php +++ b/backend/app/Services/Account/AccountService.php @@ -2,10 +2,87 @@ namespace App\Services\Account; +use App\Enums\Card\CardNetwork; +use App\Enums\Card\CardType; +use App\Models\Account\Account; +use App\Services\Account\Card\CardService; use App\Services\Generator\AccountNumberGeneratorService; use App\Services\Service; +use Illuminate\Support\Facades\DB; class AccountService extends Service { use AccountNumberGeneratorService; + + public function __construct( + private readonly CardService $cardService, + ) { + } + + /** + * Create a new bank account with optional debit card. + */ + public function createAccount( + string $userId, + string $name, + string $currency = 'PLN', + string $type = 'current', + bool $withCard = true, + CardNetwork $cardNetwork = CardNetwork::VISA, + CardType $cardType = CardType::DEBIT, + ): Account { + return DB::transaction(function () use ($userId, $name, $currency, $type, $withCard, $cardNetwork, $cardType) { + $account = Account::create([ + 'user_id' => $userId, + 'name' => $name, + 'account_number' => $this->generateAccountNumber(), + 'balance' => 0, + 'currency' => $currency, + 'type' => $type, + ]); + + if ($withCard) { + $this->cardService->createCard($account->id, $cardNetwork, $cardType); + } + + return $account->load('cards'); + }); + } + + /** + * Update account name only. + */ + public function updateName(Account $account, string $name): Account + { + $account->update(['name' => $name]); + + return $account->fresh(); + } + + /** + * Close account (only if balance is zero). + */ + public function closeAccount(Account $account): void + { + if (0 !== $account->balance) { + throw new \Exception('Cannot close account with non-zero balance.'); + } + + DB::transaction(function () use ($account) { + $account->cards()->delete(); + $account->delete(); + }); + } + + /** + * Generate unique account number. + */ + private function generateAccountNumber(): string + { + do { + $number = $this->generate(); + } while (Account::where('account_number', $number)->exists()); + + return $number; + } } diff --git a/backend/app/Services/Account/Card/CardService.php b/backend/app/Services/Account/Card/CardService.php index 3f187f6..2b43b1f 100644 --- a/backend/app/Services/Account/Card/CardService.php +++ b/backend/app/Services/Account/Card/CardService.php @@ -2,8 +2,59 @@ namespace App\Services\Account\Card; +use App\Enums\Card\CardNetwork; +use App\Enums\Card\CardType; +use App\Models\Account\Card\Card; +use App\Services\Generator\CardNumberGeneratorService; use App\Services\Service; class CardService extends Service { + use CardNumberGeneratorService; + + /** + * Create a new card for an account. + */ + public function createCard( + string $accountId, + CardNetwork $network = CardNetwork::VISA, + CardType $type = CardType::DEBIT, + ): Card { + $expiry = $this->generateExpiryDate(); + + do { + $cardNumber = $this->generateCardNumber($network); + } while (Card::where('card_number', $cardNumber)->exists()); + + return Card::create([ + 'account_id' => $accountId, + 'card_number' => $cardNumber, + 'exp_month' => $expiry['month'], + 'exp_year' => $expiry['year'], + 'cvv' => $this->generateCvv($network), + 'network' => $network->value, + 'status' => 'active', + 'type' => $type->value, + ]); + } + + /** + * Block a card. + */ + public function blockCard(Card $card): Card + { + $card->update(['status' => 'blocked']); + + return $card->fresh(); + } + + /** + * Unblock a card. + */ + public function unblockCard(Card $card): Card + { + $card->update(['status' => 'active']); + + return $card->fresh(); + } } diff --git a/backend/config/scramble.php b/backend/config/scramble.php index 9190d42..eba4646 100644 --- a/backend/config/scramble.php +++ b/backend/config/scramble.php @@ -1,7 +1,5 @@ null, + 'title' => config('app.name'), /* * Define the theme of the documentation. Available options are `light`, `dark`, and `system`. @@ -58,7 +56,7 @@ /* * Hide the schemas in the Table of Contents. Enabled by default. */ - 'hide_schemas' => false, + 'hide_schemas' => true, /* * URL to an image that displays as a small square logo next to the title, above the table of contents. @@ -95,6 +93,10 @@ */ 'servers' => null, + 'examples' => [ + 'use_faker' => true, + ], + /* * Determines how Scramble stores the descriptions of enum cases. * Available options: diff --git a/backend/database/migrations/2026_05_09_170729_accounts.php b/backend/database/migrations/2026_05_09_170729_accounts.php index 103b24d..7068541 100644 --- a/backend/database/migrations/2026_05_09_170729_accounts.php +++ b/backend/database/migrations/2026_05_09_170729_accounts.php @@ -30,6 +30,7 @@ public function up(): void $table->string('cvv', 4); $table->string('status', 20)->default('active'); $table->string('type', 20)->default('debit'); + $table->string('network', 20)->default('VISA'); $table->timestamps(); $table->index('account_id'); diff --git a/backend/routes/api.php b/backend/routes/api.php index e9ed4b0..b72f8a9 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,4 +1,7 @@ only(['index', 'store']); + Route::get('cards/{card}', [CardController::class, 'show']); + Route::patch('cards/{card}/block', [CardController::class, 'block']); + Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); }); diff --git a/backend/routes/web.php b/backend/routes/web.php index a3abb91..c9b6434 100644 --- a/backend/routes/web.php +++ b/backend/routes/web.php @@ -6,9 +6,9 @@ return redirect()->route('dashboard'); }); -Route::get('/docs', function () { - return view('vendor.redoc.redoc'); -}); +// Route::get('/docs', function () { +// return view('vendor.redoc.redoc'); +// }); Route::get('/dashboard', function () { return view('dashboard'); From 8903913d5253acb0571b098a173c347c56ac4b1b Mon Sep 17 00:00:00 2001 From: zhenias Date: Sat, 9 May 2026 23:55:07 +0200 Subject: [PATCH 03/17] Delete .php-cs-fixer.cache --- .php-cs-fixer.cache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache deleted file mode 100644 index 3146062..0000000 --- a/.php-cs-fixer.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"8.5.5","version":"3.95.1:v3.95.1#a9727678fbd12997f1d9de8f4a37824ed9df1065","indent":" ","lineEnding":"\n","rules":{"blank_line_after_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false},"class_definition":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"modifier_keywords":{"elements":["method","property"]},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","do","else","elseif","final","for","foreach","function","if","interface","namespace","private","protected","public","static","switch","trait","try","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder9\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder8\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder5\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder6\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder11\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder7\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6e71d9c34b6f0db4ae6156c7b9159479","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder10\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"dc362f0253581e37d2aba7e2ec5227e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5fe53d843532d65587cd57f1e371a1f1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder591\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6ccd125bc9b94bf9f806c696b120a4bf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder860\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"b2842f3f7da1c1c1feb8a225d061855f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1208\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"b2842f3f7da1c1c1feb8a225d061855f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder216\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"83c4c8599e71925257fe5f758892b55c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder180\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"28307ea66893c602afb196ae97681c34","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1138\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"24dc2545f75300877b99c7ac953d5601","backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1387\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"24dc2545f75300877b99c7ac953d5601","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder423\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"7ce89c2650ae3a5cd7b1290a2d1880ee","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder570\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6efc939b213ff78e16ece89452f9ad2d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1027\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder579\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder100\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder560\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"99922283f76a2023bd0e5fcaa8680ed5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1549\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"c9b991e7847a9fb4aaf364d8f871c231","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder638\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1021\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2120\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder995\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder732\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2073\/backend\/app\/Models\/Account\/Account.php":"ff83be53f7ec5329f0941a151b042c97","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder549\/backend\/app\/Models\/Account\/Card\/Card.php":"8aee98d59a78f7abac64191099ca108c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder339\/backend\/app\/Models\/Account\/Card\/Card.php":"6be2e8a3d128da1635b7c3401a19b59d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder696\/backend\/app\/Models\/Account\/Card\/Card.php":"6be2e8a3d128da1635b7c3401a19b59d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder31\/backend\/app\/Models\/Account\/Card\/Card.php":"61807e366df622d5ac6fecdc17cf0cfa","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2644\/backend\/app\/Models\/Account\/Card\/Card.php":"dea21ac314f3bcbc3373fe2b1541b1d4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1984\/backend\/app\/Models\/Account\/Card\/Card.php":"64699651a90bbb6fbe204f971be7c1b5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1264\/backend\/app\/Models\/Account\/Card\/Card.php":"ff4f4be81d82c6791c044643df4ab2ce","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1204\/backend\/app\/Models\/Account\/Card\/Card.php":"208512c9d21fe3b14f411b8bb2edc554","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder481\/backend\/app\/Models\/Account\/Account.php":"9c535d780fd773d8fa1706417cf320ac","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2116\/backend\/app\/Models\/Account\/Account.php":"d5defd4b60f6eb09f72cba9e19ac8904","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1999\/backend\/app\/Models\/Account\/Account.php":"b8e95eb4e4d249ea432e1220e33c7a1f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1025\/backend\/app\/Models\/Account\/Account.php":"b8e95eb4e4d249ea432e1220e33c7a1f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1482\/backend\/app\/Models\/Account\/Account.php":"8e4c6fbee31eef8072f17bd199bb632c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1890\/backend\/app\/Models\/Account\/Account.php":"0045aa576bf98104a475a867d8a2b3a5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1427\/backend\/app\/Models\/Account\/Account.php":"b1fbe69d14a4acc0fa98b3a2cf612cb1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2570\/backend\/app\/Models\/Account\/Account.php":"b1fbe69d14a4acc0fa98b3a2cf612cb1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1143\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"c9b991e7847a9fb4aaf364d8f871c231","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2468\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"5e5c15abf16bc2da3a78e6c8f85dfec5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1759\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"8707f66dc897df908db1f92ec5ca5260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2311\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"a42ec42284da0919720626e0a25ed72a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2070\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"2c497d1c0b78d8f83e88e53746a4e3e9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2485\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"52d240a79a4ca04cdb20e81cb197ed76","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder882\/backend\/app\/Models\/Account\/Transaction\/Transaction.php":"84bea80b29f59b1b3fbb6563c2d6969b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2390\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"3212973a8071f0d97f55f536c5149f71","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder473\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"6e0491adbfe87505fc10f70bc7df8776","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1092\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"78dbe1585499a463e838521dd2c0e00c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2582\/backend\/app\/Models\/Account\/Flik\/FlikCode.php":"0e8dbbde0091d27d3f48c75d0293ea3b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2837\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder714\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2134\/backend\/app\/Models\/User.php":"dbcc1a6c481f1fc011a706834f275435","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2268\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1060\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder483\/backend\/routes\/api.php":"8ddf74442937a375883b1def8e83af17","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder958\/backend\/routes\/api.php":"fcf6c54273513719ec1a2c9528cd9365","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder818\/backend\/routes\/api.php":"321d677374ea9c0b7ccb19504fb0abfd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder416\/backend\/routes\/api.php":"a94f8b1dd09b0e4c17e960198042c1ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2853\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2944\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder519\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"9dd3fdeeb4ccd0f06d607a59b168ff91","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder792\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder639\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"69379a76891163be3e851e5cb34168ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2064\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"47b8f4e9f3e78e4048989d526eb5fd7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1187\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"80b2270e693e4d46033208add7a29c24","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1243\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"0f6a68e4ce583fad192d4cf88ce5d4af","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1413\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder871\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"2b45b6b633af37f2b17df29d64b2fc03","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2045\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b818dd6b3694b1fbbac63a3791957b60","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1559\/backend\/routes\/api.php":"a94f8b1dd09b0e4c17e960198042c1ec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder750\/backend\/routes\/api.php":"462c91c38b315e453411f055656bc3d7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2719\/backend\/routes\/api.php":"5dac08c78294d433be2d3614ebbaa0fe","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3178\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2676\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2155\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b818dd6b3694b1fbbac63a3791957b60","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2054\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"b7baacac71378ab31a93a7383cdefa0f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2675\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"815c68cdc951bb95be3d97edea1c2c80","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2838\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"0036caab3f9af2f639c42998272395e2","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1952\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"145cdf76713b4b00e29de0cece3286a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1017\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1813\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"47b8f4e9f3e78e4048989d526eb5fd7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1075\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"64bb91bc15afa9bf6ae0ced4d9cb6ebe","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder422\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"aae55ef417ebbf1458595792b9a6d5dc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2909\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"b113891fd2b44879140caf3325843dec","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1706\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6c1e93318686e4eeb7f8240f84375292","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1280\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"17af69348e302077aa1c4ad97c81df2c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder701\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6799613d64b0f337eb5ae2293fd4b228","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2487\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder813\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1776\/backend\/config\/scramble.php":"1d747a46a21e92b994c83be0be8cbb92","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2214\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder955\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"af9384a0eec52154de700cb98f1ac05f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1314\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2753\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder614\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1793\/backend\/config\/scramble.php":"f64bdd3906d4e05f61b54f2927adbe99","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder611\/backend\/config\/scramble.php":"e84126627478323cb38261af8c685870","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1070\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder266\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder427\/backend\/config\/scramble.php":"f3798aab7c56a874aafe9cb89f0a2d38","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1939\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder297\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1542\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2325\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3562\/backend\/config\/scramble.php":"bdd391ea9705d0049cbf18a5ba9f92cc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2848\/backend\/config\/scramble.php":"bdd391ea9705d0049cbf18a5ba9f92cc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3584\/backend\/config\/scramble.php":"c730c4f3aed1e86ba6e4a45009dc5a63","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2072\/backend\/config\/scramble.php":"64a3c286d6767ba5a84a72439ffad260","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1285\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2633\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1955\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder854\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder358\/backend\/app\/Http\/Controllers\/User\/ProfileController.php":"db1f4bb9b0d9508dcc7083af68aa90e1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder791\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder767\/backend\/app\/Http\/Controllers\/User\/ProfileController.php":"db1f4bb9b0d9508dcc7083af68aa90e1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder674\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"145cdf76713b4b00e29de0cece3286a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1273\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"5b769c509ddd35649bd91975bc1f0acb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3692\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"9059d344f61d0e35cfa0cc6912b5a5e0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2129\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"480750318cc36e64323727b64b7e1fff","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3326\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"5cd0137c0d92795885b8c672db6368b3","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1763\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1562\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"8050cd2e85bbc5fb4eeba59dae5a5127","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1771\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"076669e341349694fb6289b2ae083de1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder121\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ef1ba6b72df08f0c914d0aaa24a45a90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1543\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"c6ec5374ea2aacac3fcb66abe6f8666a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3149\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2588\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"7b8a41d6fd00edc9af718e2d8a26deb8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder629\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2575\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"6f17daf92badea584fe69f73367fc3a9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3822\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2384\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2506\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"69f6b8daa144f7e26e4da863a1203e8d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3760\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"0bca438377ca43f7f68b20343343848a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2824\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3202\/backend\/app\/Http\/Controllers\/Account\/AccountController.php":"a623e49df8697d8b47efa34843d4ab49","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3280\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"0bca438377ca43f7f68b20343343848a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder544\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"6799613d64b0f337eb5ae2293fd4b228","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1590\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"7346350b708bded8ba1258219088dfdf","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2201\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"4af95ebe33d38d637822a760ef15ebb6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2004\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"830dfcf6b18dd07521ff7a57e3f4b883","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3402\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"90f0e8ca8d1b8f057e031ada871c0708","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1201\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"fc087e3cf7d5631f3247dc45e2a775a8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder265\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"994f998f861609d20b1c072fe9778343","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3411\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"15abac2a06e954ccec9ce004a304eec3","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1261\/backend\/app\/Services\/Account\/Card\/CardService.php":"44615867cece43ec8286e4c1ba7e05ab","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1462\/backend\/app\/Services\/Account\/Card\/CardService.php":"d7478b83cb0fe2221cdfc7648211ff1c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2788\/backend\/app\/Services\/Account\/Card\/CardService.php":"4cc9e391418b1ecbf644ed2155cd6f7c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1055\/backend\/app\/Services\/Account\/Card\/CardService.php":"d7478b83cb0fe2221cdfc7648211ff1c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2522\/backend\/app\/Services\/Account\/Card\/CardService.php":"ab76fcda845a0285ea1610bbdb36dfb5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder424\/backend\/app\/Services\/Account\/Card\/CardService.php":"ab76fcda845a0285ea1610bbdb36dfb5","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder106\/backend\/app\/Http\/Controllers\/Account\/Flik\/FlikCodeController.php":"8ad3e9a150bdc1eb69b6f4ad117b0c09","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3512\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b2be2f1521048ce79b4b25b090beb6e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder930\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6e76805a3e038e5b644afc21e7856fad","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3448\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6e76805a3e038e5b644afc21e7856fad","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2714\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"264e7416358bc63d2799573b0fc346f0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2184\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1405\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3487\/backend\/app\/Services\/Generator\/CardNumberGeneratorService.php":"c526571f16da20d2cf8aea279d5b4833","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2253\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"dd48142ff47e240eff9854224175e729","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1378\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"90b1a20346f340a05b4961521da432f7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2939\/backend\/routes\/web.php":"2ce84fe1dbaae1c9e349ab7236cb8efd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4017\/backend\/routes\/web.php":"2bfbce0f0e3b61306c9195825dc17e02","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1705\/backend\/app\/Services\/Generator\/CardNumberGeneratorService.php":"c526571f16da20d2cf8aea279d5b4833","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3738\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder500\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3197\/backend\/config\/scramble.php":"5fa999b0ac7434966d1a28872aef3f3a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1881\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder470\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3308\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b2be2f1521048ce79b4b25b090beb6e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1131\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"093262d6c5b1e09eda854b7f2a0f45bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1307\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1010\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4242\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"480aedb22051443ea72e4dec3cf5ad2e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder906\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"32429f990c3081b7e0b7e2d39d7af5b9","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2118\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"99753aab64a2e430c897b07b5179c1bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3259\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b5cf80a44a0df14ac9f0a4509a806a7f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder837\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"372a5547df5fd7674a896859c79b9cac","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3301\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b9297acb3787efc71f3ac56334736d90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4228\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ca6e6203ad0fdcded926c5ff66bd2e35","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3835\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"b9297acb3787efc71f3ac56334736d90","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4222\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"d79d9d1cb6706a4cc56e19d0a6ff95ab","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3291\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"4bed7d8f4b8b6bc958b228478d84170b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder496\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"a64f568af83992202fb26753128d6074","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1485\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"ef994721ec0d5f0e197bb772899ae45c","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1627\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3724\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4338\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"99753aab64a2e430c897b07b5179c1bc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder211\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"022b9a8b8370c1d2a87c2d3fdf131259","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder347\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"652280c001ef31dabd642b167621f632","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2330\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"079a0bbdc030de2af0c271fc0c3bf810","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3222\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"215a802c6ed87a4fa9640ab35485e795","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2218\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"83deb506ce4c3f79920d2f06558ae6bb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3547\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2796\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1722\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder178\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"d1124c8ea0873a1f8f3d1aa003903843","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3866\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"703169c34e030930a07559bf4ace7f19","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3837\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"88a5c0334604823baf8ce575a1e8e505","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder145\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"6c6ed95a3c2e67dd88cc8604940a9fca","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder323\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"214d5590b4fe7945d2268e31c3e92aae","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1830\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2442\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"5945b075d5663bf600b85dc4e8adbe25","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1022\/backend\/app\/Http\/Resources\/Account\/AccountResource.php":"17e03ee4fddf64286f60fc08aef128a7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3118\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2897\/backend\/app\/Http\/Resources\/Account\/Card\/CardResource.php":"906feab4eaa094913766da3a02bb8e28","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1279\/backend\/app\/Http\/Requests\/ProfileUpdateRequest.php":"79d84e54ad077cfd520561f6919d222f","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3960\/backend\/app\/Http\/Requests\/Auth\/LoginRequest.php":"0c4f93db7c6fb8bc7e3ebfea7ad8b8d4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1841\/backend\/app\/Http\/Requests\/User\/UpdateProfileRequest.php":"e9d078a50f06e4b13efe72d4dea29940","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1802\/backend\/app\/Http\/Requests\/Account\/StoreAccountRequest.php":"12f324864df690fba73b4bd02fcb1f17","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder873\/backend\/app\/Http\/Requests\/Account\/UpdateAccountRequest.php":"23739c3c1e6bccc34fa0b41daf7e056b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1180\/backend\/app\/Http\/Requests\/Account\/UpdateAccountRequest.php":"23739c3c1e6bccc34fa0b41daf7e056b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3598\/backend\/app\/Models\/User.php":"0a6f80d5e53ebaff0c0d0ee2ca136a51","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder335\/backend\/app\/Models\/User.php":"abb18c8613e708dd0c0e11ab3d20936d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3493\/backend\/app\/Models\/User.php":"abb18c8613e708dd0c0e11ab3d20936d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3016\/backend\/app\/Models\/User.php":"82ede587854c091141477e643858d250","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2923\/backend\/app\/Models\/User.php":"56d1b3d9d88c683d134b95c417a43a3e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1738\/backend\/app\/Models\/User.php":"00c18b2cce2605626ad90af043251bfc","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder430\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder687\/backend\/app\/Models\/User.php":"e7b08031a8647c98cef5272a2641ccd0","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4128\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3106\/backend\/app\/Models\/User.php":"e75ff4aab9626d9de7f63f8838102dd8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1714\/backend\/app\/Models\/User.php":"2fe9d0d48946f6f94a2341964aa325c1","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4393\/backend\/app\/Models\/User.php":"464d077029c9f6500c4f5c83379c51f4","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4038\/backend\/app\/Models\/User.php":"7260eeecdb2c7466ab8554bfe2a9ff88","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1544\/backend\/config\/scramble.php":"57e9f0bea1ea41263531bb7a405c6a87","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3965\/backend\/config\/scramble.php":"a15904163009a526d31ef6b74c488599","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4735\/backend\/config\/scramble.php":"96a3543348f52df4233467a3eb45c84d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3164\/backend\/app\/Models\/User.php":"7260eeecdb2c7466ab8554bfe2a9ff88","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3709\/backend\/app\/Models\/User.php":"f4f2774cdaa264991686dcf55cdbbab7","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder963\/backend\/app\/Models\/User.php":"84c2922abe3928a1714dbbbf6650a0dd","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2111\/backend\/app\/Models\/User.php":"ee9fcb0c7af65f97e976a88ceac10d79","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3522\/backend\/app\/Models\/User.php":"42a183c1c63364c0d5508a303d4fdc08","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder842\/backend\/database\/factories\/UserFactory.php":"dfc794c51c5878ff543951aab3f398fb","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2648\/backend\/app\/Models\/User.php":"42a183c1c63364c0d5508a303d4fdc08","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3779\/backend\/app\/Models\/User.php":"25d1536d93ead48fa532cabd127ddd0a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2629\/backend\/app\/Models\/User.php":"b698a2255a8b84f5ab051500a07611e6","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2018\/backend\/app\/Models\/User.php":"60d58a42a6dcb01d4c6510dec475fb07","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1863\/backend\/app\/Models\/User.php":"60d58a42a6dcb01d4c6510dec475fb07","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3352\/backend\/routes\/web.php":"79443b2738e96a9411c83e35646041c8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3828\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3883\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"29c56f3565b5380742d2d217beb41547","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder362\/backend\/routes\/api.php":"7dd3a2f01a7efc87d569b6c8bb30db89","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1439\/backend\/routes\/api.php":"0a59815d30fd104f65c44feb76d7e27e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2692\/backend\/routes\/api.php":"26220132f5d6afc9a1cf2097c854065a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder3231\/backend\/routes\/web.php":"79443b2738e96a9411c83e35646041c8","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1500\/backend\/config\/scramble.php":"92e71ea2416ddca8ec6e625f1315456b","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2089\/backend\/config\/scramble.php":"fd1e67e7a43734c3a1aaee74e54df206","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1540\/backend\/config\/scramble.php":"fd1e67e7a43734c3a1aaee74e54df206","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1563\/backend\/config\/scramble.php":"96a3543348f52df4233467a3eb45c84d","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder247\/backend\/config\/scramble.php":"ef5ad7668d2ebc132141a58076a9625e","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder2105\/backend\/routes\/api.php":"26220132f5d6afc9a1cf2097c854065a","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1354\/backend\/app\/Http\/Controllers\/Account\/Card\/CardController.php":"29c56f3565b5380742d2d217beb41547","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1630\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4797\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder1081\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675","\/private\/var\/folders\/8_\/mjk64jps0vd988kl1b2qspvm0000gn\/T\/PHP CS Fixertemp_folder4385\/backend\/config\/scramble.php":"de56e351f1d734279041718f21314675"}} \ No newline at end of file From 6cffb6edb73bae1f50bd8761e93d4ff2b20d4245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sun, 10 May 2026 00:47:23 +0200 Subject: [PATCH 04/17] add test for account, and conf testing db, add factory fake data. --- backend/.env.testing | 73 ++++++ backend/app/Exceptions/Handler.php | 25 +- .../Controllers/Account/AccountController.php | 2 - backend/app/Models/User.php | 5 - .../app/Services/Account/AccountService.php | 2 +- .../factories/Account/AccountFactory.php | 1 + .../factories/Account/Card/CardFactory.php | 11 + .../Account/Flik/FlikCodeFactory.php | 7 + .../Transaction/TransactionFactory.php | 11 +- backend/database/factories/UserFactory.php | 2 +- backend/phpunit.xml | 4 +- backend/tests/Feature/Account/AccountTest.php | 245 ++++++++++++++++++ .../tests/Feature/Account/Card/CardTest.php | 18 ++ .../scripts/mariadb/01-create-testing-db.sql | 3 + 14 files changed, 378 insertions(+), 31 deletions(-) create mode 100644 backend/.env.testing create mode 100644 backend/tests/Feature/Account/AccountTest.php create mode 100644 backend/tests/Feature/Account/Card/CardTest.php create mode 100644 docker/scripts/mariadb/01-create-testing-db.sql diff --git a/backend/.env.testing b/backend/.env.testing new file mode 100644 index 0000000..847693a --- /dev/null +++ b/backend/.env.testing @@ -0,0 +1,73 @@ +APP_NAME="Bank Online System" +APP_ENV=local +APP_KEY=base64:8l4We7vCbeN9iLHUXIQ4UjZhUtAYTCeyN37gjCqoQsc= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +PASSPORT_CLIENT_ID=bank-client-id +PASSPORT_CLIENT_SECRET=bank-client-secret +PASSPORT_REDIRECT_URI=http://localhost:5173/auth/callback +PASSPORT_AUTHORIZE_URL=http://localhost:5173/oauth/authorize +PASSPORT_TOKEN_URL=http://localhost:8000/oauth/token +PASSPORT_USER_ENDPOINT=http://localhost:8000/api/user + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=backend_testing +DB_USERNAME=laravel +DB_PASSWORD=laravel_password + +SESSION_DRIVER=cookie +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=localhost +SANCTUM_STATEFUL_DOMAINS=localhost:5174 + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_DRIVER=redis +CACHE_STORE=redis + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=predis +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php index 7c1c025..673c886 100644 --- a/backend/app/Exceptions/Handler.php +++ b/backend/app/Exceptions/Handler.php @@ -34,74 +34,65 @@ public function render($request, \Throwable $e): Response private function handleApiException(\Throwable $e): JsonResponse { - // MissingScopeException (Passport) if ($e instanceof MissingScopeException) { return response()->json([ 'status' => 'error', - 'message' => 'Brak wymaganych uprawnień.', + 'message' => 'Insufficient permissions to access this resource.', 'code' => 403, ], 403); } - // AuthenticationException (brak autoryzacji) if ($e instanceof AuthenticationException || $e instanceof UnauthorizedHttpException) { return response()->json([ 'status' => 'error', - 'message' => 'Brak autoryzacji.', + 'message' => 'Unauthenticated. Please provide valid credentials.', 'code' => 401, ], 401); } - // AuthorizationException (brak dostępu) if ($e instanceof AuthorizationException || $e instanceof AccessDeniedHttpException) { return response()->json([ 'status' => 'error', - 'message' => 'Brak uprawnień do tego zasobu.', + 'message' => 'No permissions for this resource.', 'code' => 403, ], 403); } - // ValidationException (błędy walidacji) if ($e instanceof ValidationException) { return response()->json([ 'status' => 'error', - 'message' => 'Błąd walidacji danych.', + 'message' => 'Error validating data.', 'errors' => $e->errors(), 'code' => 422, ], 422); } - // ModelNotFoundException (model nie znaleziony) if ($e instanceof ModelNotFoundException) { return response()->json([ 'status' => 'error', - 'message' => 'Żądany zasób nie został znaleziony.', + 'message' => 'Resource not found.', 'code' => 404, ], 404); } - // NotFoundHttpException (endpoint nie istnieje) if ($e instanceof NotFoundHttpException) { return response()->json([ 'status' => 'error', - 'message' => 'Endpoint nie istnieje.', + 'message' => 'Endpoint not found.', 'code' => 404, ], 404); } - // ThrottleRequestsException (zbyt wiele zapytań) if ($e instanceof ThrottleRequestsException) { return response()->json([ 'status' => 'error', - 'message' => 'Zbyt wiele żądań. Spróbuj ponownie później.', + 'message' => 'How many requests you have made. Please try again later.', 'code' => 429, ], 429); } - // Domyślny błąd $statusCode = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500; - // W trybie debug pokaż więcej informacji if (config('app.debug')) { return response()->json([ 'status' => 'error', @@ -115,7 +106,7 @@ private function handleApiException(\Throwable $e): JsonResponse return response()->json([ 'status' => 'error', - 'message' => $e->getMessage() ?: 'Wystąpił wewnętrzny błąd serwera.', + 'message' => $e->getMessage() ?: 'Internal Server Error', 'code' => $statusCode, ], $statusCode >= 100 && $statusCode < 600 ? $statusCode : 500); } diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index 28d3f82..6d7a019 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -24,8 +24,6 @@ public function __construct( /** * Wyświetla listę kont bankowych użytkownika. - * - * @return AccountResource::collection(Account::where('user_id', auth()->id())->with('cards')->paginate(20)) */ public function index() { diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 55f3e71..76f6f3a 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -55,11 +55,6 @@ public function cards(): HasManyThrough return $this->hasManyThrough(Card::class, Account::class); } - // public function transactions(): HasManyThrough - // { - // return $this->hasManyThrough(Transaction::class, Account::class); - // } - public function transactions() { return Transaction::whereHas('fromAccount', function ($q) { diff --git a/backend/app/Services/Account/AccountService.php b/backend/app/Services/Account/AccountService.php index bccc584..7c76b09 100644 --- a/backend/app/Services/Account/AccountService.php +++ b/backend/app/Services/Account/AccountService.php @@ -64,7 +64,7 @@ public function updateName(Account $account, string $name): Account */ public function closeAccount(Account $account): void { - if (0 !== $account->balance) { + if ($account->balance > 0) { throw new \Exception('Cannot close account with non-zero balance.'); } diff --git a/backend/database/factories/Account/AccountFactory.php b/backend/database/factories/Account/AccountFactory.php index 0b13d68..dd586b1 100644 --- a/backend/database/factories/Account/AccountFactory.php +++ b/backend/database/factories/Account/AccountFactory.php @@ -11,6 +11,7 @@ public function definition(): array { return [ 'user_id' => User::factory(), + 'name' => $this->faker->word(), 'account_number' => $this->faker->unique()->iban(), 'balance' => $this->faker->randomFloat(2, 0, 10000), 'currency' => 'PLN', diff --git a/backend/database/factories/Account/Card/CardFactory.php b/backend/database/factories/Account/Card/CardFactory.php index 1b4a821..3c0caf6 100644 --- a/backend/database/factories/Account/Card/CardFactory.php +++ b/backend/database/factories/Account/Card/CardFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories\Account\Card; +use App\Models\Account\Account; use App\Models\Account\Card\Card; use Illuminate\Database\Eloquent\Factories\Factory; @@ -17,7 +18,17 @@ class CardFactory extends Factory */ public function definition(): array { + $account = Account::factory()->create(); + return [ + 'account_id' => $account, + 'card_number' => $this->faker->creditCardNumber(), + 'cvv' => $this->faker->randomNumber(3), + 'exp_month' => $this->faker->date('m'), + 'exp_year' => $this->faker->date('Y'), + 'status' => $this->faker->randomElement(['active', 'inactive', 'blocked']), + 'type' => $this->faker->randomElement(['debit', 'credit', 'prepaid', 'virtual']), + 'network' => $this->faker->randomElement(['Visa', 'MasterCard', 'American Express', 'Discover']), ]; } } diff --git a/backend/database/factories/Account/Flik/FlikCodeFactory.php b/backend/database/factories/Account/Flik/FlikCodeFactory.php index ca796ec..739f9e9 100644 --- a/backend/database/factories/Account/Flik/FlikCodeFactory.php +++ b/backend/database/factories/Account/Flik/FlikCodeFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories\Account\Flik; +use App\Models\Account\Card\Card; use App\Models\Account\Flik\FlikCode; use Illuminate\Database\Eloquent\Factories\Factory; @@ -17,7 +18,13 @@ class FlikCodeFactory extends Factory */ public function definition(): array { + $card = Card::factory()->create(); + return [ + 'card_id' => $card, + 'code' => $this->faker->unique()->regexify('[0-9]{6}'), + 'expires_at' => $this->faker->dateTimeBetween('+1 day', '+1 month'), + 'status' => $this->faker->randomElement(['active', 'used', 'expired']), ]; } } diff --git a/backend/database/factories/Account/Transaction/TransactionFactory.php b/backend/database/factories/Account/Transaction/TransactionFactory.php index b5c34c5..d32c763 100644 --- a/backend/database/factories/Account/Transaction/TransactionFactory.php +++ b/backend/database/factories/Account/Transaction/TransactionFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories\Account\Transaction; use App\Models\Account\Transaction\Transaction; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -17,10 +18,14 @@ class TransactionFactory extends Factory */ public function definition(): array { + $fromAccountUser = User::factory()->create(); + $toAccountUser = User::factory()->create(); + $fromCardUser = $fromAccountUser->cards()->factory()->create(); + return [ - 'from_account_id' => null, - 'to_account_id' => null, - 'from_card_id' => null, + 'from_account_id' => $fromAccountUser, + 'to_account_id' => $toAccountUser, + 'from_card_id' => $fromCardUser, 'amount' => $this->faker->randomFloat(2, 1, 1000), 'description' => $this->faker->sentence(), 'type' => $this->faker->randomElement(['transfer', 'payment', 'withdrawal']), diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index f4ebfc4..6133d1f 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -27,7 +27,7 @@ public function definition(): array { return [ 'name' => fake()->name(), - 'email' => static::$email ??= fake()->unique()->safeEmail(), + 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), diff --git a/backend/phpunit.xml b/backend/phpunit.xml index e7f0a48..9f3637a 100644 --- a/backend/phpunit.xml +++ b/backend/phpunit.xml @@ -23,8 +23,8 @@ - - + + diff --git a/backend/tests/Feature/Account/AccountTest.php b/backend/tests/Feature/Account/AccountTest.php new file mode 100644 index 0000000..7d8b9aa --- /dev/null +++ b/backend/tests/Feature/Account/AccountTest.php @@ -0,0 +1,245 @@ +user = User::factory()->create(); + Account::query()->delete(); + } + + public function testListsAllUserAccounts(): void + { + Account::factory()->count(3)->create(['user_id' => $this->user->id]); + + $otherUser = User::factory()->create(); + Account::factory()->count(2)->create(['user_id' => $otherUser->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson('/api/accounts'); + + $response->assertStatus(200) + ->assertJsonCount(3, 'data') + ->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'account_number', + 'balance', + 'currency', + 'type', + 'cards', + 'created_at', + 'updated_at', + ], + ], + 'links', + 'meta', + ]); + } + + public function testCreatesNewAccount(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson('/api/accounts', [ + 'name' => 'Konto oszczędnościowe', + 'currency' => 'PLN', + 'type' => 'savings', + 'with_card' => true, + ]); + + $response->assertStatus(201) + ->assertJsonPath('data.name', 'Konto oszczędnościowe') + ->assertJsonPath('data.currency', 'PLN') + ->assertJsonPath('data.type', 'savings') + ->assertJsonPath('data.balance', '0.00') + ->assertJsonStructure([ + 'data' => [ + 'id', + 'name', + 'account_number', + 'balance', + 'currency', + 'type', + 'cards', + 'created_at', + 'updated_at', + ], + ]); + + $this->assertDatabaseHas('accounts', [ + 'user_id' => $this->user->id, + 'name' => 'Konto oszczędnościowe', + ]); + } + + public function testCreatesAccountWithoutCard(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson('/api/accounts', [ + 'name' => 'Konto bez karty', + 'with_card' => false, + ]); + + $response->assertStatus(201) + ->assertJsonCount(0, 'data.cards'); + } + + public function testShowsAccountDetails(): void + { + $account = Account::factory()->create(['user_id' => $this->user->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$account->id}"); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $account->id) + ->assertJsonPath('data.name', $account->name) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'name', + 'account_number', + 'balance', + 'currency', + 'type', + 'cards', + 'created_at', + 'updated_at', + ], + ]); + } + + public function testUpdatesAccountName(): void + { + $account = Account::factory()->create([ + 'user_id' => $this->user->id, + 'name' => 'Stara nazwa', + ]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/accounts/{$account->id}", [ + 'name' => 'Nowa nazwa', + ]); + + $response->assertStatus(200) + ->assertJsonPath('data.name', 'Nowa nazwa'); + + $this->assertDatabaseHas('accounts', [ + 'id' => $account->id, + 'name' => 'Nowa nazwa', + ]); + } + + public function testCannotUpdateAccountNumberOrBalance(): void + { + $account = Account::factory()->create([ + 'user_id' => $this->user->id, + 'account_number' => '12345678901234567890123456', + 'balance' => 100000, // 1000.00 PLN + ]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/accounts/{$account->id}", [ + 'name' => 'Nowa nazwa', + 'account_number' => '99999999999999999999999999', + 'balance' => 999999, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('accounts', [ + 'id' => $account->id, + 'account_number' => '12345678901234567890123456', + 'balance' => 100000, + ]); + } + + public function testClosesAccountWithZeroBalance(): void + { + Account::query()->each(fn ($account) => $account->delete()); + + $account = Account::factory()->create([ + 'user_id' => $this->user->id, + 'balance' => 0, + ]); + + Passport::actingAs($this->user); + + $response = $this->deleteJson("/api/accounts/{$account->id}"); + + $response->assertStatus(200) + ->assertJson(['message' => 'Account closed.']); + + $this->assertDatabaseMissing('accounts', ['id' => $account->id]); + } + + public function testCannotCloseAccountWithBalance(): void + { + $account = Account::factory()->create([ + 'user_id' => $this->user->id, + 'balance' => 50000, + ]); + + Passport::actingAs($this->user); + + $response = $this->deleteJson("/api/accounts/{$account->id}"); + + $response->assertStatus(500); // lub 422 zależnie od implementacji + + $this->assertDatabaseHas('accounts', ['id' => $account->id]); + } + + public function testRequiresAuthentication(): void + { + $response = $this->getJson('/api/accounts'); + + $response->assertStatus(401); + } + + public function testValidatesStoreRequest(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson('/api/accounts', [ + 'name' => '', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name']); + } + + public function testValidatesUpdateRequest(): void + { + $account = Account::factory()->create(['user_id' => $this->user->id]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/accounts/{$account->id}", [ + 'name' => '', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name']); + } +} diff --git a/backend/tests/Feature/Account/Card/CardTest.php b/backend/tests/Feature/Account/Card/CardTest.php new file mode 100644 index 0000000..3166da1 --- /dev/null +++ b/backend/tests/Feature/Account/Card/CardTest.php @@ -0,0 +1,18 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/docker/scripts/mariadb/01-create-testing-db.sql b/docker/scripts/mariadb/01-create-testing-db.sql new file mode 100644 index 0000000..57c3c7c --- /dev/null +++ b/docker/scripts/mariadb/01-create-testing-db.sql @@ -0,0 +1,3 @@ +CREATE DATABASE IF NOT EXISTS backend_testing CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +GRANT ALL PRIVILEGES ON backend_testing.* TO 'laravel'@'%'; +FLUSH PRIVILEGES; \ No newline at end of file From 744642fd9feb936afc56148e8803a945c2727c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sun, 10 May 2026 00:47:28 +0200 Subject: [PATCH 05/17] add test for account, and conf testing db, add factory fake data. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3ab5311..c979b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ Thumbs.db bootstrap/cache/ storage/framework/cache/ +/.php-cs-fixer.cache From d78a85af4a3ebbb75130736ec27eed274fa9c942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sun, 10 May 2026 15:53:52 +0200 Subject: [PATCH 06/17] test for account and card, and fix registration user. --- backend/app/Http/Concerns/WithPagination.php | 89 ++++++ .../Controllers/Account/AccountController.php | 23 +- .../Account/Card/CardController.php | 40 ++- backend/app/Http/Controllers/Controller.php | 2 + .../Controllers/User/ProfileController.php | 51 ++- .../Requests/Account/StoreAccountRequest.php | 2 +- .../Requests/Account/UpdateAccountRequest.php | 10 + .../Requests/User/RegisterUserRequest.php | 44 +++ .../Requests/User/UpdateProfileRequest.php | 4 +- .../Resources/Account/AccountResource.php | 1 + backend/app/Models/User.php | 4 +- backend/app/Providers/AppServiceProvider.php | 12 + backend/bootstrap/app.php | 73 +++-- backend/config/scramble.php | 15 +- backend/routes/api.php | 6 + backend/routes/auth.php | 4 +- backend/routes/web.php | 26 +- .../tests/Feature/Account/Card/CardTest.php | 299 +++++++++++++++++- 18 files changed, 608 insertions(+), 97 deletions(-) create mode 100644 backend/app/Http/Concerns/WithPagination.php create mode 100644 backend/app/Http/Requests/User/RegisterUserRequest.php diff --git a/backend/app/Http/Concerns/WithPagination.php b/backend/app/Http/Concerns/WithPagination.php new file mode 100644 index 0000000..9c8895d --- /dev/null +++ b/backend/app/Http/Concerns/WithPagination.php @@ -0,0 +1,89 @@ +integer('per_page', 20); + + return max(1, min(100, $perPage)); + } + + /** + * Get the current page from request. + */ + protected function currentPage(): int + { + return request()->integer('page', 1); + } + + /** + * Paginate a query with all request parameters. + */ + protected function paginate($query): LengthAwarePaginator + { + return $query->paginate( + perPage: $this->perPage(), + page: $this->currentPage(), + ); + } + + /** + * Simple paginate (only next/prev, no total count). + */ + protected function simplePaginate(Builder $query): Paginator + { + return $query->simplePaginate( + perPage: $this->perPage(), + page: $this->currentPage(), + ); + } + + /** + * Get pagination parameters as array. + */ + protected function paginationParams(): array + { + return [ + 'per_page' => $this->perPage(), + 'page' => $this->currentPage(), + ]; + } + + /** + * Register pagination query parameters for Scramble documentation. + * Call this in controller's __construct or method docblock. + */ + public static function paginationQueryParams(): array + { + return [ + 'per_page' => [ + 'type' => 'integer', + 'description' => 'Ilość elementów na stronę (1-100).', + 'default' => 20, + 'example' => 10, + ], + 'page' => [ + 'type' => 'integer', + 'description' => 'Numer strony.', + 'default' => 1, + 'example' => 2, + ], + ]; + } +} diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index 6d7a019..69f98c3 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -2,12 +2,16 @@ namespace App\Http\Controllers\Account; +use App\Http\Concerns\WithPagination; use App\Http\Controllers\Controller; use App\Http\Requests\Account\StoreAccountRequest; use App\Http\Requests\Account\UpdateAccountRequest; use App\Http\Resources\Account\AccountResource; use App\Models\Account\Account; use App\Services\Account\AccountService; +use Dedoc\Scramble\Attributes\BodyParameter; +use Dedoc\Scramble\Attributes\PathParameter; +use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Http\JsonResponse; /** @@ -17,6 +21,8 @@ */ class AccountController extends Controller { + use WithPagination; + public function __construct( private readonly AccountService $accountService, ) { @@ -25,18 +31,25 @@ public function __construct( /** * Wyświetla listę kont bankowych użytkownika. */ + #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] + #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] public function index() { + $accounts = Account::where('user_id', auth()->id()) + ->with('cards'); + return AccountResource::collection( - Account::where('user_id', auth()->id()) - ->with('cards') - ->paginate(20), + $this->paginate($accounts) ); } /** * Tworzy nowe konto bankowe dla użytkownika. */ + #[BodyParameter('name', description: 'Nazwa konta', type: 'string', example: 'Konto oszczędnościowe')] + #[BodyParameter('currency', description: 'Waluta konta', type: 'string', example: 'PLN')] + #[BodyParameter('type', description: 'Typ konta', type: 'string', example: 'current')] + #[BodyParameter('with_card', description: 'Czy utworzyć kartę płatniczą', type: 'boolean', example: true)] public function store(StoreAccountRequest $request): JsonResponse { $account = $this->accountService->createAccount( @@ -55,6 +68,7 @@ public function store(StoreAccountRequest $request): JsonResponse /** * Wyświetla szczegóły konta bankowego, w tym powiązane karty. */ + #[PathParameter('account', description: 'Account being viewed', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function show(Account $account): AccountResource { return new AccountResource($account->load('cards')); @@ -63,6 +77,8 @@ public function show(Account $account): AccountResource /** * Aktualizuje nazwę konta bankowego. */ + #[PathParameter('account', description: 'ID konta bankowego', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] + #[BodyParameter('name', description: 'Nowa nazwa konta', type: 'string', example: 'Konto oszczędnościowe')] public function update(UpdateAccountRequest $request, Account $account): AccountResource { $account = $this->accountService->updateName($account, $request->name); @@ -73,6 +89,7 @@ public function update(UpdateAccountRequest $request, Account $account): Account /** * Usuwa konto bankowe, zamykając je. */ + #[PathParameter('account', description: 'ID konta bankowego', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function destroy(Account $account): JsonResponse { $this->accountService->closeAccount($account); diff --git a/backend/app/Http/Controllers/Account/Card/CardController.php b/backend/app/Http/Controllers/Account/Card/CardController.php index 42028e3..c0bc6d7 100644 --- a/backend/app/Http/Controllers/Account/Card/CardController.php +++ b/backend/app/Http/Controllers/Account/Card/CardController.php @@ -2,20 +2,29 @@ namespace App\Http\Controllers\Account\Card; +use App\Http\Concerns\WithPagination; use App\Http\Controllers\Controller; use App\Http\Resources\Account\Card\CardResource; use App\Models\Account\Account; use App\Models\Account\Card\Card; use App\Services\Account\Card\CardService; +use Dedoc\Scramble\Attributes\PathParameter; +use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Http\JsonResponse; +use Illuminate\Validation\UnauthorizedException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Zarządzanie kartami płatniczymi. * * @tags Card + * + * @queryParam account integer ID konta, do którego przypisana jest karta. Required. Example: "123e4567-e89b-12d3-a456-426614174000" */ class CardController extends Controller { + use WithPagination; + public function __construct( private readonly CardService $cardService, ) { @@ -24,18 +33,32 @@ public function __construct( /** * Wyświetla listę kart płatniczych przypisanych do konta. */ + #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] + #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] + #[PathParameter('account', description: 'ID konta', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function index(Account $account) { + if ($account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Account not found or access denied.'); + } + + $cards = $account->cards(); + return CardResource::collection( - $account->cards()->paginate(20), + $this->paginate($cards) ); } /** * Tworzy nową kartę płatniczą dla konta. */ + #[PathParameter('account', description: 'ID konta', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function store(Account $account): JsonResponse { + if ($account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Account not found or access denied.'); + } + $card = $this->cardService->createCard($account->id); return new CardResource($card) @@ -46,16 +69,26 @@ public function store(Account $account): JsonResponse /** * Wyświetla szczegóły karty płatniczej. */ + #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function show(Card $card): CardResource { + if ($card->account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Card not found or access denied.'); + } + return new CardResource($card); } /** * Blokuje kartę płatniczą. */ + #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function block(Card $card): CardResource { + if ($card->account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Card not found or access denied.'); + } + $card = $this->cardService->blockCard($card); return new CardResource($card); @@ -64,8 +97,13 @@ public function block(Card $card): CardResource /** * Odblokowuje kartę płatniczą. */ + #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function unblock(Card $card): CardResource { + if ($card->account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Card not found or access denied.'); + } + $card = $this->cardService->unblockCard($card); return new CardResource($card); diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php index 3aa2580..9d475d2 100644 --- a/backend/app/Http/Controllers/Controller.php +++ b/backend/app/Http/Controllers/Controller.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Http\Concerns\WithPagination; + abstract class Controller { } diff --git a/backend/app/Http/Controllers/User/ProfileController.php b/backend/app/Http/Controllers/User/ProfileController.php index 59785e1..a52183e 100644 --- a/backend/app/Http/Controllers/User/ProfileController.php +++ b/backend/app/Http/Controllers/User/ProfileController.php @@ -3,8 +3,11 @@ namespace App\Http\Controllers\User; use App\Http\Controllers\Controller; +use App\Http\Requests\User\RegisterUserRequest; use App\Http\Requests\User\UpdateProfileRequest; use App\Models\User; +use Dedoc\Scramble\Attributes\BodyParameter; +use Dedoc\Scramble\Attributes\PathParameter; use Illuminate\Auth\Events\Registered; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -18,16 +21,15 @@ * * @tags Profile */ -#[AuthorizeToken(['user-profile'], anyScope: true)] class ProfileController extends Controller { /** * Informacja o profilu użytkownika. * * Zwraca dane aktualnego zalogowanego użytkownika. - * - * @unauthenticated */ + #[AuthorizeToken(['user-profile'], anyScope: true)] + public function show(Request $request): JsonResponse { return response()->json($request->user()->toArray()); @@ -38,6 +40,12 @@ public function show(Request $request): JsonResponse * * Aktualizuje dane profilowe użytkownika. */ + #[AuthorizeToken(['user-profile'], anyScope: true)] + #[BodyParameter('name', description: 'Nowa nazwa użytkownika', example: 'Jan Kowalski')] + #[BodyParameter('email', description: 'Nowy adres email', example: 'jan.kowalski@example.com')] + #[BodyParameter('password', description: 'Nowe hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', example: 'P@ssw0rd123!@')] + #[BodyParameter('password_confirmation', description: 'Potwierdzenie nowego hasła', example: 'P@ssw0rd123!@')] + #[BodyParameter('old_password', description: 'Aktualne hasło (wymagane przy zmianie hasła)', example: 'NewP@ssw0rd123!@')] public function update(UpdateProfileRequest $request): JsonResponse { $request->user()->update($request->validated()); @@ -53,6 +61,7 @@ public function update(UpdateProfileRequest $request): JsonResponse * * Usuwa konto zalogowanego użytkownika - na zawsze! */ + #[AuthorizeToken(['user-profile'], anyScope: true)] public function destroy(Request $request): JsonResponse { $request->user()->delete(); @@ -65,6 +74,7 @@ public function destroy(Request $request): JsonResponse * * Cofnij aktualny token dostępu użytkownika, unieważniając go. */ + #[AuthorizeToken(['user-profile'], anyScope: true)] public function revokeToken(Request $request): JsonResponse { $request->user()->token()->revoke(); @@ -76,35 +86,22 @@ public function revokeToken(Request $request): JsonResponse * Rejestracja użytkownika w systemie. * * Tworzy nowe konto użytkownika. + * + * @unauthenticated */ - public function create(Request $request): User + #[BodyParameter('name', description: 'Nazwa użytkownika', type: 'string', example: 'Jan Kowalski')] + #[BodyParameter('email', description: 'Adres email', type: 'email', example: 'jan.kowalski@example.com')] + #[BodyParameter('password', description: 'Hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', type: 'password', example: 'P@ssw0rd123!@')] + #[BodyParameter('password_confirmation', description: 'Potwierdzenie nowego hasła', type: 'string', example: 'P@ssw0rd123!@')] + public function create(RegisterUserRequest $request): User { - $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], - 'password' => [ - 'required', - 'string', - 'confirmed', - Password::min(8) - ->max(255) - ->letters() - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised(), - ], - ]); + $validated = $request->validated(); - $user = User::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => Hash::make($request->password), - ]); + unset($validated['password_confirmation']); - event(new Registered($user)); + $user = User::create($validated); - Auth::login($user); + event(new Registered($user)); return $user; } diff --git a/backend/app/Http/Requests/Account/StoreAccountRequest.php b/backend/app/Http/Requests/Account/StoreAccountRequest.php index d39d10c..0576550 100644 --- a/backend/app/Http/Requests/Account/StoreAccountRequest.php +++ b/backend/app/Http/Requests/Account/StoreAccountRequest.php @@ -30,7 +30,7 @@ public function rules(): array ]; } - public function bodyParameters(): array + public function queryParameters(): array { return [ 'name' => [ diff --git a/backend/app/Http/Requests/Account/UpdateAccountRequest.php b/backend/app/Http/Requests/Account/UpdateAccountRequest.php index fe3659c..432bf0b 100644 --- a/backend/app/Http/Requests/Account/UpdateAccountRequest.php +++ b/backend/app/Http/Requests/Account/UpdateAccountRequest.php @@ -26,4 +26,14 @@ public function rules(): array 'name' => ['required', 'string', 'max:255'], ]; } + + public function queryParameters(): array + { + return [ + 'name' => [ + 'description' => 'Nazwa konta', + 'example' => 'Konto oszczędnościowe', + ], + ]; + } } diff --git a/backend/app/Http/Requests/User/RegisterUserRequest.php b/backend/app/Http/Requests/User/RegisterUserRequest.php new file mode 100644 index 0000000..4d1e477 --- /dev/null +++ b/backend/app/Http/Requests/User/RegisterUserRequest.php @@ -0,0 +1,44 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => [ + 'required', + 'string', + 'confirmed', + Password::min(8) + ->max(255) + ->letters() + ->mixedCase() + ->numbers() + ->symbols() +// ->uncompromised(), + ], + ]; + } +} diff --git a/backend/app/Http/Requests/User/UpdateProfileRequest.php b/backend/app/Http/Requests/User/UpdateProfileRequest.php index 5860103..a719ca8 100644 --- a/backend/app/Http/Requests/User/UpdateProfileRequest.php +++ b/backend/app/Http/Requests/User/UpdateProfileRequest.php @@ -37,13 +37,13 @@ public function rules(): array ->mixedCase() ->numbers() ->symbols() - ->uncompromised(), +// ->uncompromised(), ], 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], ]; } - public function bodyParameters(): array + public function queryParameters(): array { return [ 'name' => [ diff --git a/backend/app/Http/Resources/Account/AccountResource.php b/backend/app/Http/Resources/Account/AccountResource.php index a50a753..8f901b3 100644 --- a/backend/app/Http/Resources/Account/AccountResource.php +++ b/backend/app/Http/Resources/Account/AccountResource.php @@ -9,6 +9,7 @@ /** * @property Account $resource + * @tag Account */ class AccountResource extends JsonResource { diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 76f6f3a..2b0ae76 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -19,9 +19,9 @@ use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; -#[Fillable(['id', 'name', 'email'])] +#[Fillable(['id', 'name', 'email', 'password'])] #[Hidden(['password', 'remember_token'])] -class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail +class User extends Authenticatable implements OAuthenticatable { /** @use HasFactory */ use HasFactory; diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index b0e7e62..049e062 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -3,6 +3,9 @@ namespace App\Providers; use App\Http\Responses\AuthorizationViewResponse; +use Dedoc\Scramble\Scramble; +use Dedoc\Scramble\Support\Generator\OpenApi; +use Dedoc\Scramble\Support\Generator\SecurityScheme; use Illuminate\Support\ServiceProvider; use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; use Laravel\Passport\Passport; @@ -18,6 +21,15 @@ public function register(): void public function boot(): void { + Scramble::registerApi('docs'); + + Scramble::configure() + ->withDocumentTransformers(function (OpenApi $openApi) { + $openApi->secure( + SecurityScheme::http('bearer', 'JWT'), + ); + }); + Passport::tokensExpireIn(now()->addDays(15)); Passport::refreshTokensExpireIn(now()->addDays(30)); diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index c52b05b..90e3a09 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; - return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', @@ -32,6 +31,7 @@ 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]); $middleware->group('web', [ @@ -52,7 +52,6 @@ \Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, -// \App\Http\Middleware\ForceJsonResponse::class, ]); }) // ->withSingletons([ @@ -62,40 +61,40 @@ // Illuminate\Contracts\Debug\ExceptionHandler::class => Illuminate\Foundation\Exceptions\Handler::class, // ]) ->withExceptions(function (Exceptions $exceptions) { -// $exceptions->renderable(function (ValidationException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (AuthenticationException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (AuthorizationException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (ModelNotFoundException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (NotFoundHttpException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (UnauthorizedHttpException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (AccessDeniedHttpException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (ThrottleRequestsException $e) { -// return Handler::handleException($e); -// }); -// -// $exceptions->renderable(function (MissingScopeException $e) { -// return Handler::handleException($e); -// }); + // $exceptions->renderable(function (ValidationException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (AuthenticationException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (AuthorizationException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (ModelNotFoundException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (NotFoundHttpException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (UnauthorizedHttpException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (AccessDeniedHttpException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (ThrottleRequestsException $e) { + // return Handler::handleException($e); + // }); + // + // $exceptions->renderable(function (MissingScopeException $e) { + // return Handler::handleException($e); + // }); }) ->create(); diff --git a/backend/config/scramble.php b/backend/config/scramble.php index eba4646..9b3615b 100644 --- a/backend/config/scramble.php +++ b/backend/config/scramble.php @@ -56,7 +56,7 @@ /* * Hide the schemas in the Table of Contents. Enabled by default. */ - 'hide_schemas' => true, + 'hide_schemas' => false, /* * URL to an image that displays as a small square logo next to the title, above the table of contents. @@ -97,6 +97,15 @@ 'use_faker' => true, ], + 'security' => [ + 'bearer' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + 'description' => 'Wprowadź token JWT: **Bearer {token}**', + ], + ], + /* * Determines how Scramble stores the descriptions of enum cases. * Available options: @@ -134,8 +143,8 @@ 'flatten_deep_query_parameters' => true, 'middleware' => [ - 'web', - RestrictedDocsAccess::class, +// 'web', +// RestrictedDocsAccess::class, ], 'extensions' => [], diff --git a/backend/routes/api.php b/backend/routes/api.php index b72f8a9..54644a2 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -7,6 +7,12 @@ use Illuminate\Support\Facades\Route; use Laravel\Passport\Http\Middleware\CheckToken; +Route::prefix('/')->group(function () { + return response()->json([ + 'message' => 'Access denied.' + ]); +}); + Route::middleware(['auth:api'])->group(function () { Route::prefix('/user')->group(function () { Route::middleware('guest')->withoutMiddleware(['auth:api'])->group(function () { diff --git a/backend/routes/auth.php b/backend/routes/auth.php index 56dc91f..cdd2dde 100644 --- a/backend/routes/auth.php +++ b/backend/routes/auth.php @@ -11,8 +11,8 @@ use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { - Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); +// Route::get('register', [RegisteredUserController::class, 'create']) +// ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); diff --git a/backend/routes/web.php b/backend/routes/web.php index c9b6434..1bb3a9e 100644 --- a/backend/routes/web.php +++ b/backend/routes/web.php @@ -3,21 +3,21 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { - return redirect()->route('dashboard'); + return redirect('/api'); }); // Route::get('/docs', function () { // return view('vendor.redoc.redoc'); // }); - -Route::get('/dashboard', function () { - return view('dashboard'); -})->middleware(['auth', 'verified'])->name('dashboard'); - -Route::middleware('auth')->group(function () { - Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); - Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); -}); - -require __DIR__ . '/auth.php'; +// +//Route::get('/dashboard', function () { +// return view('dashboard'); +//})->middleware(['auth', 'verified'])->name('dashboard'); +// +//Route::middleware('auth')->group(function () { +// Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); +// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); +// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +//}); +// +//require __DIR__ . '/auth.php'; diff --git a/backend/tests/Feature/Account/Card/CardTest.php b/backend/tests/Feature/Account/Card/CardTest.php index 3166da1..6900d59 100644 --- a/backend/tests/Feature/Account/Card/CardTest.php +++ b/backend/tests/Feature/Account/Card/CardTest.php @@ -2,17 +2,304 @@ namespace Tests\Feature\Account\Card; +use App\Models\Account\Account; +use App\Models\Account\Card\Card; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Laravel\Passport\Passport; use Tests\TestCase; class CardTest extends TestCase { - /** - * A basic feature test example. - */ - public function testExample(): void + use RefreshDatabase; + + private User $user; + private Account $account; + + public function setUp(): void + { + parent::setUp(); + $this->user = User::factory()->create(); + $this->account = Account::factory()->create(['user_id' => $this->user->id]); + } + + public function testListsCardsForAccount(): void + { + Card::factory()->count(3)->create(['account_id' => $this->account->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(200) + ->assertJsonCount(3, 'data') + ->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'card_number', + 'card_last_four', + 'exp_month', + 'exp_year', + 'network', + 'type', + 'status', + 'created_at', + 'updated_at', + ], + ], + 'links', + 'meta', + ]); + } + + public function testListsOnlyCardsForGivenAccount(): void + { + Card::factory()->count(2)->create(['account_id' => $this->account->id]); + + $otherAccount = Account::factory()->create(['user_id' => $this->user->id]); + Card::factory()->count(3)->create(['account_id' => $otherAccount->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(200) + ->assertJsonCount(2, 'data'); + } + + public function testCreatesNewCardForAccount(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(201) + ->assertJsonPath('data.status', 'active') + ->assertJsonPath('data.type', 'debit') + ->assertJsonStructure([ + 'data' => [ + 'id', + 'card_number', + 'card_last_four', + 'exp_month', + 'exp_year', + 'network', + 'type', + 'status', + 'created_at', + 'updated_at', + ], + ]); + + $this->assertDatabaseHas('cards', [ + 'account_id' => $this->account->id, + 'status' => 'active', + ]); + } + + public function testCreatesCardWithValidNumber(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(201); + + $cardNumber = $response->json('data.card_number'); + + // Format: 4532 **** **** 9012 + $this->assertMatchesRegularExpression( + '/^\d{4} \*{4} \*{4} \d{4}$/', + $cardNumber + ); + } + + public function testCreatesCardWithFutureExpiryDate(): void + { + Passport::actingAs($this->user); + + $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(201); + + $expYear = $response->json('data.exp_year'); + $currentYear = (int) now()->format('Y'); + + $this->assertGreaterThanOrEqual($currentYear, $expYear); + } + + public function testMasksCardNumberInResponse(): void + { + Card::factory()->create([ + 'account_id' => $this->account->id, + 'card_number' => '4532123456789012', + ]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(200) + ->assertJsonPath('data.0.card_number', '4532 **** **** 9012') + ->assertJsonPath('data.0.card_last_four', '9012'); + } + + public function testNeverExposesCvvInResponse(): void + { + Card::factory()->create([ + 'account_id' => $this->account->id, + 'cvv' => '123', + ]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); + + $response->assertStatus(200) + ->assertJsonMissing(['cvv']); + } + + public function testShowsCardDetails(): void { - $response = $this->get('/'); + $card = Card::factory()->create([ + 'account_id' => $this->account->id, + 'card_number' => '5214123456789012', + ]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/cards/{$card->id}"); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $card->id) + ->assertJsonPath('data.card_number', '5214 **** **** 9012') + ->assertJsonPath('data.card_last_four', '9012') + ->assertJsonStructure([ + 'data' => [ + 'id', + 'card_number', + 'card_last_four', + 'exp_month', + 'exp_year', + 'network', + 'type', + 'status', + 'created_at', + 'updated_at', + ], + ]); + } + + public function testBlocksCard(): void + { + $card = Card::factory()->create([ + 'account_id' => $this->account->id, + 'status' => 'active', + ]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/cards/{$card->id}/block"); + + $response->assertStatus(200) + ->assertJsonPath('data.status', 'blocked'); + + $this->assertDatabaseHas('cards', [ + 'id' => $card->id, + 'status' => 'blocked', + ]); + } + + public function testUnblocksCard(): void + { + $card = Card::factory()->create([ + 'account_id' => $this->account->id, + 'status' => 'blocked', + ]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/cards/{$card->id}/unblock"); + + $response->assertStatus(200) + ->assertJsonPath('data.status', 'active'); + + $this->assertDatabaseHas('cards', [ + 'id' => $card->id, + 'status' => 'active', + ]); + } + + public function testCannotAccessCardFromOtherUser(): void + { + $otherUser = User::factory()->create(); + $otherAccount = Account::factory()->create(['user_id' => $otherUser->id]); + $card = Card::factory()->create(['account_id' => $otherAccount->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/cards/{$card->id}"); + + $response->assertStatus(403); // lub 404 + } + + public function testCannotBlockCardFromOtherUser(): void + { + $otherUser = User::factory()->create(); + $otherAccount = Account::factory()->create(['user_id' => $otherUser->id]); + $card = Card::factory()->create(['account_id' => $otherAccount->id]); + + Passport::actingAs($this->user); + + $response = $this->patchJson("/api/cards/{$card->id}/block"); + + $response->assertStatus(403); + } + + public function testRequiresAuthentication(): void + { + $card = Card::factory()->create(['account_id' => $this->account->id]); + + $this->getJson("/api/accounts/{$this->account->id}/cards") + ->assertStatus(401); + + $this->postJson("/api/accounts/{$this->account->id}/cards") + ->assertStatus(401); + + $this->getJson("/api/cards/{$card->id}") + ->assertStatus(401); + } + + public function testReturns404ForNonExistentCard(): void + { + Passport::actingAs($this->user); + + $response = $this->getJson('/api/cards/99999999-9999-9999-9999-999999999999'); + + $response->assertStatus(404); + } + + public function testReturns404ForNonExistentAccount(): void + { + Passport::actingAs($this->user); + + $response = $this->getJson('/api/accounts/99999999-9999-9999-9999-999999999999/cards'); + + $response->assertStatus(404); + } + + public function testPaginatesCardsList(): void + { + Card::factory()->count(25)->create(['account_id' => $this->account->id]); + + Passport::actingAs($this->user); + + $response = $this->getJson("/api/accounts/{$this->account->id}/cards?per_page=10"); - $response->assertStatus(200); + $response->assertStatus(200) + ->assertJsonCount(10, 'data') + ->assertJsonPath('meta.total', 25) + ->assertJsonPath('meta.per_page', 10); } } From 11c0cd2038a4a5f3dabf3e3c6321bb3d015c8928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sun, 10 May 2026 22:27:19 +0200 Subject: [PATCH 07/17] add frontend: - view accounts - create account - create card (only default network, and type in backend) - block/unblock/delete card. backend: - fix docs - scope edit - add scope for controller - tests fix for accounts and cards. --- .gitignore | 1 + backend/.env.docker | 4 +- backend/.env.example | 6 +- backend/app/Exceptions/Handler.php | 9 + backend/app/Http/Concerns/WithPagination.php | 14 +- .../Controllers/Account/AccountController.php | 10 +- .../Account/Card/CardController.php | 27 +- backend/app/Http/Controllers/Controller.php | 2 - .../Controllers/User/ProfileController.php | 16 +- .../Requests/User/RegisterUserRequest.php | 5 +- .../Requests/User/UpdateProfileRequest.php | 4 +- .../Resources/Account/AccountResource.php | 1 + backend/app/Models/Account/Account.php | 24 +- backend/app/Models/User.php | 2 +- backend/app/Providers/AppServiceProvider.php | 4 +- .../app/Services/Account/Card/CardService.php | 8 + backend/bootstrap/app.php | 6 +- backend/config/scramble.php | 10 +- backend/docker/entrypoint.sh | 5 + backend/routes/api.php | 5 +- backend/routes/auth.php | 48 +-- backend/routes/web.php | 28 +- .../tests/Feature/Account/Card/CardTest.php | 30 +- frontend/src/App.tsx | 66 ++- frontend/src/components/Auth/LoginForm.tsx | 10 +- frontend/src/components/Auth/RegisterForm.tsx | 20 +- .../src/components/Layout/DashboardLayout.tsx | 197 +++++++++ .../src/components/Layout/GuestOnlyRoute.tsx | 4 +- .../src/components/Layout/ProtectedRoute.tsx | 21 +- frontend/src/hooks/formatedDate.tsx | 27 -- frontend/src/pages/Account/AccountPage.tsx | 305 ++++++++++++++ frontend/src/pages/Account/AccoutViewPage.tsx | 380 ++++++++++++++++++ frontend/src/pages/Account/Card/CardPage.tsx | 0 frontend/src/pages/User/ProfilePage.tsx | 37 +- frontend/src/services/accountService.ts | 105 +++++ frontend/src/services/authService.ts | 5 +- frontend/src/types/types.ts | 71 +++- frontend/src/utils/formatedDate.tsx | 88 ++++ 38 files changed, 1404 insertions(+), 201 deletions(-) create mode 100644 frontend/src/components/Layout/DashboardLayout.tsx delete mode 100644 frontend/src/hooks/formatedDate.tsx create mode 100644 frontend/src/pages/Account/AccountPage.tsx create mode 100644 frontend/src/pages/Account/AccoutViewPage.tsx create mode 100644 frontend/src/pages/Account/Card/CardPage.tsx create mode 100644 frontend/src/services/accountService.ts create mode 100644 frontend/src/utils/formatedDate.tsx diff --git a/.gitignore b/.gitignore index c979b3d..d392405 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ vendor/ # Environment .env.local .env.*.local +.env # Logs storage/logs/ diff --git a/backend/.env.docker b/backend/.env.docker index 11ce845..91df333 100644 --- a/backend/.env.docker +++ b/backend/.env.docker @@ -2,7 +2,9 @@ APP_NAME="Bank Online System" APP_ENV=local APP_KEY=base64:GApRAIeV6cXRlYg61WKVW9sz/WemoGBppXgyhgUt4DA= APP_DEBUG=true -APP_URL=http://localhost:8000 +APP_URL=http://127.0.0.1:8000 + +APP_FRONTEND_URL=http://localhost:5173 APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/backend/.env.example b/backend/.env.example index d09634a..a4e153b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,8 +1,10 @@ APP_NAME="Bank Online System" APP_ENV=local -APP_KEY=base64:GApRAIeV6cXRlYg61WKVW9sz/WemoGBppXgyhgUt4DA= +APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=http://127.0.0.1:8000 + +APP_FRONTEND_URL=http://localhost:5173 APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php index 673c886..0d5a43c 100644 --- a/backend/app/Exceptions/Handler.php +++ b/backend/app/Exceptions/Handler.php @@ -12,6 +12,7 @@ use Laravel\Passport\Exceptions\MissingScopeException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -42,6 +43,14 @@ private function handleApiException(\Throwable $e): JsonResponse ], 403); } + if ($e instanceof HttpException) { + return response()->json([ + 'status' => 'error', + 'message' => $e->getMessage(), + 'code' => 403, + ], 403); + } + if ($e instanceof AuthenticationException || $e instanceof UnauthorizedHttpException) { return response()->json([ 'status' => 'error', diff --git a/backend/app/Http/Concerns/WithPagination.php b/backend/app/Http/Concerns/WithPagination.php index 9c8895d..fdca9fa 100644 --- a/backend/app/Http/Concerns/WithPagination.php +++ b/backend/app/Http/Concerns/WithPagination.php @@ -61,7 +61,7 @@ protected function paginationParams(): array { return [ 'per_page' => $this->perPage(), - 'page' => $this->currentPage(), + 'page' => $this->currentPage(), ]; } @@ -73,16 +73,16 @@ public static function paginationQueryParams(): array { return [ 'per_page' => [ - 'type' => 'integer', + 'type' => 'integer', 'description' => 'Ilość elementów na stronę (1-100).', - 'default' => 20, - 'example' => 10, + 'default' => 20, + 'example' => 10, ], 'page' => [ - 'type' => 'integer', + 'type' => 'integer', 'description' => 'Numer strony.', - 'default' => 1, - 'example' => 2, + 'default' => 1, + 'example' => 2, ], ]; } diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index 69f98c3..ee09440 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -13,11 +13,12 @@ use Dedoc\Scramble\Attributes\PathParameter; use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Http\JsonResponse; +use Laravel\Passport\Attributes\AuthorizeToken; /** * Zarządzanie kontem bankowym. * - * @tags Account + * @tags Konto bankowe */ class AccountController extends Controller { @@ -31,6 +32,7 @@ public function __construct( /** * Wyświetla listę kont bankowych użytkownika. */ + #[AuthorizeToken(['accounts-view'], anyScope: true)] #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] public function index() @@ -39,13 +41,14 @@ public function index() ->with('cards'); return AccountResource::collection( - $this->paginate($accounts) + $this->paginate($accounts), ); } /** * Tworzy nowe konto bankowe dla użytkownika. */ + #[AuthorizeToken(['accounts-manage'], anyScope: true)] #[BodyParameter('name', description: 'Nazwa konta', type: 'string', example: 'Konto oszczędnościowe')] #[BodyParameter('currency', description: 'Waluta konta', type: 'string', example: 'PLN')] #[BodyParameter('type', description: 'Typ konta', type: 'string', example: 'current')] @@ -68,6 +71,7 @@ public function store(StoreAccountRequest $request): JsonResponse /** * Wyświetla szczegóły konta bankowego, w tym powiązane karty. */ + #[AuthorizeToken(['accounts-details'], anyScope: true)] #[PathParameter('account', description: 'Account being viewed', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function show(Account $account): AccountResource { @@ -77,6 +81,7 @@ public function show(Account $account): AccountResource /** * Aktualizuje nazwę konta bankowego. */ + #[AuthorizeToken(['accounts-manage'], anyScope: true)] #[PathParameter('account', description: 'ID konta bankowego', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] #[BodyParameter('name', description: 'Nowa nazwa konta', type: 'string', example: 'Konto oszczędnościowe')] public function update(UpdateAccountRequest $request, Account $account): AccountResource @@ -89,6 +94,7 @@ public function update(UpdateAccountRequest $request, Account $account): Account /** * Usuwa konto bankowe, zamykając je. */ + #[AuthorizeToken(['accounts-manage'], anyScope: true)] #[PathParameter('account', description: 'ID konta bankowego', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function destroy(Account $account): JsonResponse { diff --git a/backend/app/Http/Controllers/Account/Card/CardController.php b/backend/app/Http/Controllers/Account/Card/CardController.php index c0bc6d7..898cdf6 100644 --- a/backend/app/Http/Controllers/Account/Card/CardController.php +++ b/backend/app/Http/Controllers/Account/Card/CardController.php @@ -11,13 +11,13 @@ use Dedoc\Scramble\Attributes\PathParameter; use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Http\JsonResponse; -use Illuminate\Validation\UnauthorizedException; +use Laravel\Passport\Attributes\AuthorizeToken; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Zarządzanie kartami płatniczymi. * - * @tags Card + * @tags Karty * * @queryParam account integer ID konta, do którego przypisana jest karta. Required. Example: "123e4567-e89b-12d3-a456-426614174000" */ @@ -33,6 +33,7 @@ public function __construct( /** * Wyświetla listę kart płatniczych przypisanych do konta. */ + #[AuthorizeToken(['cards-view'], anyScope: true)] #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] #[PathParameter('account', description: 'ID konta', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] @@ -45,13 +46,14 @@ public function index(Account $account) $cards = $account->cards(); return CardResource::collection( - $this->paginate($cards) + $this->paginate($cards), ); } /** * Tworzy nową kartę płatniczą dla konta. */ + #[AuthorizeToken(['cards-manage'], anyScope: true)] #[PathParameter('account', description: 'ID konta', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function store(Account $account): JsonResponse { @@ -69,6 +71,7 @@ public function store(Account $account): JsonResponse /** * Wyświetla szczegóły karty płatniczej. */ + #[AuthorizeToken(['cards-details'], anyScope: true)] #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function show(Card $card): CardResource { @@ -82,6 +85,7 @@ public function show(Card $card): CardResource /** * Blokuje kartę płatniczą. */ + #[AuthorizeToken(['cards-manage'], anyScope: true)] #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function block(Card $card): CardResource { @@ -97,6 +101,7 @@ public function block(Card $card): CardResource /** * Odblokowuje kartę płatniczą. */ + #[AuthorizeToken(['cards-manage'], anyScope: true)] #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] public function unblock(Card $card): CardResource { @@ -108,4 +113,20 @@ public function unblock(Card $card): CardResource return new CardResource($card); } + + /** + * Odblokowuje kartę płatniczą. + */ + #[AuthorizeToken(['cards-manage'], anyScope: true)] + #[PathParameter('card', description: 'ID karty', type: 'string', format: 'uuid', example: '550e8400-e29b-41d4-a716-446655440000')] + public function destroy(Card $card) + { + if ($card->account->user_id !== auth()->id()) { + throw new AccessDeniedHttpException('Card not found or access denied.'); + } + + $this->cardService->deleteCard($card); + + return response()->json(null, 200); + } } diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php index 9d475d2..3aa2580 100644 --- a/backend/app/Http/Controllers/Controller.php +++ b/backend/app/Http/Controllers/Controller.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use App\Http\Concerns\WithPagination; - abstract class Controller { } diff --git a/backend/app/Http/Controllers/User/ProfileController.php b/backend/app/Http/Controllers/User/ProfileController.php index a52183e..266a1c9 100644 --- a/backend/app/Http/Controllers/User/ProfileController.php +++ b/backend/app/Http/Controllers/User/ProfileController.php @@ -7,19 +7,15 @@ use App\Http\Requests\User\UpdateProfileRequest; use App\Models\User; use Dedoc\Scramble\Attributes\BodyParameter; -use Dedoc\Scramble\Attributes\PathParameter; use Illuminate\Auth\Events\Registered; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; -use Illuminate\Validation\Rules\Password; use Laravel\Passport\Attributes\AuthorizeToken; /** * Zarządzanie profilem użytkownika. * - * @tags Profile + * @tags Użytkownik */ class ProfileController extends Controller { @@ -29,7 +25,6 @@ class ProfileController extends Controller * Zwraca dane aktualnego zalogowanego użytkownika. */ #[AuthorizeToken(['user-profile'], anyScope: true)] - public function show(Request $request): JsonResponse { return response()->json($request->user()->toArray()); @@ -40,7 +35,7 @@ public function show(Request $request): JsonResponse * * Aktualizuje dane profilowe użytkownika. */ - #[AuthorizeToken(['user-profile'], anyScope: true)] + #[AuthorizeToken(['user-profile-manage'], anyScope: true)] #[BodyParameter('name', description: 'Nowa nazwa użytkownika', example: 'Jan Kowalski')] #[BodyParameter('email', description: 'Nowy adres email', example: 'jan.kowalski@example.com')] #[BodyParameter('password', description: 'Nowe hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', example: 'P@ssw0rd123!@')] @@ -61,7 +56,7 @@ public function update(UpdateProfileRequest $request): JsonResponse * * Usuwa konto zalogowanego użytkownika - na zawsze! */ - #[AuthorizeToken(['user-profile'], anyScope: true)] + #[AuthorizeToken(['user-profile-manage'], anyScope: true)] public function destroy(Request $request): JsonResponse { $request->user()->delete(); @@ -74,7 +69,6 @@ public function destroy(Request $request): JsonResponse * * Cofnij aktualny token dostępu użytkownika, unieważniając go. */ - #[AuthorizeToken(['user-profile'], anyScope: true)] public function revokeToken(Request $request): JsonResponse { $request->user()->token()->revoke(); @@ -100,8 +94,8 @@ public function create(RegisterUserRequest $request): User unset($validated['password_confirmation']); $user = User::create($validated); - - event(new Registered($user)); + // Send mail to user, with verify email. + // event(new Registered($user)); return $user; } diff --git a/backend/app/Http/Requests/User/RegisterUserRequest.php b/backend/app/Http/Requests/User/RegisterUserRequest.php index 4d1e477..d91e206 100644 --- a/backend/app/Http/Requests/User/RegisterUserRequest.php +++ b/backend/app/Http/Requests/User/RegisterUserRequest.php @@ -2,7 +2,6 @@ namespace App\Http\Requests\User; -use App\Rules\Password\CurrentPassword; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Password; @@ -36,8 +35,8 @@ public function rules(): array ->letters() ->mixedCase() ->numbers() - ->symbols() -// ->uncompromised(), + ->symbols(), + // ->uncompromised(), ], ]; } diff --git a/backend/app/Http/Requests/User/UpdateProfileRequest.php b/backend/app/Http/Requests/User/UpdateProfileRequest.php index a719ca8..05f9c92 100644 --- a/backend/app/Http/Requests/User/UpdateProfileRequest.php +++ b/backend/app/Http/Requests/User/UpdateProfileRequest.php @@ -36,8 +36,8 @@ public function rules(): array ->letters() ->mixedCase() ->numbers() - ->symbols() -// ->uncompromised(), + ->symbols(), + // ->uncompromised(), ], 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], ]; diff --git a/backend/app/Http/Resources/Account/AccountResource.php b/backend/app/Http/Resources/Account/AccountResource.php index 8f901b3..38772bd 100644 --- a/backend/app/Http/Resources/Account/AccountResource.php +++ b/backend/app/Http/Resources/Account/AccountResource.php @@ -9,6 +9,7 @@ /** * @property Account $resource + * * @tag Account */ class AccountResource extends JsonResource diff --git a/backend/app/Models/Account/Account.php b/backend/app/Models/Account/Account.php index b6a40d2..5067969 100644 --- a/backend/app/Models/Account/Account.php +++ b/backend/app/Models/Account/Account.php @@ -28,7 +28,7 @@ class Account extends Model protected function casts(): array { return [ - 'balance' => 'decimal:2', + 'balance' => 'integer', ]; } @@ -58,8 +58,28 @@ public function transactions() ->orWhere('to_account_id', $this->id); } + public function getBalanceInZlotyAttribute(): float + { + return $this->balance / 100; + } + public function getFormattedBalanceAttribute(): string { - return number_format($this->balance / 100, 2, '.', '') . ' ' . $this->currency; + return number_format($this->balance / 100, 2, ',', ' ') . ' ' . $this->currency; + } + + public function deposit(int $amountInCents): void + { + $this->balance += $amountInCents; + $this->save(); + } + + public function withdraw(int $amountInCents): void + { + if ($this->balance < $amountInCents) { + throw new \Exception('Insufficient funds.'); + } + $this->balance -= $amountInCents; + $this->save(); } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 2b0ae76..100f2a3 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -21,7 +21,7 @@ #[Fillable(['id', 'name', 'email', 'password'])] #[Hidden(['password', 'remember_token'])] -class User extends Authenticatable implements OAuthenticatable +class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail { /** @use HasFactory */ use HasFactory; diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 049e062..b395d08 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -37,7 +37,8 @@ public function boot(): void Passport::tokensCan([ // Podstawowe informacje o użytkowniku - 'user-profile' => 'Odczyt podstawowych danych profilu', + 'user-profile' => 'Odczyt podstawowych danych profilu', + 'user-profile-manage' => 'Edycja podstawowych danych profilu, oraz usuwanie i edycja konta.', // Informacje finansowe 'balance-view' => 'Odczyt bieżącego salda konta', @@ -60,6 +61,7 @@ public function boot(): void 'accounts-view' => 'Odczyt listy kont bankowych', 'accounts-details' => 'Odczyt szczegółów konta (numer, typ, waluta)', 'accounts-statement' => 'Pobieranie wyciągów bankowych', + 'accounts-manage' => 'Zarządzanie kontami (tworzenia, edycja, usuwanie)', // Blik / Płatności mobilne 'blik-generate' => 'Generowanie kodu BLIK', diff --git a/backend/app/Services/Account/Card/CardService.php b/backend/app/Services/Account/Card/CardService.php index 2b43b1f..df089ef 100644 --- a/backend/app/Services/Account/Card/CardService.php +++ b/backend/app/Services/Account/Card/CardService.php @@ -57,4 +57,12 @@ public function unblockCard(Card $card): Card return $card->fresh(); } + + /** + * Delete a card. + */ + public function deleteCard(Card $card): void + { + $card->delete(); + } } diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 90e3a09..5fd4c87 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -54,9 +54,9 @@ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]); }) -// ->withSingletons([ -// \Illuminate\Contracts\Debug\ExceptionHandler::class => \App\Exceptions\Handler::class, -// ]) + ->withSingletons([ + \Illuminate\Contracts\Debug\ExceptionHandler::class => \App\Exceptions\Handler::class, + ]) // ->withSingletons([ // Illuminate\Contracts\Debug\ExceptionHandler::class => Illuminate\Foundation\Exceptions\Handler::class, // ]) diff --git a/backend/config/scramble.php b/backend/config/scramble.php index 9b3615b..6712bd3 100644 --- a/backend/config/scramble.php +++ b/backend/config/scramble.php @@ -99,10 +99,10 @@ 'security' => [ 'bearer' => [ - 'type' => 'http', - 'scheme' => 'bearer', + 'type' => 'http', + 'scheme' => 'bearer', 'bearerFormat' => 'JWT', - 'description' => 'Wprowadź token JWT: **Bearer {token}**', + 'description' => 'Wprowadź token JWT: **Bearer {token}**', ], ], @@ -143,8 +143,8 @@ 'flatten_deep_query_parameters' => true, 'middleware' => [ -// 'web', -// RestrictedDocsAccess::class, + // 'web', + // RestrictedDocsAccess::class, ], 'extensions' => [], diff --git a/backend/docker/entrypoint.sh b/backend/docker/entrypoint.sh index e47fcf6..bb77d0d 100755 --- a/backend/docker/entrypoint.sh +++ b/backend/docker/entrypoint.sh @@ -21,6 +21,11 @@ if [ ! -f "vendor/autoload.php" ]; then fi # Generate app key if not exists +if [ ! -f .env ]; then + echo "📄 Creating .env from .env.example..." + cp .env.example .env +fi + if [ -z "$APP_KEY" ] || [ "$APP_KEY" = "" ]; then echo "🔑 Generating APP_KEY..." php artisan key:generate --force diff --git a/backend/routes/api.php b/backend/routes/api.php index 54644a2..d2cd109 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -9,13 +9,13 @@ Route::prefix('/')->group(function () { return response()->json([ - 'message' => 'Access denied.' + 'message' => 'Access denied.', ]); }); Route::middleware(['auth:api'])->group(function () { Route::prefix('/user')->group(function () { - Route::middleware('guest')->withoutMiddleware(['auth:api'])->group(function () { + Route::middleware('guest')->withoutMiddleware(['auth:api', 'verified'])->group(function () { Route::post('/register', [ProfileController::class, 'create']); }); @@ -40,4 +40,5 @@ Route::get('cards/{card}', [CardController::class, 'show']); Route::patch('cards/{card}/block', [CardController::class, 'block']); Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); + Route::delete('cards/{card}', [CardController::class, 'destroy']); }); diff --git a/backend/routes/auth.php b/backend/routes/auth.php index cdd2dde..5cd5f66 100644 --- a/backend/routes/auth.php +++ b/backend/routes/auth.php @@ -11,8 +11,8 @@ use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { -// Route::get('register', [RegisteredUserController::class, 'create']) -// ->name('register'); + // Route::get('register', [RegisteredUserController::class, 'create']) + // ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); @@ -34,25 +34,25 @@ ->name('password.store'); }); -Route::middleware('auth')->group(function () { - Route::get('verify-email', EmailVerificationPromptController::class) - ->name('verification.notice'); - - Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); - - Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); - - Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) - ->name('password.confirm'); - - Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); - - Route::put('password', [PasswordController::class, 'update'])->name('password.update'); - - Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); -}); +// Route::middleware('auth')->group(function () { +// Route::get('verify-email', EmailVerificationPromptController::class) +// ->name('verification.notice'); +// +// Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) +// ->middleware(['signed', 'throttle:6,1']) +// ->name('verification.verify'); +// +// Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) +// ->middleware('throttle:6,1') +// ->name('verification.send'); +// +// Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) +// ->name('password.confirm'); +// +// Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); +// +// Route::put('password', [PasswordController::class, 'update'])->name('password.update'); +// +// Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) +// ->name('logout'); +// }); diff --git a/backend/routes/web.php b/backend/routes/web.php index 1bb3a9e..e6248ee 100644 --- a/backend/routes/web.php +++ b/backend/routes/web.php @@ -3,21 +3,17 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { - return redirect('/api'); + return redirect()->route('login'); }); -// Route::get('/docs', function () { -// return view('vendor.redoc.redoc'); -// }); -// -//Route::get('/dashboard', function () { -// return view('dashboard'); -//})->middleware(['auth', 'verified'])->name('dashboard'); -// -//Route::middleware('auth')->group(function () { -// Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); -// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); -// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); -//}); -// -//require __DIR__ . '/auth.php'; +Route::get('/dashboard', function () { + return view('dashboard'); +})->middleware(['auth'])->name('dashboard'); + +Route::middleware('auth')->group(function () { + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +}); + +require __DIR__ . '/auth.php'; diff --git a/backend/tests/Feature/Account/Card/CardTest.php b/backend/tests/Feature/Account/Card/CardTest.php index 6900d59..4cda13a 100644 --- a/backend/tests/Feature/Account/Card/CardTest.php +++ b/backend/tests/Feature/Account/Card/CardTest.php @@ -19,7 +19,7 @@ class CardTest extends TestCase public function setUp(): void { parent::setUp(); - $this->user = User::factory()->create(); + $this->user = User::factory()->create(); $this->account = Account::factory()->create(['user_id' => $this->user->id]); } @@ -94,7 +94,7 @@ public function testCreatesNewCardForAccount(): void $this->assertDatabaseHas('cards', [ 'account_id' => $this->account->id, - 'status' => 'active', + 'status' => 'active', ]); } @@ -111,7 +111,7 @@ public function testCreatesCardWithValidNumber(): void // Format: 4532 **** **** 9012 $this->assertMatchesRegularExpression( '/^\d{4} \*{4} \*{4} \d{4}$/', - $cardNumber + $cardNumber, ); } @@ -123,7 +123,7 @@ public function testCreatesCardWithFutureExpiryDate(): void $response->assertStatus(201); - $expYear = $response->json('data.exp_year'); + $expYear = $response->json('data.exp_year'); $currentYear = (int) now()->format('Y'); $this->assertGreaterThanOrEqual($currentYear, $expYear); @@ -132,7 +132,7 @@ public function testCreatesCardWithFutureExpiryDate(): void public function testMasksCardNumberInResponse(): void { Card::factory()->create([ - 'account_id' => $this->account->id, + 'account_id' => $this->account->id, 'card_number' => '4532123456789012', ]); @@ -149,7 +149,7 @@ public function testNeverExposesCvvInResponse(): void { Card::factory()->create([ 'account_id' => $this->account->id, - 'cvv' => '123', + 'cvv' => '123', ]); Passport::actingAs($this->user); @@ -163,7 +163,7 @@ public function testNeverExposesCvvInResponse(): void public function testShowsCardDetails(): void { $card = Card::factory()->create([ - 'account_id' => $this->account->id, + 'account_id' => $this->account->id, 'card_number' => '5214123456789012', ]); @@ -195,7 +195,7 @@ public function testBlocksCard(): void { $card = Card::factory()->create([ 'account_id' => $this->account->id, - 'status' => 'active', + 'status' => 'active', ]); Passport::actingAs($this->user); @@ -206,7 +206,7 @@ public function testBlocksCard(): void ->assertJsonPath('data.status', 'blocked'); $this->assertDatabaseHas('cards', [ - 'id' => $card->id, + 'id' => $card->id, 'status' => 'blocked', ]); } @@ -215,7 +215,7 @@ public function testUnblocksCard(): void { $card = Card::factory()->create([ 'account_id' => $this->account->id, - 'status' => 'blocked', + 'status' => 'blocked', ]); Passport::actingAs($this->user); @@ -226,16 +226,16 @@ public function testUnblocksCard(): void ->assertJsonPath('data.status', 'active'); $this->assertDatabaseHas('cards', [ - 'id' => $card->id, + 'id' => $card->id, 'status' => 'active', ]); } public function testCannotAccessCardFromOtherUser(): void { - $otherUser = User::factory()->create(); + $otherUser = User::factory()->create(); $otherAccount = Account::factory()->create(['user_id' => $otherUser->id]); - $card = Card::factory()->create(['account_id' => $otherAccount->id]); + $card = Card::factory()->create(['account_id' => $otherAccount->id]); Passport::actingAs($this->user); @@ -246,9 +246,9 @@ public function testCannotAccessCardFromOtherUser(): void public function testCannotBlockCardFromOtherUser(): void { - $otherUser = User::factory()->create(); + $otherUser = User::factory()->create(); $otherAccount = Account::factory()->create(['user_id' => $otherUser->id]); - $card = Card::factory()->create(['account_id' => $otherAccount->id]); + $card = Card::factory()->create(['account_id' => $otherAccount->id]); Passport::actingAs($this->user); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3b6de51..bf4f334 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,15 +4,17 @@ import { CssBaseline, Box } from '@mui/material'; import { AuthProvider } from './context/AuthContext'; import { ToastProvider } from './context/ToastContext'; import { LoadingProvider } from './context/LoadingContext'; -import { Header } from './components/Layout/Header'; import { ProtectedRoute } from './components/Layout/ProtectedRoute'; -import { LoadingOverlay } from './components/UI/LoadingOverlay'; import { LoginPage } from './pages/Forms/LoginPage'; import { RegisterPage } from './pages/Forms/RegisterPage'; import { BalancePage } from './pages/User/BalancePage'; import {JSX} from "react"; import {ProfilePage} from "./pages/User/ProfilePage"; import {DashboardPage} from "./pages/Dashboard/DashboardPage"; +import {GuestOnlyRoute} from "./components/Layout/GuestOnlyRoute"; +import {DashboardLayout} from "./components/Layout/DashboardLayout"; +import {AccountPage} from "./pages/Account/AccountPage"; +import {AccountViewPage} from "./pages/Account/AccoutViewPage"; const theme = createTheme({ palette: { @@ -40,6 +42,11 @@ const theme = createTheme({ }, }, components: { + MuiSvgIcon: { + defaultProps: { + fontSize: 'small', + }, + }, MuiButton: { styleOverrides: { root: { @@ -76,41 +83,26 @@ function App(): JSX.Element { - -
- - - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - - - - {/**/} + + {/* Public - bez headera i menu */} + }> + } /> + } /> + + + {/* Protected z menu bocznym i headerem */} + }> + }> + } /> + } /> + } /> + } /> + } /> + + + + } /> + diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index d9d9ddb..168c136 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -43,12 +43,18 @@ export const LoginForm = ({ onLogin, onSwitchToRegister }: LoginFormProps) => { } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Błąd logowania'; setError(errorMessage); - showError(errorMessage); } finally { stopLoading(); } }; + const canLogin = () => { + return ( + email.length > 0 && + password.length > 0 + ); + }; + return ( { fullWidth variant="contained" size="large" - disabled={isLoading} + disabled={isLoading || !canLogin()} startIcon={isLoading ? : } sx={{ mt: 2, py: 1.5 }} > diff --git a/frontend/src/components/Auth/RegisterForm.tsx b/frontend/src/components/Auth/RegisterForm.tsx index fb9a4e7..8c23f13 100644 --- a/frontend/src/components/Auth/RegisterForm.tsx +++ b/frontend/src/components/Auth/RegisterForm.tsx @@ -31,14 +31,6 @@ export const RegisterForm = ({ onRegister, onSwitchToLogin }: RegisterFormProps) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); - - if (password !== passwordConfirmation) { - const errorMessage = 'Hasła nie są takie same'; - setError(errorMessage); - showError(errorMessage); - return; - } - startLoading(); try { @@ -54,6 +46,14 @@ export const RegisterForm = ({ onRegister, onSwitchToLogin }: RegisterFormProps) } }; + const canRegister = () => { + return ( + name.length > 0 && + email.length > 0 && + password.length > 0 + ); + }; + return ( setPasswordConfirmation(e.target.value)} required disabled={isLoading} + error={passwordConfirmation.length > 0 && password !== passwordConfirmation} + helperText={passwordConfirmation.length > 0 && password !== passwordConfirmation ? 'Hasła nie są zgodne' : ''} variant="outlined" /> @@ -139,7 +141,7 @@ export const RegisterForm = ({ onRegister, onSwitchToLogin }: RegisterFormProps) fullWidth variant="contained" size="large" - disabled={isLoading} + disabled={isLoading || !canRegister()} startIcon={isLoading ? : } sx={{ mt: 2, py: 1.5 }} > diff --git a/frontend/src/components/Layout/DashboardLayout.tsx b/frontend/src/components/Layout/DashboardLayout.tsx new file mode 100644 index 0000000..9b9ad44 --- /dev/null +++ b/frontend/src/components/Layout/DashboardLayout.tsx @@ -0,0 +1,197 @@ +import { useState } from 'react'; +import { Outlet, useNavigate, useLocation, Link } from 'react-router-dom'; +import { + Box, + Drawer, + AppBar, + Toolbar, + List, + Typography, + Divider, + IconButton, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Avatar, + Menu, + MenuItem, +} from '@mui/material'; +import { + Menu as MenuIcon, + Dashboard as DashboardIcon, + AccountBalance as AccountIcon, + CreditCard as CardIcon, + Payment as PaymentIcon, + History as HistoryIcon, + Person as PersonIcon, + Logout as LogoutIcon, + ChevronLeft as ChevronLeftIcon, + AccountBalanceWallet as BalanceIcon, + Warning as WarningIcon +} from '@mui/icons-material'; +import { useAuth } from '../../context/AuthContext'; +import { Badge } from '@mui/material'; + +const DRAWER_WIDTH = 240; + +const menuItems = [ + { text: 'Dashboard', icon: , path: '/dashboard' }, + { text: 'Saldo', icon: , path: '/balance' }, + { text: 'Konta', icon: , path: '/accounts' }, + { text: 'Karty', icon: , path: '/cards' }, + { text: 'Przelewy', icon: , path: '/transfer' }, + { text: 'FLIK', icon: , path: '/flik' }, + { text: 'Historia', icon: , path: '/transactions' }, +]; + +const getNameApp: string = import.meta.env.VITE_APP_NAME + +export const DashboardLayout = () => { + const [open, setOpen] = useState(true); + const [anchorEl, setAnchorEl] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + const { user, handleLogout } = useAuth(); + + const toggleDrawer = () => setOpen(!open); + + const handleMenu = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleProfile = () => { + navigate('/profile'); + handleClose(); + }; + + const handleLogoutClick = async () => { + await handleLogout(); + navigate('/login'); + handleClose(); + }; + + return ( + + theme.zIndex.drawer + 1, borderRadius: 0 }}> + + + {open ? : } + + + 🏦 {getNameApp} + + + + + {user?.name || user?.email} + + + + + {(user?.name || user?.email || '?').charAt(0).toUpperCase()} + + + + + + + + + Profil + + + + + Wyloguj + + + + + + + + + + {menuItems.map((item) => ( + + navigate(item.path)} + > + {item.icon} + + + + ))} + + + + + navigate('/profile')} + > + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/Layout/GuestOnlyRoute.tsx b/frontend/src/components/Layout/GuestOnlyRoute.tsx index c4d6cb7..6dba6d7 100644 --- a/frontend/src/components/Layout/GuestOnlyRoute.tsx +++ b/frontend/src/components/Layout/GuestOnlyRoute.tsx @@ -1,4 +1,4 @@ -import { Navigate } from 'react-router-dom'; +import {Navigate, Outlet} from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; export const GuestOnlyRoute = ({ children }: any) => { @@ -16,5 +16,5 @@ export const GuestOnlyRoute = ({ children }: any) => { return ; } - return children; + return ; }; \ No newline at end of file diff --git a/frontend/src/components/Layout/ProtectedRoute.tsx b/frontend/src/components/Layout/ProtectedRoute.tsx index 4a0c923..8fc89b3 100644 --- a/frontend/src/components/Layout/ProtectedRoute.tsx +++ b/frontend/src/components/Layout/ProtectedRoute.tsx @@ -1,21 +1,16 @@ -import { Navigate } from 'react-router-dom'; +import { Navigate, Outlet } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; -import { Box, CircularProgress, Typography, Container } from '@mui/material'; -import React from "react"; +import { Box, CircularProgress, Typography } from '@mui/material'; -export const ProtectedRoute = ({ children }: any) => { +export const ProtectedRoute = () => { const { user, loading } = useAuth(); if (loading) { return ( - - - - - Ładowanie... - - - + + + Ładowanie... + ); } @@ -23,5 +18,5 @@ export const ProtectedRoute = ({ children }: any) => { return ; } - return <>{children}; + return ; }; \ No newline at end of file diff --git a/frontend/src/hooks/formatedDate.tsx b/frontend/src/hooks/formatedDate.tsx deleted file mode 100644 index 7b5f602..0000000 --- a/frontend/src/hooks/formatedDate.tsx +++ /dev/null @@ -1,27 +0,0 @@ -export const formatDate = (date: Date | string | undefined) => { - if (!date) return 'Brak danych'; - const d = new Date(date); - - return d.toLocaleString('pl-PL', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); -} - -export const formatDateShort = (date: Date | string | undefined) => { - if (!date) return 'Brak danych'; - const d = new Date(date); - - return d.toLocaleString('pl-PL', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); -} \ No newline at end of file diff --git a/frontend/src/pages/Account/AccountPage.tsx b/frontend/src/pages/Account/AccountPage.tsx new file mode 100644 index 0000000..30cdc75 --- /dev/null +++ b/frontend/src/pages/Account/AccountPage.tsx @@ -0,0 +1,305 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Box, + Typography, + Card, + CardContent, + CardActionArea, + Grid, + Chip, + Skeleton, + Alert, + Pagination, + Stack, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, + FormControlLabel, + Switch, + CircularProgress, +} from '@mui/material'; +import { + AccountBalance as AccountIcon, + Add as AddIcon, +} from '@mui/icons-material'; +import { getAccounts, createAccount } from '../../services/accountService'; +import type { Account, PaginationMeta } from '../../types/types'; + +export const AccountPage = () => { + const [accounts, setAccounts] = useState([]); + const [meta, setMeta] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const navigate = useNavigate(); + + // Dialog state + const [openDialog, setOpenDialog] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [formError, setFormError] = useState(null); + const [formData, setFormData] = useState({ + name: 'Konto moje na dzisiaj', + currency: 'PLN', + type: 'current', + with_card: true, + }); + + const loadAccounts = async (pageNumber: number = 1) => { + setLoading(true); + setError(null); + try { + const response = await getAccounts(); + setAccounts(response.data); + setMeta(response.meta); + } catch (err: any) { + setError(err.message || 'Błąd pobierania kont'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadAccounts(page); + }, [page]); + + const handlePageChange = (_: React.ChangeEvent, value: number) => { + setPage(value); + }; + + const handleAccountClick = (id: string) => { + navigate(`/accounts/${id}`); + }; + + const handleOpenDialog = () => { + setFormData({ name: '', currency: 'PLN', type: 'current', with_card: true }); + setFormError(null); + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + }; + + const handleFormChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value, + })); + }; + + const handleSubmit = async () => { + setFormError(null); + setSubmitting(true); + + try { + await createAccount(formData); + handleCloseDialog(); + await loadAccounts(); + } catch (err: any) { + setFormError(err.message || 'Błąd tworzenia konta'); + } finally { + setSubmitting(false); + } + }; + + const getAccountTypeLabel = (type: string): string => { + return type === 'savings' ? 'Oszczędnościowe' : 'Bieżące'; + }; + + const getAccountTypeColor = (type: string): 'success' | 'primary' => { + return type === 'savings' ? 'success' : 'primary'; + }; + + return ( + + + Konta bankowe + + + + {error && ( + + {error} + + )} + + {loading ? ( + + {[1, 2].map((i) => ( + + + + ))} + + ) : accounts.length === 0 ? ( + + + + + Brak kont bankowych + + + Utwórz swoje pierwsze konto bankowe + + + + + ) : ( + <> + + {accounts.map((account) => ( + + + handleAccountClick(account.id)}> + + + + + {account.name} + + + {account.account_number.replace(/(\d{2})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4 $5 $6 $7')} + + + + + + + {account.balance} {account.currency} + + + + {account.cards?.slice(0, 3).map((card) => ( + + ))} + {account.cards && account.cards.length > 3 && ( + + )} + {(!account.cards || account.cards.length === 0) && ( + + Brak kart + + )} + + + + + + ))} + + + {meta && meta.last_page > 1 && ( + + + + Wyświetlono {meta.from ?? 0}-{meta.to ?? 0} z {meta.total} kont + + + )} + + )} + + {/* Create Account Dialog */} + + Nowe konto bankowe + + {formError && ( + + {formError} + + )} + + + + PLN - Polski złoty + EUR - Euro + USD - Dolar amerykański + + + Bieżące + Oszczędnościowe + + + } + label="Utwórz kartę debetową do konta" + /> + + + + + + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/pages/Account/AccoutViewPage.tsx b/frontend/src/pages/Account/AccoutViewPage.tsx new file mode 100644 index 0000000..6c06f18 --- /dev/null +++ b/frontend/src/pages/Account/AccoutViewPage.tsx @@ -0,0 +1,380 @@ +// src/pages/Accounts/AccountViewPage.tsx + +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, + Typography, + Card as CardPage, + CardContent, + Grid, + Chip, + Skeleton, + Alert, + Button, + Stack, + Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, + CircularProgress, +} from '@mui/material'; +import { + ArrowBack as BackIcon, + CreditCard as CardIcon, + Add as AddIcon, + Block as BlockIcon, + LockOpen as UnblockIcon, + Delete as DeleteIcon, +} from '@mui/icons-material'; +import { + getAccount, + getCards, + createCard, + blockCard, + unblockCard, + deleteCard, +} from '../../services/accountService'; +import type { Account, Card } from '../../types/types'; +import {formatDate, formatDateShort} from "../../utils/formatedDate"; + +export const AccountViewPage = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [account, setAccount] = useState(null); + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Add card dialog + const [openAddDialog, setOpenAddDialog] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [network, setNetwork] = useState('visa'); + const [cardType, setCardType] = useState('debit'); + + // Delete confirmation + const [deleteCardId, setDeleteCardId] = useState(null); + + const loadData = async () => { + if (!id) return; + setLoading(true); + setError(null); + try { + const [accountData, cardsData] = await Promise.all([ + getAccount(id), + getCards(id), + ]); + setAccount(accountData); + setCards(cardsData.data || []); + } catch (err: any) { + setError(err.message || 'Błąd pobierania danych'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, [id]); + + const handleBlockCard = async (cardId: string) => { + try { + await blockCard(cardId); + await loadData(); + } catch (err: any) { + setError(err.message); + } + }; + + const handleUnblockCard = async (cardId: string) => { + try { + await unblockCard(cardId); + await loadData(); + } catch (err: any) { + setError(err.message); + } + }; + + const handleDeleteCard = async () => { + if (!deleteCardId) return; + try { + await deleteCard(deleteCardId); + setDeleteCardId(null); + await loadData(); + } catch (err: any) { + setError(err.message); + } + }; + + const handleAddCard = async () => { + if (!id) return; + setSubmitting(true); + try { + await createCard(id, network, cardType); + setOpenAddDialog(false); + setNetwork('visa'); + setCardType('debit'); + await loadData(); + } catch (err: any) { + setError(err.message); + } finally { + setSubmitting(false); + } + }; + + const getStatusColor = (status: string): 'success' | 'error' | 'warning' | 'default' => { + switch (status) { + case 'active': return 'success'; + case 'blocked': return 'error'; + case 'inactive': return 'warning'; + case 'expired': return 'default'; + default: return 'default'; + } + }; + + const getStatusLabel = (status: string): string => { + switch (status) { + case 'active': return 'Aktywna'; + case 'blocked': return 'Zablokowana'; + case 'inactive': return 'Nieaktywna'; + case 'expired': return 'Wygasła'; + default: return status; + } + }; + + const getAccountTypeLabel = (type: string): string => { + return type === 'savings' ? 'Oszczędnościowe' : 'Bieżące'; + }; + + const formatAccountNumber = (number: string): string => { + return number.replace(/(\d{2})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4 $5 $6 $7'); + }; + + if (loading) { + return ( + + + + + ); + } + + if (error || !account) { + return ( + + + {error || 'Nie znaleziono konta'} + + ); + } + + return ( + + + + {error && setError(null)}>{error}} + + {/* Account Details Card */} + + + + + + {account.name} + + + {formatAccountNumber(account.account_number)} + + + + + + + + Saldo + + {account.balance} {account.currency} + + + + Waluta + {account.currency} + + + Utworzono + + {formatDate(account.created_at)} + + + + + + + {/* Cards Section */} + + + + Karty ({cards.length}) + + + + + + + {cards.length > 0 ? ( + + + + + Karta + Network + Ważność + Status + Akcje + + + + {cards.map((card) => ( + + + + + + + •••• {card.card_last_four} + + + {card.type === 'debit' ? 'Debetowa' : card.type} + + + + + + + + + {String(card.exp_month).padStart(2, '0')}/{card.exp_year} + + + + + + + {card.status === 'active' && ( + handleBlockCard(card.id)} title="Zablokuj"> + + + )} + {card.status === 'blocked' && ( + handleUnblockCard(card.id)} title="Odblokuj"> + + + )} + setDeleteCardId(card.id)} title="Usuń"> + + + + + + ))} + +
+
+ ) : ( + + + + + Brak kart + + + To konto nie ma przypisanych kart płatniczych + + + + + )} + + {/* Add Card Dialog */} + setOpenAddDialog(false)} maxWidth="xs" fullWidth> + Dodaj nową kartę + + + setNetwork(e.target.value)} + select + fullWidth + > + Visa + Mastercard + + setCardType(e.target.value)} + select + fullWidth + > + Debetowa + Kredytowa + Wirtualna + + + + + + + + + + {/* Delete Confirmation Dialog */} + setDeleteCardId(null)}> + Usunąć kartę? + + + Ta operacja jest nieodwracalna. Karta zostanie trwale usunięta. + + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/pages/Account/Card/CardPage.tsx b/frontend/src/pages/Account/Card/CardPage.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/User/ProfilePage.tsx b/frontend/src/pages/User/ProfilePage.tsx index 91542c5..bcc2f92 100644 --- a/frontend/src/pages/User/ProfilePage.tsx +++ b/frontend/src/pages/User/ProfilePage.tsx @@ -12,7 +12,7 @@ import EditIcon from '@mui/icons-material/Edit'; import {LoadingSpinner} from "../../components/Assets/Svg/LoadingSpinner"; import {useToast} from "../../context/ToastContext"; import {useLoading} from "../../context/LoadingContext"; -import {formatDate, formatDateShort} from "../../hooks/formatedDate"; +import {formatDate, formatDateShort} from "../../utils/formatedDate"; import {updateUserProfile} from "../../services/authService"; import {ProfileUpdateData} from "../../types/types"; @@ -81,13 +81,19 @@ export const ProfilePage = () => {
- {error && ( - - {error} - - )} - + {!user?.email_verified_at && ( + + Twój adres email nie został zweryfikowany. Sprawdź skrzynkę odbiorczą. + + )} + + {error && ( + + {error} + + )} + { }} /> + + Zmiana hasła (wypełnij tylko jeśli chcesz zmienić) diff --git a/frontend/src/services/accountService.ts b/frontend/src/services/accountService.ts new file mode 100644 index 0000000..9cb0409 --- /dev/null +++ b/frontend/src/services/accountService.ts @@ -0,0 +1,105 @@ +import {fetchWithAuth} from "./authService"; +import {Account, ApiError, Card, PaginationResponse} from "../types/types"; + +export const getAccounts = async (page: number = 1, perPage: number = 20): Promise> => { + const response = await fetchWithAuth(`/accounts?page=${page}&per_page=${perPage}`, { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania danych o kontach bankowych.'); + } + + return await response.json(); +}; + +export const createAccount = async (data: { + name: string; + currency?: string; + type?: string; + with_card?: boolean; +}): Promise => { + const response = await fetchWithAuth('/accounts', { + method: 'POST', + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd tworzenia konta'); + } + + return await response.json(); +}; + +export const getAccount = async (id: string): Promise => { + const response = await fetchWithAuth(`/accounts/${id}`, { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania konta'); + } + + return (await response.json()).data; +}; + + +export const getCards = async (accountId: string): Promise> => { + const response = await fetchWithAuth(`/accounts/${accountId}/cards`, { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania kart'); + } + + return await response.json(); +}; + +export const createCard = async (accountId: string, network: string, type: string): Promise => { + const response = await fetchWithAuth(`/accounts/${accountId}/cards`, { + method: 'POST', + body: JSON.stringify({ network, type }), + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd tworzenia karty'); + } + + return (await response.json()).data; +}; + +export const blockCard = async (cardId: string): Promise => { + const response = await fetchWithAuth(`/cards/${cardId}/block`, { + method: 'PATCH', + }); + + if (!response.ok) { + throw new Error('Nie udało się zablokować karty'); + } +}; + +export const unblockCard = async (cardId: string): Promise => { + const response = await fetchWithAuth(`/cards/${cardId}/unblock`, { + method: 'PATCH', + }); + + if (!response.ok) { + throw new Error('Nie udało się odblokować karty'); + } +}; + +export const deleteCard = async (cardId: string): Promise => { + const response = await fetchWithAuth(`/cards/${cardId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Nie udało się usunąć karty'); + } +}; \ No newline at end of file diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 37a43f9..ac6a3b7 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -34,6 +34,7 @@ export const loginWithPassword = async (email: string, password: string): Promis if (!response.ok) { const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd logowania'); } @@ -85,7 +86,9 @@ export const getUserInfo = async (): Promise => { }); if (!response.ok) { - throw new Error('Nie udało się pobrać danych użytkownika'); + const error: ApiError = await response.json(); + + throw new Error(error.message || 'Nie udało się pobrać danych użytkownika'); } return await response.json(); diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 44235a4..651a08d 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -8,6 +8,38 @@ export interface User { updated_at?: string; } +// Pagination +export interface PaginationLink { + url: string | null; + label: string; + page: number | null; + active: boolean; +} + +export interface PaginationMeta { + current_page: number; + from: number | null; + last_page: number; + links: PaginationLink[]; + path: string; + per_page: number; + to: number | null; + total: number; +} + +export interface PaginationLinks { + first: string; + last: string; + prev: string | null; + next: string | null; +} + +export interface PaginationResponse { + data: T[]; + links: PaginationLinks; + meta: PaginationMeta; +} + // Authentication types export interface LoginCredentials { email: string; @@ -41,7 +73,9 @@ export interface ApiResponse { } export interface ApiError { - message: string; + status?: string, + message?: string; + code?: number, errors?: Record; } @@ -131,3 +165,38 @@ export interface LoadingContextType { startLoading: () => void; stopLoading: () => void; } + +// Accounts +export interface Account { + id: string; + name: string; + account_number: string; + balance: string; + currency: string; + type: string; + cards: Card[]; + created_at: string; + updated_at: string; +} + +export interface Card { + id: string; + card_number: string; + card_last_four: string; + exp_month: number; + exp_year: number; + network: string; + type: string; + status: string; + created_at: string; + updated_at: string; +} + +export interface Transaction { + id: string; + amount: number; + description: string; + type: string; + status: string; + created_at: string; +} diff --git a/frontend/src/utils/formatedDate.tsx b/frontend/src/utils/formatedDate.tsx new file mode 100644 index 0000000..1f1f02a --- /dev/null +++ b/frontend/src/utils/formatedDate.tsx @@ -0,0 +1,88 @@ +export const formatDate = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + + return d.toLocaleDateString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +}; + +export const formatDateShort = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + + return d.toLocaleDateString('pl-PL', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); +}; + +export const formatDateTime = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + + return d.toLocaleString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export const formatDateTimeShort = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + + return d.toLocaleString('pl-PL', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export const formatDateTimeWithSeconds = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + + return d.toLocaleString('pl-PL', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +}; + +export const formatTime = (date: Date | string | undefined): string => { + if (!date) return '--:--'; + const d = new Date(date); + + return d.toLocaleTimeString('pl-PL', { + hour: '2-digit', + minute: '2-digit', + }); +}; + +export const formatRelative = (date: Date | string | undefined): string => { + if (!date) return 'Brak danych'; + const d = new Date(date); + const now = new Date(); + const diff = now.getTime() - d.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (seconds < 60) return 'Przed chwilą'; + if (minutes < 60) return `${minutes} min. temu`; + if (hours < 24) return `${hours} godz. temu`; + if (days < 7) return `${days} dni temu`; + return formatDate(d); +}; \ No newline at end of file From 4c692b3dc554dd87c57566295f370c8770c434c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Sun, 10 May 2026 22:30:33 +0200 Subject: [PATCH 08/17] fix tsconfig.node.json - add composite. --- frontend/tsconfig.node.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 2512c6c..9a6850d 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -7,7 +7,8 @@ "skipLibCheck": true, "noEmit": true, "strict": true, - "isolatedModules": true + "isolatedModules": true, + "composite": true }, "include": ["vite.config.ts"] } \ No newline at end of file From 81e957fbc287ffb36a3aab9138f7620dc48fad64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Mon, 11 May 2026 11:54:25 +0200 Subject: [PATCH 09/17] fixed frontend. --- frontend/biome.json | 44 + frontend/eslint.config.js | 21 - frontend/package-lock.json | 1058 +++-------------- frontend/package.json | 13 +- frontend/src/pages/Account/AccountPage.tsx | 15 +- frontend/src/pages/Account/AccoutViewPage.tsx | 112 +- frontend/src/pages/User/ProfilePage.tsx | 6 +- frontend/src/services/accountService.ts | 13 + frontend/src/types/types.ts | 2 +- frontend/src/utils/formatAccount.ts | 43 + frontend/src/utils/formatCard.ts | 68 ++ .../utils/{formatedDate.tsx => formatDate.ts} | 0 frontend/src/utils/formatMoney.ts | 106 ++ 13 files changed, 533 insertions(+), 968 deletions(-) create mode 100644 frontend/biome.json delete mode 100644 frontend/eslint.config.js create mode 100644 frontend/src/utils/formatAccount.ts create mode 100644 frontend/src/utils/formatCard.ts rename frontend/src/utils/{formatedDate.tsx => formatDate.ts} (100%) create mode 100644 frontend/src/utils/formatMoney.ts diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..678626b --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", "dist", "build"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error", + "noUnusedVariables": "error" + }, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "useConst": "error", + "useTemplate": "error", + "noVar": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "always" + } + } +} \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index ea36dd3..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{js,jsx}'], - extends: [ - js.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - globals: globals.browser, - parserOptions: { ecmaFeatures: { jsx: true } }, - }, - }, -]) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed336d1..e05dc80 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,15 +27,11 @@ "zod": "^4.4.3" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@biomejs/biome": "^2.4.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.5.0", - "eslint": "^10.2.1", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.5.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "vite": "^8.0.10" @@ -66,43 +62,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, "node_modules/@babel/generator": { "version": "7.29.1", "license": "MIT", @@ -117,21 +76,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "license": "MIT", @@ -150,22 +94,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "license": "MIT", @@ -180,26 +108,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.29.3", "license": "MIT", @@ -261,6 +169,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.15.tgz", + "integrity": "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.15", + "@biomejs/cli-darwin-x64": "2.4.15", + "@biomejs/cli-linux-arm64": "2.4.15", + "@biomejs/cli-linux-arm64-musl": "2.4.15", + "@biomejs/cli-linux-x64": "2.4.15", + "@biomejs/cli-linux-x64-musl": "2.4.15", + "@biomejs/cli-win32-arm64": "2.4.15", + "@biomejs/cli-win32-x64": "2.4.15" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.15.tgz", + "integrity": "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.15.tgz", + "integrity": "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.15.tgz", + "integrity": "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.15.tgz", + "integrity": "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.15.tgz", + "integrity": "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.15.tgz", + "integrity": "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.15.tgz", + "integrity": "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.15.tgz", + "integrity": "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -447,116 +518,6 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, "node_modules/@fontsource/roboto": { "version": "5.2.10", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz", @@ -576,62 +537,6 @@ "react-hook-form": "^7.55.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "license": "MIT", @@ -640,15 +545,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "license": "MIT", @@ -1308,16 +1204,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "dev": true, - "license": "MIT" - }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", @@ -1330,11 +1216,6 @@ "@types/react": "*" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1395,40 +1276,6 @@ } } }, - "node_modules/acorn": { - "version": "8.16.0", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1535,14 +1382,6 @@ "npm": ">=6" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.10.27", "dev": true, @@ -1567,17 +1406,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1748,11 +1576,6 @@ "node": ">= 6" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/cookie": { "version": "1.1.1", "license": "MIT", @@ -1789,19 +1612,6 @@ "node": ">= 6" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -1854,11 +1664,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -1976,175 +1781,8 @@ "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/fast-glob": { "version": "3.3.3", @@ -2176,16 +1814,6 @@ "node": ">= 6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -2212,17 +1840,6 @@ } } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2242,38 +1859,6 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "dev": true, - "license": "ISC" - }, "node_modules/follow-redirects": { "version": "1.16.0", "funding": [ @@ -2342,14 +1927,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "license": "MIT", @@ -2394,17 +1971,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "17.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/goober": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", @@ -2457,19 +2023,6 @@ "node": ">= 0.4" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2485,14 +2038,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/ignore": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2509,14 +2054,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2580,11 +2117,6 @@ "node": ">=0.12.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2609,58 +2141,12 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "dev": true, @@ -2937,20 +2423,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2963,14 +2435,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -3032,20 +2496,6 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "10.2.5", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -3079,11 +2529,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.38", "dev": true, @@ -3149,50 +2594,6 @@ "node": ">= 6" } }, - "node_modules/optionator": { - "version": "0.9.4", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3223,22 +2624,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -3451,14 +2836,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3483,14 +2860,6 @@ "node": ">=10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3743,37 +3112,10 @@ "version": "0.27.0", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "license": "MIT" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3978,17 +3320,6 @@ "license": "0BSD", "optional": true }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "dev": true, @@ -4018,14 +3349,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4109,61 +3432,12 @@ } } }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.4.3", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } } } } diff --git a/frontend/package.json b/frontend/package.json index b0a241a..848013c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,8 +6,11 @@ "scripts": { "dev": "vite", "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "lint": "biome check src/", + "lint:fix": "biome check --fix src/", + "format": "biome format --write src/", + "check": "biome ci src/" }, "dependencies": { "@emotion/react": "^11.14.0", @@ -29,15 +32,11 @@ "zod": "^4.4.3" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@biomejs/biome": "^2.4.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.5.0", - "eslint": "^10.2.1", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.5.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "vite": "^8.0.10" diff --git a/frontend/src/pages/Account/AccountPage.tsx b/frontend/src/pages/Account/AccountPage.tsx index 30cdc75..46892a9 100644 --- a/frontend/src/pages/Account/AccountPage.tsx +++ b/frontend/src/pages/Account/AccountPage.tsx @@ -29,6 +29,8 @@ import { } from '@mui/icons-material'; import { getAccounts, createAccount } from '../../services/accountService'; import type { Account, PaginationMeta } from '../../types/types'; +import {formatMoney} from "../../utils/formatMoney"; +import {formatAccountNumber, isValidAccountNumber} from "../../utils/formatAccount"; export const AccountPage = () => { const [accounts, setAccounts] = useState([]); @@ -43,7 +45,7 @@ export const AccountPage = () => { const [submitting, setSubmitting] = useState(false); const [formError, setFormError] = useState(null); const [formData, setFormData] = useState({ - name: 'Konto moje na dzisiaj', + name: 'Konto bieżące', currency: 'PLN', type: 'current', with_card: true, @@ -53,7 +55,7 @@ export const AccountPage = () => { setLoading(true); setError(null); try { - const response = await getAccounts(); + const response = await getAccounts(pageNumber); setAccounts(response.data); setMeta(response.meta); } catch (err: any) { @@ -164,11 +166,11 @@ export const AccountPage = () => { - + {account.name} - {account.account_number.replace(/(\d{2})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4 $5 $6 $7')} + {formatAccountNumber(account.account_number)} { onChange={handleFormChange} select fullWidth - disabled > PLN - Polski złoty - EUR - Euro - USD - Dolar amerykański { name="with_card" checked={formData.with_card} onChange={handleFormChange} - disabled + required /> } label="Utwórz kartę debetową do konta" diff --git a/frontend/src/pages/Account/AccoutViewPage.tsx b/frontend/src/pages/Account/AccoutViewPage.tsx index 6c06f18..95a331e 100644 --- a/frontend/src/pages/Account/AccoutViewPage.tsx +++ b/frontend/src/pages/Account/AccoutViewPage.tsx @@ -44,14 +44,17 @@ import { createCard, blockCard, unblockCard, - deleteCard, + deleteCard, getDeleteAccount, } from '../../services/accountService'; import type { Account, Card } from '../../types/types'; -import {formatDate, formatDateShort} from "../../utils/formatedDate"; +import {formatDate, formatDateShort} from "../../utils/formatDate"; +import {formatAccountNumber} from "../../utils/formatAccount"; +import {useToast} from "../../context/ToastContext"; export const AccountViewPage = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { showError, showSuccess } = useToast(); const [account, setAccount] = useState(null); const [cards, setCards] = useState([]); @@ -67,6 +70,9 @@ export const AccountViewPage = () => { // Delete confirmation const [deleteCardId, setDeleteCardId] = useState(null); + // Delete account configmation + const [deleteAccountId, setDeleteAccountId] = useState(null); + const loadData = async () => { if (!id) return; setLoading(true); @@ -118,6 +124,18 @@ export const AccountViewPage = () => { } }; + const handleDeleteAccount = async () => { + if (!deleteAccountId) return; + try { + await getDeleteAccount(deleteAccountId); + setDeleteAccountId(null); + navigate('/accounts'); + showSuccess('Konto bankowe zostało usunięte.'); + } catch (err: any) { + setError(err.message); + } + }; + const handleAddCard = async () => { if (!id) return; setSubmitting(true); @@ -158,10 +176,6 @@ export const AccountViewPage = () => { return type === 'savings' ? 'Oszczędnościowe' : 'Bieżące'; }; - const formatAccountNumber = (number: string): string => { - return number.replace(/(\d{2})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4 $5 $6 $7'); - }; - if (loading) { return ( @@ -183,22 +197,22 @@ export const AccountViewPage = () => { } return ( - - - {error && setError(null)}>{error}} + {error && setError(null)}>{error}} {/* Account Details Card */} - + - + - + {account.name} - + {formatAccountNumber(account.account_number)} @@ -210,38 +224,42 @@ export const AccountViewPage = () => { - + Saldo {account.balance} {account.currency} - + Waluta {account.currency} - + Utworzono {formatDate(account.created_at)} + + setDeleteAccountId(account.id)} title="Usuń"> + + {/* Cards Section */} - + - + Karty ({cards.length}) - - + {cards.length > 0 ? ( @@ -259,8 +277,8 @@ export const AccountViewPage = () => { {cards.map((card) => ( - - + + •••• {card.card_last_four} @@ -272,7 +290,7 @@ export const AccountViewPage = () => { - + {String(card.exp_month).padStart(2, '0')}/{card.exp_year} @@ -285,19 +303,22 @@ export const AccountViewPage = () => { /> - + {card.status === 'active' && ( - handleBlockCard(card.id)} title="Zablokuj"> - + handleBlockCard(card.id)} title="Zablokuj"> + )} {card.status === 'blocked' && ( - handleUnblockCard(card.id)} title="Odblokuj"> - + handleUnblockCard(card.id)} title="Odblokuj"> + )} - setDeleteCardId(card.id)} title="Usuń"> - + setDeleteCardId(card.id)} title="Usuń"> + @@ -307,16 +328,16 @@ export const AccountViewPage = () => { ) : ( - + - + Brak kart - + To konto nie ma przypisanych kart płatniczych - @@ -327,13 +348,14 @@ export const AccountViewPage = () => { setOpenAddDialog(false)} maxWidth="xs" fullWidth> Dodaj nową kartę - + setNetwork(e.target.value)} select fullWidth + disabled > Visa Mastercard @@ -344,6 +366,7 @@ export const AccountViewPage = () => { onChange={(e) => setCardType(e.target.value)} select fullWidth + disabled > Debetowa Kredytowa @@ -354,7 +377,7 @@ export const AccountViewPage = () => { @@ -375,6 +398,23 @@ export const AccountViewPage = () => { + + {/* Delete Confirmation Dialog */} + setDeleteAccountId(null)}> + Usunąć konto bankowe? + + + Ta operacja jest nieodwracalna. Konto zostanie trwale usunięta. + Jeśli jest więcej od 0 na koncie, to konto nie może zostać usunięte. + + + + + + + ); }; \ No newline at end of file diff --git a/frontend/src/pages/User/ProfilePage.tsx b/frontend/src/pages/User/ProfilePage.tsx index bcc2f92..6932d26 100644 --- a/frontend/src/pages/User/ProfilePage.tsx +++ b/frontend/src/pages/User/ProfilePage.tsx @@ -12,7 +12,7 @@ import EditIcon from '@mui/icons-material/Edit'; import {LoadingSpinner} from "../../components/Assets/Svg/LoadingSpinner"; import {useToast} from "../../context/ToastContext"; import {useLoading} from "../../context/LoadingContext"; -import {formatDate, formatDateShort} from "../../utils/formatedDate"; +import {formatDateTime} from "../../utils/formatDate"; import {updateUserProfile} from "../../services/authService"; import {ProfileUpdateData} from "../../types/types"; @@ -119,7 +119,7 @@ export const ProfilePage = () => { { => { return (await response.json()).data; }; +export const getDeleteAccount = async (id: string) => { + const response = await fetchWithAuth(`/accounts/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd usuwania konta'); + } + + return await response.json(); +}; +// Card export const getCards = async (accountId: string): Promise> => { const response = await fetchWithAuth(`/accounts/${accountId}/cards`, { method: 'GET', diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 651a08d..8e54015 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -171,7 +171,7 @@ export interface Account { id: string; name: string; account_number: string; - balance: string; + balance: number; currency: string; type: string; cards: Card[]; diff --git a/frontend/src/utils/formatAccount.ts b/frontend/src/utils/formatAccount.ts new file mode 100644 index 0000000..7a3228f --- /dev/null +++ b/frontend/src/utils/formatAccount.ts @@ -0,0 +1,43 @@ +/** + * Format IBAN/NRB account number with spaces every 4 digits. + * "98123456780000000012345678" → "98 1234 5678 0000 0000 1234 5678" + */ +export const formatAccountNumber = (number: string): string => { + const cleaned = number.replace(/\s/g, ''); + return cleaned.replace(/(.{4})/g, '$1 ').trim(); +}; + +/** + * Mask account number - show only first 2 and last 4 digits. + * "98123456780000000012345678" → "98 **** **** **** **** 5678" + */ +export const maskAccountNumber = (number: string): string => { + const cleaned = number.replace(/\s/g, ''); + const first = cleaned.slice(0, 2); + const last = cleaned.slice(-4); + return `${first} **** **** **** **** ${last}`; +}; + +/** + * Get last 4 digits of account number. + * "98123456780000000012345678" → "5678" + */ +export const lastFourAccount = (number: string): string => { + return number.replace(/\s/g, '').slice(-4); +}; + +/** + * Convert 26-digit NRB to IBAN (add "PL" prefix). + * "98123456780000000012345678" → "PL98123456780000000012345678" + */ +export const toIBAN = (number: string): string => { + return 'PL' + number.replace(/\s/g, ''); +}; + +/** + * Check if account number has valid length (26 digits). + */ +export const isValidAccountNumber = (number: string): boolean => { + const cleaned = number.replace(/\s/g, ''); + return /^\d{26}$/.test(cleaned); +}; \ No newline at end of file diff --git a/frontend/src/utils/formatCard.ts b/frontend/src/utils/formatCard.ts new file mode 100644 index 0000000..53f7e10 --- /dev/null +++ b/frontend/src/utils/formatCard.ts @@ -0,0 +1,68 @@ +/** + * Format card number with spaces every 4 digits. + * "4532123456789012" → "4532 1234 5678 9012" + */ +export const formatCardNumber = (number: string): string => { + const cleaned = number.replace(/\s/g, ''); + return cleaned.replace(/(.{4})/g, '$1 ').trim(); +}; + +/** + * Mask card number - show first 4 and last 4 digits. + * "4532123456789012" → "4532 **** **** 9012" + */ +export const maskCardNumber = (number: string): string => { + const cleaned = number.replace(/\s/g, ''); + const first = cleaned.slice(0, 4); + const last = cleaned.slice(-4); + return `${first} **** **** ${last}`; +}; + +/** + * Get last 4 digits of card number. + * "4532123456789012" → "9012" + */ +export const lastFour = (number: string): string => { + return number.replace(/\s/g, '').slice(-4); +}; + +/** + * Format expiry date. + * { exp_month: 5, exp_year: 2030 } → "05/2030" + * { exp_month: 12, exp_year: 2024 } → "12/2024" + */ +export const formatExpiry = (month: number, year: number): string => { + return `${String(month).padStart(2, '0')}/${year}`; +}; + +/** + * Check if card is expired. + */ +export const isCardExpired = (month: number, year: number): boolean => { + const now = new Date(); + const expiry = new Date(year, month, 0); // last day of the month + return now > expiry; +}; + +/** + * Detect card network from number prefix. + * "4532..." → "Visa" + * "5214..." → "Mastercard" + */ +export const detectNetwork = (number: string): string => { + const cleaned = number.replace(/\s/g, ''); + if (cleaned.startsWith('4')) return 'Visa'; + if (/^5[1-5]/.test(cleaned)) return 'Mastercard'; + return 'Unknown'; +}; + +/** + * Get network color for UI. + */ +export const getNetworkColor = (network: string): string => { + switch (network.toLowerCase()) { + case 'visa': return '#1a1f71'; + case 'mastercard': return '#eb001b'; + default: return '#666'; + } +}; \ No newline at end of file diff --git a/frontend/src/utils/formatedDate.tsx b/frontend/src/utils/formatDate.ts similarity index 100% rename from frontend/src/utils/formatedDate.tsx rename to frontend/src/utils/formatDate.ts diff --git a/frontend/src/utils/formatMoney.ts b/frontend/src/utils/formatMoney.ts new file mode 100644 index 0000000..d4bd55b --- /dev/null +++ b/frontend/src/utils/formatMoney.ts @@ -0,0 +1,106 @@ +/** + * Convert cents to zloty string. + * 499 → "4.99" + * 0 → "0.00" + */ +export const centsToZloty = (cents: number): string => { + return (cents / 100).toFixed(2); +}; + +/** + * Convert zloty to cents. + * 4.99 → 499 + * 0.01 → 1 + */ +export const zlotyToCents = (zloty: number): number => { + return Math.round(zloty * 100); +}; + +/** + * Format amount in PLN with currency symbol. + * 499 → "4,99 PLN" + * 250000 → "2 500,00 PLN" + */ +export const formatMoney = (cents: number, currency: string = 'PLN'): string => { + return new Intl.NumberFormat('pl-PL', { + style: 'currency', + currency: currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(cents / 100); +}; + +/** + * Format amount without currency symbol. + * 499 → "4,99" + * 250000 → "2 500,00" + */ +export const formatAmount = (cents: number): string => { + return new Intl.NumberFormat('pl-PL', { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(cents / 100); +}; + +/** + * Format amount with space separator, no currency. + * 499 → "4.99" (API format, kropka zamiast przecinka) + */ +export const formatAmountForApi = (cents: number): string => { + return (cents / 100).toFixed(2); +}; + +/** + * Parse formatted money string to cents. + * "4,99 PLN" → 499 + * "2 500,00" → 250000 + */ +export const parseMoneyToCents = (formatted: string): number => { + const cleaned = formatted + .replace(/[^\d,.-]/g, '') + .replace(',', '.'); + return Math.round(parseFloat(cleaned) * 100); +}; + +/** + * Format balance with color indicator. + * +499 → "+4,99" (green) + * -499 → "-4,99" (red) + * 0 → "0,00" + */ +export const formatBalance = (cents: number): { text: string; isPositive: boolean; isZero: boolean } => { + const amount = cents / 100; + return { + text: new Intl.NumberFormat('pl-PL', { + style: 'decimal', + signDisplay: 'always', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount), + isPositive: cents > 0, + isZero: cents === 0, + }; +}; + +/** + * Short format for large amounts. + * 100000 → "1 000,00" + * 1000000 → "10 000,00" + * For bigger: use formatMoney + */ +export const formatMoneyShort = (cents: number, currency: string = 'PLN'): string => { + const amount = cents / 100; + + if (Math.abs(amount) >= 1_000_000) { + return new Intl.NumberFormat('pl-PL', { + style: 'currency', + currency: currency, + notation: 'compact', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + } + + return formatMoney(cents, currency); +}; \ No newline at end of file From 87994d13fd9025e70eb0b1961f525f55a3dd7f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Mon, 11 May 2026 16:54:46 +0200 Subject: [PATCH 10/17] fixed frontend/backend. --- backend/app/Http/Concerns/WithPagination.php | 32 +++---------------- .../src/components/Layout/DashboardLayout.tsx | 6 ++-- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/backend/app/Http/Concerns/WithPagination.php b/backend/app/Http/Concerns/WithPagination.php index fdca9fa..4017e8a 100644 --- a/backend/app/Http/Concerns/WithPagination.php +++ b/backend/app/Http/Concerns/WithPagination.php @@ -5,18 +5,12 @@ use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\Paginator; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Query\Builder as QueryBuilder; +use Illuminate\Database\Eloquent\Relations\Relation; -#[QueryParameter('per_page', description: 'Number of items per page.', type: 'int', default: 10, example: 20)] -#[QueryParameter('page', description: 'Current page number.', type: 'int', default: 1, example: 2)] trait WithPagination { - /** - * Get the number of items per page from request. - * Default: 20, Max: 100, Min: 1. - * - * @query per_page integer - */ protected function perPage(): int { $perPage = request()->integer('per_page', 20); @@ -24,18 +18,12 @@ protected function perPage(): int return max(1, min(100, $perPage)); } - /** - * Get the current page from request. - */ protected function currentPage(): int { return request()->integer('page', 1); } - /** - * Paginate a query with all request parameters. - */ - protected function paginate($query): LengthAwarePaginator + protected function paginate(EloquentBuilder|QueryBuilder|Relation $query): LengthAwarePaginator { return $query->paginate( perPage: $this->perPage(), @@ -43,10 +31,7 @@ protected function paginate($query): LengthAwarePaginator ); } - /** - * Simple paginate (only next/prev, no total count). - */ - protected function simplePaginate(Builder $query): Paginator + protected function simplePaginate(EloquentBuilder|QueryBuilder|Relation $query): Paginator { return $query->simplePaginate( perPage: $this->perPage(), @@ -54,9 +39,6 @@ protected function simplePaginate(Builder $query): Paginator ); } - /** - * Get pagination parameters as array. - */ protected function paginationParams(): array { return [ @@ -65,10 +47,6 @@ protected function paginationParams(): array ]; } - /** - * Register pagination query parameters for Scramble documentation. - * Call this in controller's __construct or method docblock. - */ public static function paginationQueryParams(): array { return [ diff --git a/frontend/src/components/Layout/DashboardLayout.tsx b/frontend/src/components/Layout/DashboardLayout.tsx index 9b9ad44..565f8d4 100644 --- a/frontend/src/components/Layout/DashboardLayout.tsx +++ b/frontend/src/components/Layout/DashboardLayout.tsx @@ -141,6 +141,7 @@ export const DashboardLayout = () => { variant="persistent" anchor="left" open={open} + onClose={handleClose} sx={{ width: DRAWER_WIDTH, flexShrink: 0, @@ -149,6 +150,7 @@ export const DashboardLayout = () => { boxSizing: 'border-box', mt: 8, borderRadius: 0, + height: 'calc(100% - 64px)', }, }} > @@ -186,8 +188,8 @@ export const DashboardLayout = () => { flexGrow: 1, p: 3, mt: 8, - width: open ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%', - transition: 'margin-left 0.3s', + width: '100%', + maxWidth: '100%', }} > From 8102176c02ed38a8420042c9289220098b2e00dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 13:43:09 +0200 Subject: [PATCH 11/17] . --- backend/Dockerfile | 11 - backend/app/Exceptions/Handler.php | 12 +- backend/app/Http/Concerns/WithPagination.php | 11 +- .../Controllers/Account/AccountController.php | 37 +- .../Account/Card/CardController.php | 9 +- .../Notification/NotificationController.php | 78 ++++ .../Controllers/User/ProfileController.php | 123 ++++- .../Middleware/Adult/AdultCheckMiddleware.php | 34 ++ .../Requests/User/RegisterUserRequest.php | 27 +- .../Requests/User/UpdateProfileRequest.php | 52 +-- .../Resources/Account/BalanceResource.php | 24 + .../Resources/Account/Card/CardResource.php | 2 + .../Notification/NotificationResource.php | 29 ++ backend/app/Models/Account/Account.php | 1 - backend/app/Models/Account/Card/Card.php | 1 + .../Account/Transaction/Transaction.php | 25 +- backend/app/Models/Passport/Client.php | 5 - backend/app/Models/User.php | 65 ++- .../Guardian/GuardianApprovedNotification.php | 53 +++ .../Guardian/GuardianRejectedNotification.php | 53 +++ .../Guardian/GuardianRequestNotification.php | 58 +++ .../GuardianRequestSentNotification.php | 53 +++ backend/app/Providers/AppServiceProvider.php | 15 +- backend/app/Rules/Adult/AdultGuardian.php | 38 ++ .../app/Services/Guardian/GuardianService.php | 46 ++ backend/bootstrap/app.php | 41 +- backend/bootstrap/providers.php | 2 + .../factories/Account/AccountFactory.php | 30 +- .../factories/Account/Card/CardFactory.php | 4 +- .../Account/Flik/FlikCodeFactory.php | 6 +- backend/database/factories/UserFactory.php | 4 + .../0001_01_01_000000_create_users_table.php | 8 + .../migrations/2026_05_09_170729_accounts.php | 2 +- ...5_09_173714_create_notifications_table.php | 4 +- backend/database/seeders/DatabaseSeeder.php | 1 + .../database/seeders/PassportClientSeeder.php | 41 +- backend/database/seeders/UserSeeder.php | 64 +++ backend/docker/entrypoint.sh | 14 +- backend/docker/nginx.conf | 8 + backend/docker/php-fpm.conf | 4 + .../views/vendor/passport/authorize.blade.php | 347 ++++++++++++-- backend/routes/api.php | 24 +- backend/routes/auth.php | 48 +- docker-compose.yml | 9 +- frontend/src/App.tsx | 326 ++++++++++--- frontend/src/components/Field/DateField.tsx | 106 +++++ .../src/components/Layout/DashboardLayout.tsx | 7 +- .../Notification/NotificationBell.tsx | 223 +++++++++ frontend/src/context/AuthContext.tsx | 2 + frontend/src/pages/Account/AccountPage.tsx | 3 +- .../pages/Guardian/GuardianApprovalPage.tsx | 139 ++++++ .../pages/Notification/NotificationsPage.tsx | 275 +++++++++++ frontend/src/pages/User/BalancePage.tsx | 208 +++++++-- frontend/src/pages/User/ProfilePage.tsx | 440 +++++++++++++----- frontend/src/services/authService.ts | 5 + frontend/src/services/guardianService.ts | 24 + frontend/src/services/notificationService.tsx | 54 +++ frontend/src/types/types.ts | 42 +- frontend/src/utils/formatDate.ts | 89 ++-- frontend/vite.config.ts | 7 +- 60 files changed, 2981 insertions(+), 492 deletions(-) create mode 100644 backend/app/Http/Controllers/Notification/NotificationController.php create mode 100644 backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php create mode 100644 backend/app/Http/Resources/Account/BalanceResource.php create mode 100644 backend/app/Http/Resources/Notification/NotificationResource.php create mode 100644 backend/app/Notifications/Guardian/GuardianApprovedNotification.php create mode 100644 backend/app/Notifications/Guardian/GuardianRejectedNotification.php create mode 100644 backend/app/Notifications/Guardian/GuardianRequestNotification.php create mode 100644 backend/app/Notifications/Guardian/GuardianRequestSentNotification.php create mode 100644 backend/app/Rules/Adult/AdultGuardian.php create mode 100644 backend/app/Services/Guardian/GuardianService.php create mode 100644 backend/database/seeders/UserSeeder.php create mode 100644 frontend/src/components/Field/DateField.tsx create mode 100644 frontend/src/components/Notification/NotificationBell.tsx create mode 100644 frontend/src/pages/Guardian/GuardianApprovalPage.tsx create mode 100644 frontend/src/pages/Notification/NotificationsPage.tsx create mode 100644 frontend/src/services/guardianService.ts create mode 100644 frontend/src/services/notificationService.tsx diff --git a/backend/Dockerfile b/backend/Dockerfile index 437dd71..73c7cf0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,20 +8,9 @@ RUN docker-php-ext-install pdo pdo_mysql RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -COPY ./docker/nginx.conf /etc/nginx/http.d/default.conf -COPY ./docker/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf -COPY ./docker/supervisord.conf /etc/supervisord.conf - -RUN mkdir -p storage/logs bootstrap/cache && chmod -R 775 storage bootstrap/cache - -#COPY ./docker/entrypoint.sh /entrypoint.sh -#RUN chmod +x /entrypoint.sh - COPY docker/entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/entrypoint.sh EXPOSE 80 -#ENTRYPOINT ["/entrypoint.sh"] - ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php index 0d5a43c..a6763eb 100644 --- a/backend/app/Exceptions/Handler.php +++ b/backend/app/Exceptions/Handler.php @@ -68,10 +68,18 @@ private function handleApiException(\Throwable $e): JsonResponse } if ($e instanceof ValidationException) { + $errors = $e->errors(); + $count = collect($errors)->flatten()->count(); + $firstMessage = collect($errors)->flatten()->first(); + + $message = $count > 1 + ? $firstMessage . ' (and ' . ($count - 1) . ' more error' . ($count > 2 ? 's' : '') . ')' + : $firstMessage; + return response()->json([ 'status' => 'error', - 'message' => 'Error validating data.', - 'errors' => $e->errors(), + 'message' => $message, + 'errors' => $errors, 'code' => 422, ], 422); } diff --git a/backend/app/Http/Concerns/WithPagination.php b/backend/app/Http/Concerns/WithPagination.php index 4017e8a..3ef9b28 100644 --- a/backend/app/Http/Concerns/WithPagination.php +++ b/backend/app/Http/Concerns/WithPagination.php @@ -2,12 +2,11 @@ namespace App\Http\Concerns; -use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; -use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Query\Builder as QueryBuilder; trait WithPagination { @@ -23,6 +22,10 @@ protected function currentPage(): int return request()->integer('page', 1); } + /** + * @param EloquentBuilder|QueryBuilder|Relation $query + * @return LengthAwarePaginator + */ protected function paginate(EloquentBuilder|QueryBuilder|Relation $query): LengthAwarePaginator { return $query->paginate( @@ -31,6 +34,10 @@ protected function paginate(EloquentBuilder|QueryBuilder|Relation $query): Lengt ); } + /** + * @param EloquentBuilder|QueryBuilder|Relation $query + * @return LengthAwarePaginator + */ protected function simplePaginate(EloquentBuilder|QueryBuilder|Relation $query): Paginator { return $query->simplePaginate( diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index ee09440..0b18d3f 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -7,12 +7,15 @@ use App\Http\Requests\Account\StoreAccountRequest; use App\Http\Requests\Account\UpdateAccountRequest; use App\Http\Resources\Account\AccountResource; +use App\Http\Resources\Account\BalanceResource; use App\Models\Account\Account; use App\Services\Account\AccountService; use Dedoc\Scramble\Attributes\BodyParameter; use Dedoc\Scramble\Attributes\PathParameter; use Dedoc\Scramble\Attributes\QueryParameter; +use Dedoc\Scramble\Attributes\Response; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Laravel\Passport\Attributes\AuthorizeToken; /** @@ -31,18 +34,42 @@ public function __construct( /** * Wyświetla listę kont bankowych użytkownika. + * */ #[AuthorizeToken(['accounts-view'], anyScope: true)] #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] #[QueryParameter('page', description: 'Numer obecnej strony.', type: 'int', default: 1, example: 2)] - public function index() + public function index(): AnonymousResourceCollection { $accounts = Account::where('user_id', auth()->id()) - ->with('cards'); + ->with('cards') + ->paginate( + perPage: $this->perPage(), + page: $this->currentPage(), + ); - return AccountResource::collection( - $this->paginate($accounts), - ); + return AccountResource::collection($accounts); + } + + /** + * Saldo + * + * Wyświetlanie salda według kont w którym jest waluta. + */ + #[AuthorizeToken(['accounts-view'], anyScope: true)] + public function balance(): JsonResponse + { + $balances = auth()->user()->accounts() + ->select('currency') + ->selectRaw('SUM(balance) as total_balance') + ->groupBy('currency') + ->get(); + + return response()->json([ + 'balances' => BalanceResource::collection($balances), + /** @example 2500.00 */ + 'total' => number_format($balances->where('currency', 'PLN')->sum('total_balance') / 100, 2, '.', ''), + ]); } /** diff --git a/backend/app/Http/Controllers/Account/Card/CardController.php b/backend/app/Http/Controllers/Account/Card/CardController.php index 898cdf6..bb195ce 100644 --- a/backend/app/Http/Controllers/Account/Card/CardController.php +++ b/backend/app/Http/Controllers/Account/Card/CardController.php @@ -43,11 +43,12 @@ public function index(Account $account) throw new AccessDeniedHttpException('Account not found or access denied.'); } - $cards = $account->cards(); - - return CardResource::collection( - $this->paginate($cards), + $cards = $account->cards()->paginate( + perPage: $this->perPage(), + page: $this->currentPage(), ); + + return CardResource::collection($cards); } /** diff --git a/backend/app/Http/Controllers/Notification/NotificationController.php b/backend/app/Http/Controllers/Notification/NotificationController.php new file mode 100644 index 0000000..d1e1d61 --- /dev/null +++ b/backend/app/Http/Controllers/Notification/NotificationController.php @@ -0,0 +1,78 @@ +user() + ->notifications() + ->latest() + ->paginate( + perPage: $this->perPage(), + page: $this->currentPage(), + ); + + return NotificationResource::collection( + $notifications, + ); + } + + /** + * Nie przeczytane powiadomienia. + */ + public function unreadCount(Request $request): JsonResponse + { + return response()->json([ + 'count' => $request->user()->unreadNotifications()->count(), + ]); + } + + /** + * Odczytwanie powiadomienia. + */ + #[PathParameter('notificationId', description: 'Identyfikator powiadomienia', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + public function markAsRead(string $notificationId, Request $request): JsonResponse + { + $notification = $request->user() + ->notifications() + ->where('id', $notificationId) + ->firstOrFail(); + + $notification->markAsRead(); + + return response()->json(['message' => 'Notification marked as read.']); + } + + /** + * Odczytać wszystkie powiadomienia. + */ + public function markAllAsRead(Request $request): JsonResponse + { + $request->user()->unreadNotifications->markAsRead(); + + return response()->json(['message' => 'All notifications marked as read.']); + } +} diff --git a/backend/app/Http/Controllers/User/ProfileController.php b/backend/app/Http/Controllers/User/ProfileController.php index 266a1c9..08bfa17 100644 --- a/backend/app/Http/Controllers/User/ProfileController.php +++ b/backend/app/Http/Controllers/User/ProfileController.php @@ -6,10 +6,14 @@ use App\Http\Requests\User\RegisterUserRequest; use App\Http\Requests\User\UpdateProfileRequest; use App\Models\User; +use App\Services\Guardian\GuardianService; use Dedoc\Scramble\Attributes\BodyParameter; +use Dedoc\Scramble\Attributes\PathParameter; +use Dedoc\Scramble\Attributes\Response; use Illuminate\Auth\Events\Registered; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; use Laravel\Passport\Attributes\AuthorizeToken; /** @@ -19,6 +23,11 @@ */ class ProfileController extends Controller { + public function __construct( + private readonly GuardianService $guardianService, + ) { + } + /** * Informacja o profilu użytkownika. * @@ -38,12 +47,37 @@ public function show(Request $request): JsonResponse #[AuthorizeToken(['user-profile-manage'], anyScope: true)] #[BodyParameter('name', description: 'Nowa nazwa użytkownika', example: 'Jan Kowalski')] #[BodyParameter('email', description: 'Nowy adres email', example: 'jan.kowalski@example.com')] + #[BodyParameter('guardian_email', description: 'E-mail opiekuna, który powinien zatwierdzić że należysz do niego.', example: 'opiekun@example.com')] + #[BodyParameter('guardian_delete', description: 'Usuwanie e-mail opiekuna, jeśli dziecko jest pełnoletnie.', example: false)] + #[BodyParameter('date_of_birth', description: 'Data urodzenia użytkownika', example: '2000-01-01')] #[BodyParameter('password', description: 'Nowe hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', example: 'P@ssw0rd123!@')] #[BodyParameter('password_confirmation', description: 'Potwierdzenie nowego hasła', example: 'P@ssw0rd123!@')] #[BodyParameter('old_password', description: 'Aktualne hasło (wymagane przy zmianie hasła)', example: 'NewP@ssw0rd123!@')] public function update(UpdateProfileRequest $request): JsonResponse { - $request->user()->update($request->validated()); + $validated = $request->validated(); + $user = $request->user(); + + if (isset($validated['guardian_email']) && ! $user->guardian_id) { + $guardian = User::query()->where('email', $validated['guardian_email'])->first(); + + $validated['guardian_id'] = $guardian->id; + + $this->guardianService->sendGuardianRequest($user, $guardian); + + unset($validated['guardian_email']); + } + + if (isset($validated['guardian_delete']) && $user->guardian_id && $user->isAdult()) { + $validated['guardian_id'] = null; + $validated['guardian_approved_at'] = null; + } + + if (isset($validated['date_of_birth']) && $user->date_of_birth) { + throw ValidationException::withMessages(['date_of_birth' => ['Date of birth cannot be changed after it has been set.']]); + } + + $user->update($validated); return response()->json([ 'message' => 'Profile updated successfully', @@ -83,20 +117,91 @@ public function revokeToken(Request $request): JsonResponse * * @unauthenticated */ - #[BodyParameter('name', description: 'Nazwa użytkownika', type: 'string', example: 'Jan Kowalski')] - #[BodyParameter('email', description: 'Adres email', type: 'email', example: 'jan.kowalski@example.com')] - #[BodyParameter('password', description: 'Hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', type: 'password', example: 'P@ssw0rd123!@')] - #[BodyParameter('password_confirmation', description: 'Potwierdzenie nowego hasła', type: 'string', example: 'P@ssw0rd123!@')] public function create(RegisterUserRequest $request): User { $validated = $request->validated(); unset($validated['password_confirmation']); - $user = User::create($validated); - // Send mail to user, with verify email. - // event(new Registered($user)); + // Email verified, because it doesn't work locally :) + $validated['email_verified_at'] = now(); + + return User::create($validated); + } + + /** + * Zatwierdzanie opiekuństwa, które dziecko wysłało. + * + * Zatwierdzenie opiekuństwa nad kontem dziecka. + */ + #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + public function approveGuardian(string $wardId): JsonResponse + { + $guardian = auth()->user(); + $ward = User::query()->where('id', $wardId)->first(); + + if (! $ward) { + return response()->json([ + 'message' => 'User not found.', + ], 404); + } + + if (! $guardian->isAdult()) { + throw ValidationException::withMessages(['guardian' => ['Guardian must be at least 18 years old.']]); + } - return $user; + if (isset($ward->guardian_approved_at) && $ward->guardian_approved_at) { + return response()->json([ + 'message' => 'Guardian is approved.', + ], 422); + } + + if ($guardian->id === $ward->id) { + return response()->json([ + 'message' => 'You cannot approve yourself as your own guardian.', + ], 422); + } + + $this->guardianService->approveGuardian($guardian, $ward); + + return response()->json([ + 'message' => 'Guardian approval confirmed.', + ]); + } + + /** + * Odrzucenie opiekuństwa, które dziecko wysłało. + * + * Odrzucenie opiekuństwa nad kontem dziecka. + */ + #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + public function rejectGuardian(string $wardId): JsonResponse + { + $guardian = auth()->user(); + $ward = User::query()->where('id', $wardId)->first(); + + if (! $ward) { + return response()->json([ + 'message' => 'User not found.', + ], 404); + } + + if (isset($ward->guardian_approved_at) && $ward->guardian_approved_at) { + return response()->json([ + 'message' => 'Guardian is approved.', + ], 422); + } + + if ($guardian->id === $ward->id) { + return response()->json([ + 'message' => 'You cannot reject yourself as your own guardian.', + ], 422); + } + + $this->guardianService->rejectGuardian($guardian, $ward); + + return response()->json([ + 'message' => 'Guardian request rejected.', + ]); } } diff --git a/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php b/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php new file mode 100644 index 0000000..3d4fe64 --- /dev/null +++ b/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php @@ -0,0 +1,34 @@ +user()->toArray(); + + $isAdult = $user['is_adult'] ?? false; + $hasGuardian = isset($user['guardian']['id']) && $user['guardian']['id'] && $user['guardian']['guardian_approved_at']; + $hasDateOfBirth = isset($user['date_of_birth']) && $user['date_of_birth']; + + if (!$hasDateOfBirth) { + throw new AccessDeniedHttpException('Access denied. You must set date of birth.'); + } + + if (! $isAdult && ! $hasGuardian) { + throw new AccessDeniedHttpException('Access denied. You must be 18 or older, or have a legal guardian assigned to your account.'); + } + + return $next($request); + } +} diff --git a/backend/app/Http/Requests/User/RegisterUserRequest.php b/backend/app/Http/Requests/User/RegisterUserRequest.php index d91e206..1a7def0 100644 --- a/backend/app/Http/Requests/User/RegisterUserRequest.php +++ b/backend/app/Http/Requests/User/RegisterUserRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\User; +use Dedoc\Scramble\Attributes\BodyParameter; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Password; @@ -24,19 +25,35 @@ public function authorize(): bool public function rules(): array { return [ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'email', 'max:255', 'unique:users,email'], - 'password' => [ + /** @example Jan Kowalski */ + 'name' => ['required', 'string', 'max:255'], + /** @example jan.kowalski@example.com */ + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + /** @example "2000-01-01" */ + 'date_of_birth' => ['sometimes', 'date', 'date_format:Y-m-d', 'before_or_equal:today'], + /** @example "P@ssw0rd123!@" */ + 'password' => [ 'required', 'string', 'confirmed', - Password::min(8) + new Password(8) ->max(255) ->letters() ->mixedCase() ->numbers() ->symbols(), - // ->uncompromised(), + ], + /** @example "P@ssw0rd123!@" */ + 'password_confirmation' => ['sometimes', 'string', 'same:password'], + ]; + } + + public function bodyParameters(): array + { + return [ + 'password_confirmation' => [ + 'description' => 'Must match the password field.', + 'example' => 'P@ssw0rd123!@', ], ]; } diff --git a/backend/app/Http/Requests/User/UpdateProfileRequest.php b/backend/app/Http/Requests/User/UpdateProfileRequest.php index 05f9c92..86cdfa2 100644 --- a/backend/app/Http/Requests/User/UpdateProfileRequest.php +++ b/backend/app/Http/Requests/User/UpdateProfileRequest.php @@ -2,9 +2,11 @@ namespace App\Http\Requests\User; +use App\Rules\Adult\AdultGuardian; use App\Rules\Password\CurrentPassword; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Password; class UpdateProfileRequest extends FormRequest @@ -24,9 +26,25 @@ public function authorize(): bool */ public function rules(): array { + $user = $this->user(); + return [ - 'name' => ['sometimes', 'string', 'max:255'], - 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $this->user()->id], + /** @example Jan Kowalski */ + 'name' => ['sometimes', 'string', 'max:255'], + /** @example jan.kowalski@example.com */ + 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $user->id], + /** @example "2000-01-01" */ + 'date_of_birth' => ['sometimes', 'date', 'date_format:Y-m-d', 'before_or_equal:today'], + /** @example guardian@example.com */ + 'guardian_email' => [ + 'nullable', + 'email', + 'max:255', + Rule::exists('users', 'email'), + Rule::notIn([$user->email]), + new AdultGuardian(), + ], + /** @example "P@ssw0rd123!@" */ 'password' => [ 'sometimes', 'string', @@ -37,31 +55,13 @@ public function rules(): array ->mixedCase() ->numbers() ->symbols(), - // ->uncompromised(), - ], - 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], - ]; - } - - public function queryParameters(): array - { - return [ - 'name' => [ - 'description' => 'Nowa nazwa użytkownika', - 'example' => 'Jan Kowalski', - ], - 'email' => [ - 'description' => 'Nowy adres email', - 'example' => 'jan@example.com', - ], - 'password' => [ - 'description' => 'Nowe hasło (min. 8 znaków, max. 255 znaków, musi być potwierdzone)', - 'example' => 'newpassword123', - ], - 'password_confirmation' => [ - 'description' => 'Potwierdzenie nowego hasła', - 'example' => 'newpassword123', ], + /** @example "P@ssw0rd123!@" */ + 'password_confirmation' => ['sometimes', 'string', 'same:password'], + /** @example "O&l*dP@ssw0rd123!@" */ + 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], + /** @example false */ + 'guardian_delete' => ['sometimes', 'boolean'], ]; } } diff --git a/backend/app/Http/Resources/Account/BalanceResource.php b/backend/app/Http/Resources/Account/BalanceResource.php new file mode 100644 index 0000000..a28336a --- /dev/null +++ b/backend/app/Http/Resources/Account/BalanceResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + /** @example PLN */ + 'currency' => $this->currency, + /** @example 2500.00 */ + 'balance' => number_format($this->total_balance / 100, 2, '.', ''), + ]; + } +} diff --git a/backend/app/Http/Resources/Account/Card/CardResource.php b/backend/app/Http/Resources/Account/Card/CardResource.php index 9f0fe96..b7bf6cd 100644 --- a/backend/app/Http/Resources/Account/Card/CardResource.php +++ b/backend/app/Http/Resources/Account/Card/CardResource.php @@ -29,6 +29,8 @@ public function toArray(Request $request): array $this->card_number, substr($this->card_number, -4), ), + /* @example 123 */ + 'cvv' => $this->cvv, /* @example 12 */ 'exp_month' => $this->exp_month, /* @example 2024 */ diff --git a/backend/app/Http/Resources/Notification/NotificationResource.php b/backend/app/Http/Resources/Notification/NotificationResource.php new file mode 100644 index 0000000..e3300ae --- /dev/null +++ b/backend/app/Http/Resources/Notification/NotificationResource.php @@ -0,0 +1,29 @@ + $this->id, + /** @example "approved" */ + 'type' => $this->data['type'] ?? $this->type, + /** @example "Powiadomienie" */ + 'title' => $this->data['title'] ?? 'Powiadomienie', + /** @example "Treść powiadomienia" */ + 'body' => $this->data['body'] ?? '', + /** @example {} */ + 'data' => $this->data, + /** @example "2026-05-13T10:30:00.000000Z" */ + 'read_at' => $this->read_at?->toISOString(), + /** @example "2026-05-13T10:30:00.000000Z" */ + 'created_at' => $this->created_at->toISOString(), + ]; + } +} diff --git a/backend/app/Models/Account/Account.php b/backend/app/Models/Account/Account.php index 5067969..fc53192 100644 --- a/backend/app/Models/Account/Account.php +++ b/backend/app/Models/Account/Account.php @@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; #[Fillable(['user_id', 'name', 'account_number', 'balance', 'currency', 'type'])] -// #[Hidden(['account_number'])] class Account extends Model { use HasFactory; diff --git a/backend/app/Models/Account/Card/Card.php b/backend/app/Models/Account/Card/Card.php index 9b8676e..6d4b0ec 100644 --- a/backend/app/Models/Account/Card/Card.php +++ b/backend/app/Models/Account/Card/Card.php @@ -28,6 +28,7 @@ protected function casts(): array return [ 'exp_month' => 'integer', 'exp_year' => 'integer', + 'cvv' => 'integer', ]; } diff --git a/backend/app/Models/Account/Transaction/Transaction.php b/backend/app/Models/Account/Transaction/Transaction.php index d06deb6..732399a 100644 --- a/backend/app/Models/Account/Transaction/Transaction.php +++ b/backend/app/Models/Account/Transaction/Transaction.php @@ -26,10 +26,33 @@ class Transaction extends Model protected function casts(): array { return [ - 'amount' => 'decimal:2', + 'amount' => 'integer', ]; } + public function isExpenseFor(string $accountId): bool + { + return $this->from_account_id === $accountId; + } + + public function isIncomeFor(string $accountId): bool + { + return $this->to_account_id === $accountId; + } + + public function signedAmountFor(string $accountId): int + { + if ($this->isExpenseFor($accountId)) { + return -$this->amount; + } + + if ($this->isIncomeFor($accountId)) { + return +$this->amount; + } + + return 0; + } + public function fromAccount(): BelongsTo { return $this->belongsTo(Account::class, 'from_account_id'); diff --git a/backend/app/Models/Passport/Client.php b/backend/app/Models/Passport/Client.php index 078698f..8ceacc8 100644 --- a/backend/app/Models/Passport/Client.php +++ b/backend/app/Models/Passport/Client.php @@ -7,13 +7,8 @@ class Client extends BaseClient { - /** - * Określa, czy klient może pominąć ekran autoryzacji. - * Dla własnych aplikacji (frontend) zwracamy 'true'. - */ public function skipsAuthorization(Authenticatable $user, array $scopes = []): bool { - // Dla klientów first-party pomijamy ekran autoryzacji return $this->firstParty(); } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 100f2a3..7a40694 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -6,12 +6,14 @@ use App\Models\Account\Card\Card; use App\Models\Account\Flik\FlikCode; use App\Models\Account\Transaction\Transaction; +use Carbon\Carbon; use Database\Factories\UserFactory; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -19,7 +21,7 @@ use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; -#[Fillable(['id', 'name', 'email', 'password'])] +#[Fillable(['id', 'name', 'email', 'date_of_birth', 'guardian_id', 'guardian_approved_at', 'password'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail { @@ -37,14 +39,40 @@ class User extends Authenticatable implements OAuthenticatable, MustVerifyEmail protected function casts(): array { return [ - 'id' => 'string', - 'name' => 'string', - 'email' => 'string', - 'email_verified_at' => 'datetime', - 'password' => 'hashed', + 'id' => 'string', + 'name' => 'string', + 'email' => 'string', + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'date_of_birth' => 'datetime:Y-m-d', + 'guardian_approved_at' => 'datetime', ]; } + public function guardian(): BelongsTo + { + return $this->belongsTo(User::class, 'guardian_id'); + } + + public function wards(): HasMany + { + return $this->hasMany(User::class, 'guardian_id'); + } + + public function isAdult(): bool + { + if (! $this->date_of_birth) { + return false; + } + + return $this->date_of_birth->age >= 18; + } + + public function requiresGuardian(): bool + { + return ! $this->isAdult(); + } + public function accounts(): HasMany { return $this->hasMany(Account::class); @@ -78,4 +106,29 @@ public function receivedTransactions() { return $this->hasManyThrough(Transaction::class, Account::class, 'user_id', 'to_account_id'); } + + /** + * @property array{id: string, name: string, email: string, guardian_approved_at: string|null}|null $guardian + * @property bool $is_adult + */ + public function toArray(): array + { + $array = parent::toArray(); + + $array['is_adult'] = $this->isAdult(); + + if (isset($array['guardian_id']) && $array['guardian_id']) { + $guardian = $this->guardian()->first(['id', 'name', 'email']); + $array['guardian'] = $guardian?->toArray(); + $array['guardian']['guardian_approved_at'] = $array['guardian_approved_at']; + + unset($array['guardian_id'], $array['guardian_approved_at']); + + return $array; + } + + unset($array['guardian_id']); + + return $array; + } } diff --git a/backend/app/Notifications/Guardian/GuardianApprovedNotification.php b/backend/app/Notifications/Guardian/GuardianApprovedNotification.php new file mode 100644 index 0000000..6b26793 --- /dev/null +++ b/backend/app/Notifications/Guardian/GuardianApprovedNotification.php @@ -0,0 +1,53 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + public function toDatabase(object $notifiable): array + { + return [ + 'type' => 'guardian_approved', + 'title' => 'Opiekun potwierdzony!', + 'body' => "{$this->guardian->name} zatwierdził opiekę nad Twoim kontem.", + 'guardian_id' => $this->guardian->id, + 'guardian_name' => $this->guardian->name, + 'action' => 'view_account', + ]; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + ]; + } +} diff --git a/backend/app/Notifications/Guardian/GuardianRejectedNotification.php b/backend/app/Notifications/Guardian/GuardianRejectedNotification.php new file mode 100644 index 0000000..aa7dbe2 --- /dev/null +++ b/backend/app/Notifications/Guardian/GuardianRejectedNotification.php @@ -0,0 +1,53 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + public function toDatabase(object $notifiable): array + { + return [ + 'type' => 'guardian_rejected', + 'title' => 'Opiekun odrzucił prośbę', + 'body' => "{$this->guardian->name} odrzucił prośbę o opiekę.", + 'guardian_id' => $this->guardian->id, + 'guardian_name' => $this->guardian->name, + 'action' => 'find_new_guardian', + ]; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + ]; + } +} diff --git a/backend/app/Notifications/Guardian/GuardianRequestNotification.php b/backend/app/Notifications/Guardian/GuardianRequestNotification.php new file mode 100644 index 0000000..b89573e --- /dev/null +++ b/backend/app/Notifications/Guardian/GuardianRequestNotification.php @@ -0,0 +1,58 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + public function toDatabase(object $notifiable): array + { + return [ + 'type' => 'guardian_request', + 'title' => 'Nowa prośba o opiekę', + 'body' => "{$this->ward->name} prosi o potwierdzenie opieki.", + 'ward_id' => $this->ward->id, + 'ward_name' => $this->ward->name, + 'ward_email' => $this->ward->email, + 'action' => 'approve_guardian', + 'action_url' => '/guardian/approve/' . $this->ward->id, + 'ward_date_of_birth' => $this->ward->date_of_birth?->format('Y-m-d'), + 'ward_age' => $this->ward->date_of_birth?->age, + 'ward_email' => $this->ward->email, + ]; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + ]; + } +} diff --git a/backend/app/Notifications/Guardian/GuardianRequestSentNotification.php b/backend/app/Notifications/Guardian/GuardianRequestSentNotification.php new file mode 100644 index 0000000..3075ca7 --- /dev/null +++ b/backend/app/Notifications/Guardian/GuardianRequestSentNotification.php @@ -0,0 +1,53 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + public function toDatabase(object $notifiable): array + { + return [ + 'type' => 'guardian_request_sent', + 'title' => 'Prośba wysłana', + 'body' => "Wysłano prośbę do {$this->guardian->name}. Oczekuje na zatwierdzenie.", + 'guardian_id' => $this->guardian->id, + 'guardian_name' => $this->guardian->name, + 'status' => 'pending', + ]; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + ]; + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index b395d08..c214b90 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -6,6 +6,7 @@ use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; use Illuminate\Support\ServiceProvider; use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; use Laravel\Passport\Passport; @@ -14,13 +15,13 @@ class AppServiceProvider extends ServiceProvider { public function register(): void { - $this->app->bind(AuthorizationViewResponseContract::class, function () { - return new AuthorizationViewResponse('vendor.passport.authorize'); - }); + // } public function boot(): void { + Passport::authorizationView('vendor.passport.authorize'); + Scramble::registerApi('docs'); Scramble::configure() @@ -28,6 +29,13 @@ public function boot(): void $openApi->secure( SecurityScheme::http('bearer', 'JWT'), ); + + $openApi->secure( + SecurityScheme::oauth2() + ->flow('password', function (OAuthFlow $flow) { + $flow->tokenUrl(config('app.url') . '/oauth/token'); + }) + ); }); Passport::tokensExpireIn(now()->addDays(15)); @@ -99,6 +107,7 @@ public function boot(): void Passport::defaultScopes([ 'user-profile', + 'user-profile-manage', 'balance-view', 'balance-history', diff --git a/backend/app/Rules/Adult/AdultGuardian.php b/backend/app/Rules/Adult/AdultGuardian.php new file mode 100644 index 0000000..122e5bd --- /dev/null +++ b/backend/app/Rules/Adult/AdultGuardian.php @@ -0,0 +1,38 @@ +user(); + + if ($requester && $requester->isAdult()) { + $fail('You are already an adult. Guardian is not required.'); + + return; + } + + $guardian = User::query()->where('email', $value)->first(); + + if (! $guardian) { + $fail('Guardian with this email does not exist.'); + + return; + } + + if (! $guardian->date_of_birth || $guardian->date_of_birth->age < 18) { + $fail('Guardian must be at least 18 years old.'); + } + } +} diff --git a/backend/app/Services/Guardian/GuardianService.php b/backend/app/Services/Guardian/GuardianService.php new file mode 100644 index 0000000..08dbbd4 --- /dev/null +++ b/backend/app/Services/Guardian/GuardianService.php @@ -0,0 +1,46 @@ +notify(new GuardianRequestNotification($ward)); + + // Ward send info + $ward->notify(new GuardianRequestSentNotification($guardian)); + } + + public function approveGuardian(User $guardian, User $ward): void + { + $ward->update([ + 'guardian_id' => $guardian->id, + 'guardian_approved_at' => now(), + ]); + + $ward->notify(new GuardianApprovedNotification($guardian)); + } + + public function rejectGuardian(User $guardian, User $ward): void + { + if ($ward->guardian_id === $guardian->id) { + $ward->update(['guardian_id' => null]); + } + + $ward->update([ + 'guardian_id' => $guardian->id, + 'guardian_approved_at' => now(), + ]); + + $ward->notify(new GuardianRejectedNotification($guardian)); + } +} diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 5fd4c87..e23edb2 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -31,7 +31,7 @@ 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - + 'adult' => \App\Http\Middleware\Adult\AdultCheckMiddleware::class, ]); $middleware->group('web', [ @@ -57,44 +57,7 @@ ->withSingletons([ \Illuminate\Contracts\Debug\ExceptionHandler::class => \App\Exceptions\Handler::class, ]) -// ->withSingletons([ -// Illuminate\Contracts\Debug\ExceptionHandler::class => Illuminate\Foundation\Exceptions\Handler::class, -// ]) ->withExceptions(function (Exceptions $exceptions) { - // $exceptions->renderable(function (ValidationException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (AuthenticationException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (AuthorizationException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (ModelNotFoundException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (NotFoundHttpException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (UnauthorizedHttpException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (AccessDeniedHttpException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (ThrottleRequestsException $e) { - // return Handler::handleException($e); - // }); - // - // $exceptions->renderable(function (MissingScopeException $e) { - // return Handler::handleException($e); - // }); + }) ->create(); diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php index fc94ae6..cc1e4af 100644 --- a/backend/bootstrap/providers.php +++ b/backend/bootstrap/providers.php @@ -1,7 +1,9 @@ faker->randomElement(['PLN', 'USD', 'EURO']); + return [ 'user_id' => User::factory(), - 'name' => $this->faker->word(), - 'account_number' => $this->faker->unique()->iban(), - 'balance' => $this->faker->randomFloat(2, 0, 10000), - 'currency' => 'PLN', - 'type' => 'current', + 'name' => match($currency) { + 'PLN' => $this->faker->randomElement(['Konto osobiste', 'Konto oszczędnościowe', 'Konto premium', 'SuperKonto']), + 'USD' => 'Konto walutowe USD', + 'EURO' => 'Konto walutowe EUR', + }, + 'account_number' => $this->generate(), + 'balance' => $this->faker->numberBetween(100, 1500000), + 'currency' => $currency, + 'type' => $this->faker->randomElement(['current', 'savings']), ]; } + + public function withCards(int $count = 1): static + { + return $this->afterCreating(function (Account $account) use ($count) { + Card::factory()->count($count)->create([ + 'account_id' => $account->id, + ]); + }); + } } diff --git a/backend/database/factories/Account/Card/CardFactory.php b/backend/database/factories/Account/Card/CardFactory.php index 3c0caf6..ddecf35 100644 --- a/backend/database/factories/Account/Card/CardFactory.php +++ b/backend/database/factories/Account/Card/CardFactory.php @@ -18,10 +18,8 @@ class CardFactory extends Factory */ public function definition(): array { - $account = Account::factory()->create(); - return [ - 'account_id' => $account, + 'account_id' => Account::factory(), 'card_number' => $this->faker->creditCardNumber(), 'cvv' => $this->faker->randomNumber(3), 'exp_month' => $this->faker->date('m'), diff --git a/backend/database/factories/Account/Flik/FlikCodeFactory.php b/backend/database/factories/Account/Flik/FlikCodeFactory.php index 739f9e9..d79310b 100644 --- a/backend/database/factories/Account/Flik/FlikCodeFactory.php +++ b/backend/database/factories/Account/Flik/FlikCodeFactory.php @@ -18,12 +18,10 @@ class FlikCodeFactory extends Factory */ public function definition(): array { - $card = Card::factory()->create(); - return [ - 'card_id' => $card, + 'card_id' => Card::factory(), 'code' => $this->faker->unique()->regexify('[0-9]{6}'), - 'expires_at' => $this->faker->dateTimeBetween('+1 day', '+1 month'), + 'expires_at' => $this->faker->dateTimeBetween('+1 minut', '+2 minut'), 'status' => $this->faker->randomElement(['active', 'used', 'expired']), ]; } diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index 6133d1f..7d697f9 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -31,6 +31,10 @@ public function definition(): array 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'date_of_birth' => fake()->dateTimeBetween( + now()->subYears(25), + now()->subYears(13), + )->format('Y-m-d'), ]; } diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php index e7e014f..913183f 100644 --- a/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,10 +13,18 @@ public function up(): void $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); + $table->date('date_of_birth')->nullable(); + $table->uuid('guardian_id')->nullable(); + $table->timestamp('guardian_approved_at')->nullable(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); + + $table->foreign('guardian_id') + ->references('id') + ->on('users') + ->onDelete('set null'); }); Schema::create('password_reset_tokens', function (Blueprint $table) { diff --git a/backend/database/migrations/2026_05_09_170729_accounts.php b/backend/database/migrations/2026_05_09_170729_accounts.php index 7068541..9809e9d 100644 --- a/backend/database/migrations/2026_05_09_170729_accounts.php +++ b/backend/database/migrations/2026_05_09_170729_accounts.php @@ -16,7 +16,7 @@ public function up(): void $table->string('name'); $table->string('account_number', 32)->unique(); $table->bigInteger('balance')->default(0); - $table->string('currency', 3)->default('PLN'); + $table->string('currency', 5)->default('PLN'); $table->string('type', 20)->default('current'); $table->timestamps(); }); diff --git a/backend/database/migrations/2026_05_09_173714_create_notifications_table.php b/backend/database/migrations/2026_05_09_173714_create_notifications_table.php index b51ae15..fe54f90 100644 --- a/backend/database/migrations/2026_05_09_173714_create_notifications_table.php +++ b/backend/database/migrations/2026_05_09_173714_create_notifications_table.php @@ -12,7 +12,9 @@ public function up(): void Schema::create('notifications', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('type'); - $table->morphs('notifiable'); + $table->string('notifiable_type'); + $table->uuid('notifiable_id'); + $table->index(['notifiable_type', 'notifiable_id']); $table->text('data'); $table->timestamp('read_at')->nullable(); $table->timestamps(); diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index 76dd6d5..7aac343 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -25,6 +25,7 @@ public function run(): void $this->call([ PassportClientSeeder::class, + UserSeeder::class, ]); } } diff --git a/backend/database/seeders/PassportClientSeeder.php b/backend/database/seeders/PassportClientSeeder.php index 5df221b..03ffd27 100644 --- a/backend/database/seeders/PassportClientSeeder.php +++ b/backend/database/seeders/PassportClientSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -14,23 +13,13 @@ public function run(): void $clientId = config('passport.client_id', 'bank-client-id'); $clientSecret = config('passport.client_secret', 'bank-client-secret'); $nameApp = config('app.name', 'Bank Online System'); - $redirectUri = config('passport.redirect_uri', ''); - $grantTypes = ['authorization_code', 'refresh_token', 'password', 'client_credentials']; - - $user = User::where('email', 'test@example.com')->first(); - - if (! $user) { - $user = User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => Hash::make('password'), - ]); - - $this->command->info('✅ User created: ' . $user->email); - $this->command->info(' Password: password'); - } else { - $this->command->info('⚠️ User already exists: ' . $user->email); - } + $redirectUris = [ + config('passport.redirect_uri', ''), + 'http://localhost:8000/auth/callback', + 'http://localhost:5174/auth/callback', + 'http://127.0.0.1:8000/auth/callback' + ]; + $grantTypes = ['authorization_code', 'refresh_token', 'password']; $exists = DB::table('oauth_clients') ->where('id', $clientId) @@ -38,22 +27,23 @@ public function run(): void ->exists(); if ($exists) { - $this->command->info('⚠️ Passport client already exists. Updating...'); + $this->command->info('⚠️ Passport client already exists. Updating...'); DB::table('oauth_clients') ->where('id', $clientId) ->orWhere('name', $nameApp) ->update([ 'secret' => Hash::make($clientSecret), - 'redirect_uris' => json_encode([$redirectUri]), + 'redirect_uris' => json_encode($redirectUris), 'updated_at' => now(), 'grant_types' => json_encode($grantTypes), ]); $this->command->info('✅ Passport client updating!'); - $this->command->info(' Client ID: ' . $clientId); + $this->command->info(' Client ID: ' . $clientId); $this->command->info(' Client Secret: ' . $clientSecret); - $this->command->info(' Redirect URI: ' . $redirectUri); + $this->command->info(' Redirect URI: ' . implode(', ', $redirectUris)); + $this->command->info(' Grant types: ' . implode(', ', $grantTypes)); return; } @@ -63,16 +53,15 @@ public function run(): void 'name' => $nameApp, 'secret' => Hash::make($clientSecret), 'provider' => 'users', - 'redirect_uris' => json_encode([$redirectUri]), + 'redirect_uris' => json_encode($redirectUris), 'grant_types' => json_encode($grantTypes), 'revoked' => 0, - 'created_at' => now(), - 'updated_at' => now(), ]); $this->command->info('✅ Passport client created!'); $this->command->info(' Client ID: ' . $clientId); $this->command->info(' Client Secret: ' . $clientSecret); - $this->command->info(' Redirect URI: ' . $redirectUri); + $this->command->info(' Redirect URI: ' . implode(', ', $redirectUris)); + $this->command->info(' Grant types: ' . implode(', ', $grantTypes)); } } diff --git a/backend/database/seeders/UserSeeder.php b/backend/database/seeders/UserSeeder.php new file mode 100644 index 0000000..a3dda1f --- /dev/null +++ b/backend/database/seeders/UserSeeder.php @@ -0,0 +1,64 @@ + 'Marek Nowak', 'email' => 'marek@example.com'], + ['name' => 'Anna Kowalska', 'email' => 'anna@example.com'], + ['name' => 'Jan Wiśniewski', 'email' => 'jan@example.com'], + ['name' => 'Katarzyna Zielińska', 'email' => 'kasia@example.com'], + ['name' => 'Piotr Wójcik', 'email' => 'piotr@example.com'], + ['name' => 'Magdalena Szymańska', 'email' => 'magda@example.com'], + ['name' => 'Tomasz Kamiński', 'email' => 'tomasz@example.com'], + ['name' => 'Agnieszka Lewandowska', 'email' => 'aga@example.com'], + ['name' => 'Krzysztof Dąbrowski', 'email' => 'krzysztof@example.com'], + ['name' => 'Test User', 'email' => 'test@example.com'], + ]; + + $created = 0; + $existing = 0; + + foreach ($usersData as $data) { + $user = User::query()->where('email', $data['email'])->first(); + + if ($user) { + $this->command->warn('⚠️ User already exists: ' . $user->email); + $this->command->line(' Password: password'); + ++$existing; + + continue; + } + + $user = User::factory()->create([ + 'name' => $data['name'], + 'email' => $data['email'], + ]); + + $accountsCount = 5; + Account::factory()->count($accountsCount)->withCards(rand(1, 3))->create([ + 'user_id' => $user->id, + ]); + + $this->command->info('✅ User created: ' . $user->email); + $this->command->line(' Password: password'); + ++$created; + } + + $this->command->newLine(); + $this->command->info('📊 Summary:'); + $this->command->line(' Created: ' . $created); + $this->command->line(' Existing: ' . $existing); + $this->command->line(' Total: ' . count($usersData)); + } +} diff --git a/backend/docker/entrypoint.sh b/backend/docker/entrypoint.sh index bb77d0d..83575e4 100755 --- a/backend/docker/entrypoint.sh +++ b/backend/docker/entrypoint.sh @@ -3,9 +3,19 @@ set -e echo "🚀 Starting Laravel application..." -# Prepare PHP-FPM directories -mkdir -p /var/run/php-fpm /var/log/php-fpm +# Copy conf file to container +echo "📁 Copying configuration files..." +cp -f docker/nginx.conf /etc/nginx/http.d/default.conf +cp -f docker/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf +cp -f docker/supervisord.conf /etc/supervisord.conf + +# PHP-FPM && Logs +mkdir -p /var/run/php-fpm /var/log/php-fpm /var/log/nginx chown -R nobody:nobody /var/run/php-fpm /var/log/php-fpm +chown -R nginx:nginx /var/log/nginx + +# Conf file for laravel. +mkdir -p storage/logs bootstrap/cache && chmod -R 775 storage bootstrap/cache # Check if composer.json exists if [ ! -f "composer.json" ]; then diff --git a/backend/docker/nginx.conf b/backend/docker/nginx.conf index ca3b7c1..1ecd4ee 100644 --- a/backend/docker/nginx.conf +++ b/backend/docker/nginx.conf @@ -18,6 +18,9 @@ server { charset utf-8; + error_log /var/log/nginx/error.log; + fastcgi_intercept_errors on; + location / { try_files $uri $uri/ /index.php?$query_string; } @@ -30,8 +33,13 @@ server { location ~ \.php$ { fastcgi_pass php_fpm; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param QUERY_STRING $query_string; include fastcgi_params; fastcgi_hide_header X-Powered-By; + + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; } location ~ /\.(?!well-known).* { diff --git a/backend/docker/php-fpm.conf b/backend/docker/php-fpm.conf index 20caae1..880f423 100644 --- a/backend/docker/php-fpm.conf +++ b/backend/docker/php-fpm.conf @@ -17,3 +17,7 @@ request_terminate_timeout = 300 ; Environment variables env[DB_HOST] = mariadb env[REDIS_HOST] = redis + +catch_workers_output = yes +php_admin_value[error_log] = /var/log/php-fpm/php-fpm-error.log +php_admin_flag[log_errors] = on diff --git a/backend/resources/views/vendor/passport/authorize.blade.php b/backend/resources/views/vendor/passport/authorize.blade.php index aad01ef..6591957 100644 --- a/backend/resources/views/vendor/passport/authorize.blade.php +++ b/backend/resources/views/vendor/passport/authorize.blade.php @@ -3,57 +3,316 @@ - Autoryzacja - Bank Online - + Autoryzacja - {{ config('app.name') }} + + + - -
-
-

Bank Online System

-

Autoryzacja aplikacji

+ +
+
+
+ security +
+
+

Aplikacja żądająca dostępu

+

{{ $client->name }}

+
-
-

- {{ $client->name }} chce uzyskać dostęp do Twojego konta. -

- - @if(count($scopes) > 0) -
-

Uprawnienia:

-
    - @foreach($scopes as $scope) -
  • {{ $scope->description }}
  • - @endforeach -
+
+
+ warning_amber +

Ta aplikacja chce uzyskać dostęp do Twojego konta bankowego. Przejrzyj i wybierz uprawnienia, które chcesz przyznać.

+
+ +

Wybierz uprawnienia

+ + @if(count($scopes) === 0 || (count($scopes) === 1 && $scopes[0]->id === '*')) +
+ admin_panel_settings +

Pełny dostęp — Aplikacja żąda dostępu do wszystkich funkcji. Możesz ograniczyć uprawnienia odznaczając wybrane pozycje.

+
+ + @php + $allScopes = \Laravel\Passport\Passport::scopes(); + @endphp +
+ @foreach($allScopes as $scope) + + @endforeach +
+ @else +
+ @foreach($scopes as $scope) + + @endforeach
@endif -
-
- -
- @csrf - - - - -
- - -
- @csrf - @method('DELETE') - - - - -
+ + +
+
+ @csrf + + + + + +
+ +
+ @csrf + @method('DELETE') + + + + +
+
+ +
+ + diff --git a/backend/routes/api.php b/backend/routes/api.php index d2cd109..99d9373 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Account\AccountController; use App\Http\Controllers\Account\Card\CardController; +use App\Http\Controllers\Notification\NotificationController; use App\Http\Controllers\User\ProfileController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -14,6 +15,14 @@ }); Route::middleware(['auth:api'])->group(function () { + Route::post('/guardian/approve/{wardId}', [ProfileController::class, 'approveGuardian']); + Route::post('/guardian/reject/{wardId}', [ProfileController::class, 'rejectGuardian']); + + Route::get('/notifications', [NotificationController::class, 'index']); + Route::get('/notifications/unread-count', [NotificationController::class, 'unreadCount']); + Route::patch('/notifications/{notificationId}/read', [NotificationController::class, 'markAsRead']); + Route::patch('/notifications/read-all', [NotificationController::class, 'markAllAsRead']); + Route::prefix('/user')->group(function () { Route::middleware('guest')->withoutMiddleware(['auth:api', 'verified'])->group(function () { Route::post('/register', [ProfileController::class, 'create']); @@ -35,10 +44,13 @@ }); }); - Route::apiResource('accounts', AccountController::class); - Route::apiResource('accounts.cards', CardController::class)->only(['index', 'store']); - Route::get('cards/{card}', [CardController::class, 'show']); - Route::patch('cards/{card}/block', [CardController::class, 'block']); - Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); - Route::delete('cards/{card}', [CardController::class, 'destroy']); + Route::middleware(['adult'])->group(function () { + Route::get('accounts/balance', [AccountController::class, 'balance']); + Route::apiResource('accounts', AccountController::class); + Route::apiResource('accounts.cards', CardController::class)->only(['index', 'store']); + Route::get('cards/{card}', [CardController::class, 'show']); + Route::patch('cards/{card}/block', [CardController::class, 'block']); + Route::patch('cards/{card}/unblock', [CardController::class, 'unblock']); + Route::delete('cards/{card}', [CardController::class, 'destroy']); + }); }); diff --git a/backend/routes/auth.php b/backend/routes/auth.php index 5cd5f66..04b544c 100644 --- a/backend/routes/auth.php +++ b/backend/routes/auth.php @@ -11,8 +11,8 @@ use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { - // Route::get('register', [RegisteredUserController::class, 'create']) - // ->name('register'); + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); @@ -34,25 +34,25 @@ ->name('password.store'); }); -// Route::middleware('auth')->group(function () { -// Route::get('verify-email', EmailVerificationPromptController::class) -// ->name('verification.notice'); -// -// Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) -// ->middleware(['signed', 'throttle:6,1']) -// ->name('verification.verify'); -// -// Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) -// ->middleware('throttle:6,1') -// ->name('verification.send'); -// -// Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) -// ->name('password.confirm'); -// -// Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); -// -// Route::put('password', [PasswordController::class, 'update'])->name('password.update'); -// -// Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) -// ->name('logout'); -// }); + Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::get('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); + }); diff --git a/docker-compose.yml b/docker-compose.yml index fd597d2..4f1c6a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ +x-defaults: &defaults + environment: + - TZ=Europe/Warsaw + services: # MariaDB Database mariadb: + <<: *defaults image: mariadb:11.4 container_name: bank-app-mariadb environment: @@ -24,6 +29,7 @@ services: # Redis Cache redis: + <<: *defaults image: redis:7-alpine container_name: bank-app-redis ports: @@ -40,7 +46,7 @@ services: # Backend (Laravel with PHP-FPM + Nginx) backend: -# image: php:8.4-fpm-alpine + <<: *defaults build: context: ./backend dockerfile: Dockerfile @@ -65,6 +71,7 @@ services: # Frontend (React) frontend: + <<: *defaults build: context: ./frontend dockerfile: Dockerfile diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bf4f334..7ffdd44 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; +import {ThemeProvider, createTheme, Theme} from '@mui/material/styles'; import { CssBaseline, Box } from '@mui/material'; import { AuthProvider } from './context/AuthContext'; import { ToastProvider } from './context/ToastContext'; @@ -8,71 +8,275 @@ import { ProtectedRoute } from './components/Layout/ProtectedRoute'; import { LoginPage } from './pages/Forms/LoginPage'; import { RegisterPage } from './pages/Forms/RegisterPage'; import { BalancePage } from './pages/User/BalancePage'; -import {JSX} from "react"; -import {ProfilePage} from "./pages/User/ProfilePage"; -import {DashboardPage} from "./pages/Dashboard/DashboardPage"; -import {GuestOnlyRoute} from "./components/Layout/GuestOnlyRoute"; -import {DashboardLayout} from "./components/Layout/DashboardLayout"; -import {AccountPage} from "./pages/Account/AccountPage"; -import {AccountViewPage} from "./pages/Account/AccoutViewPage"; +import { JSX } from "react"; +import { ProfilePage } from "./pages/User/ProfilePage"; +import { DashboardPage } from "./pages/Dashboard/DashboardPage"; +import { GuestOnlyRoute } from "./components/Layout/GuestOnlyRoute"; +import { DashboardLayout } from "./components/Layout/DashboardLayout"; +import { AccountPage } from "./pages/Account/AccountPage"; +import { AccountViewPage } from "./pages/Account/AccoutViewPage"; +import { GuardianApprovalPage } from "./pages/Guardian/GuardianApprovalPage"; +import { NotificationBell } from "./components/Notification/NotificationBell"; +import { NotificationsPage } from "./pages/Notification/NotificationsPage"; const theme = createTheme({ - palette: { - primary: { - main: '#1976d2', - }, - secondary: { - main: '#dc004e', - }, - background: { - default: '#f5f5f5', - paper: '#ffffff', - }, - }, - typography: { - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', - h4: { - fontWeight: 600, - }, - h5: { - fontWeight: 600, - }, - h6: { - fontWeight: 600, - }, - }, - components: { - MuiSvgIcon: { - defaultProps: { - fontSize: 'small', - }, - }, - MuiButton: { - styleOverrides: { - root: { - textTransform: 'none', - borderRadius: 4, - fontWeight: 600, - }, - }, + palette: { + mode: 'light', + primary: { + main: '#1976d2', + light: '#42a5f5', + dark: '#1565c0', + contrastText: '#ffffff', + }, + secondary: { + main: '#dc004e', + light: '#ff5983', + dark: '#9a0036', + contrastText: '#ffffff', + }, + background: { + default: '#f5f5f5', + paper: '#ffffff', + }, + text: { + primary: 'rgba(0, 0, 0, 0.87)', + secondary: 'rgba(0, 0, 0, 0.6)', + disabled: 'rgba(0, 0, 0, 0.38)', + }, + divider: 'rgba(0, 0, 0, 0.12)', + error: { + main: '#d32f2f', + light: '#ef5350', + dark: '#c62828', + contrastText: '#ffffff', + }, + warning: { + main: '#ed6c02', + light: '#ff9800', + dark: '#e65100', + contrastText: '#ffffff', + }, + info: { + main: '#0288d1', + light: '#03a9f4', + dark: '#01579b', + contrastText: '#ffffff', + }, + success: { + main: '#2e7d32', + light: '#4caf50', + dark: '#1b5e20', + contrastText: '#ffffff', + }, + grey: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + }, }, - MuiPaper: { - styleOverrides: { - root: { - borderRadius: 8, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h4: { + fontWeight: 600, + }, + h5: { + fontWeight: 600, + }, + h6: { + fontWeight: 600, }, - }, }, - MuiTextField: { - styleOverrides: { - root: { - '& .MuiOutlinedInput-root': { - borderRadius: 8, - }, - }, - }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: '#f5f5f5', + color: 'rgba(0, 0, 0, 0.87)', + }, + }, + }, + MuiSvgIcon: { + defaultProps: { + fontSize: 'small', + }, + }, + MuiButton: { + styleOverrides: { + root: ({ theme }) => ({ + textTransform: 'none', + borderRadius: 4, + fontWeight: 600, + color: theme.palette.text.primary, + }), + contained: ({ theme }) => ({ + color: theme.palette.primary.contrastText, + }), + containedSecondary: ({ theme }: any) => ({ + color: theme.palette.secondary.contrastText, + }), + outlined: ({ theme }) => ({ + color: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + }), + }, + }, + MuiPaper: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: 8, + backgroundColor: theme.palette.background.paper, + color: theme.palette.getContrastText(theme.palette.background.paper), + }), + }, + }, + MuiCard: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + color: theme.palette.getContrastText(theme.palette.background.paper), + }), + }, + }, + MuiCardContent: { + styleOverrides: { + root: { + color: 'inherit', + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: ({ theme }) => ({ + '& .MuiOutlinedInput-root': { + borderRadius: 8, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + }, + '& .MuiInputLabel-root': { + color: theme.palette.text.secondary, + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.divider, + }, + }), + }, + }, + MuiTableCell: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.text.primary, + borderBottom: `1px solid ${theme.palette.divider}`, + }), + head: ({ theme }) => ({ + color: theme.palette.text.primary, + fontWeight: 600, + backgroundColor: theme.palette.grey[50], + }), + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, + }, + }, + }, + MuiAlert: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.text.primary, + '& .MuiAlert-message': { + color: theme.palette.text.primary, + }, + }), + }, + }, + MuiChip: { + styleOverrides: { + root: { + fontWeight: 600, + }, + filled: ({ theme }) => ({ + color: theme.palette.primary.contrastText, + }), + outlined: ({ theme }) => ({ + color: theme.palette.text.primary, + borderColor: theme.palette.grey[400], + }), + }, + }, + MuiTypography: { + styleOverrides: { + root: { + color: 'inherit', + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: ({ theme }) => ({ + borderColor: theme.palette.divider, + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.text.primary, + }), + }, + }, + MuiListItemText: { + styleOverrides: { + primary: ({ theme }) => ({ + color: theme.palette.text.primary, + }), + secondary: ({ theme }) => ({ + color: theme.palette.text.secondary, + }), + }, + }, + MuiMenuItem: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.text.primary, + }), + }, + }, + MuiAppBar: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + }), + }, + }, + MuiDrawer: { + styleOverrides: { + paper: ({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + }), + }, + }, + MuiTooltip: { + styleOverrides: { + tooltip: ({ theme }) => ({ + backgroundColor: theme.palette.grey[800], + color: theme.palette.getContrastText(theme.palette.grey[800]), + }), + }, + }, }, - }, }); function App(): JSX.Element { @@ -98,6 +302,8 @@ function App(): JSX.Element { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/components/Field/DateField.tsx b/frontend/src/components/Field/DateField.tsx new file mode 100644 index 0000000..4eca0ac --- /dev/null +++ b/frontend/src/components/Field/DateField.tsx @@ -0,0 +1,106 @@ +import React, { useMemo } from 'react'; +import { FormControl, FormHelperText, InputLabel, OutlinedInput } from '@mui/material'; + +interface DateFieldProps { + value: string | Date | null | undefined; + onChange: (value: string) => void; + formatDate: (date: string | Date | null | undefined) => string; + label?: string; + helperText?: string; + error?: boolean; + errorText?: string; + required?: boolean; + fullWidth?: boolean; + disabled?: boolean; + size?: 'small' | 'medium'; + maxDate?: string | Date | (() => string | Date); + minDate?: string | Date | (() => string | Date); + id?: string; + sx?: React.CSSProperties; +} + +export const DateField: React.FC = ({ + value, + onChange, + formatDate, + label = 'Data', + helperText, + error = false, + errorText, + required = false, + fullWidth = true, + disabled = false, + size = 'medium', + maxDate, + minDate, + id, + sx, +}) => { + const inputId = id || `date-field-${Math.random().toString(36).slice(2, 11)}`; + const formattedValue = formatDate(value) || ''; + const hasValue = Boolean(value && value !== ''); + + const maxDateStr = useMemo(() => { + if (!maxDate) return undefined; + if (typeof maxDate === 'function') { + const result = maxDate(); + return result instanceof Date ? result.toISOString().split('T')[0] : result; + } + return maxDate instanceof Date ? maxDate.toISOString().split('T')[0] : maxDate; + }, [maxDate]); + + const minDateStr = useMemo(() => { + if (!minDate) return undefined; + if (typeof minDate === 'function') { + const result = minDate(); + return result instanceof Date ? result.toISOString().split('T')[0] : result; + } + return minDate instanceof Date ? minDate.toISOString().split('T')[0] : minDate; + }, [minDate]); + + const displayHelperText = error && errorText ? errorText : helperText; + + return ( + + + {label} + + onChange(e.target.value)} + label={label} + notched + disabled={disabled} + inputProps={{ + max: maxDateStr, + min: minDateStr, + }} + sx={{ + ...sx, + ...(!hasValue && !disabled && { + '& input[type="date"]': { + color: 'transparent', + '&::-webkit-datetime-edit': { color: 'transparent' }, + '&::-webkit-datetime-edit-fields-wrapper': { color: 'transparent' }, + '&::-webkit-calendar-picker-indicator': { opacity: 0.6 }, + }, + }), + }} + /> + {displayHelperText && ( + + {displayHelperText} + + )} + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/Layout/DashboardLayout.tsx b/frontend/src/components/Layout/DashboardLayout.tsx index 565f8d4..6675a67 100644 --- a/frontend/src/components/Layout/DashboardLayout.tsx +++ b/frontend/src/components/Layout/DashboardLayout.tsx @@ -28,10 +28,12 @@ import { Logout as LogoutIcon, ChevronLeft as ChevronLeftIcon, AccountBalanceWallet as BalanceIcon, - Warning as WarningIcon + Warning as WarningIcon, + NotificationImportantRounded as NotificationIcon } from '@mui/icons-material'; import { useAuth } from '../../context/AuthContext'; import { Badge } from '@mui/material'; +import {NotificationBell} from "../Notification/NotificationBell"; const DRAWER_WIDTH = 240; @@ -43,6 +45,7 @@ const menuItems = [ { text: 'Przelewy', icon: , path: '/transfer' }, { text: 'FLIK', icon: , path: '/flik' }, { text: 'Historia', icon: , path: '/transactions' }, + { text: 'Powiadomienia', icon: , path: '/notifications' }, ]; const getNameApp: string = import.meta.env.VITE_APP_NAME @@ -93,6 +96,8 @@ export const DashboardLayout = () => { + + {user?.name || user?.email} diff --git a/frontend/src/components/Notification/NotificationBell.tsx b/frontend/src/components/Notification/NotificationBell.tsx new file mode 100644 index 0000000..959031c --- /dev/null +++ b/frontend/src/components/Notification/NotificationBell.tsx @@ -0,0 +1,223 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Badge, + IconButton, + Menu, + MenuItem, + Typography, + Box, + Divider, + Button, + ListItemText, + ListItemIcon, +} from '@mui/material'; +import { + Notifications as NotificationsIcon, + Circle as CircleIcon, + ArrowForward as ViewAllIcon, +} from '@mui/icons-material'; +import { useAuth } from '../../context/AuthContext'; +import { getNotifications, markAsRead, getUnreadCount } from '../../services/notificationService'; +import { Notification } from '../../types/types'; + +export const NotificationBell = () => { + const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [anchorEl, setAnchorEl] = useState(null); + const navigate = useNavigate(); + const { user } = useAuth(); + + const fetchData = useCallback(async () => { + if (!user) return; + try { + const [notifs, count] = await Promise.all([ + getNotifications(1, 5), + getUnreadCount(), + ]); + setNotifications(notifs.data); + setUnreadCount(count); + } catch (err) { + console.error('Failed to fetch notifications:', err); + } + }, [user]); + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); + }, [fetchData]); + + const handleRead = async (id: string) => { + try { + await markAsRead(id); + fetchData(); + } catch (err) { + console.error('Failed to mark as read:', err); + } + }; + + const handleAction = (notification: Notification) => { + handleRead(notification.id); + + if (notification.data?.action === 'approve_guardian' && notification.data?.ward_id) { + navigate(`/guardian/approve/${notification.data.ward_id}`, { + state: { + ward: { + id: notification.data.ward_id, + name: notification.data.ward_name, + email: notification.data.ward_email, + date_of_birth: notification.data.ward_date_of_birth, + age: notification.data.ward_age, + }, + }, + }); + } + }; + + const getIconColor = (type: string): string => { + switch (type) { + case 'guardian_request': return 'warning.main'; + case 'guardian_approved': return 'success.main'; + case 'guardian_rejected': return 'error.main'; + default: return 'info.main'; + } + }; + + const open = Boolean(anchorEl); + + return ( + <> + setAnchorEl(e.currentTarget)} + color="inherit" + aria-label="notifications" + > + + + + + + setAnchorEl(null)} + slotProps={{ + paper: { + sx: { width: 380, maxHeight: 480 }, + }, + }} + transformOrigin={{ horizontal: 'right', vertical: 'top' }} + anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} + > + {notifications.length === 0 && ( + + + + )} + + {notifications.map((n) => ( + + handleAction(n)} + sx={{ + bgcolor: n.read_at ? 'transparent' : 'action.hover', + borderLeft: 3, + borderColor: getIconColor(n.type), + py: 1.5, + }} + > + + + + + {n.title} + + } + secondary={ + + + {n.body} + + + {new Date(n.created_at).toLocaleString('pl-PL')} + + + } + /> + + + {n.data?.action === 'approve_guardian' && !n.read_at && ( + + + + )} + + + + ))} + + {/* Link to full notifications page */} + { + setAnchorEl(null); + navigate('/notifications'); + }} + sx={{ justifyContent: 'center' }} + > + + + + + Zobacz wszystkie powiadomienia + + } + /> + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index aaba218..39345b9 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -58,6 +58,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const value: AuthContextType = { loading, user, + setUser, + loadUser, isAuthenticated, login: handleLogin, register: handleRegister, diff --git a/frontend/src/pages/Account/AccountPage.tsx b/frontend/src/pages/Account/AccountPage.tsx index 46892a9..7e28f6d 100644 --- a/frontend/src/pages/Account/AccountPage.tsx +++ b/frontend/src/pages/Account/AccountPage.tsx @@ -29,8 +29,7 @@ import { } from '@mui/icons-material'; import { getAccounts, createAccount } from '../../services/accountService'; import type { Account, PaginationMeta } from '../../types/types'; -import {formatMoney} from "../../utils/formatMoney"; -import {formatAccountNumber, isValidAccountNumber} from "../../utils/formatAccount"; +import {formatAccountNumber} from "../../utils/formatAccount"; export const AccountPage = () => { const [accounts, setAccounts] = useState([]); diff --git a/frontend/src/pages/Guardian/GuardianApprovalPage.tsx b/frontend/src/pages/Guardian/GuardianApprovalPage.tsx new file mode 100644 index 0000000..12e5f5f --- /dev/null +++ b/frontend/src/pages/Guardian/GuardianApprovalPage.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; +import { + Container, + Typography, + Box, + Card, + CardContent, + Button, + Alert, +} from '@mui/material'; +import { + CheckCircle as ApproveIcon, + Cancel as RejectIcon, +} from '@mui/icons-material'; +import { useToast } from '../../context/ToastContext'; +import { fetchWithAuth } from '../../services/authService'; +import {ApiError} from "../../types/types"; + +interface WardData { + id: string; + name: string; + email: string; + date_of_birth?: string; + age?: number; +} + +export const GuardianApprovalPage = () => { + const { wardId } = useParams<{ wardId: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + const { showSuccess, showError } = useToast(); + + const ward = location.state?.ward as WardData | undefined; + + const [processing, setProcessing] = useState(false); + + if (!ward) { + return ( + + + Brak danych użytkownika. Otwórz tę stronę z powiadomienia. + + + + ); + } + + const handleApprove = async () => { + setProcessing(true); + try { + const response = await fetchWithAuth(`/guardian/approve/${wardId}`, { + method: 'POST', + }); + + showSuccess('Opieka została potwierdzona!'); + navigate('/dashboard'); + } catch (err: any) { + showError(err?.errors?.guardian[0] || err?.message || 'Błąd zatwierdzania opieki'); + } finally { + setProcessing(false); + } + }; + + const handleReject = async () => { + setProcessing(true); + try { + const response = await fetchWithAuth(`/guardian/reject/${wardId}`, { + method: 'POST', + }); + + showSuccess('Prośba o opiekę została odrzucona.'); + navigate('/dashboard'); + } catch (err: any) { + showError(err.message || 'Błąd odrzucania opieki'); + } finally { + setProcessing(false); + } + }; + + return ( + + + + + Prośba o opiekę + + + + {ward.name} ({ward.email}) prosi Cię o potwierdzenie opieki prawnej nad swoim kontem bankowym. + + + {ward.date_of_birth && ( + + + Data urodzenia: {ward.date_of_birth} + + {ward.age !== undefined && ( + + Wiek: {ward.age} lat + + )} + + )} + + + ⚠️ Potwierdzając, bierzesz odpowiedzialność za operacje tego użytkownika. + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/pages/Notification/NotificationsPage.tsx b/frontend/src/pages/Notification/NotificationsPage.tsx new file mode 100644 index 0000000..9a85565 --- /dev/null +++ b/frontend/src/pages/Notification/NotificationsPage.tsx @@ -0,0 +1,275 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Container, + Typography, + Box, + Card, + CardContent, + Button, + IconButton, + Chip, + Divider, + List, + ListItem, + ListItemButton, + ListItemText, + Badge, + Pagination, + Skeleton, + Alert, + Tooltip, +} from '@mui/material'; +import { + NotificationsActive as UnreadIcon, + DoneAll as MarkAllReadIcon, + ArrowBack as BackIcon, + Circle as CircleIcon, + DeleteOutlineRounded as DeleteIcon, +} from '@mui/icons-material'; +import { useAuth } from '../../context/AuthContext'; +import { useToast } from '../../context/ToastContext'; +import { getNotifications, markAsRead, markAllAsRead } from '../../services/notificationService'; +import { Notification } from '../../types/types'; +import { formatRelative } from '../../utils/formatDate'; + +export const NotificationsPage = () => { + const navigate = useNavigate(); + const { showSuccess, showError } = useToast(); + + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const perPage = 50; + + const fetchNotifications = useCallback(async () => { + setLoading(true); + try { + const response = await getNotifications(page, perPage); + setNotifications(response.data); + setTotalPages(response.meta.last_page || 1); + } catch (err: any) { + showError(err.message || 'Błąd pobierania powiadomień'); + } finally { + setLoading(false); + } + }, [page, perPage]); + + useEffect(() => { + fetchNotifications(); + }, [fetchNotifications]); + + const handleMarkAsRead = async (id: string, e?: React.MouseEvent) => { + e?.stopPropagation(); + try { + await markAsRead(id); + setNotifications(prev => prev.map(n => + n.id === id ? { ...n, read_at: new Date().toISOString() } : n + )); + showSuccess('Oznaczono jako przeczytane'); + } catch (err: any) { + showError(err.message); + } + }; + + const handleMarkAllAsRead = async () => { + try { + await markAllAsRead(); + setNotifications(prev => prev.map(n => ({ ...n, read_at: new Date().toISOString() }))); + showSuccess('Wszystkie oznaczone jako przeczytane'); + } catch (err: any) { + showError(err.message); + } + }; + + const handleNotificationClick = (notification: Notification) => { + if (!notification.read_at) { + handleMarkAsRead(notification.id); + } + + // Nawigacja w zależności od typu + switch (notification.data?.action) { + case 'approve_guardian': + if (notification.data.ward_id) { + navigate(`/guardian/approve/${notification.data.ward_id}`, { + state: { + ward: { + id: notification.data.ward_id, + name: notification.data.ward_name, + email: notification.data.ward_email, + date_of_birth: notification.data.ward_date_of_birth, + age: notification.data.ward_age, + }, + }, + }); + } + break; + case 'view_account': + navigate('/accounts'); + break; + default: + // Nic nie rób — tylko oznacz jako przeczytane + break; + } + }; + + const getTypeColor = (type: string) => { + switch (type) { + case 'guardian_request': return 'warning'; + case 'guardian_approved': return 'success'; + case 'guardian_rejected': return 'error'; + case 'guardian_request_sent': return 'info'; + default: return 'default'; + } + }; + + const getTypeLabel = (type: string) => { + switch (type) { + case 'guardian_request': return 'Prośba o opiekę'; + case 'guardian_approved': return 'Opiekun zatwierdzony'; + case 'guardian_rejected': return 'Opiekun odrzucony'; + case 'guardian_request_sent': return 'Prośba wysłana'; + default: return 'Powiadomienie'; + } + }; + + const unreadCount = notifications.filter(n => !n.read_at).length; + + return ( + + {/* Header */} + + navigate(-1)}> + + + + Powiadomienia + + {unreadCount > 0 && ( + + + + )} + + + {/* Unread badge */} + {unreadCount > 0 && ( + } + sx={{ mb: 2 }} + > + Masz {unreadCount} nieprzeczytanych powiadomień + + )} + + {/* Lista powiadomień */} + + {loading ? ( + + {[...Array(5)].map((_, i) => ( + + ))} + + ) : notifications.length === 0 ? ( + + + Brak powiadomień + + + ) : ( + + {notifications.map((notification, index) => ( + + + handleMarkAsRead(notification.id, e)} + size="small" + > + + + + ) + } + > + handleNotificationClick(notification)} + sx={{ + bgcolor: notification.read_at ? 'transparent' : 'action.hover', + borderLeft: 4, + borderColor: `${getTypeColor(notification.type)}.main`, + py: 2, + pr: 6, // miejsce na secondaryAction + }} + > + + + {notification.title} + + + {!notification.read_at && ( + + )} + + } + secondary={ + + + {notification.body} + + + {formatRelative(notification.created_at)} + + + } + /> + + + {index < notifications.length - 1 && } + + ))} + + )} + + + {/* Pagination */} + {totalPages > 1 && ( + + setPage(value)} + color="primary" + /> + + )} + + ); +}; \ No newline at end of file diff --git a/frontend/src/pages/User/BalancePage.tsx b/frontend/src/pages/User/BalancePage.tsx index 1116f93..44f3dd7 100644 --- a/frontend/src/pages/User/BalancePage.tsx +++ b/frontend/src/pages/User/BalancePage.tsx @@ -3,7 +3,6 @@ import { useAuth } from '../../context/AuthContext'; import { fetchWithAuth } from '../../services/authService'; import { Container, - Paper, Typography, Box, Grid, @@ -11,15 +10,60 @@ import { CardContent, Alert, CircularProgress, - Divider + Divider, + Chip, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Tooltip } from '@mui/material'; -import { AccountBalance, AccountBalanceWallet, Receipt, Settings } from '@mui/icons-material'; +import { + AccountBalanceWallet, + AccountBalance, + Receipt, + InfoOutlined, + Euro, + AttachMoney, + CurrencyExchange +} from '@mui/icons-material'; + +interface CurrencyBalance { + currency: string; + balance: string; +} + +interface BalanceResponse { + balances: CurrencyBalance[]; + total: string; +} + +const getCurrencyIcon = (currency: string) => { + switch (currency) { + case 'EURO': return ; + case 'USD': return ; + case 'PLN': return ; + default: return ; + } +}; + +const getCurrencyColor = (currency: string) => { + switch (currency) { + case 'PLN': return 'success'; + case 'EURO': return 'info'; + case 'USD': return 'warning'; + default: return 'default'; + } +}; export const BalancePage = () => { const { user } = useAuth(); - const [balance, setBalance] = useState(null); + const [balance, setBalance] = useState(); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); useEffect(() => { fetchBalance(); @@ -27,14 +71,14 @@ export const BalancePage = () => { const fetchBalance = async () => { try { - const response = await fetchWithAuth('/user/balance'); + const response = await fetchWithAuth('/accounts/balance'); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Błąd pobierania salda'); } - setBalance(data.balance); + setBalance(data); } catch (err: any) { setError(err.message || 'Nie udało się pobrać salda'); console.error(err); @@ -58,6 +102,7 @@ export const BalancePage = () => { return ( + {/* Nagłówek */} Witaj, {user?.name || user?.email}! @@ -68,9 +113,10 @@ export const BalancePage = () => { - {/* Saldo */} - - + {/* Lewa kolumna - Saldo i lista walut */} + + {/* Główne saldo PLN */} + @@ -97,20 +143,129 @@ export const BalancePage = () => { Twoje saldo wynosi: - {balance !== null ? `${balance} PLN` : '---'} + {balance?.total || '0.00'} PLN + + + + + Tylko waluta PLN + + + )} + + {/* Lista walut */} + + + + + + Salda na kontach + + + + + + {!balance?.balances || balance.balances.length === 0 ? ( + Brak dostępnych kont + ) : ( + + + + + Waluta + Saldo + Status + + + + {balance.balances.map((item, index) => ( + + + + + {getCurrencyIcon(item.currency)} + + + {item.currency} + + + + + + {item.balance} + + + + : undefined} + label={item.currency === 'PLN' ? 'Podsumowanie' : 'Wymaga przeliczenia'} + size="small" + color={getCurrencyColor(item.currency)} + variant={item.currency === 'PLN' ? 'filled' : 'outlined'} + sx={{ fontWeight: 'bold' }} + /> + + + ))} + +
+
+ )} +
+
- {/* Menu boczne */} - + {/* Prawa kolumna - Menu */} + - + - + Moje konto @@ -120,9 +275,16 @@ export const BalancePage = () => { - + - + Moje transakcje @@ -131,18 +293,6 @@ export const BalancePage = () => { - - - - - - Ustawienia - - Konfiguracja konta - - - - diff --git a/frontend/src/pages/User/ProfilePage.tsx b/frontend/src/pages/User/ProfilePage.tsx index 6932d26..10b6e6c 100644 --- a/frontend/src/pages/User/ProfilePage.tsx +++ b/frontend/src/pages/User/ProfilePage.tsx @@ -1,27 +1,35 @@ -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { useAuth } from '../../context/AuthContext'; import { Container, Typography, Box, - Grid, Alert, - Divider, TextField, Button + Divider, + TextField, + Button, + Card, + InputLabel, + Stack, + Paper, } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; -import {LoadingSpinner} from "../../components/Assets/Svg/LoadingSpinner"; -import {useToast} from "../../context/ToastContext"; -import {useLoading} from "../../context/LoadingContext"; -import {formatDateTime} from "../../utils/formatDate"; -import {updateUserProfile} from "../../services/authService"; -import {ProfileUpdateData} from "../../types/types"; +import { LoadingSpinner } from "../../components/Assets/Svg/LoadingSpinner"; +import { useToast } from "../../context/ToastContext"; +import { useLoading } from "../../context/LoadingContext"; +import { calculateAge, formatDateInput, formatDateTime, isAdult, today } from "../../utils/formatDate"; +import { updateUserProfile } from "../../services/authService"; +import { ProfileUpdateData } from "../../types/types"; +import { DateField } from "../../components/Field/DateField"; export const ProfilePage = () => { - const { user } = useAuth(); + const { user, loadUser } = useAuth(); const [error, setError] = useState(''); const [email, setEmail] = useState(''); const [name, setName] = useState(''); const [password, setPassword] = useState(''); + const [dateOfBirth, setDateOfBirth] = useState(''); + const [guardianEmail, setGuardianEmail] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [oldPassword, setOldPassword] = useState(''); const { showError, showSuccess } = useToast(); @@ -31,11 +39,65 @@ export const ProfilePage = () => { if (user) { setName(user.name || ''); setEmail(user.email || ''); + setDateOfBirth(user.date_of_birth || ''); } }, [user]); + const handleRemoveGuardian = async () => { + setError(''); + startLoading(); + + const payload: ProfileUpdateData = { + name: user?.name || '', + email: user?.email || '', + guardian_delete: true, + }; + + try { + await updateUserProfile(payload); + showSuccess('Opiekun został odłączony.'); + loadUser(); + } catch (err: any) { + const errorMessage = err.message || 'Błąd odłączania opiekuna'; + setError(errorMessage); + showError(errorMessage); + } finally { + stopLoading(); + } + }; + + const handleSetGuardian = async () => { + setError(''); + startLoading(); + + if (user?.guardian?.id) { + showError('Twoje konto zostało przypisane do opiekuna, nie możesz jeszcze raz wysyłać informacji do opiekuna.'); + return; + } + + const payload: ProfileUpdateData = { + guardian_email: guardianEmail || undefined, + }; + + try { + await updateUserProfile(payload); + + setGuardianEmail(''); + + showSuccess('Powiadomienie zostało wysłane do opiekuna w celu potwierdzenia.'); + loadUser(); + } catch (err: any) { + const errorMessage = err.errors.guardian_email[0] || err.message || 'Błąd odłączania opiekuna'; + setError(errorMessage); + showError(errorMessage); + } finally { + stopLoading(); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setError(''); startLoading(); @@ -45,6 +107,7 @@ export const ProfilePage = () => { password: password || undefined, password_confirmation: confirmPassword || undefined, old_password: oldPassword || undefined, + date_of_birth: !user?.date_of_birth && dateOfBirth ? dateOfBirth : undefined, }; try { @@ -55,6 +118,7 @@ export const ProfilePage = () => { setOldPassword(''); showSuccess('Profil zaktualizowany pomyślnie!'); + loadUser(); } catch (err: any) { const errorMessage = err.message || 'Błąd aktualizacji profilu'; setError(errorMessage); @@ -68,135 +132,271 @@ export const ProfilePage = () => { return ( (name !== user?.name) || (email !== user?.email) || - password.length > 0 + (dateOfBirth !== user?.date_of_birth) || + (guardianEmail !== user?.guardian?.email) || + password.length > 0 || + confirmPassword.length > 0 || + oldPassword.length > 0 ); }; + const age = calculateAge(dateOfBirth); + const adult = isAdult(dateOfBirth); + const hasGuardian = Boolean(user?.guardian?.id); + return ( - - - + + + + Profil użytkownika + + Zmiana danych osobowych i ustawień konta. - - + + {!user?.email_verified_at && ( - + Twój adres email nie został zweryfikowany. Sprawdź skrzynkę odbiorczą. )} + {!user?.date_of_birth && ( + + Nie wpisano daty urodzenia. Bez tego nie można kontynuować działania w systemie. + Jeżeli nie masz 18 lat, poproś opiekuna o założenie konta i podpięcie Cię do swojego konta. + + )} + {error && ( - + {error} )} - setName(e.target.value)} - required - disabled={isLoading} - variant="outlined" - /> - - setEmail(e.target.value)} - required - disabled={isLoading} - variant="outlined" - /> - - - - - - - - Zmiana hasła (wypełnij tylko jeśli chcesz zmienić) - - - setOldPassword(e.target.value)} - disabled={isLoading} - variant="outlined" - /> - - setPassword(e.target.value)} - disabled={isLoading} - variant="outlined" - /> - - setConfirmPassword(e.target.value)} - disabled={isLoading || !password} - variant="outlined" - error={confirmPassword.length > 0 && password !== confirmPassword} - helperText={confirmPassword.length > 0 && password !== confirmPassword ? 'Hasła nie są zgodne' : ''} - /> - - - + + + Dane osobowe + + + setName(e.target.value)} + required + disabled={isLoading} + variant="outlined" + /> + + setEmail(e.target.value)} + required + disabled={isLoading} + variant="outlined" + /> + + setDateOfBirth(value)} + formatDate={formatDateInput} + label="Data urodzenia" + maxDate={today()} + required + disabled={!!user?.date_of_birth} + /> + + {age !== null && !adult && ( + + {adult + ? '' + : `Wiek: ${age} lat (niepełnoletni)` + } + + )} + + {dateOfBirth && ( + + {adult && hasGuardian && ( + <> + + Opiekun prawny + - + + + + {user?.guardian?.name} + + + {user?.guardian?.email} + + + Zatwierdzono: {formatDateTime(user?.guardian?.guardian_approved_at)} + + + + + + )} + + {!adult && ( + <> + + Opiekun prawny + + + {!hasGuardian && ( + + Masz mniej niż 18 lat. Aby korzystać z konta, musisz mieć przypisanego opiekuna prawnego. + + )} + + {hasGuardian ? ( + + + + {user?.guardian?.name} + + + {user?.guardian?.email} + + + Zatwierdzono: {formatDateTime(user?.guardian?.guardian_approved_at)} + + + {!user?.guardian?.guardian_approved_at && ( + + Oczekuje na zatwierdzenie przez opiekuna. + + )} + + ) : ( + <> + + Wpisz email opiekuna, który otrzyma zaproszenie: + + setGuardianEmail(e.target.value)} + helperText="Opiekun musi mieć ukończone 18 lat i posiadać konto w banku." + /> + + + + )} + + )} + + )} + + + + + Informacje o koncie + + + + + Data ostatniej zmiany + + + {user?.updated_at ? formatDateTime(user.updated_at) : 'Brak danych'} + + + + + + Data potwierdzenia e-mail + + + {user?.email_verified_at ? formatDateTime(user.email_verified_at) : 'Brak danych'} + + + + + + + Zmiana hasła + + + setOldPassword(e.target.value)} + disabled={isLoading} + variant="outlined" + /> + + setPassword(e.target.value)} + disabled={isLoading} + variant="outlined" + /> + + setConfirmPassword(e.target.value)} + disabled={isLoading || !password} + variant="outlined" + error={confirmPassword.length > 0 && password !== confirmPassword} + helperText={confirmPassword.length > 0 && password !== confirmPassword ? 'Hasła nie są zgodne' : ''} + /> + + + + + ); }; \ No newline at end of file diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index ac6a3b7..3084ed7 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -192,6 +192,11 @@ export const fetchWithAuth = async (url: string, options: FetchOptions = {}): Pr } } + if (!response.ok) { + const error: ApiError = await response.json(); + throw error; + } + return response; }; diff --git a/frontend/src/services/guardianService.ts b/frontend/src/services/guardianService.ts new file mode 100644 index 0000000..20fca05 --- /dev/null +++ b/frontend/src/services/guardianService.ts @@ -0,0 +1,24 @@ +import { fetchWithAuth } from './authService'; +import { ApiError } from '../types/types'; + +export const approveGuardian = async (wardId: string): Promise => { + const response = await fetchWithAuth(`/guardian/approve/${wardId}`, { + method: 'POST', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd zatwierdzania opieki.'); + } +}; + +export const rejectGuardian = async (wardId: string): Promise => { + const response = await fetchWithAuth(`/guardian/reject/${wardId}`, { + method: 'POST', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd odrzucania opieki.'); + } +}; \ No newline at end of file diff --git a/frontend/src/services/notificationService.tsx b/frontend/src/services/notificationService.tsx new file mode 100644 index 0000000..66e6a11 --- /dev/null +++ b/frontend/src/services/notificationService.tsx @@ -0,0 +1,54 @@ +import { fetchWithAuth } from "./authService"; +import {ApiError, Notification, NotificationCount, PaginationResponse} from "../types/types"; + +export const getNotifications = async ( + page: number = 1, + perPage: number = 10 +): Promise> => { + const response = await fetchWithAuth(`/notifications?page=${page}&per_page=${perPage}`, { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania powiadomień.'); + } + + return await response.json(); +}; + +export const getUnreadCount = async (): Promise => { + const response = await fetchWithAuth('/notifications/unread-count', { + method: 'GET', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd pobierania liczby nieprzeczytanych powiadomień.'); + } + + const data: NotificationCount = await response.json(); + return data.count; +}; + +export const markAsRead = async (id: string): Promise => { + const response = await fetchWithAuth(`/notifications/${id}/read`, { + method: 'PATCH', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd oznaczania powiadomienia jako przeczytane.'); + } +}; + +export const markAllAsRead = async (): Promise => { + const response = await fetchWithAuth('/notifications/read-all', { + method: 'PATCH', + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'Błąd oznaczania wszystkich powiadomień jako przeczytane.'); + } +}; \ No newline at end of file diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 8e54015..0b9a2a7 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -2,10 +2,19 @@ export interface User { id?: number; name?: string; - email: string; + email?: string; email_verified_at?: string; + date_of_birth?: string; created_at?: string; updated_at?: string; + guardian?: Guardian; +} + +export interface Guardian { + id?: string; + name?: string; + email?: string; + guardian_approved_at?: string; } // Pagination @@ -86,6 +95,9 @@ export interface ProfileUpdateData { password?: string; password_confirmation?: string; old_password?: string; + date_of_birth?: string; + guardian_email?: string; + guardian_delete?: boolean; } export interface ProfileUpdateResponse { @@ -141,6 +153,8 @@ export interface ToastData { export interface AuthContextType { loading?: boolean; user: User | null; + setUser: (user: User | null) => void; + loadUser: () => void; login: (email: string, password: string) => Promise; register: (name: string, email: string, password: string) => Promise; logout: () => void; @@ -200,3 +214,29 @@ export interface Transaction { status: string; created_at: string; } + +// Notification +export interface Notification { + id: string; + type: string; + title: string; + body: string; + data: { + ward_id?: string; + ward_name?: string; + guardian_id?: string; + guardian_name?: string; + action?: string; + action_url?: string; + status?: string; + ward_date_of_birth?: string; + ward_age?: number; + ward_email?: string; + }; + read_at: string | null; + created_at: string; +} + +export interface NotificationCount { + count: number; +} \ No newline at end of file diff --git a/frontend/src/utils/formatDate.ts b/frontend/src/utils/formatDate.ts index 1f1f02a..5baccc3 100644 --- a/frontend/src/utils/formatDate.ts +++ b/frontend/src/utils/formatDate.ts @@ -1,73 +1,37 @@ export const formatDate = (date: Date | string | undefined): string => { if (!date) return 'Brak danych'; const d = new Date(date); - - return d.toLocaleDateString('pl-PL', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); + return d.toLocaleDateString('pl-PL', { year: 'numeric', month: 'long', day: 'numeric' }); }; export const formatDateShort = (date: Date | string | undefined): string => { if (!date) return 'Brak danych'; const d = new Date(date); - - return d.toLocaleDateString('pl-PL', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }); + return d.toLocaleDateString('pl-PL', { year: 'numeric', month: '2-digit', day: '2-digit' }); }; export const formatDateTime = (date: Date | string | undefined): string => { if (!date) return 'Brak danych'; const d = new Date(date); - - return d.toLocaleString('pl-PL', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); + return d.toLocaleString('pl-PL', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; export const formatDateTimeShort = (date: Date | string | undefined): string => { if (!date) return 'Brak danych'; const d = new Date(date); - - return d.toLocaleString('pl-PL', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - }); + return d.toLocaleString('pl-PL', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; export const formatDateTimeWithSeconds = (date: Date | string | undefined): string => { if (!date) return 'Brak danych'; const d = new Date(date); - - return d.toLocaleString('pl-PL', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); + return d.toLocaleString('pl-PL', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); }; export const formatTime = (date: Date | string | undefined): string => { if (!date) return '--:--'; const d = new Date(date); - - return d.toLocaleTimeString('pl-PL', { - hour: '2-digit', - minute: '2-digit', - }); + return d.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); }; export const formatRelative = (date: Date | string | undefined): string => { @@ -85,4 +49,45 @@ export const formatRelative = (date: Date | string | undefined): string => { if (hours < 24) return `${hours} godz. temu`; if (days < 7) return `${days} dni temu`; return formatDate(d); +}; + +export const formatDateInput = (date: Date | string | null | undefined): string => { + if (!date) return ''; + const d = typeof date === 'string' ? new Date(date) : date; + if (isNaN(d.getTime())) return ''; + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + +export const today = (): string => new Date().toISOString().split('T')[0]; + +export const yearsAgo = (years: number): string => { + const d = new Date(); + d.setFullYear(d.getFullYear() - years); + return d.toISOString().split('T')[0]; +}; + +export const yearsFromNow = (years: number): string => { + const d = new Date(); + d.setFullYear(d.getFullYear() + years); + return d.toISOString().split('T')[0]; +}; + +export const calculateAge = (dateOfBirth: Date | string | undefined): number | null => { + if (!dateOfBirth) return null; + const birth = new Date(dateOfBirth); + const now = new Date(); + let age = now.getFullYear() - birth.getFullYear(); + const monthDiff = now.getMonth() - birth.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birth.getDate())) { + age--; + } + return age; +}; + +export const isAdult = (dateOfBirth: Date | string | undefined): boolean => { + const age = calculateAge(dateOfBirth); + return age !== null && age >= 18; }; \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c9c895f..5a8dbb1 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -13,7 +13,12 @@ export default defineConfig({ }, '/oauth': { target: 'http://backend:80', - changeOrigin: true, + changeOrigin: false, + secure: false, + }, + '/login': { + target: 'http://backend:80', + changeOrigin: false, secure: false, } } From 951b994fc6d665a4712697be10dda10b3cfe5dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 13:45:39 +0200 Subject: [PATCH 12/17] . --- backend/app/Exceptions/Handler.php | 4 +-- backend/app/Http/Concerns/WithPagination.php | 5 ---- .../Controllers/Account/AccountController.php | 8 +++--- .../Notification/NotificationController.php | 3 +-- .../Controllers/User/ProfileController.php | 6 ++--- .../Middleware/Adult/AdultCheckMiddleware.php | 4 +-- .../Requests/User/RegisterUserRequest.php | 19 +++++++------- .../Requests/User/UpdateProfileRequest.php | 24 ++++++++--------- .../Resources/Account/BalanceResource.php | 4 +-- .../Notification/NotificationResource.php | 26 +++++++++---------- backend/app/Models/Account/Account.php | 1 - backend/app/Models/User.php | 3 +-- backend/app/Providers/AppServiceProvider.php | 5 +--- .../factories/Account/AccountFactory.php | 4 +-- .../database/seeders/PassportClientSeeder.php | 6 ++--- backend/routes/auth.php | 8 +++--- 16 files changed, 57 insertions(+), 73 deletions(-) diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php index a6763eb..2e94d1c 100644 --- a/backend/app/Exceptions/Handler.php +++ b/backend/app/Exceptions/Handler.php @@ -68,8 +68,8 @@ private function handleApiException(\Throwable $e): JsonResponse } if ($e instanceof ValidationException) { - $errors = $e->errors(); - $count = collect($errors)->flatten()->count(); + $errors = $e->errors(); + $count = collect($errors)->flatten()->count(); $firstMessage = collect($errors)->flatten()->first(); $message = $count > 1 diff --git a/backend/app/Http/Concerns/WithPagination.php b/backend/app/Http/Concerns/WithPagination.php index 3ef9b28..aafa968 100644 --- a/backend/app/Http/Concerns/WithPagination.php +++ b/backend/app/Http/Concerns/WithPagination.php @@ -22,10 +22,6 @@ protected function currentPage(): int return request()->integer('page', 1); } - /** - * @param EloquentBuilder|QueryBuilder|Relation $query - * @return LengthAwarePaginator - */ protected function paginate(EloquentBuilder|QueryBuilder|Relation $query): LengthAwarePaginator { return $query->paginate( @@ -35,7 +31,6 @@ protected function paginate(EloquentBuilder|QueryBuilder|Relation $query): Lengt } /** - * @param EloquentBuilder|QueryBuilder|Relation $query * @return LengthAwarePaginator */ protected function simplePaginate(EloquentBuilder|QueryBuilder|Relation $query): Paginator diff --git a/backend/app/Http/Controllers/Account/AccountController.php b/backend/app/Http/Controllers/Account/AccountController.php index 0b18d3f..5b19a60 100644 --- a/backend/app/Http/Controllers/Account/AccountController.php +++ b/backend/app/Http/Controllers/Account/AccountController.php @@ -13,7 +13,6 @@ use Dedoc\Scramble\Attributes\BodyParameter; use Dedoc\Scramble\Attributes\PathParameter; use Dedoc\Scramble\Attributes\QueryParameter; -use Dedoc\Scramble\Attributes\Response; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Laravel\Passport\Attributes\AuthorizeToken; @@ -34,7 +33,6 @@ public function __construct( /** * Wyświetla listę kont bankowych użytkownika. - * */ #[AuthorizeToken(['accounts-view'], anyScope: true)] #[QueryParameter('per_page', description: 'Ilość elementów na stronę.', type: 'int', default: 20, example: 30)] @@ -52,7 +50,7 @@ public function index(): AnonymousResourceCollection } /** - * Saldo + * Saldo. * * Wyświetlanie salda według kont w którym jest waluta. */ @@ -67,8 +65,8 @@ public function balance(): JsonResponse return response()->json([ 'balances' => BalanceResource::collection($balances), - /** @example 2500.00 */ - 'total' => number_format($balances->where('currency', 'PLN')->sum('total_balance') / 100, 2, '.', ''), + /* @example 2500.00 */ + 'total' => number_format($balances->where('currency', 'PLN')->sum('total_balance') / 100, 2, '.', ''), ]); } diff --git a/backend/app/Http/Controllers/Notification/NotificationController.php b/backend/app/Http/Controllers/Notification/NotificationController.php index d1e1d61..990c0cf 100644 --- a/backend/app/Http/Controllers/Notification/NotificationController.php +++ b/backend/app/Http/Controllers/Notification/NotificationController.php @@ -6,7 +6,6 @@ use App\Http\Controllers\Controller; use App\Http\Resources\Notification\NotificationResource; use Dedoc\Scramble\Attributes\PathParameter; -use Dedoc\Scramble\Attributes\QueryParameter; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; @@ -53,7 +52,7 @@ public function unreadCount(Request $request): JsonResponse /** * Odczytwanie powiadomienia. */ - #[PathParameter('notificationId', description: 'Identyfikator powiadomienia', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + #[PathParameter('notificationId', description: 'Identyfikator powiadomienia', type: 'int', example: '123e4567-e89b-12d3-a456-426614174000')] public function markAsRead(string $notificationId, Request $request): JsonResponse { $notification = $request->user() diff --git a/backend/app/Http/Controllers/User/ProfileController.php b/backend/app/Http/Controllers/User/ProfileController.php index 08bfa17..cec1985 100644 --- a/backend/app/Http/Controllers/User/ProfileController.php +++ b/backend/app/Http/Controllers/User/ProfileController.php @@ -9,8 +9,6 @@ use App\Services\Guardian\GuardianService; use Dedoc\Scramble\Attributes\BodyParameter; use Dedoc\Scramble\Attributes\PathParameter; -use Dedoc\Scramble\Attributes\Response; -use Illuminate\Auth\Events\Registered; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -134,7 +132,7 @@ public function create(RegisterUserRequest $request): User * * Zatwierdzenie opiekuństwa nad kontem dziecka. */ - #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: '123e4567-e89b-12d3-a456-426614174000')] public function approveGuardian(string $wardId): JsonResponse { $guardian = auth()->user(); @@ -174,7 +172,7 @@ public function approveGuardian(string $wardId): JsonResponse * * Odrzucenie opiekuństwa nad kontem dziecka. */ - #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: "123e4567-e89b-12d3-a456-426614174000")] + #[PathParameter('wardId', description: 'Identyfikator użytkownika podopiecznego.', type: 'int', example: '123e4567-e89b-12d3-a456-426614174000')] public function rejectGuardian(string $wardId): JsonResponse { $guardian = auth()->user(); diff --git a/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php b/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php index 3d4fe64..6ff4799 100644 --- a/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php +++ b/backend/app/Http/Middleware/Adult/AdultCheckMiddleware.php @@ -19,9 +19,9 @@ public function handle(Request $request, \Closure $next): Response $isAdult = $user['is_adult'] ?? false; $hasGuardian = isset($user['guardian']['id']) && $user['guardian']['id'] && $user['guardian']['guardian_approved_at']; - $hasDateOfBirth = isset($user['date_of_birth']) && $user['date_of_birth']; + $hasDateOfBirth = isset($user['date_of_birth']) && $user['date_of_birth']; - if (!$hasDateOfBirth) { + if (! $hasDateOfBirth) { throw new AccessDeniedHttpException('Access denied. You must set date of birth.'); } diff --git a/backend/app/Http/Requests/User/RegisterUserRequest.php b/backend/app/Http/Requests/User/RegisterUserRequest.php index 1a7def0..85633e1 100644 --- a/backend/app/Http/Requests/User/RegisterUserRequest.php +++ b/backend/app/Http/Requests/User/RegisterUserRequest.php @@ -2,7 +2,6 @@ namespace App\Http\Requests\User; -use Dedoc\Scramble\Attributes\BodyParameter; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Password; @@ -25,14 +24,14 @@ public function authorize(): bool public function rules(): array { return [ - /** @example Jan Kowalski */ - 'name' => ['required', 'string', 'max:255'], - /** @example jan.kowalski@example.com */ - 'email' => ['required', 'email', 'max:255', 'unique:users,email'], - /** @example "2000-01-01" */ + /* @example Jan Kowalski */ + 'name' => ['required', 'string', 'max:255'], + /* @example jan.kowalski@example.com */ + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + /* @example "2000-01-01" */ 'date_of_birth' => ['sometimes', 'date', 'date_format:Y-m-d', 'before_or_equal:today'], - /** @example "P@ssw0rd123!@" */ - 'password' => [ + /* @example "P@ssw0rd123!@" */ + 'password' => [ 'required', 'string', 'confirmed', @@ -43,7 +42,7 @@ public function rules(): array ->numbers() ->symbols(), ], - /** @example "P@ssw0rd123!@" */ + /* @example "P@ssw0rd123!@" */ 'password_confirmation' => ['sometimes', 'string', 'same:password'], ]; } @@ -53,7 +52,7 @@ public function bodyParameters(): array return [ 'password_confirmation' => [ 'description' => 'Must match the password field.', - 'example' => 'P@ssw0rd123!@', + 'example' => 'P@ssw0rd123!@', ], ]; } diff --git a/backend/app/Http/Requests/User/UpdateProfileRequest.php b/backend/app/Http/Requests/User/UpdateProfileRequest.php index 86cdfa2..0b90c62 100644 --- a/backend/app/Http/Requests/User/UpdateProfileRequest.php +++ b/backend/app/Http/Requests/User/UpdateProfileRequest.php @@ -29,13 +29,13 @@ public function rules(): array $user = $this->user(); return [ - /** @example Jan Kowalski */ - 'name' => ['sometimes', 'string', 'max:255'], - /** @example jan.kowalski@example.com */ - 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $user->id], - /** @example "2000-01-01" */ - 'date_of_birth' => ['sometimes', 'date', 'date_format:Y-m-d', 'before_or_equal:today'], - /** @example guardian@example.com */ + /* @example Jan Kowalski */ + 'name' => ['sometimes', 'string', 'max:255'], + /* @example jan.kowalski@example.com */ + 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $user->id], + /* @example "2000-01-01" */ + 'date_of_birth' => ['sometimes', 'date', 'date_format:Y-m-d', 'before_or_equal:today'], + /* @example guardian@example.com */ 'guardian_email' => [ 'nullable', 'email', @@ -44,7 +44,7 @@ public function rules(): array Rule::notIn([$user->email]), new AdultGuardian(), ], - /** @example "P@ssw0rd123!@" */ + /* @example "P@ssw0rd123!@" */ 'password' => [ 'sometimes', 'string', @@ -56,11 +56,11 @@ public function rules(): array ->numbers() ->symbols(), ], - /** @example "P@ssw0rd123!@" */ + /* @example "P@ssw0rd123!@" */ 'password_confirmation' => ['sometimes', 'string', 'same:password'], - /** @example "O&l*dP@ssw0rd123!@" */ - 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], - /** @example false */ + /* @example "O&l*dP@ssw0rd123!@" */ + 'old_password' => ['required_with:password', 'string', 'max:255', new CurrentPassword()], + /* @example false */ 'guardian_delete' => ['sometimes', 'boolean'], ]; } diff --git a/backend/app/Http/Resources/Account/BalanceResource.php b/backend/app/Http/Resources/Account/BalanceResource.php index a28336a..ca37180 100644 --- a/backend/app/Http/Resources/Account/BalanceResource.php +++ b/backend/app/Http/Resources/Account/BalanceResource.php @@ -15,9 +15,9 @@ class BalanceResource extends JsonResource public function toArray(Request $request): array { return [ - /** @example PLN */ + /* @example PLN */ 'currency' => $this->currency, - /** @example 2500.00 */ + /* @example 2500.00 */ 'balance' => number_format($this->total_balance / 100, 2, '.', ''), ]; } diff --git a/backend/app/Http/Resources/Notification/NotificationResource.php b/backend/app/Http/Resources/Notification/NotificationResource.php index e3300ae..6c6aeb6 100644 --- a/backend/app/Http/Resources/Notification/NotificationResource.php +++ b/backend/app/Http/Resources/Notification/NotificationResource.php @@ -10,19 +10,19 @@ class NotificationResource extends JsonResource public function toArray(Request $request): array { return [ - /** @example "550e8400-e29b-41d4-a716-446655440000" */ - 'id' => $this->id, - /** @example "approved" */ - 'type' => $this->data['type'] ?? $this->type, - /** @example "Powiadomienie" */ - 'title' => $this->data['title'] ?? 'Powiadomienie', - /** @example "Treść powiadomienia" */ - 'body' => $this->data['body'] ?? '', - /** @example {} */ - 'data' => $this->data, - /** @example "2026-05-13T10:30:00.000000Z" */ - 'read_at' => $this->read_at?->toISOString(), - /** @example "2026-05-13T10:30:00.000000Z" */ + /* @example "550e8400-e29b-41d4-a716-446655440000" */ + 'id' => $this->id, + /* @example "approved" */ + 'type' => $this->data['type'] ?? $this->type, + /* @example "Powiadomienie" */ + 'title' => $this->data['title'] ?? 'Powiadomienie', + /* @example "Treść powiadomienia" */ + 'body' => $this->data['body'] ?? '', + /* @example {} */ + 'data' => $this->data, + /* @example "2026-05-13T10:30:00.000000Z" */ + 'read_at' => $this->read_at?->toISOString(), + /* @example "2026-05-13T10:30:00.000000Z" */ 'created_at' => $this->created_at->toISOString(), ]; } diff --git a/backend/app/Models/Account/Account.php b/backend/app/Models/Account/Account.php index fc53192..9b4a350 100644 --- a/backend/app/Models/Account/Account.php +++ b/backend/app/Models/Account/Account.php @@ -6,7 +6,6 @@ use App\Models\Account\Transaction\Transaction; use App\Models\User; use Illuminate\Database\Eloquent\Attributes\Fillable; -use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 7a40694..0e3f399 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -6,7 +6,6 @@ use App\Models\Account\Card\Card; use App\Models\Account\Flik\FlikCode; use App\Models\Account\Transaction\Transaction; -use Carbon\Carbon; use Database\Factories\UserFactory; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Attributes\Fillable; @@ -109,7 +108,7 @@ public function receivedTransactions() /** * @property array{id: string, name: string, email: string, guardian_approved_at: string|null}|null $guardian - * @property bool $is_adult + * @property bool $is_adult */ public function toArray(): array { diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index c214b90..c05bf78 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -2,20 +2,17 @@ namespace App\Providers; -use App\Http\Responses\AuthorizationViewResponse; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; use Illuminate\Support\ServiceProvider; -use Laravel\Passport\Contracts\AuthorizationViewResponse as AuthorizationViewResponseContract; use Laravel\Passport\Passport; class AppServiceProvider extends ServiceProvider { public function register(): void { - // } public function boot(): void @@ -34,7 +31,7 @@ public function boot(): void SecurityScheme::oauth2() ->flow('password', function (OAuthFlow $flow) { $flow->tokenUrl(config('app.url') . '/oauth/token'); - }) + }), ); }); diff --git a/backend/database/factories/Account/AccountFactory.php b/backend/database/factories/Account/AccountFactory.php index 5fc3ab7..1a6cfa2 100644 --- a/backend/database/factories/Account/AccountFactory.php +++ b/backend/database/factories/Account/AccountFactory.php @@ -17,8 +17,8 @@ public function definition(): array $currency = $this->faker->randomElement(['PLN', 'USD', 'EURO']); return [ - 'user_id' => User::factory(), - 'name' => match($currency) { + 'user_id' => User::factory(), + 'name' => match ($currency) { 'PLN' => $this->faker->randomElement(['Konto osobiste', 'Konto oszczędnościowe', 'Konto premium', 'SuperKonto']), 'USD' => 'Konto walutowe USD', 'EURO' => 'Konto walutowe EUR', diff --git a/backend/database/seeders/PassportClientSeeder.php b/backend/database/seeders/PassportClientSeeder.php index 03ffd27..d7a6023 100644 --- a/backend/database/seeders/PassportClientSeeder.php +++ b/backend/database/seeders/PassportClientSeeder.php @@ -13,13 +13,13 @@ public function run(): void $clientId = config('passport.client_id', 'bank-client-id'); $clientSecret = config('passport.client_secret', 'bank-client-secret'); $nameApp = config('app.name', 'Bank Online System'); - $redirectUris = [ + $redirectUris = [ config('passport.redirect_uri', ''), 'http://localhost:8000/auth/callback', 'http://localhost:5174/auth/callback', - 'http://127.0.0.1:8000/auth/callback' + 'http://127.0.0.1:8000/auth/callback', ]; - $grantTypes = ['authorization_code', 'refresh_token', 'password']; + $grantTypes = ['authorization_code', 'refresh_token', 'password']; $exists = DB::table('oauth_clients') ->where('id', $clientId) diff --git a/backend/routes/auth.php b/backend/routes/auth.php index 04b544c..efe5ab7 100644 --- a/backend/routes/auth.php +++ b/backend/routes/auth.php @@ -11,8 +11,8 @@ use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { - Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); @@ -34,7 +34,7 @@ ->name('password.store'); }); - Route::middleware('auth')->group(function () { +Route::middleware('auth')->group(function () { Route::get('verify-email', EmailVerificationPromptController::class) ->name('verification.notice'); @@ -55,4 +55,4 @@ Route::get('logout', [AuthenticatedSessionController::class, 'destroy']) ->name('logout'); - }); +}); From ca5d1a8599342c679fceffb339165898b6816be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 20:51:30 +0200 Subject: [PATCH 13/17] . --- .github/workflows/backend.yml | 93 +++++++++++++++++++ backend/database/factories/UserFactory.php | 12 ++- backend/tests/Feature/Account/AccountTest.php | 34 ++++--- .../tests/Feature/Account/Card/CardTest.php | 26 +++--- .../tests/Feature/Auth/AuthenticationTest.php | 2 +- backend/tests/Feature/ExampleTest.php | 3 +- 6 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/backend.yml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 0000000..40e7abc --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,93 @@ +# .github/workflows/ci.yml +# Backend CI pipeline for PHP 8.4 with Composer caching +# Runs on PRs to develop and manual dispatch for main branch AWS deployments + +name: Backend CI + +on: + # Trigger on pull requests targeting develop branch + pull_request: + branches: + - develop + paths: + - 'backend/**' + - '.github/workflows/ci.yml' + + # Manual trigger for main branch (AWS deployment gate) + # All tests must pass before pushing to AWS + workflow_dispatch: + branches: + - main + +jobs: + backend-tests: + name: Backend Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./backend + + steps: + # Checkout the repository code + - name: Checkout code + uses: actions/checkout@v4 + + # Setup PHP 8.4 with required extensions + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, pdo, pdo_mysql, xml, ctype, json, tokenizer + coverage: xdebug + + # Cache Composer dependencies for faster builds + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: | + ~/.composer/cache/files + ~/.composer/cache/repo + key: ${{ runner.os }}-composer-${{ hashFiles('backend/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + # Install Composer dependencies + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + # Run PHP syntax check + - name: Check PHP syntax + run: find . -name '*.php' -not -path './vendor/*' -exec php -l {} \; + + # Run PHP CS Fixer in dry-run mode to check code style + - name: Check code style + run: | + if [ -f vendor/bin/php-cs-fixer ]; then + vendor/bin/php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.dist.php || true + fi + + # Run PHPUnit tests + # All tests must pass - failure blocks merge / deployment + - name: Run PHPUnit tests + run: | + if [ -f vendor/bin/phpunit ]; then + vendor/bin/phpunit --testdox --colors=never + else + echo "phpunit not found - skipping tests" + exit 1 + fi + + # Run static analysis if PHPStan is configured + - name: Run PHPStan analysis + run: | + if [ -f vendor/bin/phpstan ]; then + vendor/bin/phpstan analyse --no-progress --memory-limit=512M + fi + continue-on-error: true + + # Job-level status check + # Explicitly fail the job if any previous step failed + # This ensures merge / deployment is blocked on test failures + outputs: + tests-passed: ${{ job.status == 'success' }} \ No newline at end of file diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index 7d697f9..cfde827 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Models\Account\Account; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; @@ -32,7 +33,7 @@ public function definition(): array 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), 'date_of_birth' => fake()->dateTimeBetween( - now()->subYears(25), + now()->subYears(30), now()->subYears(13), )->format('Y-m-d'), ]; @@ -47,4 +48,13 @@ public function unverified(): static 'email_verified_at' => null, ]); } + + public function withAccount(int $count = 1): static + { + return $this->afterCreating(function (User $account) use ($count) { + Account::factory()->count($count)->create([ + 'user_id' => $account->id, + ]); + }); + } } diff --git a/backend/tests/Feature/Account/AccountTest.php b/backend/tests/Feature/Account/AccountTest.php index 7d8b9aa..aacd25a 100644 --- a/backend/tests/Feature/Account/AccountTest.php +++ b/backend/tests/Feature/Account/AccountTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Account; use App\Models\Account\Account; +use App\Models\Account\Card\Card; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Passport\Passport; @@ -17,18 +18,24 @@ class AccountTest extends TestCase public function setUp(): void { parent::setUp(); - $this->user = User::factory()->create(); + $this->user = User::factory()->create([ + 'date_of_birth' => now()->subYears(30), + ]); Account::query()->delete(); + Card::query()->delete(); } public function testListsAllUserAccounts(): void { Account::factory()->count(3)->create(['user_id' => $this->user->id]); - $otherUser = User::factory()->create(); - Account::factory()->count(2)->create(['user_id' => $otherUser->id]); + User::factory() + ->withAccount(2) + ->create([ + 'date_of_birth' => now()->subYears(30), + ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view']); $response = $this->getJson('/api/accounts'); @@ -55,7 +62,7 @@ public function testListsAllUserAccounts(): void public function testCreatesNewAccount(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->postJson('/api/accounts', [ 'name' => 'Konto oszczędnościowe', @@ -91,7 +98,7 @@ public function testCreatesNewAccount(): void public function testCreatesAccountWithoutCard(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->postJson('/api/accounts', [ 'name' => 'Konto bez karty', @@ -106,7 +113,7 @@ public function testShowsAccountDetails(): void { $account = Account::factory()->create(['user_id' => $this->user->id]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-details']); $response = $this->getJson("/api/accounts/{$account->id}"); @@ -135,7 +142,7 @@ public function testUpdatesAccountName(): void 'name' => 'Stara nazwa', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->patchJson("/api/accounts/{$account->id}", [ 'name' => 'Nowa nazwa', @@ -158,7 +165,7 @@ public function testCannotUpdateAccountNumberOrBalance(): void 'balance' => 100000, // 1000.00 PLN ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->patchJson("/api/accounts/{$account->id}", [ 'name' => 'Nowa nazwa', @@ -184,10 +191,11 @@ public function testClosesAccountWithZeroBalance(): void 'balance' => 0, ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->deleteJson("/api/accounts/{$account->id}"); + $response->assertStatus(200) ->assertJson(['message' => 'Account closed.']); @@ -201,7 +209,7 @@ public function testCannotCloseAccountWithBalance(): void 'balance' => 50000, ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->deleteJson("/api/accounts/{$account->id}"); @@ -219,7 +227,7 @@ public function testRequiresAuthentication(): void public function testValidatesStoreRequest(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->postJson('/api/accounts', [ 'name' => '', @@ -233,7 +241,7 @@ public function testValidatesUpdateRequest(): void { $account = Account::factory()->create(['user_id' => $this->user->id]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['accounts-view', 'accounts-manage']); $response = $this->patchJson("/api/accounts/{$account->id}", [ 'name' => '', diff --git a/backend/tests/Feature/Account/Card/CardTest.php b/backend/tests/Feature/Account/Card/CardTest.php index 4cda13a..719deda 100644 --- a/backend/tests/Feature/Account/Card/CardTest.php +++ b/backend/tests/Feature/Account/Card/CardTest.php @@ -19,7 +19,9 @@ class CardTest extends TestCase public function setUp(): void { parent::setUp(); - $this->user = User::factory()->create(); + $this->user = User::factory()->create([ + 'date_of_birth' => now()->subYears(30), + ]); $this->account = Account::factory()->create(['user_id' => $this->user->id]); } @@ -27,7 +29,7 @@ public function testListsCardsForAccount(): void { Card::factory()->count(3)->create(['account_id' => $this->account->id]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view']); $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); @@ -60,7 +62,7 @@ public function testListsOnlyCardsForGivenAccount(): void $otherAccount = Account::factory()->create(['user_id' => $this->user->id]); Card::factory()->count(3)->create(['account_id' => $otherAccount->id]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view']); $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); @@ -70,7 +72,7 @@ public function testListsOnlyCardsForGivenAccount(): void public function testCreatesNewCardForAccount(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view', 'cards-manage']); $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); @@ -100,7 +102,7 @@ public function testCreatesNewCardForAccount(): void public function testCreatesCardWithValidNumber(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-manage']); $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); @@ -117,7 +119,7 @@ public function testCreatesCardWithValidNumber(): void public function testCreatesCardWithFutureExpiryDate(): void { - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-manage']); $response = $this->postJson("/api/accounts/{$this->account->id}/cards"); @@ -136,7 +138,7 @@ public function testMasksCardNumberInResponse(): void 'card_number' => '4532123456789012', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view']); $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); @@ -152,7 +154,7 @@ public function testNeverExposesCvvInResponse(): void 'cvv' => '123', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view']); $response = $this->getJson("/api/accounts/{$this->account->id}/cards"); @@ -167,7 +169,7 @@ public function testShowsCardDetails(): void 'card_number' => '5214123456789012', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-details']); $response = $this->getJson("/api/cards/{$card->id}"); @@ -198,7 +200,7 @@ public function testBlocksCard(): void 'status' => 'active', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-manage']); $response = $this->patchJson("/api/cards/{$card->id}/block"); @@ -218,7 +220,7 @@ public function testUnblocksCard(): void 'status' => 'blocked', ]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-manage']); $response = $this->patchJson("/api/cards/{$card->id}/unblock"); @@ -293,7 +295,7 @@ public function testPaginatesCardsList(): void { Card::factory()->count(25)->create(['account_id' => $this->account->id]); - Passport::actingAs($this->user); + Passport::actingAs($this->user, ['cards-view']); $response = $this->getJson("/api/accounts/{$this->account->id}/cards?per_page=10"); diff --git a/backend/tests/Feature/Auth/AuthenticationTest.php b/backend/tests/Feature/Auth/AuthenticationTest.php index cc7df8c..5369a3f 100644 --- a/backend/tests/Feature/Auth/AuthenticationTest.php +++ b/backend/tests/Feature/Auth/AuthenticationTest.php @@ -46,7 +46,7 @@ public function testUsersCanLogout(): void { $user = User::factory()->create(); - $response = $this->actingAs($user)->post('/logout'); + $response = $this->actingAs($user)->get('/logout'); $this->assertGuest(); $response->assertRedirect('/'); diff --git a/backend/tests/Feature/ExampleTest.php b/backend/tests/Feature/ExampleTest.php index b8ab24a..2b02cd9 100644 --- a/backend/tests/Feature/ExampleTest.php +++ b/backend/tests/Feature/ExampleTest.php @@ -14,6 +14,7 @@ public function testTheApplicationReturnsASuccessfulResponse(): void { $response = $this->get('/'); - $response->assertStatus(200); + $response->assertStatus(302); + $response->assertRedirect('/login'); } } From 12da99686dffb31a10f4cf5a40a63b0cac7fb123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 20:57:40 +0200 Subject: [PATCH 14/17] . --- .github/workflows/backend.yml | 72 +++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 40e7abc..0030d8e 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,5 +1,5 @@ # .github/workflows/ci.yml -# Backend CI pipeline for PHP 8.4 with Composer caching +# Backend CI pipeline for PHP 8.4 with MySQL service, Composer caching # Runs on PRs to develop and manual dispatch for main branch AWS deployments name: Backend CI @@ -24,6 +24,34 @@ jobs: name: Backend Tests runs-on: ubuntu-latest + # MySQL service container for integration tests + services: + mysql: + image: mysql:8.0 + env: + MYSQL_DATABASE: backend_testing + MYSQL_USER: laravel + MYSQL_PASSWORD: laravel_password + MYSQL_ROOT_PASSWORD: root_password + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + # Redis service for cache/queue/session + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd="redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + defaults: run: working-directory: ./backend @@ -38,9 +66,20 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.4' - extensions: mbstring, pdo, pdo_mysql, xml, ctype, json, tokenizer + extensions: mbstring, pdo, pdo_mysql, xml, ctype, json, tokenizer, redis coverage: xdebug + # Wait for MySQL to be ready + - name: Wait for MySQL + run: | + for i in {1..30}; do + if mysqladmin ping -h 127.0.0.1 -u laravel -plaravel_password --silent; then + break + fi + echo "Waiting for MySQL..." + sleep 1 + done + # Cache Composer dependencies for faster builds - name: Cache Composer dependencies uses: actions/cache@v4 @@ -56,9 +95,19 @@ jobs: - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - # Run PHP syntax check - - name: Check PHP syntax - run: find . -name '*.php' -not -path './vendor/*' -exec php -l {} \; + # Generate application key + - name: Generate app key + run: php artisan key:generate + + # Run database migrations for testing + - name: Run migrations + env: + DB_HOST: 127.0.0.1 + DB_DATABASE: backend_testing + DB_USERNAME: laravel + DB_PASSWORD: laravel_password + REDIS_HOST: 127.0.0.1 + run: php artisan migrate --force --env=testing # Run PHP CS Fixer in dry-run mode to check code style - name: Check code style @@ -70,6 +119,12 @@ jobs: # Run PHPUnit tests # All tests must pass - failure blocks merge / deployment - name: Run PHPUnit tests + env: + DB_HOST: 127.0.0.1 + DB_DATABASE: backend_testing + DB_USERNAME: laravel + DB_PASSWORD: laravel_password + REDIS_HOST: 127.0.0.1 run: | if [ -f vendor/bin/phpunit ]; then vendor/bin/phpunit --testdox --colors=never @@ -78,13 +133,6 @@ jobs: exit 1 fi - # Run static analysis if PHPStan is configured - - name: Run PHPStan analysis - run: | - if [ -f vendor/bin/phpstan ]; then - vendor/bin/phpstan analyse --no-progress --memory-limit=512M - fi - continue-on-error: true # Job-level status check # Explicitly fail the job if any previous step failed From 8b8fe3f5195391f8bf86e1da6d6fd75edb8ab02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 20:59:12 +0200 Subject: [PATCH 15/17] . --- .github/workflows/backend.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 0030d8e..91ce0f1 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -95,10 +95,6 @@ jobs: - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - # Generate application key - - name: Generate app key - run: php artisan key:generate - # Run database migrations for testing - name: Run migrations env: From 254636063f336267b9e0b78a4fa0b30d5d8696d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 21:05:12 +0200 Subject: [PATCH 16/17] . --- .github/workflows/backend.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 91ce0f1..ae24c68 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -95,6 +95,10 @@ jobs: - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader + # Generate application key + - name: Generate passport keys + run: php artisan passport:keys + # Run database migrations for testing - name: Run migrations env: From 254d0d49d5a1c6d1095ba7ea985fbf5f781584d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BBenia?= Date: Thu, 14 May 2026 21:07:59 +0200 Subject: [PATCH 17/17] . --- backend/tests/Feature/Account/AccountTest.php | 1 - backend/tests/Feature/Account/Card/CardTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/Feature/Account/AccountTest.php b/backend/tests/Feature/Account/AccountTest.php index aacd25a..37717b6 100644 --- a/backend/tests/Feature/Account/AccountTest.php +++ b/backend/tests/Feature/Account/AccountTest.php @@ -195,7 +195,6 @@ public function testClosesAccountWithZeroBalance(): void $response = $this->deleteJson("/api/accounts/{$account->id}"); - $response->assertStatus(200) ->assertJson(['message' => 'Account closed.']); diff --git a/backend/tests/Feature/Account/Card/CardTest.php b/backend/tests/Feature/Account/Card/CardTest.php index 719deda..a85352e 100644 --- a/backend/tests/Feature/Account/Card/CardTest.php +++ b/backend/tests/Feature/Account/Card/CardTest.php @@ -19,7 +19,7 @@ class CardTest extends TestCase public function setUp(): void { parent::setUp(); - $this->user = User::factory()->create([ + $this->user = User::factory()->create([ 'date_of_birth' => now()->subYears(30), ]); $this->account = Account::factory()->create(['user_id' => $this->user->id]);