search functionality

This commit is contained in:
mark 2025-05-17 22:17:09 +08:00
parent a0426842a6
commit 7925015da1
9 changed files with 583 additions and 174 deletions

View File

@ -21,131 +21,41 @@ use Exception;
class PersonController extends Controller class PersonController extends Controller
{ {
/**
* Public Search API - Search for persons without authentication. // Public search functionality has been moved to PublicSearchController
* Allows filtering by multiple criteria.
* public function getRecord($id): JsonResponse
* @param Request $request
* @return JsonResponse
*/
public function publicSearch(Request $request): JsonResponse
{ {
try { try {
// Start with the base query try {
$query = Person::query(); $person = Person::with([
'migration',
// Apply filters for Person fields 'naturalization',
if ($request->has('firstName') && $request->firstName !== 'all') { 'residence',
$query->where('christian_name', 'LIKE', "%{$request->firstName}%"); 'family',
} 'internment'
])->findOrFail($id);
if ($request->has('lastName') && $request->lastName !== 'all') { } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
$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) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Person not found with the provided ID card number' 'message' => 'Record not found'
], 404); ], 404);
} }
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => new PersonResource($person), 'data' => new PersonResource($person),
'message' => 'Person found by ID card number' 'message' => 'Record retrieved successfully'
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Failed to retrieve person by ID card number', 'message' => 'Failed to retrieve record',
'error' => $e->getMessage() 'error' => $e->getMessage()
], 500); ], 500);
} }
} }
/**
* Display a listing of the resource.
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
try { 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 public function store(StorePersonRequest $request): JsonResponse
{ {
try { try {
@ -250,12 +154,6 @@ class PersonController extends Controller
} }
} }
/**
* Display the specified resource.
*
* @param string $id
* @return JsonResponse
*/
public function show(string $id): JsonResponse public function show(string $id): JsonResponse
{ {
try { 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 public function update(UpdatePersonRequest $request, string $id): JsonResponse
{ {
try { 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 public function destroy(string $id): JsonResponse
{ {
try { try {

View File

@ -0,0 +1,290 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\PersonCollection;
use App\Models\Person;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Exception;
class PublicSearchController extends Controller
{
/**
* Search for persons with various filters
*
* @param Request $request
* @return JsonResponse
*/
public function search(Request $request): JsonResponse
{
try {
$query = $this->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);
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Cors
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Add CORS headers to the response
$response->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;
}
}

View File

@ -17,10 +17,7 @@ class ResidenceResource extends JsonResource
return $this->resource ? [ return $this->resource ? [
'residence_id' => $this->residence_id, 'residence_id' => $this->residence_id,
'person_id' => $this->person_id, 'person_id' => $this->person_id,
'darwin' => $this->darwin, 'town_or_city' => $this->town_or_city,
'katherine' => $this->katherine,
'tennant_creek' => $this->tennant_creek,
'alice_springs' => $this->alice_springs,
'home_at_death' => $this->home_at_death, 'home_at_death' => $this->home_at_death,
'created_at' => $this->created_at, 'created_at' => $this->created_at,
'updated_at' => $this->updated_at, 'updated_at' => $this->updated_at,

View File

@ -15,19 +15,11 @@ class Residence extends Model
protected $fillable = [ protected $fillable = [
'person_id', 'person_id',
'darwin', 'town_or_city',
'katherine',
'tennant_creek',
'alice_springs',
'home_at_death', 'home_at_death',
]; ];
protected $casts = [ protected $casts = [];
'darwin' => 'boolean',
'katherine' => 'boolean',
'tennant_creek' => 'boolean',
'alice_springs' => 'boolean',
];
// Relationship // Relationship
public function person() public function person()

View File

@ -5,6 +5,7 @@ use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
use App\Http\Middleware\Cors;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -14,7 +15,16 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->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: [ $middleware->api(append: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]); ]);

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('residence', function (Blueprint $table) {
// Add the new town_or_city column
$table->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');
});
}
};

View File

@ -3,24 +3,16 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PersonController; use App\Http\Controllers\PersonController;
use App\Http\Controllers\PublicSearchController;
use App\Http\Controllers\AuthController; 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'); Route::post('/login', [AuthController::class, 'login'])->name('login');
// Public search endpoint - allows searching without authentication // Public search endpoints - allow searching without authentication
Route::get('/persons/search', [PersonController::class, 'publicSearch'])->name('persons.public.search'); 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 // Protected routes - require Sanctum authentication
Route::middleware('auth:sanctum')->group(function () { Route::middleware('auth:sanctum')->group(function () {

View File

@ -53,9 +53,8 @@ class PublicSearchApiTest extends TestCase
Residence::create([ Residence::create([
'person_id' => $person1->person_id, 'person_id' => $person1->person_id,
'address' => '123 Main St', 'town_or_city' => 'Sydney',
'suburb' => 'Sydney', 'home_at_death' => 'Sydney, NSW',
'state' => 'NSW',
]); ]);
// Person 2: Maria Mueller from Austria, arrived in 1885 at age 22, settled in Melbourne // 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([ Residence::create([
'person_id' => $person2->person_id, 'person_id' => $person2->person_id,
'address' => '456 High St', 'town_or_city' => 'Melbourne',
'suburb' => 'Melbourne', 'home_at_death' => 'Melbourne, VIC',
'state' => 'VIC',
]); ]);
// Person 3: Robert Johnson from England, arrived in 1890 at age 30, settled in Brisbane // 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([ Residence::create([
'person_id' => $person3->person_id, 'person_id' => $person3->person_id,
'address' => '789 Queen St', 'town_or_city' => 'Brisbane',
'suburb' => 'Brisbane', 'home_at_death' => 'Brisbane, QLD',
'state' => 'QLD',
]); ]);
} }
@ -194,17 +191,51 @@ class PublicSearchApiTest extends TestCase
*/ */
public function test_filter_by_settlement_location() 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 // First, verify we have all records without filters
$response = $this->getJson('/api/persons/search'); $response = $this->getJson('/api/persons/search');
$response->assertStatus(200) $response->assertStatus(200)
->assertJsonCount(3, 'data.data'); ->assertJsonCount(3, 'data.data');
// Now test with a more specific test that shouldn't depend on SQL dialect // Now test filtering by settlement location using the new town_or_city field
$response = $this->getJson('/api/persons/search?lastName=Johnson'); $response = $this->getJson('/api/persons/search?settlementLocation=Sydney');
$response->assertStatus(200) $response->assertStatus(200)
->assertJsonCount(1, 'data.data') ->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'); ->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. * 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.' '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&regionOfOrigin=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');
}
} }