From 7925015da124a8037b951270b13dc6a1ce5db691 Mon Sep 17 00:00:00 2001 From: mark Date: Sat, 17 May 2025 22:17:09 +0800 Subject: [PATCH] search functionality --- app/Http/Controllers/PersonController.php | 149 +-------- .../Controllers/PublicSearchController.php | 290 ++++++++++++++++++ app/Http/Middleware/Cors.php | 35 +++ app/Http/Resources/ResidenceResource.php | 5 +- app/Models/Residence.php | 12 +- bootstrap/app.php | 12 +- ...s_with_town_or_city_in_residence_table.php | 44 +++ routes/api.php | 20 +- tests/Feature/PublicSearchApiTest.php | 190 +++++++++++- 9 files changed, 583 insertions(+), 174 deletions(-) create mode 100644 app/Http/Controllers/PublicSearchController.php create mode 100644 app/Http/Middleware/Cors.php create mode 100644 database/migrations/2025_05_17_015300_replace_boolean_locations_with_town_or_city_in_residence_table.php diff --git a/app/Http/Controllers/PersonController.php b/app/Http/Controllers/PersonController.php index f45d85c..b57ec98 100644 --- a/app/Http/Controllers/PersonController.php +++ b/app/Http/Controllers/PersonController.php @@ -21,131 +21,41 @@ use Exception; class PersonController extends Controller { - /** - * Public Search API - Search for persons without authentication. - * Allows filtering by multiple criteria. - * - * @param Request $request - * @return JsonResponse - */ - public function publicSearch(Request $request): JsonResponse + + // Public search functionality has been moved to PublicSearchController + + public function getRecord($id): JsonResponse { try { - // Start with the base query - $query = Person::query(); - - // Apply filters for Person fields - if ($request->has('firstName') && $request->firstName !== 'all') { - $query->where('christian_name', 'LIKE', "%{$request->firstName}%"); - } - - if ($request->has('lastName') && $request->lastName !== 'all') { - $query->where('surname', 'LIKE', "%{$request->lastName}%"); - } - - // Filter by region of origin (place_of_birth in Person table) - if ($request->has('regionOfOrigin') && $request->regionOfOrigin !== 'all') { - $query->where('place_of_birth', 'LIKE', "%{$request->regionOfOrigin}%"); - } - - // For filters that need to access related tables, use whereHas - - // Filter by Year of Arrival (in Migration table) - if ($request->has('yearOfArrival') && $request->yearOfArrival !== 'all') { - $year = $request->yearOfArrival; - $query->whereHas('migration', function (Builder $query) use ($year) { - $query->whereYear('date_of_arrival_aus', $year) - ->orWhereYear('date_of_arrival_nt', $year); - }); - } - - // Filter by Age at Migration (requires calculation) - if ($request->has('ageAtMigration') && $request->ageAtMigration !== 'all') { - $ageAtMigration = (int) $request->ageAtMigration; - - $query->whereHas('migration', function (Builder $query) use ($ageAtMigration) { - $query->whereRaw('YEAR(date_of_arrival_aus) - YEAR(person.date_of_birth) = ?', [$ageAtMigration]) - ->orWhereRaw('YEAR(date_of_arrival_nt) - YEAR(person.date_of_birth) = ?', [$ageAtMigration]); - }); - } - - // Filter by settlement location (in Residence table) - if ($request->has('settlementLocation') && $request->settlementLocation !== 'all') { - $location = $request->settlementLocation; - $query->whereHas('residence', function (Builder $query) use ($location) { - $query->where(function ($q) use ($location) { - $q->where('address', 'LIKE', "%{$location}%") - ->orWhere('suburb', 'LIKE', "%{$location}%") - ->orWhere('state', 'LIKE', "%{$location}%"); - }); - }); - } - - // Paginate the results (default 10 per page, can be customized with the 'per_page' parameter) - $perPage = $request->input('per_page', 10); - $persons = $query->paginate($perPage); - - // Eager load related models for the collection - $persons->getCollection()->each->load([ - 'migration', - 'naturalization', - 'residence', - 'family', - 'internment' - ]); - - return response()->json([ - 'success' => true, - 'data' => new PersonCollection($persons), - 'message' => 'Public search results retrieved successfully' - ]); - } catch (Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve search results', - 'error' => $e->getMessage() - ], 500); - } - } - /** - * Find a person by ID card number. - * - * @param string $idCardNo - * @return JsonResponse - */ - public function findByIdCard(string $idCardNo): JsonResponse - { - try { - $person = Person::with(['migration', 'naturalization', 'residence', 'family', 'internment']) - ->where('id_card_no', $idCardNo) - ->first(); - - if (!$person) { + try { + $person = Person::with([ + 'migration', + 'naturalization', + 'residence', + 'family', + 'internment' + ])->findOrFail($id); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return response()->json([ 'success' => false, - 'message' => 'Person not found with the provided ID card number' + 'message' => 'Record not found' ], 404); } return response()->json([ 'success' => true, 'data' => new PersonResource($person), - 'message' => 'Person found by ID card number' + 'message' => 'Record retrieved successfully' ]); } catch (Exception $e) { return response()->json([ 'success' => false, - 'message' => 'Failed to retrieve person by ID card number', + 'message' => 'Failed to retrieve record', 'error' => $e->getMessage() ], 500); } } - /** - * Display a listing of the resource. - * - * @param Request $request - * @return JsonResponse - */ + public function index(Request $request): JsonResponse { try { @@ -187,12 +97,6 @@ class PersonController extends Controller } } - /** - * Store a newly created resource in storage. - * - * @param StorePersonRequest $request - * @return JsonResponse - */ public function store(StorePersonRequest $request): JsonResponse { try { @@ -250,12 +154,6 @@ class PersonController extends Controller } } - /** - * Display the specified resource. - * - * @param string $id - * @return JsonResponse - */ public function show(string $id): JsonResponse { try { @@ -284,13 +182,6 @@ class PersonController extends Controller } } - /** - * Update the specified resource in storage. - * - * @param UpdatePersonRequest $request - * @param string $id - * @return JsonResponse - */ public function update(UpdatePersonRequest $request, string $id): JsonResponse { try { @@ -371,12 +262,6 @@ class PersonController extends Controller } } - /** - * Remove the specified resource from storage. - * - * @param string $id - * @return JsonResponse - */ public function destroy(string $id): JsonResponse { try { diff --git a/app/Http/Controllers/PublicSearchController.php b/app/Http/Controllers/PublicSearchController.php new file mode 100644 index 0000000..b8e2d9e --- /dev/null +++ b/app/Http/Controllers/PublicSearchController.php @@ -0,0 +1,290 @@ +buildSearchQuery($request); + $persons = $this->paginateAndLoadRelations($query, $request); + + return response()->json([ + 'success' => true, + 'data' => new PersonCollection($persons), + 'message' => 'Public search results retrieved successfully' + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve search results', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Build the search query with all filters applied + * + * @param Request $request + * @return Builder + */ + private function buildSearchQuery(Request $request): Builder + { + $query = Person::query(); + $useOrConditions = $this->shouldUseOrConditions($request); + + // Apply basic filters (firstName, lastName, etc.) + $this->applyBasicFilters($query, $request, $useOrConditions); + + // Apply relation-based filters + $this->applyYearOfArrivalFilter($query, $request); + $this->applyAgeAtMigrationFilter($query, $request); + $this->applySettlementLocationFilter($query, $request); + + return $query; + } + + /** + * Determine if OR logic should be used between filters + * + * @param Request $request + * @return bool + */ + private function shouldUseOrConditions(Request $request): bool + { + $exactMatch = $request->boolean('exactMatch'); + $useOrLogic = $request->boolean('useOrLogic'); + + return $useOrLogic || $exactMatch; + } + + /** + * Apply filters for fields in the Person table + * + * @param Builder $query + * @param Request $request + * @param bool $useOrConditions + * @return void + */ + private function applyBasicFilters(Builder $query, Request $request, bool $useOrConditions): void + { + $filters = $this->collectBasicFilters($request); + + if (empty($filters)) { + return; + } + + if ($useOrConditions) { + $this->applyFiltersWithOrLogic($query, $filters); + } else { + $this->applyFiltersWithAndLogic($query, $filters); + } + } + + /** + * Collect basic filters from the request + * + * @param Request $request + * @return array + */ + private function collectBasicFilters(Request $request): array + { + $exactMatch = $request->boolean('exactMatch'); + $filters = []; + + $filterFields = [ + 'id_card_no' => 'id_card_no', + 'firstName' => 'christian_name', + 'lastName' => 'surname', + 'regionOfOrigin' => 'place_of_birth' + ]; + + foreach ($filterFields as $requestKey => $dbField) { + if ($request->has($requestKey) && $request->input($requestKey) !== 'all') { + $value = $request->input($requestKey); + + if ($exactMatch) { + // For exact matching, use raw query with BINARY for strict case-sensitive matching + $filters[] = [$dbField, 'raw', $value]; + } else { + // For partial matching, use standard LIKE with wildcards + $filters[] = [$dbField, 'LIKE', "%{$value}%"]; + } + } + } + + return $filters; + } + + /** + * Apply filters using OR logic + * + * @param Builder $query + * @param array $filters + * @return void + */ + private function applyFiltersWithOrLogic(Builder $query, array $filters): void + { + $query->where(function ($q) use ($filters) { + foreach ($filters as $index => $filter) { + $method = $index === 0 ? 'where' : 'orWhere'; + + if ($filter[1] === 'raw') { + // Handle raw exact matching (case-sensitive) + $field = $filter[0]; + $value = $filter[2]; + $q->whereRaw("BINARY {$field} = ?", [$value]); + } else { + // Handle standard operators + $q->{$method}($filter[0], $filter[1], $filter[2]); + } + } + }); + } + + /** + * Apply filters using AND logic + * + * @param Builder $query + * @param array $filters + * @return void + */ + private function applyFiltersWithAndLogic(Builder $query, array $filters): void + { + foreach ($filters as $filter) { + if ($filter[1] === 'raw') { + // Handle raw exact matching (case-sensitive) + $field = $filter[0]; + $value = $filter[2]; + $query->whereRaw("BINARY {$field} = ?", [$value]); + } else { + // Handle standard operators + $query->where($filter[0], $filter[1], $filter[2]); + } + } + } + + /** + * Filter by Year of Arrival + * + * @param Builder $query + * @param Request $request + * @return void + */ + private function applyYearOfArrivalFilter(Builder $query, Request $request): void + { + if ($request->has('yearOfArrival') && $request->yearOfArrival !== 'all') { + $year = $request->yearOfArrival; + $query->whereHas('migration', function (Builder $subQuery) use ($year) { + $subQuery->whereYear('date_of_arrival_aus', $year) + ->orWhereYear('date_of_arrival_nt', $year); + }); + } + } + + /** + * Filter by Age at Migration + * + * @param Builder $query + * @param Request $request + * @return void + */ + private function applyAgeAtMigrationFilter(Builder $query, Request $request): void + { + if ($request->has('ageAtMigration') && $request->ageAtMigration !== 'all') { + $ageAtMigration = (int) $request->ageAtMigration; + + $query->whereHas('migration', function (Builder $subQuery) use ($ageAtMigration) { + $subQuery->whereRaw('YEAR(date_of_arrival_aus) - YEAR(person.date_of_birth) = ?', [$ageAtMigration]) + ->orWhereRaw('YEAR(date_of_arrival_nt) - YEAR(person.date_of_birth) = ?', [$ageAtMigration]); + }); + } + } + + /** + * Filter by Settlement Location + * + * @param Builder $query + * @param Request $request + * @return void + */ + private function applySettlementLocationFilter(Builder $query, Request $request): void + { + if ($request->has('settlementLocation') && $request->settlementLocation !== 'all') { + $location = $request->settlementLocation; + $query->whereHas('residence', function (Builder $subQuery) use ($location) { + $subQuery->where('town_or_city', 'LIKE', "%{$location}%"); + }); + } + } + + /** + * Paginate results and load related models + * + * @param Builder $query + * @param Request $request + * @return \Illuminate\Pagination\LengthAwarePaginator + */ + private function paginateAndLoadRelations(Builder $query, Request $request): \Illuminate\Pagination\LengthAwarePaginator + { + $perPage = $request->input('per_page', 10); + $persons = $query->paginate($perPage); + + // Eager load related models + $persons->getCollection()->each->load([ + 'migration', + 'naturalization', + 'residence', + 'family', + 'internment' + ]); + + return $persons; + } + + /** + * Get a specific migrant record by ID + * + * @param mixed $id + * @return JsonResponse + */ + public function getRecord($id): JsonResponse + { + try { + $person = Person::with([ + 'migration', + 'naturalization', + 'residence', + 'family', + 'internment' + ])->findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $person, + 'message' => 'Record retrieved successfully' + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Record not found', + 'error' => $e->getMessage() + ], 404); + } + } +} diff --git a/app/Http/Middleware/Cors.php b/app/Http/Middleware/Cors.php new file mode 100644 index 0000000..83d582b --- /dev/null +++ b/app/Http/Middleware/Cors.php @@ -0,0 +1,35 @@ +headers->set('Access-Control-Allow-Origin', '*'); + $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS'); + $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, X-CSRF-TOKEN'); + $response->headers->set('Access-Control-Max-Age', '86400'); // 24 hours + + // Handle preflight OPTIONS requests + if ($request->isMethod('OPTIONS')) { + return response()->json('', 200); + } + + return $response; + } +} diff --git a/app/Http/Resources/ResidenceResource.php b/app/Http/Resources/ResidenceResource.php index 32c9520..8dfffa0 100644 --- a/app/Http/Resources/ResidenceResource.php +++ b/app/Http/Resources/ResidenceResource.php @@ -17,10 +17,7 @@ class ResidenceResource extends JsonResource return $this->resource ? [ 'residence_id' => $this->residence_id, 'person_id' => $this->person_id, - 'darwin' => $this->darwin, - 'katherine' => $this->katherine, - 'tennant_creek' => $this->tennant_creek, - 'alice_springs' => $this->alice_springs, + 'town_or_city' => $this->town_or_city, 'home_at_death' => $this->home_at_death, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, diff --git a/app/Models/Residence.php b/app/Models/Residence.php index 03c9105..eec31e5 100644 --- a/app/Models/Residence.php +++ b/app/Models/Residence.php @@ -15,19 +15,11 @@ class Residence extends Model protected $fillable = [ 'person_id', - 'darwin', - 'katherine', - 'tennant_creek', - 'alice_springs', + 'town_or_city', 'home_at_death', ]; - protected $casts = [ - 'darwin' => 'boolean', - 'katherine' => 'boolean', - 'tennant_creek' => 'boolean', - 'alice_springs' => 'boolean', - ]; + protected $casts = []; // Relationship public function person() diff --git a/bootstrap/app.php b/bootstrap/app.php index 61b8c6e..5ea4036 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Http\Request; use Laravel\Sanctum\Sanctum; +use App\Http\Middleware\Cors; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -14,7 +15,16 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware) { - // Register Sanctum middleware for API authentication + // Add CORS middleware globally to handle cross-origin requests + $middleware->web(prepend: [ + Cors::class, + ]); + + // Register Sanctum middleware for API authentication with CORS handling + $middleware->api(prepend: [ + Cors::class, // Add CORS first to handle preflight requests + ]); + $middleware->api(append: [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, ]); diff --git a/database/migrations/2025_05_17_015300_replace_boolean_locations_with_town_or_city_in_residence_table.php b/database/migrations/2025_05_17_015300_replace_boolean_locations_with_town_or_city_in_residence_table.php new file mode 100644 index 0000000..2707e17 --- /dev/null +++ b/database/migrations/2025_05_17_015300_replace_boolean_locations_with_town_or_city_in_residence_table.php @@ -0,0 +1,44 @@ +string('town_or_city', 100)->nullable()->after('person_id'); + + // Remove the boolean location columns + $table->dropColumn([ + 'darwin', + 'katherine', + 'tennant_creek', + 'alice_springs' + ]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('residence', function (Blueprint $table) { + // Add back the boolean location columns + $table->boolean('darwin')->default(false)->after('person_id'); + $table->boolean('katherine')->default(false)->after('darwin'); + $table->boolean('tennant_creek')->default(false)->after('katherine'); + $table->boolean('alice_springs')->default(false)->after('tennant_creek'); + + // Remove the town_or_city column + $table->dropColumn('town_or_city'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 87f4ed2..97b9c27 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,24 +3,16 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\PersonController; +use App\Http\Controllers\PublicSearchController; use App\Http\Controllers\AuthController; -/* -|-------------------------------------------------------------------------- -| API Routes -|-------------------------------------------------------------------------- -| -| Here is where you can register API routes for your application. These -| routes are loaded by the RouteServiceProvider and all of them will -| be assigned to the "api" middleware group. Make something great! -| -*/ - -// Public routes - no authentication required Route::post('/login', [AuthController::class, 'login'])->name('login'); -// Public search endpoint - allows searching without authentication -Route::get('/persons/search', [PersonController::class, 'publicSearch'])->name('persons.public.search'); +// Public search endpoints - allow searching without authentication +Route::get('/persons/search', [PublicSearchController::class, 'search'])->name('persons.public.search'); + +// Public endpoint to get a specific migrant's full record +Route::get('/migrants/{person_id}', [PublicSearchController::class, 'getRecord'])->name('migrants.get'); // Protected routes - require Sanctum authentication Route::middleware('auth:sanctum')->group(function () { diff --git a/tests/Feature/PublicSearchApiTest.php b/tests/Feature/PublicSearchApiTest.php index 9880e17..479d89d 100644 --- a/tests/Feature/PublicSearchApiTest.php +++ b/tests/Feature/PublicSearchApiTest.php @@ -53,9 +53,8 @@ class PublicSearchApiTest extends TestCase Residence::create([ 'person_id' => $person1->person_id, - 'address' => '123 Main St', - 'suburb' => 'Sydney', - 'state' => 'NSW', + 'town_or_city' => 'Sydney', + 'home_at_death' => 'Sydney, NSW', ]); // Person 2: Maria Mueller from Austria, arrived in 1885 at age 22, settled in Melbourne @@ -77,9 +76,8 @@ class PublicSearchApiTest extends TestCase Residence::create([ 'person_id' => $person2->person_id, - 'address' => '456 High St', - 'suburb' => 'Melbourne', - 'state' => 'VIC', + 'town_or_city' => 'Melbourne', + 'home_at_death' => 'Melbourne, VIC', ]); // Person 3: Robert Johnson from England, arrived in 1890 at age 30, settled in Brisbane @@ -101,9 +99,8 @@ class PublicSearchApiTest extends TestCase Residence::create([ 'person_id' => $person3->person_id, - 'address' => '789 Queen St', - 'suburb' => 'Brisbane', - 'state' => 'QLD', + 'town_or_city' => 'Brisbane', + 'home_at_death' => 'Brisbane, QLD', ]); } @@ -194,17 +191,51 @@ class PublicSearchApiTest extends TestCase */ public function test_filter_by_settlement_location() { - // Let's test with a simpler approach to avoid database-specific SQL issues // First, verify we have all records without filters $response = $this->getJson('/api/persons/search'); $response->assertStatus(200) ->assertJsonCount(3, 'data.data'); - // Now test with a more specific test that shouldn't depend on SQL dialect - $response = $this->getJson('/api/persons/search?lastName=Johnson'); + // Now test filtering by settlement location using the new town_or_city field + $response = $this->getJson('/api/persons/search?settlementLocation=Sydney'); $response->assertStatus(200) ->assertJsonCount(1, 'data.data') - ->assertJsonPath('data.data.0.surname', 'Johnson'); + ->assertJsonPath('data.data.0.surname', 'Smith'); + } + + /** + * Test that the new town_or_city field stores and retrieves correctly through the API. + */ + public function test_town_or_city_field_works_correctly() + { + // Create a new person record with a residence + $person = Person::create([ + 'surname' => 'Darwin', + 'christian_name' => 'Charles', + 'full_name' => 'Charles Darwin', + 'date_of_birth' => '1809-02-12', + 'place_of_birth' => 'Shrewsbury, England', + 'occupation' => 'Naturalist', + 'id_card_no' => 'TEST-004' + ]); + + // Create a residence record with the new town_or_city field + $residence = Residence::create([ + 'person_id' => $person->person_id, + 'town_or_city' => 'Darwin', // Using the town name as a test + 'home_at_death' => 'Down House, Kent' + ]); + + // Retrieve the person through the API + $response = $this->getJson("/api/migrants/{$person->person_id}"); + + // Assert the response contains the correct town_or_city value + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Record retrieved successfully' + ]) + ->assertJsonPath('data.residence.town_or_city', 'Darwin'); } /** @@ -232,6 +263,60 @@ class PublicSearchApiTest extends TestCase ->assertJsonCount(3, 'data.data'); } + /** + * Test that a migrant's full record can be retrieved by ID. + */ + public function test_get_migrant_by_id() + { + // Get a person ID from the test data + $person = Person::where('id_card_no', 'TEST-001')->first(); + + // Call the endpoint + $response = $this->getJson("/api/migrants/{$person->person_id}"); + + // Assert response structure and content + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Record retrieved successfully' + ]) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'person_id', + 'surname', + 'christian_name', + 'full_name', + 'date_of_birth', + 'place_of_birth', + 'occupation', + 'id_card_no', + // Related models + 'migration', + 'naturalization', + 'residence', + 'family', + 'internment' + ], + 'message' + ]); + + // Verify the correct record was returned + $response->assertJsonPath('data.id_card_no', 'TEST-001'); + $response->assertJsonPath('data.christian_name', 'John'); + $response->assertJsonPath('data.surname', 'Smith'); + } + + /** + * Test that requesting a non-existent migrant ID returns 404. + */ + public function test_get_nonexistent_migrant_returns_404() + { + $response = $this->getJson('/api/migrants/999999'); + + $response->assertStatus(404); + } + /** * Test that using "all" as a filter value results in no filtering for that field. */ @@ -302,4 +387,83 @@ class PublicSearchApiTest extends TestCase 'message' => 'Unauthenticated.' ]); } + + /** + * Test strict case-insensitive matching with at least one correct field returning results + * despite other incorrect fields. This validates OR logic in search when a special parameter is used. + */ + public function test_one_correct_field_with_multiple_incorrect() + { + // Create a special test person with unique attributes for this test + $specialPerson = Person::create([ + 'surname' => 'UniqueLastName', + 'christian_name' => 'UniqueFirstName', + 'full_name' => 'UniqueFirstName UniqueLastName', + 'date_of_birth' => '1930-05-15', + 'place_of_birth' => 'UniqueRegion, UniqueCountry', + 'occupation' => 'Developer', + 'id_card_no' => 'UNIQUE-ID-123' + ]); + + Migration::create([ + 'person_id' => $specialPerson->person_id, + 'date_of_arrival_aus' => '1960-03-20', + 'date_of_arrival_nt' => '1960-04-10', + ]); + + Residence::create([ + 'person_id' => $specialPerson->person_id, + 'town_or_city' => 'UniqueCity', + 'home_at_death' => 'UniqueCity, UniqueState', + ]); + + // Test case-insensitive matching for id_card_no + $response = $this->getJson('/api/persons/search?id_card_no=unique-id-123'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.id_card_no', 'UNIQUE-ID-123'); + + // Test case-insensitive matching for firstName + $response = $this->getJson('/api/persons/search?firstName=uniquefirstname'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.christian_name', 'UniqueFirstName'); + + // Test case-insensitive matching for lastName + $response = $this->getJson('/api/persons/search?lastName=uniquelastname'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.surname', 'UniqueLastName'); + + // Test the OR logic where we provide one correct field and multiple incorrect ones + // Using the useOrLogic parameter to apply OR condition instead of AND + $response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=unique-id-123&firstName=WrongName&lastName=WrongSurname'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.id_card_no', 'UNIQUE-ID-123'); + + // Another OR logic test with a different correct field + $response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=WRONG-ID&firstName=uniquefirstname®ionOfOrigin=WrongRegion'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.christian_name', 'UniqueFirstName'); + + // Test mixing capitalization in the correct field + $response = $this->getJson('/api/persons/search?useOrLogic=true&id_card_no=WRONG-ID&firstName=WrongName&lastName=UniQueLastNAME'); + + $response->assertStatus(200) + ->assertJsonCount(1, 'data.data') + ->assertJsonPath('data.data.0.surname', 'UniqueLastName'); + + // Verify that normal behavior without useOrLogic returns no results for mixed inputs + $response = $this->getJson('/api/persons/search?id_card_no=unique-id-123&firstName=WrongName'); + + $response->assertStatus(200) + ->assertJsonCount(0, 'data.data'); + } }