From bd9317780eb2957e6bfd7575a3b341be3c89c6fb Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 08:03:04 +0900 Subject: [PATCH 01/13] feat: add UpdateUserCommand --- .../UseCommand/User/UpdateUserCommand.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/app/Packages/Features/CommandUseCases/UseCommand/User/UpdateUserCommand.php diff --git a/src/app/Packages/Features/CommandUseCases/UseCommand/User/UpdateUserCommand.php b/src/app/Packages/Features/CommandUseCases/UseCommand/User/UpdateUserCommand.php new file mode 100644 index 0000000..4176a5c --- /dev/null +++ b/src/app/Packages/Features/CommandUseCases/UseCommand/User/UpdateUserCommand.php @@ -0,0 +1,46 @@ + Date: Thu, 2 Jul 2026 08:03:04 +0900 Subject: [PATCH 02/13] feat: implement UpdateUserUseCase --- .../UseCase/User/UpdateUserUseCase.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/app/Packages/Features/CommandUseCases/UseCase/User/UpdateUserUseCase.php diff --git a/src/app/Packages/Features/CommandUseCases/UseCase/User/UpdateUserUseCase.php b/src/app/Packages/Features/CommandUseCases/UseCase/User/UpdateUserUseCase.php new file mode 100644 index 0000000..a99e045 --- /dev/null +++ b/src/app/Packages/Features/CommandUseCases/UseCase/User/UpdateUserUseCase.php @@ -0,0 +1,30 @@ + $command->id, + 'first_name' => $command->firstName, + 'last_name' => $command->lastName, + 'email' => $command->email, + 'age_range' => $command->ageRange, + 'subscription_tier' => $command->subscriptionTier, + 'subscription_expires_at' => $command->subscriptionExpiresAt, + ]); + + return $this->userRepository->updateUser($entity); + } +} \ No newline at end of file From 62ea7ce748ae6f0c991af7b7833087b49253b711 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 08:03:04 +0900 Subject: [PATCH 03/13] test: add unit tests for UpdateUserCommand --- .../CommandUseCases/UpdateUserCommandTest.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/app/Packages/Features/Tests/CommandUseCases/UpdateUserCommandTest.php diff --git a/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserCommandTest.php b/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserCommandTest.php new file mode 100644 index 0000000..0e4b4ad --- /dev/null +++ b/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserCommandTest.php @@ -0,0 +1,72 @@ + $faker->unique()->randomNumber(5), + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'age_range' => $faker->randomElement(['teens', '20s', '30s', '40s', '50s', '60plus']), + 'subscription_tier' => $faker->randomElement(['free', 'premium']), + ], $overrides); + } + + public function test_fromArray_creates_command_with_valid_data(): void + { + $data = $this->validData(); + $command = UpdateUserCommand::fromArray($data); + + $this->assertSame((int) $data['id'], $command->id); + $this->assertSame($data['first_name'], $command->firstName); + $this->assertSame($data['last_name'], $command->lastName); + $this->assertSame($data['email'], $command->email); + $this->assertSame($data['age_range'], $command->ageRange); + $this->assertSame($data['subscription_tier'], $command->subscriptionTier); + $this->assertNull($command->subscriptionExpiresAt); + } + + public function test_fromArray_sets_subscription_expires_at_when_provided(): void + { + $command = UpdateUserCommand::fromArray($this->validData([ + 'subscription_expires_at' => '2027-01-01 00:00:00', + ])); + + $this->assertSame('2027-01-01 00:00:00', $command->subscriptionExpiresAt); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('missingKeyProvider')] + public function test_fromArray_throws_when_required_key_is_missing(string $missingKey): void + { + $data = $this->validData(); + unset($data[$missingKey]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Missing required key: {$missingKey}"); + + UpdateUserCommand::fromArray($data); + } + + public static function missingKeyProvider(): array + { + return [ + ['id'], + ['first_name'], + ['last_name'], + ['email'], + ['age_range'], + ['subscription_tier'], + ]; + } +} From 45effe3146631073008a2ecc9b3d420f8d072297 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 08:03:04 +0900 Subject: [PATCH 04/13] test: add unit tests for UpdateUserUseCase --- .../CommandUseCases/UpdateUserUseCaseTest.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/app/Packages/Features/Tests/CommandUseCases/UpdateUserUseCaseTest.php diff --git a/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserUseCaseTest.php b/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserUseCaseTest.php new file mode 100644 index 0000000..1c205db --- /dev/null +++ b/src/app/Packages/Features/Tests/CommandUseCases/UpdateUserUseCaseTest.php @@ -0,0 +1,68 @@ + $faker->unique()->randomNumber(5), + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'age_range' => $faker->randomElement(['teens', '20s', '30s', '40s', '50s', '60plus']), + 'subscription_tier' => 'free', + ]); + } + + private function buildDto(): UserDto + { + $faker = FakerFactory::create(); + + return new UserDto( + id: $faker->unique()->randomNumber(5), + firstName: $faker->firstName(), + lastName: $faker->lastName(), + email: $faker->unique()->safeEmail(), + ageRange: 'teens', + subscriptionTier: 'free', + subscriptionExpiresAt: null, + ); + } + + public function test_handle_passes_entity_to_repository_and_returns_dto(): void + { + $command = $this->buildCommand(); + $dto = $this->buildDto(); + + /** @var UserRepositroyInterface|MockInterface $repository */ + $repository = Mockery::mock(UserRepositroyInterface::class); + $repository->shouldReceive('updateUser') + ->once() + ->with(Mockery::type(UserEntity::class)) + ->andReturn($dto); + + $result = (new UpdateUserUseCase($repository))->handle($command); + + $this->assertSame($dto, $result); + } +} \ No newline at end of file From 68d41e378975bfd33f115d6b3068e49a499c9422 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 08:55:51 +0900 Subject: [PATCH 05/13] refactor: move UserViewModel to QueryUseCases/ViewModel/User --- .../ViewModel/User/UserViewModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/app/Packages/Features/{CommandUseCases => QueryUseCases}/ViewModel/User/UserViewModel.php (91%) diff --git a/src/app/Packages/Features/CommandUseCases/ViewModel/User/UserViewModel.php b/src/app/Packages/Features/QueryUseCases/ViewModel/User/UserViewModel.php similarity index 91% rename from src/app/Packages/Features/CommandUseCases/ViewModel/User/UserViewModel.php rename to src/app/Packages/Features/QueryUseCases/ViewModel/User/UserViewModel.php index 96ca814..17402a1 100644 --- a/src/app/Packages/Features/CommandUseCases/ViewModel/User/UserViewModel.php +++ b/src/app/Packages/Features/QueryUseCases/ViewModel/User/UserViewModel.php @@ -1,6 +1,6 @@ Date: Thu, 2 Jul 2026 08:55:51 +0900 Subject: [PATCH 06/13] refactor: move UserViewModelFactory to QueryUseCases/Factory/ViewModel --- .../Factory/ViewModel/UserViewModelFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/app/Packages/Features/{CommandUseCases => QueryUseCases}/Factory/ViewModel/UserViewModelFactory.php (60%) diff --git a/src/app/Packages/Features/CommandUseCases/Factory/ViewModel/UserViewModelFactory.php b/src/app/Packages/Features/QueryUseCases/Factory/ViewModel/UserViewModelFactory.php similarity index 60% rename from src/app/Packages/Features/CommandUseCases/Factory/ViewModel/UserViewModelFactory.php rename to src/app/Packages/Features/QueryUseCases/Factory/ViewModel/UserViewModelFactory.php index a4e2a3d..519e15b 100644 --- a/src/app/Packages/Features/CommandUseCases/Factory/ViewModel/UserViewModelFactory.php +++ b/src/app/Packages/Features/QueryUseCases/Factory/ViewModel/UserViewModelFactory.php @@ -1,8 +1,8 @@ Date: Thu, 2 Jul 2026 08:55:51 +0900 Subject: [PATCH 07/13] refactor: update UserController import to use QueryUseCases UserViewModelFactory --- src/app/Packages/Features/Controller/UserController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Packages/Features/Controller/UserController.php b/src/app/Packages/Features/Controller/UserController.php index b16b67f..eec537f 100644 --- a/src/app/Packages/Features/Controller/UserController.php +++ b/src/app/Packages/Features/Controller/UserController.php @@ -3,7 +3,7 @@ namespace App\Packages\Features\Controller; use App\Http\Controllers\Controller; -use App\Packages\Features\CommandUseCases\Factory\ViewModel\UserViewModelFactory; +use App\Packages\Features\QueryUseCases\Factory\ViewModel\UserViewModelFactory; use App\Packages\Features\CommandUseCases\UseCase\User\CreateUserUseCase; use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; use App\Packages\Features\QueryUseCases\UseCase\User\GetUserByIdUseCase; From 807cf03db9eb7d71bfbf075a76ab2ff693b63686 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 08:55:52 +0900 Subject: [PATCH 08/13] test: move UserViewModelTest to QueryUseCases/Tests/ViewModel --- .../Tests/ViewModel}/UserViewModelTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/app/Packages/Features/{Tests/CommandUseCases => QueryUseCases/Tests/ViewModel}/UserViewModelTest.php (93%) diff --git a/src/app/Packages/Features/Tests/CommandUseCases/UserViewModelTest.php b/src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelTest.php similarity index 93% rename from src/app/Packages/Features/Tests/CommandUseCases/UserViewModelTest.php rename to src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelTest.php index ffde5ce..007cd58 100644 --- a/src/app/Packages/Features/Tests/CommandUseCases/UserViewModelTest.php +++ b/src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelTest.php @@ -1,8 +1,8 @@ Date: Thu, 2 Jul 2026 08:55:52 +0900 Subject: [PATCH 09/13] test: move UserViewModelFactoryTest to QueryUseCases/Tests/ViewModel --- .../Tests/ViewModel}/UserViewModelFactoryTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/app/Packages/Features/{Tests/CommandUseCases => QueryUseCases/Tests/ViewModel}/UserViewModelFactoryTest.php (78%) diff --git a/src/app/Packages/Features/Tests/CommandUseCases/UserViewModelFactoryTest.php b/src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelFactoryTest.php similarity index 78% rename from src/app/Packages/Features/Tests/CommandUseCases/UserViewModelFactoryTest.php rename to src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelFactoryTest.php index 32938a5..13074c3 100644 --- a/src/app/Packages/Features/Tests/CommandUseCases/UserViewModelFactoryTest.php +++ b/src/app/Packages/Features/QueryUseCases/Tests/ViewModel/UserViewModelFactoryTest.php @@ -1,9 +1,9 @@ Date: Thu, 2 Jul 2026 08:55:52 +0900 Subject: [PATCH 10/13] chore: add root .gitignore to exclude .idea --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ From d8bcc714208ff110c9f3b43d72215d33b3224196 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 09:37:44 +0900 Subject: [PATCH 11/13] feat: add updateUser action to UserController --- .../Features/Controller/UserController.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/app/Packages/Features/Controller/UserController.php b/src/app/Packages/Features/Controller/UserController.php index eec537f..9917562 100644 --- a/src/app/Packages/Features/Controller/UserController.php +++ b/src/app/Packages/Features/Controller/UserController.php @@ -5,7 +5,9 @@ use App\Http\Controllers\Controller; use App\Packages\Features\QueryUseCases\Factory\ViewModel\UserViewModelFactory; use App\Packages\Features\CommandUseCases\UseCase\User\CreateUserUseCase; +use App\Packages\Features\CommandUseCases\UseCase\User\UpdateUserUseCase; use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; +use App\Packages\Features\CommandUseCases\UseCommand\User\UpdateUserCommand; use App\Packages\Features\QueryUseCases\UseCase\User\GetUserByIdUseCase; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -44,6 +46,41 @@ public function getUserById( } } + public function updateUser( + Request $request, + UpdateUserUseCase $useCase, + ): JsonResponse { + try { + $command = UpdateUserCommand::fromArray(array_merge( + $request->all(), + ['id' => $request->route('id')], + )); + $dto = $useCase->handle($command); + + return response()->json([ + 'status' => 'success', + 'data' => UserViewModelFactory::build($dto)->toArray(), + ], 200); + } catch (\Exception $exception) { + if ($exception->getMessage() === 'User not found.') { + return response()->json([ + 'status' => 'error', + 'message' => 'User not found.', + ], 404); + } + + Log::error('Failed to update user', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + + return response()->json([ + 'status' => 'error', + 'message' => 'Internal Server Error', + ], 500); + } + } + public function createUser( Request $request, CreateUserUseCase $useCase, From 1e7423bea2ab88d978f8d46d87eb175250948be4 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 09:37:44 +0900 Subject: [PATCH 12/13] feat: register PATCH /api/v1/users/{id} route --- src/routes/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/api.php b/src/routes/api.php index 7f5dcb8..a3d59bd 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -11,5 +11,6 @@ Route::get('/heritages/{id}', [WorldHeritageController::class, 'getWorldHeritageById']); Route::get('/users/{id}', [UserController::class, 'getUserById']); + Route::patch('/users/{id}', [UserController::class, 'updateUser']); Route::post('/user/create', [UserController::class, 'createUser']); }); \ No newline at end of file From 0af8b3d6800e59762b1970f8194b2b337aa22c30 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Thu, 2 Jul 2026 09:37:44 +0900 Subject: [PATCH 13/13] test: add integration test for PATCH /api/v1/users/{id} --- .../Features/Tests/UpdateUserTest.php | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/app/Packages/Features/Tests/UpdateUserTest.php diff --git a/src/app/Packages/Features/Tests/UpdateUserTest.php b/src/app/Packages/Features/Tests/UpdateUserTest.php new file mode 100644 index 0000000..b375ab5 --- /dev/null +++ b/src/app/Packages/Features/Tests/UpdateUserTest.php @@ -0,0 +1,109 @@ +truncate(); + } + + protected function tearDown(): void + { + $this->truncate(); + parent::tearDown(); + } + + private function truncate(): void + { + if (env('APP_ENV') === 'testing') { + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0;'); + User::truncate(); + DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;'); + } + } + + private function seedUser(array $overrides = []): User + { + $faker = FakerFactory::create(); + + return User::create(array_merge([ + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'password' => bcrypt('password'), + 'age_range' => 'teens', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ], $overrides)); + } + + private function validPayload(array $overrides = []): array + { + $faker = FakerFactory::create(); + + return array_merge([ + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'age_range' => '30s', + 'subscription_tier' => 'premium', + ], $overrides); + } + + public function test_update_user_returns_200_with_updated_data(): void + { + $user = $this->seedUser(); + + $payload = $this->validPayload(['email' => 'updated@example.com']); + $response = $this->patchJson("/api/v1/users/{$user->id}", $payload); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'status', + 'data' => [ + 'id', + 'first_name', + 'last_name', + 'email', + 'age_range', + 'subscription_tier', + 'subscription_expires_at', + ], + ]) + ->assertJsonFragment([ + 'status' => 'success', + 'email' => 'updated@example.com', + ]); + } + + public function test_update_user_returns_404_when_user_not_found(): void + { + $response = $this->patchJson('/api/v1/users/999999', $this->validPayload()); + + $response->assertStatus(404) + ->assertJsonFragment([ + 'status' => 'error', + 'message' => 'User not found.', + ]); + } + + public function test_update_user_returns_500_when_required_key_is_missing(): void + { + $user = $this->seedUser(); + $payload = $this->validPayload(); + unset($payload['email']); + + $response = $this->patchJson("/api/v1/users/{$user->id}", $payload); + + $response->assertStatus(500) + ->assertJsonFragment(['status' => 'error']); + } +}