From c49743bbf394df45ae0c3d9cf9086cc48706ac05 Mon Sep 17 00:00:00 2001
From: mahdahar <89adham@gmail.com>
Date: Mon, 13 Apr 2026 11:25:41 +0700
Subject: [PATCH] fix: expand patient check matching and stabilize tests\n\n-
allow hyphens and dots in patient identifiers\n- support email and phone
lookups in patient existence checks\n- update OpenAPI docs and feature tests
for the new request contract\n- load .env during PHPUnit bootstrap so the
test database config is available
---
app/Controllers/Patient/PatientController.php | 108 +++---
phpunit.xml.dist | 11 +-
public/api-docs.bundled.yaml | 19 +-
public/paths/patients.yaml | 47 ++-
tests/feature/Patients/PatientCheckTest.php | 320 +++++++-----------
tests/phpunit-bootstrap.php | 4 +
6 files changed, 233 insertions(+), 276 deletions(-)
diff --git a/app/Controllers/Patient/PatientController.php b/app/Controllers/Patient/PatientController.php
index 27666c5..e10f931 100755
--- a/app/Controllers/Patient/PatientController.php
+++ b/app/Controllers/Patient/PatientController.php
@@ -17,8 +17,8 @@ class PatientController extends Controller {
$this->db = \Config\Database::connect();
$this->model = new PatientModel();
$this->rules = [
- 'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
- 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
+ 'PatientID' => 'required|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
+ 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
'Sex' => 'required',
@@ -157,8 +157,8 @@ class PatientController extends Controller {
$rules = [];
$fieldRules = [
- 'PatientID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
- 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]',
+ 'PatientID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
+ 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
'Sex' => 'permit_empty',
'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
@@ -252,46 +252,66 @@ class PatientController extends Controller {
}
}
- public function patientCheck() {
- try {
- $PatientID = $this->request->getVar('PatientID');
- $EmailAddress1 = $this->request->getVar('EmailAddress1');
-
- $tableName = '';
- $searchName = '';
-
- if (!empty($PatientID)){
- $tableName = 'PatientID';
- $searchName = $PatientID;
- } elseif (!empty($EmailAddress1)){
- $tableName = 'EmailAddress1';
- $searchName = $EmailAddress1;
- } else {
- return $this->respond([
- 'status' => 'error',
- 'message' => 'PatientID or EmailAddress1 parameter is required.',
- 'data' => null
- ], 400);
- }
-
- $patient = $this->db->table('patient')
- ->where($tableName, $searchName)
- ->get()
- ->getRowArray();
-
- if (!$patient) {
- return $this->respond([
- 'status' => 'success',
- 'message' => "$tableName not found.",
- 'data' => true,
- ], 200);
- }
-
- return $this->respond([
- 'status' => 'success',
- 'message' => "$tableName already exists.",
- 'data' => false,
- ], 200);
+ public function patientCheck() {
+ try {
+ $PatientID = $this->request->getVar('PatientID');
+ $EmailAddress = $this->request->getVar('EmailAddress');
+ $EmailAddress1 = $this->request->getVar('EmailAddress1');
+ $EmailAddress2 = $this->request->getVar('EmailAddress2');
+ $Phone = $this->request->getVar('Phone');
+
+ if (!empty($PatientID)){
+ if (!preg_match('/^[A-Za-z0-9.-]+$/', (string) $PatientID)) {
+ return $this->respond([
+ 'status' => 'error',
+ 'message' => 'PatientID format is invalid.',
+ 'data' => null
+ ], 400);
+ }
+
+ $patient = $this->db->table('patient')
+ ->where('PatientID', $PatientID)
+ ->get()
+ ->getRowArray();
+ } elseif (!empty($EmailAddress) || !empty($EmailAddress1) || !empty($EmailAddress2)){
+ $searchEmail = $EmailAddress ?: $EmailAddress1 ?: $EmailAddress2;
+
+ $patient = $this->db->table('patient')
+ ->groupStart()
+ ->where('EmailAddress1', $searchEmail)
+ ->orWhere('EmailAddress2', $searchEmail)
+ ->groupEnd()
+ ->get()
+ ->getRowArray();
+ } elseif (!empty($Phone)){
+ $patient = $this->db->table('patient')
+ ->groupStart()
+ ->where('Phone', $Phone)
+ ->orWhere('MobilePhone', $Phone)
+ ->groupEnd()
+ ->get()
+ ->getRowArray();
+ } else {
+ return $this->respond([
+ 'status' => 'error',
+ 'message' => 'PatientID, EmailAddress, or Phone parameter is required.',
+ 'data' => null
+ ], 400);
+ }
+
+ if (!$patient) {
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => !empty($PatientID) ? 'PatientID not found.' : (!empty($Phone) ? 'Phone not found.' : 'EmailAddress not found.'),
+ 'data' => true,
+ ], 200);
+ }
+
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => !empty($PatientID) ? 'PatientID already exists.' : (!empty($Phone) ? 'Phone already exists.' : 'EmailAddress already exists.'),
+ 'data' => false,
+ ], 200);
} catch (\Exception $e) {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index c8d761d..d669e73 100755
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -54,12 +54,5 @@
-
-
-
-
-
-
-
-
-
+
+
diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml
index db3d6bc..8a827b5 100755
--- a/public/api-docs.bundled.yaml
+++ b/public/api-docs.bundled.yaml
@@ -2931,12 +2931,29 @@ paths:
schema:
type: string
description: Patient ID to check
- - name: EmailAddress1
+ - name: EmailAddress
in: query
schema:
type: string
format: email
description: Email address to check
+ - name: EmailAddress1
+ in: query
+ schema:
+ type: string
+ format: email
+ description: Email address alias to check
+ - name: EmailAddress2
+ in: query
+ schema:
+ type: string
+ format: email
+ description: Email address alias to check
+ - name: Phone
+ in: query
+ schema:
+ type: string
+ description: Phone number to check
responses:
'200':
description: Patient check result
diff --git a/public/paths/patients.yaml b/public/paths/patients.yaml
index 67892c1..537ef2e 100755
--- a/public/paths/patients.yaml
+++ b/public/paths/patients.yaml
@@ -94,24 +94,41 @@
/api/patient/check:
get:
tags: [Patient]
- summary: Check if patient exists
+ summary: Check if patient exists
security:
- bearerAuth: []
parameters:
- - name: PatientID
- in: query
- schema:
- type: string
- description: Patient ID to check
- - name: EmailAddress1
- in: query
- schema:
- type: string
- format: email
- description: Email address to check
- responses:
- '200':
- description: Patient check result
+ - name: PatientID
+ in: query
+ schema:
+ type: string
+ description: Patient ID to check
+ - name: EmailAddress
+ in: query
+ schema:
+ type: string
+ format: email
+ description: Email address to check
+ - name: EmailAddress1
+ in: query
+ schema:
+ type: string
+ format: email
+ description: Email address alias to check
+ - name: EmailAddress2
+ in: query
+ schema:
+ type: string
+ format: email
+ description: Email address alias to check
+ - name: Phone
+ in: query
+ schema:
+ type: string
+ description: Phone number to check
+ responses:
+ '200':
+ description: Patient check result
content:
application/json:
schema:
diff --git a/tests/feature/Patients/PatientCheckTest.php b/tests/feature/Patients/PatientCheckTest.php
index e88a430..691b15c 100755
--- a/tests/feature/Patients/PatientCheckTest.php
+++ b/tests/feature/Patients/PatientCheckTest.php
@@ -1,213 +1,119 @@
-withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'PatientID' => 'SMAJ1',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertSame(false, $data['data']);
}
-
- /**
- * Test Case 1: Check existing PatientID
- * Expected: 200 OK with data: false (already exists)
- */
- public function testCheckPatientIDExists()
- {
- // Assuming SMAJ1 exists in the test database
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => 'SMAJ1'
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- $this->assertEquals('success', $data['status']);
- // If patient exists, data should be false (not available)
- if ($data['data'] === false) {
- $this->assertStringContainsString('already exists', $data['message']);
- }
- }
-
- /**
- * Test Case 2: Check non-existing PatientID
- * Expected: 200 OK with data: true (available)
- */
- public function testCheckPatientIDNotExists()
- {
- $faker = Factory::create();
-
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => 'NONEXISTENT' . $faker->numberBetween(100000, 999999)
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- $this->assertEquals('success', $data['status']);
- $this->assertTrue($data['data']); // Available means true
- $this->assertStringContainsString('not found', $data['message']);
- }
-
- /**
- * Test Case 3: Check existing Email
- * Expected: 200 OK with data: false (already exists)
- *
- * NOTE: Requires a known email in the database
- */
- public function testCheckEmailFormat()
- {
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'EmailAddress1' => 'test@example.com'
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- $this->assertEquals('success', $data['status']);
- $this->assertIsBool($data['data']);
- }
-
- /**
- * Test Case 4: Check non-existing Email
- * Expected: 200 OK with data: true (available)
- */
- public function testCheckEmailNotExists()
- {
- $faker = Factory::create();
-
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'EmailAddress1' => 'nonexistent_' . $faker->numberBetween(100000, 999999) . '@test.com'
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- $this->assertEquals('success', $data['status']);
- $this->assertTrue($data['data']); // Available means true
- $this->assertStringContainsString('not found', $data['message']);
- }
-
- /**
- * Test Case 5: Check with empty PatientID
- * Expected: May return 500 or handle gracefully
- */
- public function testCheckWithEmptyPatientID()
- {
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => ''
- ]);
-
- // Should either return 200 (graceful handling) or 500 (error)
- // Workaround for environment wrapping JSON in HTML causing null status code
- if ($result->getStatusCode() === null) {
- $body = strip_tags($result->getBody());
- $data = json_decode($body, true);
- $this->assertEquals('error', $data['status'] ?? '');
- $this->assertEquals('PatientID or EmailAddress1 parameter is required.', $data['message'] ?? '');
- return;
- }
-
- $this->assertContains($result->getStatusCode(), [200, 400, 500]);
- }
-
- /**
- * Test Case 6: Check for response structure
- * Expected: Response should have status, message, and data keys
- */
- public function testCheckResponseStructure()
- {
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => 'TEST123'
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- $this->assertArrayHasKey('status', $data);
- $this->assertArrayHasKey('message', $data);
- $this->assertArrayHasKey('data', $data);
- }
-
- /**
- * Test Case 7: Check with special characters in PatientID
- * Expected: Should handle gracefully
- */
- public function testCheckWithSpecialCharacters()
- {
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => "TEST'123"
- ]);
-
- // Should not crash - either 200 or appropriate error
- // Workaround for environment wrapping
- if ($result->getStatusCode() === null) {
- $body = strip_tags($result->getBody());
- $data = json_decode($body, true);
- $this->assertEquals('success', $data['status'] ?? '');
- // message could be 'PatientID not found.' or '... already exists.'
- $this->assertArrayHasKey('message', $data);
- return;
- }
-
-
- $this->assertContains($result->getStatusCode(), [200, 400, 500]);
- }
-
- /**
- * Test Case 8: Check with very long PatientID
- * Expected: Should handle gracefully
- */
- public function testCheckWithVeryLongPatientID()
- {
- $longId = str_repeat('A', 100);
-
- $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
- 'PatientID' => $longId
- ]);
-
- $result->assertStatus(200);
-
- $json = $result->getJSON();
- $data = json_decode($json, true);
-
- // Long ID that doesn't exist should return true (available)
- $this->assertEquals('success', $data['status']);
- }
-}
+
+ public function testCheckPatientIDWithHyphenExists()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'PatientID' => 'SMAJ-1',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertIsBool($data['data']);
+ }
+
+ public function testCheckPatientIDNotExists()
+ {
+ $faker = Factory::create();
+
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'PatientID' => 'NONEXISTENT-' . $faker->numberBetween(100000, 999999),
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertTrue($data['data']);
+ }
+
+ public function testCheckEmailAddressExists()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'EmailAddress' => 'dummy1@test.com',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertIsBool($data['data']);
+ }
+
+ public function testCheckEmailAddressAliasExists()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'EmailAddress1' => 'dummy1@test.com',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertIsBool($data['data']);
+ }
+
+ public function testCheckPhoneExists()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'Phone' => '092029',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('success', $data['status']);
+ $this->assertIsBool($data['data']);
+ }
+
+ public function testCheckWithoutParams()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint);
+
+ $result->assertStatus(400);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertSame('error', $data['status']);
+ $this->assertSame('PatientID, EmailAddress, or Phone parameter is required.', $data['message']);
+ }
+
+ public function testCheckResponseStructure()
+ {
+ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
+ 'PatientID' => 'TEST123',
+ ]);
+
+ $result->assertStatus(200);
+ $data = json_decode($result->getJSON(), true);
+
+ $this->assertArrayHasKey('status', $data);
+ $this->assertArrayHasKey('message', $data);
+ $this->assertArrayHasKey('data', $data);
+ }
+}
diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php
index ef88ffd..471792f 100755
--- a/tests/phpunit-bootstrap.php
+++ b/tests/phpunit-bootstrap.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php';
+use CodeIgniter\Config\DotEnv;
+use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\MigrationRunner;
use Config\Database;
use Config\Migrations as MigrationsConfig;
@@ -14,6 +16,8 @@ if (defined('CLQMS_PHPUNIT_BOOTSTRAPPED') || ENVIRONMENT !== 'testing') {
define('CLQMS_PHPUNIT_BOOTSTRAPPED', true);
+(new DotEnv(ROOTPATH))->load();
+
$db = Database::connect('tests');
$forge = Database::forge('tests');