feat: support partial patient patch updates
Implement true PATCH behavior so omitted fields stay unchanged, while null can explicitly clear nullable nested data. Align patient update tests and OpenAPI schemas/responses with the new 200/400/404 contract.
This commit is contained in:
parent
ae56e34885
commit
e99a60fe93
@ -6,7 +6,7 @@ use CodeIgniter\Controller;
|
|||||||
use App\Libraries\ValueSet;
|
use App\Libraries\ValueSet;
|
||||||
use App\Models\Patient\PatientModel;
|
use App\Models\Patient\PatientModel;
|
||||||
|
|
||||||
class PatientController extends Controller {
|
class PatientController extends Controller {
|
||||||
use ResponseTrait;
|
use ResponseTrait;
|
||||||
|
|
||||||
protected $db;
|
protected $db;
|
||||||
@ -110,7 +110,7 @@ class PatientController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function update($InternalPID = null) {
|
public function update($InternalPID = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true) ?? [];
|
||||||
|
|
||||||
if (!$InternalPID || !ctype_digit((string) $InternalPID)) {
|
if (!$InternalPID || !ctype_digit((string) $InternalPID)) {
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
@ -119,30 +119,97 @@ class PatientController extends Controller {
|
|||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$input['InternalPID'] = (int) $InternalPID;
|
if (!is_array($input) || $input === []) {
|
||||||
|
return $this->respond([
|
||||||
// Khusus untuk Override PATIDT
|
'status' => 'failed',
|
||||||
$type = $input['PatIdt']['IdentifierType'] ?? null;
|
'message' => 'Patch payload is required.'
|
||||||
$identifierRulesMap = $this->getPatIdtIdentifierRulesMap();
|
], 400);
|
||||||
if ($type === null || $type === '' || !is_string($type)) {
|
}
|
||||||
$identifierRule = 'permit_empty|max_length[255]';
|
|
||||||
$this->rules['PatIdt.IdentifierType'] = 'permit_empty';
|
if (array_key_exists('PatIdt', $input) && $input['PatIdt'] !== null && !is_array($input['PatIdt'])) {
|
||||||
$this->rules['PatIdt.Identifier'] = $identifierRule;
|
return $this->failValidationErrors([
|
||||||
} else {
|
'PatIdt' => 'PatIdt must be an object or null.'
|
||||||
$identifierRule = $identifierRulesMap[$type] ?? 'permit_empty|max_length[255]';
|
]);
|
||||||
$this->rules['PatIdt.IdentifierType'] = 'required';
|
}
|
||||||
$this->rules['PatIdt.Identifier'] = $identifierRule;
|
|
||||||
}
|
$patchRules = $this->buildPatchRules($input);
|
||||||
|
if ($patchRules !== [] && !$this->validateData($input, $patchRules)) {
|
||||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
return $this->failValidationErrors($this->validator->getErrors());
|
||||||
try {
|
}
|
||||||
$InternalPID = $this->model->updatePatient($input);
|
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data $InternalPID update successfully" ]);
|
try {
|
||||||
|
$updatedPid = $this->model->updatePatientPartial((int) $InternalPID, $input);
|
||||||
|
if ($updatedPid === null) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => "data $InternalPID not found"
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond([ 'status' => 'success', 'message' => "data $updatedPid update successfully" ], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPatchRules(array $input): array
|
||||||
|
{
|
||||||
|
$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]',
|
||||||
|
'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]',
|
||||||
|
'NameMiddle' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
|
||||||
|
'NameMaiden' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
|
||||||
|
'NameLast' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
|
||||||
|
'Suffix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
|
||||||
|
'PlaceOfBirth' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[100]',
|
||||||
|
'Citizenship' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[100]',
|
||||||
|
'Street_1' => 'permit_empty|regex_match[/^[A-Za-z0-9\'.,\/\- ]+$/]|max_length[255]',
|
||||||
|
'Street_2' => 'permit_empty|regex_match[/^[A-Za-z0-9\'.,\/\- ]+$/]|max_length[255]',
|
||||||
|
'Street_3' => 'permit_empty|regex_match[/^[A-Za-z0-9\'.,\/\- ]+$/]|max_length[255]',
|
||||||
|
'EmailAddress1' => 'permit_empty|valid_email|max_length[100]',
|
||||||
|
'EmailAddress2' => 'permit_empty|valid_email|max_length[100]',
|
||||||
|
'Birthdate' => 'permit_empty',
|
||||||
|
'ZIP' => 'permit_empty|is_natural|max_length[10]',
|
||||||
|
'Phone' => 'permit_empty|regex_match[/^\\+?[0-9]{8,15}$/]',
|
||||||
|
'MobilePhone' => 'permit_empty|regex_match[/^\\+?[0-9]{8,15}$/]',
|
||||||
|
'Country' => 'permit_empty|max_length[10]',
|
||||||
|
'Race' => 'permit_empty|max_length[100]',
|
||||||
|
'MaritalStatus' => 'permit_empty',
|
||||||
|
'Religion' => 'permit_empty|max_length[100]',
|
||||||
|
'Ethnic' => 'permit_empty|max_length[100]',
|
||||||
|
'isDead' => 'permit_empty',
|
||||||
|
'TimeOfDeath' => 'permit_empty',
|
||||||
|
'PatCom' => 'permit_empty|string',
|
||||||
|
'PatAtt' => 'permit_empty',
|
||||||
|
'LinkTo' => 'permit_empty',
|
||||||
|
'Custodian' => 'permit_empty',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($fieldRules as $field => $rule) {
|
||||||
|
if (array_key_exists($field, $input) && $field !== 'PatIdt') {
|
||||||
|
$rules[$field] = $rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('PatIdt', $input) && $input['PatIdt'] !== null) {
|
||||||
|
$type = $input['PatIdt']['IdentifierType'] ?? null;
|
||||||
|
$identifierRulesMap = $this->getPatIdtIdentifierRulesMap();
|
||||||
|
$identifierRule = is_string($type)
|
||||||
|
? ($identifierRulesMap[$type] ?? 'required|max_length[255]')
|
||||||
|
: 'required|max_length[255]';
|
||||||
|
|
||||||
|
$rules['PatIdt.IdentifierType'] = 'required';
|
||||||
|
$rules['PatIdt.Identifier'] = $identifierRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
|
||||||
private function getPatIdtIdentifierRulesMap(): array
|
private function getPatIdtIdentifierRulesMap(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -196,7 +196,7 @@ class PatientModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatePatient($input) {
|
public function updatePatient($input) {
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$modelPatIdt = new PatIdtModel();
|
$modelPatIdt = new PatIdtModel();
|
||||||
$modelPatCom = new PatComModel();
|
$modelPatCom = new PatComModel();
|
||||||
@ -363,6 +363,133 @@ class PatientModel extends BaseModel {
|
|||||||
return $date->format('j M Y');
|
return $date->format('j M Y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatePatientPartial(int $InternalPID, array $input): ?int {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$modelPatIdt = new PatIdtModel();
|
||||||
|
$modelPatCom = new PatComModel();
|
||||||
|
$modelPatAtt = new PatAttModel();
|
||||||
|
|
||||||
|
$patient = $this->find($InternalPID);
|
||||||
|
if (!$patient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasPatIdt = array_key_exists('PatIdt', $input);
|
||||||
|
$hasPatCom = array_key_exists('PatCom', $input);
|
||||||
|
$hasPatAtt = array_key_exists('PatAtt', $input);
|
||||||
|
|
||||||
|
$patIdt = $hasPatIdt ? $input['PatIdt'] : null;
|
||||||
|
$patCom = $hasPatCom ? $input['PatCom'] : null;
|
||||||
|
$patAtt = $hasPatAtt ? $input['PatAtt'] : null;
|
||||||
|
|
||||||
|
unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);
|
||||||
|
|
||||||
|
if (array_key_exists('Custodian', $input)) {
|
||||||
|
if (is_array($input['Custodian'])) {
|
||||||
|
$input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input['Custodian'] !== null && $input['Custodian'] !== '') {
|
||||||
|
$input['Custodian'] = (int) $input['Custodian'];
|
||||||
|
} else {
|
||||||
|
$input['Custodian'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('LinkTo', $input)) {
|
||||||
|
if (is_array($input['LinkTo'])) {
|
||||||
|
$internalPids = array_column($input['LinkTo'], 'InternalPID');
|
||||||
|
$internalPids = array_filter($internalPids, static fn($pid) => $pid !== null && $pid !== '');
|
||||||
|
$input['LinkTo'] = empty($internalPids) ? null : implode(',', $internalPids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedMap = array_flip($this->allowedFields);
|
||||||
|
$patientUpdate = array_intersect_key($input, $allowedMap);
|
||||||
|
unset($patientUpdate['CreateDate'], $patientUpdate['DelDate']);
|
||||||
|
|
||||||
|
$db->transBegin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$previousData = $this->find($InternalPID) ?? [];
|
||||||
|
|
||||||
|
if ($patientUpdate !== []) {
|
||||||
|
$this->where('InternalPID', $InternalPID)->set($patientUpdate)->update();
|
||||||
|
$this->checkDbError($db, 'Update patient');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPatIdt) {
|
||||||
|
if ($patIdt === null || $patIdt === []) {
|
||||||
|
$modelPatIdt->deletePatIdt((string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Delete patidt');
|
||||||
|
} else {
|
||||||
|
$modelPatIdt->updatePatIdt($patIdt, (string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Update patidt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPatCom) {
|
||||||
|
if ($patCom === null) {
|
||||||
|
$modelPatCom->deletePatCom((string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Delete patcom');
|
||||||
|
} else {
|
||||||
|
$modelPatCom->updatePatCom((string) $patCom, (string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Update PatCom');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPatAtt) {
|
||||||
|
if ($patAtt === null) {
|
||||||
|
$modelPatAtt->deletePatAtt((string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Delete patatt');
|
||||||
|
} else {
|
||||||
|
$modelPatAtt->updatePatAtt((array) $patAtt, (string) $InternalPID);
|
||||||
|
$this->checkDbError($db, 'Update/Delete patatt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$afterData = array_merge((array) $previousData, $patientUpdate);
|
||||||
|
$auditDiff = $this->buildAuditDiff((array) $previousData, $afterData);
|
||||||
|
$changedFields = array_column($auditDiff, 'field');
|
||||||
|
|
||||||
|
if ($hasPatIdt) {
|
||||||
|
$changedFields[] = 'PatIdt';
|
||||||
|
}
|
||||||
|
if ($hasPatCom) {
|
||||||
|
$changedFields[] = 'PatCom';
|
||||||
|
}
|
||||||
|
if ($hasPatAtt) {
|
||||||
|
$changedFields[] = 'PatAtt';
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditService::logData(
|
||||||
|
'PATIENT_DEMOGRAPHICS_UPDATED',
|
||||||
|
'UPDATE',
|
||||||
|
'patient',
|
||||||
|
(string) $InternalPID,
|
||||||
|
'patient',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'Patient data updated',
|
||||||
|
[
|
||||||
|
'diff' => $auditDiff,
|
||||||
|
'changed_fields' => array_values(array_unique($changedFields)),
|
||||||
|
'validation_profile' => 'patient.patch',
|
||||||
|
],
|
||||||
|
['entity_version' => 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
$db->transCommit();
|
||||||
|
|
||||||
|
return $InternalPID;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$db->transRollback();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildAuditDiff(array $before, array $after): array {
|
private function buildAuditDiff(array $before, array $after): array {
|
||||||
$diff = [];
|
$diff = [];
|
||||||
$fields = array_unique(array_merge(array_keys($before), array_keys($after)));
|
$fields = array_unique(array_merge(array_keys($before), array_keys($after)));
|
||||||
|
|||||||
@ -2907,7 +2907,7 @@ paths:
|
|||||||
patch:
|
patch:
|
||||||
tags:
|
tags:
|
||||||
- Patient
|
- Patient
|
||||||
summary: Update patient
|
summary: Partially update patient
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2922,10 +2922,14 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Patient'
|
$ref: '#/components/schemas/PatientPatch'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Patient updated successfully
|
description: Patient updated successfully
|
||||||
|
'400':
|
||||||
|
description: Validation error
|
||||||
|
'404':
|
||||||
|
description: Patient not found
|
||||||
/api/report/{orderID}:
|
/api/report/{orderID}:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@ -8322,6 +8326,155 @@ components:
|
|||||||
CreateDate:
|
CreateDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
PatientPatch:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Partial patient update payload.
|
||||||
|
Omitted fields are left unchanged. Send null explicitly to clear nullable fields.
|
||||||
|
properties:
|
||||||
|
PatientID:
|
||||||
|
type: string
|
||||||
|
maxLength: 30
|
||||||
|
pattern: ^[A-Za-z0-9]+$
|
||||||
|
description: Internal patient identifier
|
||||||
|
AlternatePID:
|
||||||
|
type: string
|
||||||
|
maxLength: 30
|
||||||
|
pattern: ^[A-Za-z0-9]+$
|
||||||
|
Prefix:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
enum:
|
||||||
|
- Mr
|
||||||
|
- Mrs
|
||||||
|
- Ms
|
||||||
|
- Dr
|
||||||
|
- Prof
|
||||||
|
Sex:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
description: '1: Female, 2: Male'
|
||||||
|
NameFirst:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
pattern: ^[A-Za-z'\. ]+$
|
||||||
|
NameMiddle:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
NameMaiden:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
NameLast:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
Suffix:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
Birthdate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: ISO 8601 UTC datetime
|
||||||
|
PlaceOfBirth:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Citizenship:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Street_1:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
Street_2:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
Street_3:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
ZIP:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
pattern: ^[0-9]+$
|
||||||
|
Phone:
|
||||||
|
type: string
|
||||||
|
pattern: ^\+?[0-9]{8,15}$
|
||||||
|
MobilePhone:
|
||||||
|
type: string
|
||||||
|
pattern: ^\+?[0-9]{8,15}$
|
||||||
|
EmailAddress1:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 100
|
||||||
|
EmailAddress2:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 100
|
||||||
|
PatIdt:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/PatientIdentifier'
|
||||||
|
nullable: true
|
||||||
|
LinkTo:
|
||||||
|
type: array
|
||||||
|
description: Array of linked patient references
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LinkedPatient'
|
||||||
|
Custodian:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/Custodian'
|
||||||
|
nullable: true
|
||||||
|
isDead:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- '0'
|
||||||
|
- '1'
|
||||||
|
description: '0: No (alive), 1: Yes (deceased)'
|
||||||
|
TimeOfDeath:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: ISO 8601 UTC datetime of death
|
||||||
|
PatCom:
|
||||||
|
type: string
|
||||||
|
description: Patient comment/notes
|
||||||
|
nullable: true
|
||||||
|
PatAtt:
|
||||||
|
type: array
|
||||||
|
description: Patient address entries
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PatAttEntry'
|
||||||
|
Province:
|
||||||
|
type: integer
|
||||||
|
description: Province AreaGeoID (foreign key to areageo table)
|
||||||
|
City:
|
||||||
|
type: integer
|
||||||
|
description: City AreaGeoID (foreign key to areageo table)
|
||||||
|
Country:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
description: Country ISO 3-letter code (e.g., IDN, USA)
|
||||||
|
Race:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
MaritalStatus:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- A
|
||||||
|
- B
|
||||||
|
- D
|
||||||
|
- M
|
||||||
|
- S
|
||||||
|
- W
|
||||||
|
description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed'
|
||||||
|
Religion:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Ethnic:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
TestMapDetail:
|
TestMapDetail:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -44,7 +44,7 @@ PatAttEntry:
|
|||||||
type: string
|
type: string
|
||||||
description: Address text
|
description: Address text
|
||||||
|
|
||||||
Patient:
|
Patient:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- PatientID
|
- PatientID
|
||||||
@ -180,9 +180,144 @@ Patient:
|
|||||||
Religion:
|
Religion:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 100
|
maxLength: 100
|
||||||
Ethnic:
|
Ethnic:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 100
|
maxLength: 100
|
||||||
|
|
||||||
|
PatientPatch:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Partial patient update payload.
|
||||||
|
Omitted fields are left unchanged. Send null explicitly to clear nullable fields.
|
||||||
|
properties:
|
||||||
|
PatientID:
|
||||||
|
type: string
|
||||||
|
maxLength: 30
|
||||||
|
pattern: '^[A-Za-z0-9]+$'
|
||||||
|
description: Internal patient identifier
|
||||||
|
AlternatePID:
|
||||||
|
type: string
|
||||||
|
maxLength: 30
|
||||||
|
pattern: '^[A-Za-z0-9]+$'
|
||||||
|
Prefix:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
enum: [Mr, Mrs, Ms, Dr, Prof]
|
||||||
|
Sex:
|
||||||
|
type: string
|
||||||
|
enum: ['1', '2']
|
||||||
|
description: '1: Female, 2: Male'
|
||||||
|
NameFirst:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
pattern: "^[A-Za-z'\\. ]+$"
|
||||||
|
NameMiddle:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
NameMaiden:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
NameLast:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 60
|
||||||
|
Suffix:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
Birthdate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: ISO 8601 UTC datetime
|
||||||
|
PlaceOfBirth:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Citizenship:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Street_1:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
Street_2:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
Street_3:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
ZIP:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
pattern: '^[0-9]+$'
|
||||||
|
Phone:
|
||||||
|
type: string
|
||||||
|
pattern: "^\\+?[0-9]{8,15}$"
|
||||||
|
MobilePhone:
|
||||||
|
type: string
|
||||||
|
pattern: "^\\+?[0-9]{8,15}$"
|
||||||
|
EmailAddress1:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 100
|
||||||
|
EmailAddress2:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
maxLength: 100
|
||||||
|
PatIdt:
|
||||||
|
allOf:
|
||||||
|
- $ref: 'patient.yaml#/PatientIdentifier'
|
||||||
|
nullable: true
|
||||||
|
LinkTo:
|
||||||
|
type: array
|
||||||
|
description: Array of linked patient references
|
||||||
|
items:
|
||||||
|
$ref: 'patient.yaml#/LinkedPatient'
|
||||||
|
Custodian:
|
||||||
|
allOf:
|
||||||
|
- $ref: 'patient.yaml#/Custodian'
|
||||||
|
nullable: true
|
||||||
|
isDead:
|
||||||
|
type: string
|
||||||
|
enum: ['0', '1']
|
||||||
|
description: '0: No (alive), 1: Yes (deceased)'
|
||||||
|
TimeOfDeath:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: ISO 8601 UTC datetime of death
|
||||||
|
PatCom:
|
||||||
|
type: string
|
||||||
|
description: Patient comment/notes
|
||||||
|
nullable: true
|
||||||
|
PatAtt:
|
||||||
|
type: array
|
||||||
|
description: Patient address entries
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
$ref: 'patient.yaml#/PatAttEntry'
|
||||||
|
Province:
|
||||||
|
type: integer
|
||||||
|
description: Province AreaGeoID (foreign key to areageo table)
|
||||||
|
City:
|
||||||
|
type: integer
|
||||||
|
description: City AreaGeoID (foreign key to areageo table)
|
||||||
|
Country:
|
||||||
|
type: string
|
||||||
|
maxLength: 10
|
||||||
|
description: Country ISO 3-letter code (e.g., IDN, USA)
|
||||||
|
Race:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
MaritalStatus:
|
||||||
|
type: string
|
||||||
|
enum: [A, B, D, M, S, W]
|
||||||
|
description: 'A: Annulled, B: Separated, D: Divorced, M: Married, S: Single, W: Widowed'
|
||||||
|
Religion:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
Ethnic:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
|
||||||
PatientListResponse:
|
PatientListResponse:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags: [Patient]
|
tags: [Patient]
|
||||||
summary: Update patient
|
summary: Partially update patient
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -165,7 +165,11 @@
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/patient.yaml#/Patient'
|
$ref: '../components/schemas/patient.yaml#/PatientPatch'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Patient updated successfully
|
description: Patient updated successfully
|
||||||
|
'400':
|
||||||
|
description: Validation error
|
||||||
|
'404':
|
||||||
|
description: Patient not found
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/999999', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/999999', $payload);
|
||||||
|
|
||||||
$result->assertStatus(201); // Update returns success even if no rows found (depending on logic)
|
$result->assertStatus(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,7 +99,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
$result->assertStatus(201);
|
$result->assertStatus(200);
|
||||||
$json = $result->getJSON();
|
$json = $result->getJSON();
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
$this->assertEquals('success', $data['status']);
|
$this->assertEquals('success', $data['status']);
|
||||||
@ -141,7 +141,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
$result->assertStatus(201);
|
$result->assertStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,7 +178,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
$result->assertStatus(201);
|
$result->assertStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -217,7 +217,7 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
$result->assertStatus(201);
|
$result->assertStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdatePatientWithSimIdentifierSuccess()
|
public function testUpdatePatientWithSimIdentifierSuccess()
|
||||||
@ -242,7 +242,25 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
];
|
];
|
||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
$result->assertStatus(201);
|
$result->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPatchSingleFieldSuccess()
|
||||||
|
{
|
||||||
|
$faker = Factory::create('id_ID');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'NameFirst' => $faker->firstName,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
|
$result->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPatchEmptyPayloadShouldFail()
|
||||||
|
{
|
||||||
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', []);
|
||||||
|
$result->assertStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdatePatientWithSimIdentifierInvalidShouldFail()
|
public function testUpdatePatientWithSimIdentifierInvalidShouldFail()
|
||||||
@ -312,14 +330,11 @@ class PatientUpdateTest extends CIUnitTestCase
|
|||||||
|
|
||||||
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
$result = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/1', $payload);
|
||||||
|
|
||||||
$result->assertStatus(500);
|
$result->assertStatus(400);
|
||||||
$json = $result->getJSON();
|
$json = $result->getJSON();
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
if (isset($data['messages']) && is_array($data['messages'])) {
|
$this->assertArrayHasKey('messages', $data);
|
||||||
$this->assertArrayHasKey('error', $data['messages']);
|
$this->assertArrayHasKey('PatIdt.IdentifierType', $data['messages']);
|
||||||
} else {
|
|
||||||
$this->assertArrayHasKey('error', $data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user