diff --git a/app/Controllers/Test/TestMapController.php b/app/Controllers/Test/TestMapController.php index 7af0c8b..a7fc61b 100644 --- a/app/Controllers/Test/TestMapController.php +++ b/app/Controllers/Test/TestMapController.php @@ -17,38 +17,41 @@ class TestMapController extends BaseController { public function __construct() { $this->db = \Config\Database::connect(); - $this->model = new TestMapModel; - $this->modelDetail = new TestMapDetailModel; + $this->model = new TestMapModel; + $this->modelDetail = new TestMapDetailModel; $this->rules = [ - 'TestCode' => 'required', 'HostID' => 'required|integer', 'ClientID' => 'required|integer', ]; } - public function index() { - $rows = $this->model->getUniqueGroupings(); - if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } - - $rows = ValueSet::transformLabels($rows, [ - 'HostType' => 'entity_type', - 'ClientType' => 'entity_type', - ]); - - return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); - } + public function index() { + $rows = $this->model->getUniqueGroupings(); + if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } + + $rows = ValueSet::transformLabels($rows, [ + 'HostType' => 'entity_type', + 'ClientType' => 'entity_type', + ]); + + $rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows); + + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); + } public function show($id = null) { $row = $this->model->where('TestMapID',$id)->where('EndDate', null)->first(); if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); } - $row = ValueSet::transformLabels([$row], [ - 'HostType' => 'entity_type', - 'ClientType' => 'entity_type', - ])[0]; - - // Include testmapdetail records - $row['details'] = $this->modelDetail->getDetailsByTestMap($id); + $row = ValueSet::transformLabels([$row], [ + 'HostType' => 'entity_type', + 'ClientType' => 'entity_type', + ])[0]; + + $row = $this->sanitizeTopLevelPayload($row); + + // Include testmapdetail records + $row['details'] = $this->modelDetail->getDetailsByTestMap($id); return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200); } @@ -120,8 +123,16 @@ class TestMapController extends BaseController { 'ClientType' => 'entity_type', ]); + $rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows); + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); - } + } + + private function sanitizeTopLevelPayload(array $row): array + { + unset($row['TestCode'], $row['testcode']); + return $row; + } public function batchCreate() { $items = $this->request->getJSON(true); diff --git a/app/Controllers/Test/TestsController.php b/app/Controllers/Test/TestsController.php index e0c184d..c2a84d5 100644 --- a/app/Controllers/Test/TestsController.php +++ b/app/Controllers/Test/TestsController.php @@ -111,12 +111,15 @@ class TestsController extends BaseController $row['reftxt'] = $this->modelRefTxt->getFormattedByTestSiteID($id); } } - - return $this->respond([ - 'status' => 'success', - 'message' => 'Data fetched successfully', - 'data' => $row, - ], 200); + + // Keep /api/test payload focused on test definition fields. + unset($row['testmap']); + + return $this->respond([ + 'status' => 'success', + 'message' => 'Data fetched successfully', + 'data' => $row, + ], 200); } public function create() @@ -390,12 +393,8 @@ class TestsController extends BaseController break; - case 'TITLE': - if (isset($input['testmap']) && is_array($input['testmap'])) { - $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); - } - - break; + case 'TITLE': + break; case 'TEST': case 'PARAM': @@ -418,10 +417,7 @@ class TestsController extends BaseController break; } - if ((TestValidationService::isTechnicalTest($typeCode) || TestValidationService::isCalc($typeCode)) && isset($input['testmap']) && is_array($input['testmap'])) { - $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); - } - } + } private function saveTechDetails($testSiteID, $data, $action, $typeCode) { diff --git a/app/Models/Test/TestDefSiteModel.php b/app/Models/Test/TestDefSiteModel.php index 38a993c..532fc26 100644 --- a/app/Models/Test/TestDefSiteModel.php +++ b/app/Models/Test/TestDefSiteModel.php @@ -145,43 +145,32 @@ class TestDefSiteModel extends BaseModel { $typeCode = $row['TestType'] ?? ''; - if (TestValidationService::isCalc($typeCode)) { - $row['testdefcal'] = $db->table('testdefcal') - ->select('testdefcal.*, d.DisciplineName, dept.DepartmentName') - ->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left') - ->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left') - ->where('testdefcal.TestSiteID', $TestSiteID) - ->where('testdefcal.EndDate IS NULL') - ->get()->getResultArray(); - - $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); - - } elseif (TestValidationService::isGroup($typeCode)) { - $testDefGrpModel = new \App\Models\Test\TestDefGrpModel(); - $row['testdefgrp'] = $testDefGrpModel->getGroupMembers($TestSiteID); - - $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); - - } elseif (TestValidationService::isTitle($typeCode)) { - $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); - - } elseif (TestValidationService::isTechnicalTest($typeCode)) { - // Technical details are now flattened into the main row - if ($row['DisciplineID']) { - $discipline = $db->table('discipline')->where('DisciplineID', $row['DisciplineID'])->get()->getRowArray(); - $row['DisciplineName'] = $discipline['DisciplineName'] ?? null; + if (TestValidationService::isCalc($typeCode)) { + $row['testdefcal'] = $db->table('testdefcal') + ->select('testdefcal.*, d.DisciplineName, dept.DepartmentName') + ->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left') + ->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left') + ->where('testdefcal.TestSiteID', $TestSiteID) + ->where('testdefcal.EndDate IS NULL') + ->get()->getResultArray(); + + } elseif (TestValidationService::isGroup($typeCode)) { + $testDefGrpModel = new \App\Models\Test\TestDefGrpModel(); + $row['testdefgrp'] = $testDefGrpModel->getGroupMembers($TestSiteID); + + } elseif (TestValidationService::isTitle($typeCode)) { + + } elseif (TestValidationService::isTechnicalTest($typeCode)) { + // Technical details are now flattened into the main row + if ($row['DisciplineID']) { + $discipline = $db->table('discipline')->where('DisciplineID', $row['DisciplineID'])->get()->getRowArray(); + $row['DisciplineName'] = $discipline['DisciplineName'] ?? null; } - if ($row['DepartmentID']) { - $department = $db->table('department')->where('DepartmentID', $row['DepartmentID'])->get()->getRowArray(); - $row['DepartmentName'] = $department['DepartmentName'] ?? null; - } - - $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); - } + if ($row['DepartmentID']) { + $department = $db->table('department')->where('DepartmentID', $row['DepartmentID'])->get()->getRowArray(); + $row['DepartmentName'] = $department['DepartmentName'] ?? null; + } + } return $row; } diff --git a/docs/audit-logging.md b/docs/audit-logging.md deleted file mode 100644 index 162de27..0000000 --- a/docs/audit-logging.md +++ /dev/null @@ -1,352 +0,0 @@ -# Audit Logging Strategy (Implementation Ready) - -## 1) Purpose, Scope, and Non-Goals - -This document defines the production audit logging contract for CLQMS. - -### Purpose - -- Provide a single, normalized audit model for compliance, investigations, and operations. -- Ensure every protected workflow writes consistent, queryable audit records. -- Make behavior deterministic across API controllers, services, jobs, and integrations. - -### Scope - -This applies to four log tables: - -- `logpatient` - patient identity, demographics, consent, insurance, and visit/ADT events. -- `logorder` - orders, specimen lifecycle, results lifecycle, and QC. -- `logmaster` - test/master configuration, value sets, role/permission updates, infrastructure configuration. -- `logsystem` - authentication, authorization, import/export, jobs, and system integrity operations. - -### Non-goals - -- This is not a replacement for metrics/tracing systems (Prometheus, APM, etc.). -- This is not a full immutable ledger; tamper evidence is implemented with controls described below. - -## 2) Table Ownership - -Use this mapping to choose the target table and minimum event shape. - -| Event family | Table | Minimum keys in `Context` | Example `EventID` | -| --- | --- | --- | --- | -| Patient create/update/merge | `logpatient` | `route`, `request_id`, `entity_version` | `PATIENT_REGISTERED` | -| Consent/insurance changes | `logpatient` | `consent_type` or `payer_id` | `PATIENT_CONSENT_UPDATED` | -| Visit ADT transitions | `logpatient` | `visit_id`, `from_status`, `to_status` | `VISIT_TRANSFERRED` | -| Order create/cancel/reopen | `logorder` | `order_id`, `priority`, `source` | `ORDER_CREATED` | -| Specimen lifecycle | `logorder` | `specimen_id`, `specimen_status` | `SPECIMEN_RECEIVED` | -| Result lifecycle | `logorder` | `result_id`, `verification_state` | `RESULT_AMENDED` | -| QC lifecycle | `logorder` | `qc_run_id`, `instrument_id` | `QC_RECORDED` | -| Value sets/test definitions | `logmaster` | `config_group`, `change_ticket` | `VALUESET_ITEM_RETIRED` | -| Roles/permissions/users | `logmaster` | `target_user_id`, `target_role` | `USER_ROLE_CHANGED` | -| Login/logout/token/auth failures | `logsystem` | `auth_flow`, `failure_reason` (on failure) | `AUTH_LOGIN_FAILED` | -| Import/export/jobs/integration | `logsystem` | `batch_id`, `record_count`, `job_name` | `IMPORT_JOB_FINISHED` | -| Purge/archive/legal hold | `logsystem` | `archive_id`, `policy_name`, `approved_by` | `AUDIT_PURGE_EXECUTED` | - -## 3) Canonical Schema (All Four Tables) - -All four tables MUST implement the same logical columns. Physical PK name may vary (`LogPatientID`, `LogOrderID`, etc.). - -### 3.1 Column contract - -| Column | Type | Required | Max length | Description | Example | -| --- | --- | --- | --- | --- | --- | -| `LogID` (or table-specific PK) | `BIGINT UNSIGNED AUTO_INCREMENT` | Yes | N/A | Surrogate key per table | `987654` | -| `TblName` | `VARCHAR(64)` | Yes | 64 | Source business table | `patient` | -| `RecID` | `VARCHAR(64)` | Yes | 64 | Primary identifier of affected entity | `PAT000123` | -| `FldName` | `VARCHAR(128)` | Conditional | 128 | Changed field name, null for multi-field/bulk | `NameLast` | -| `FldValuePrev` | `TEXT` | Conditional | 65535 | Previous value (string or JSON) | `{"status":"PENDING"}` | -| `FldValueNew` | `TEXT` | Conditional | 65535 | New value (string or JSON) | `{"status":"VERIFIED"}` | -| `UserID` | `VARCHAR(64)` | Yes | 64 | Actor user id, or `SYSTEM` for non-user actions | `USR001` | -| `SiteID` | `VARCHAR(32)` | Yes | 32 | Facility/site context | `SITE01` | -| `DIDType` | `VARCHAR(32)` | No | 32 | Device identifier type | `UUID` | -| `DID` | `VARCHAR(128)` | No | 128 | Device identifier value | `6b8f...` | -| `MachineID` | `VARCHAR(128)` | No | 128 | Host/workstation identifier | `WS-LAB-07` | -| `SessionID` | `VARCHAR(128)` | Yes | 128 | Auth or workflow session identifier | `sess_abc123` | -| `AppID` | `VARCHAR(64)` | Yes | 64 | Calling client/application id | `clqms-api` | -| `ProcessID` | `VARCHAR(128)` | No | 128 | Process/workflow/job id | `job_20260325_01` | -| `WebPageID` | `VARCHAR(128)` | No | 128 | UI route/page id if user-driven | `patient-detail` | -| `EventID` | `VARCHAR(80)` | Yes | 80 | Canonical event code | `RESULT_RELEASED` | -| `ActivityID` | `VARCHAR(24)` | Yes | 24 | Canonical action enum | `UPDATE` | -| `Reason` | `VARCHAR(512)` | No | 512 | User/system reason or ticket reference | `Critical value corrected` | -| `LogDate` | `DATETIME(3)` | Yes | N/A | Event time in UTC | `2026-03-25 04:45:12.551` | -| `Context` | `JSON` (preferred) or `LONGTEXT` | Yes | N/A | Structured metadata payload | See section 5 | -| `IpAddress` | `VARCHAR(45)` | No | 45 | IPv4/IPv6 remote address | `10.10.2.44` | - -### 3.2 Required/conditional rules - -- `FldName`, `FldValuePrev`, and `FldValueNew` are required for single-field changes. -- For multi-field changes, set `FldName = NULL` and store a compact JSON diff under `Context.diff`. -- For non-mutating events (`READ`, `LOGIN`, `EXPORT`, `IMPORT`), `FldValuePrev` and `FldValueNew` may be null. -- `Context` is required for all rows. At minimum include `request_id` and `route` (or `job_name` for non-HTTP jobs). - -## 4) DDL Template and Indexing - -Use this template when creating a log table. Replace `${TABLE}` and `${PK}`. - -```sql -CREATE TABLE `${TABLE}` ( - `${PK}` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - `TblName` VARCHAR(64) NOT NULL, - `RecID` VARCHAR(64) NOT NULL, - `FldName` VARCHAR(128) NULL, - `FldValuePrev` TEXT NULL, - `FldValueNew` TEXT NULL, - `UserID` VARCHAR(64) NOT NULL, - `SiteID` VARCHAR(32) NOT NULL, - `DIDType` VARCHAR(32) NULL, - `DID` VARCHAR(128) NULL, - `MachineID` VARCHAR(128) NULL, - `SessionID` VARCHAR(128) NOT NULL, - `AppID` VARCHAR(64) NOT NULL, - `ProcessID` VARCHAR(128) NULL, - `WebPageID` VARCHAR(128) NULL, - `EventID` VARCHAR(80) NOT NULL, - `ActivityID` VARCHAR(24) NOT NULL, - `Reason` VARCHAR(512) NULL, - `LogDate` DATETIME(3) NOT NULL, - `Context` JSON NOT NULL, - `IpAddress` VARCHAR(45) NULL, - PRIMARY KEY (`${PK}`), - INDEX `idx_${TABLE}_logdate` (`LogDate`), - INDEX `idx_${TABLE}_recid_logdate` (`RecID`, `LogDate`), - INDEX `idx_${TABLE}_userid_logdate` (`UserID`, `LogDate`), - INDEX `idx_${TABLE}_eventid_logdate` (`EventID`, `LogDate`), - INDEX `idx_${TABLE}_site_logdate` (`SiteID`, `LogDate`) -); -``` - -Optional JSON path index (DB engine specific): - -- `Context.request_id` -- `Context.batch_id` -- `Context.job_name` - -## 5) Context JSON Contract - -`Context` MUST be valid JSON. Keep payload compact and predictable. - -### 5.1 Required keys for all events - -```json -{ - "request_id": "a4f5b6c7", - "route": "PATCH /api/patient/123", - "timestamp_utc": "2026-03-25T04:45:12.551Z", - "entity_type": "patient", - "entity_version": 7 -} -``` - -### 5.2 Additional keys by event class - -- Patient/order/result mutation: `diff` (array of changed fields), `validation_profile`. -- Import/export/jobs: `batch_id`, `record_count`, `success_count`, `failure_count`, `job_name`. -- Auth/security events: `auth_flow`, `failure_reason`, `token_type` (never token value). -- Retention operations: `policy_name`, `archive_id`, `approved_by`, `window_start`, `window_end`. - -### 5.3 Size and shape limits - -- Maximum serialized `Context` size: 16 KB. -- `diff` array should include only audited fields, not entire entity snapshots. -- Store references (`file_id`, `blob_ref`) instead of large payloads. - -## 6) Activity and Event Catalog Governance - -`EventID` values MUST come from the ValueSet library, not hardcoded inline strings. - -- Source file: `app/Libraries/Data/event_id.json` -- Runtime access: `\App\Libraries\ValueSet::getRaw('event_id')` -- Optional label lookup for reporting: `\App\Libraries\ValueSet::getLabel('event_id', $eventId)` - -### 6.1 Allowed `ActivityID` - -`CREATE`, `UPDATE`, `DELETE`, `READ`, `MERGE`, `SPLIT`, `CANCEL`, `REOPEN`, `VERIFY`, `AMEND`, `RETRACT`, `RELEASE`, `IMPORT`, `EXPORT`, `LOGIN`, `LOGOUT`, `LOCK`, `UNLOCK`, `RESET` - -### 6.2 `EventID` naming pattern - -- Format: `__` -- Character set: uppercase A-Z, numbers, underscore. -- Max length: 80. -- Examples: `PATIENT_DEMOGRAPHICS_UPDATED`, `ORDER_CANCELLED`, `AUTH_LOGIN_FAILED`. - -### 6.3 Catalog lifecycle - -- New `EventID` requires docs update and test coverage. -- New `EventID` must be added to `app/Libraries/Data/event_id.json` and deployed with cache refresh (`ValueSet::clearCache()`). -- Never repurpose an existing `EventID` to mean something else. -- Deprecated `EventID` remains queryable and documented for historical data. - -## 7) Minimum Event Coverage (Must Implement) - -### 7.1 `logpatient` - -- `PATIENT_REGISTERED`, `PATIENT_DEMOGRAPHICS_UPDATED`, `PATIENT_MERGED`, `PATIENT_UNMERGED` -- `PATIENT_IDENTIFIER_UPDATED`, `PATIENT_CONSENT_UPDATED`, `PATIENT_INSURANCE_UPDATED` -- `VISIT_ADMITTED`, `VISIT_TRANSFERRED`, `VISIT_DISCHARGED`, `VISIT_STATUS_UPDATED` - -### 7.2 `logorder` - -- `ORDER_CREATED`, `ORDER_CANCELLED`, `ORDER_REOPENED`, `ORDER_TEST_ADDED`, `ORDER_TEST_REMOVED` -- `SPECIMEN_COLLECTED`, `SPECIMEN_RECEIVED`, `SPECIMEN_REJECTED`, `SPECIMEN_ALIQUOTED`, `SPECIMEN_DISPOSED` -- `RESULT_ENTERED`, `RESULT_UPDATED`, `RESULT_VERIFIED`, `RESULT_AMENDED`, `RESULT_RELEASED`, `RESULT_RETRACTED`, `RESULT_CORRECTED` -- `QC_RECORDED`, `QC_FAILED`, `QC_OVERRIDE_APPLIED` - -### 7.3 `logmaster` - -- `VALUESET_ITEM_CREATED`, `VALUESET_ITEM_UPDATED`, `VALUESET_ITEM_RETIRED` -- `TEST_DEFINITION_UPDATED`, `REFERENCE_RANGE_UPDATED`, `TEST_PANEL_MEMBERSHIP_UPDATED` -- `ANALYZER_CONFIG_UPDATED`, `INTEGRATION_CONFIG_UPDATED`, `CODING_SYSTEM_UPDATED` -- `USER_CREATED`, `USER_DISABLED`, `USER_PASSWORD_RESET`, `USER_ROLE_CHANGED`, `USER_PERMISSION_CHANGED` -- `SITE_CREATED`, `SITE_UPDATED`, `WORKSTATION_UPDATED` - -### 7.4 `logsystem` - -- `AUTH_LOGIN_SUCCESS`, `AUTH_LOGOUT_SUCCESS`, `AUTH_LOGIN_FAILED`, `AUTH_LOCKOUT_TRIGGERED` -- `TOKEN_ISSUED`, `TOKEN_REFRESHED`, `TOKEN_REVOKED`, `AUTHORIZATION_FAILED` -- `IMPORT_JOB_STARTED`, `IMPORT_JOB_FINISHED`, `EXPORT_JOB_STARTED`, `EXPORT_JOB_FINISHED` -- `JOB_STARTED`, `JOB_FINISHED`, `INTEGRATION_SYNC_STARTED`, `INTEGRATION_SYNC_FINISHED` -- `AUDIT_ARCHIVE_EXECUTED`, `AUDIT_PURGE_EXECUTED`, `LEGAL_HOLD_APPLIED`, `LEGAL_HOLD_RELEASED` - -## 8) Capture Rules (Application Behavior) - -### 8.1 Write timing - -- For mutating transactions, write audit record in the same DB transaction where feasible. -- If asynchronous logging is required, enqueue within transaction and process with at-least-once delivery. - -### 8.2 Failure policy - -- Compliance-critical writes (patient, order, result, role/permission): fail request if audit write fails. -- Operational-only writes (non-critical job checkpoints): continue request, emit error log, retry in background. -- All audit write failures must produce `logsystem` event `AUDIT_WRITE_FAILED` with sanitized details. - -### 8.3 Diff policy - -- Single-field change: set `FldName`, `FldValuePrev`, `FldValueNew`. -- Multi-field change: set `FldName = NULL`, keep prev/new null or compact summary, place canonical diff in `Context.diff`. -- Bulk operations: include `batch_id`, `record_count`, sample `affected_ids` (capped), and source. - -## 9) Security and Privacy Controls - -### 9.1 Never log - -- Passwords, raw JWTs, API secrets, private keys, OTP values. -- Full clinical free text unless explicitly required by policy. - -### 9.2 Masking rules - -- Identifiers with high sensitivity should be masked in `FldValuePrev/New` when not required. -- Token-like strings should be fully removed and replaced with `[REDACTED]`. -- Use deterministic masking where correlation is needed (e.g., hash + prefix). - -### 9.3 Access control - -- Insert permissions only for API/service accounts. -- No update/delete privileges for regular runtime users. -- Read access to logs is role-restricted and audited. - -### 9.4 Tamper evidence - -- Enable DB audit on DDL changes to log tables. -- Store periodic checksum snapshots of recent log ranges in secure storage. -- Record checksum run outcomes in `logsystem` (`AUDIT_CHECKSUM_CREATED`, `AUDIT_CHECKSUM_FAILED`). - -## 10) Retention, Archive, and Purge - -### 10.1 Default retention - -- `logpatient`: 7 years -- `logorder`: 7 years -- `logmaster`: 5 years -- `logsystem`: 2 years - -If regional policy requires longer periods, policy overrides these defaults. - -### 10.2 Archive workflow - -1. Select eligible rows by `LogDate` and legal-hold status. -2. Export to immutable archive format (compressed JSONL or parquet). -3. Verify checksums and row counts. -4. Write `AUDIT_ARCHIVE_EXECUTED` entry in `logsystem`. - -### 10.3 Purge workflow - -1. Require approval reference (`approved_by`, `change_ticket`). -2. Purge archived rows only. -3. Write `AUDIT_PURGE_EXECUTED` entry with table, date window, count, and archive reference. - -## 11) Operational Monitoring - -Track these SLIs/SLOs: - -- Audit write success rate >= 99.9% for critical domains. -- P95 audit insert latency < 50 ms. -- Queue backlog age < 5 minutes (if async path is used). -- Zero unreviewed `AUDIT_WRITE_FAILED` older than 24 hours. - -Alert on: - -- Sustained write failures. -- Sudden drop in expected event volume. -- Purge/archive jobs without corresponding `logsystem` records. - -## 12) Migration Strategy for Existing Logs - -1. Inventory current columns and event vocabulary in all four tables. -2. Add missing canonical columns with nullable defaults. -3. Backfill required values (`AppID`, `SessionID`, `Context` minimum keys) where derivable. -4. Introduce canonical `EventID` mapping table for legacy names. -5. Enforce NOT NULL constraints only after backfill validation succeeds. - -## 13) Testing Requirements - -### 13.1 Automated tests - -- Feature tests for representative endpoints must assert audit row creation. -- Assert table target, `ActivityID`, `EventID`, `RecID`, and required `Context` keys. -- Assert `EventID` exists in `\App\Libraries\ValueSet::getRaw('event_id')`. -- Add negative tests for audit failure policy (critical path blocks, non-critical path retries). - -### 13.2 Test matrix minimum - -- One success and one failure scenario per major domain (`patient`, `order`, `master`, `system`). -- One bulk operation scenario validating `batch_id` and counts. -- One security scenario validating redaction of sensitive fields. - -## 14) Implementation Checklist (Phased) - -### Phase 1 - Schema and constants - -1. Create/align all four log tables to canonical schema. -2. Add shared enums/constants for `ActivityID` and `EventID`. -3. Add and maintain `app/Libraries/Data/event_id.json` as the `EventID` source of truth. -4. Add DB indexes listed in section 4. - -### Phase 2 - Audit service - -1. Implement centralized audit writer service. -2. Add helpers to normalize actor/device/session/context. -3. Add diff builder utility for single and multi-field changes. - -### Phase 3 - Instrumentation - -1. Instrument patient and order flows first (compliance-critical). -2. Instrument master and system flows. -3. Add fallback/retry path and `AUDIT_WRITE_FAILED` emission. - -### Phase 4 - Validation and rollout - -1. Add feature tests and failure-path tests. -2. Validate dashboards/queries for each table. -3. Release with runbook updates and retention job schedule. - -## 15) Acceptance Criteria - -The implementation is complete when all statements below are true: - -- Every protected endpoint emits at least one canonical audit row. -- Each row has valid `ActivityID`, `EventID` (present in ValueSet `event_id`), `LogDate` (UTC), and non-empty `Context` with required keys. -- Sensitive values are redacted/masked per section 9. -- Archive and purge operations are fully traceable in `logsystem`. -- Tests cover critical success/failure paths and pass in CI. diff --git a/docs/test-calc-engine.md b/docs/test-calc-engine.md deleted file mode 100644 index 94687a6..0000000 --- a/docs/test-calc-engine.md +++ /dev/null @@ -1,337 +0,0 @@ -# Calculator Service Operators Reference - -## Overview - -The `CalculatorService` (`app/Services/CalculatorService.php`) evaluates formulas with Symfony's `ExpressionLanguage`. This document lists the operators, functions, and constants that are available in the current implementation. - ---- - -## API Endpoints - -All endpoints live under `/api` and accept JSON. Responses use the standard `{ status, message, data }` envelope unless stated otherwise. - -### Calculate By Test Site - -Uses the `testdefcal` definition for a test site. The incoming body supplies the variables required by the formula. - -```http -POST /api/calc/testsite/123 -Content-Type: application/json - -{ - "result": 85, - "gender": "female", - "age": 30 -} -``` - -Response: - -```json -{ - "status": "success", - "data": { - "result": 92.4, - "testSiteID": 123, - "formula": "{result} * {factor} + {age}", - "variables": { - "result": 85, - "gender": "female", - "age": 30 - } - } -} -``` - -### Calculate By Code Or Name - -Evaluates a configured calculation by `TestSiteCode` or `TestSiteName`. Returns a compact map with a single key/value or `{}` on failure. - -```http -POST /api/calc/testcode/GLU -Content-Type: application/json - -{ - "result": 110, - "factor": 1.1 -} -``` - -Response: - -```json -{ - "GLU": 121 -} -``` - ---- - -## Supported Operators - -### Arithmetic Operators - -| Operator | Description | Example | Result | -|----------|-------------|---------|--------| -| `+` | Addition | `5 + 3` | `8` | -| `-` | Subtraction | `10 - 4` | `6` | -| `*` | Multiplication | `6 * 7` | `42` | -| `/` | Division | `20 / 4` | `5` | -| `%` | Modulo | `20 % 6` | `2` | -| `**` | Exponentiation (power) | `2 ** 3` | `8` | - -### Comparison Operators - -| Operator | Description | Example | -|----------|-------------|---------| -| `==` | Equal | `{result} == 10` | -| `!=` | Not equal | `{result} != 10` | -| `<` | Less than | `{result} < 10` | -| `<=` | Less than or equal | `{result} <= 10` | -| `>` | Greater than | `{result} > 10` | -| `>=` | Greater than or equal | `{result} >= 10` | - -### Logical Operators - -| Operator | Description | Example | -|----------|-------------|---------| -| `and` / `&&` | Logical AND | `{result} > 0 and {factor} > 0` | -| `or` / `||` | Logical OR | `{gender} == 1 or {gender} == 2` | -| `!` / `not` | Logical NOT | `not ({result} > 0)` | - -### Conditional Operators - -| Operator | Description | Example | -|----------|-------------|---------| -| `?:` | Ternary | `{result} > 10 ? {result} : 10` | -| `??` | Null coalescing | `{result} ?? 0` | - -### Parentheses - -Use parentheses to control operation precedence: - -``` -(2 + 3) * 4 // Result: 20 -2 + 3 * 4 // Result: 14 -``` - -### Notes - -- `^` is bitwise XOR (not exponentiation). Use `**` for powers. -- Variables must be numeric after normalization (gender is mapped to 0/1/2). - ---- - -## Functions - -Only the default ExpressionLanguage functions are available: - -| Function | Description | Example | -|----------|-------------|---------| -| `min(a, b, ...)` | Minimum value | `min({result}, 10)` | -| `max(a, b, ...)` | Maximum value | `max({result}, 10)` | -| `constant(name)` | PHP constant by name | `constant("PHP_INT_MAX")` | -| `enum(name)` | PHP enum case by name | `enum("App\\Enum\\Status::Active")` | - ---- - -## Constants - -ExpressionLanguage recognizes boolean and null literals: - -| Constant | Value | Description | -|----------|-------|-------------| -| `true` | `true` | Boolean true | -| `false` | `false` | Boolean false | -| `null` | `null` | Null value | - ---- - -## Variables in CalculatorService - -When using `calculateFromDefinition()`, the following variables are automatically available: - -| Variable | Description | Type | -|----------|-------------|------| -| `{result}` | The test result value | Float | -| `{factor}` | Calculation factor (default: 1) | Float | -| `{gender}` | Gender value (0=Unknown, 1=Female, 2=Male) | Integer | -| `{age}` | Patient age | Float | -| `{ref_low}` | Reference range low value | Float | -| `{ref_high}` | Reference range high value | Float | - -### Gender Mapping - -The `gender` variable accepts the following values: - -| Value | Description | -|-------|-------------| -| `0` | Unknown | -| `1` | Female | -| `2` | Male | - -Or use string values: `'unknown'`, `'female'`, `'male'` - ---- - -## Implicit Multiplication - -Implicit multiplication is not supported. Always use `*` between values: - -| Expression | Use Instead | -|------------|-------------| -| `2x` | `2 * x` | -| `{result}{factor}` | `{result} * {factor}` | - ---- - -## Usage Examples - -### Basic Calculation - -```php -use App\Services\CalculatorService; - -$calculator = new CalculatorService(); - -// Simple arithmetic -$result = $calculator->calculate("5 + 3 * 2"); -// Result: 11 - -// Using min/max -$result = $calculator->calculate("max({result}, 10)", ['result' => 7]); -// Result: 10 -``` - -### With Variables - -```php -$formula = "{result} * {factor} + 10"; -$variables = [ - 'result' => 5.2, - 'factor' => 2 -]; - -$result = $calculator->calculate($formula, $variables); -// Result: 20.4 -``` - -### BMI Calculation - -```php -$formula = "{weight} / ({height} ** 2)"; -$variables = [ - 'weight' => 70, // kg - 'height' => 1.75 // meters -]; - -$result = $calculator->calculate($formula, $variables); -// Result: 22.86 -``` - -### Gender-Based Calculation - -```php -// Apply different multipliers based on gender -$formula = "{result} * (1 + 0.1 * {gender})"; -$variables = [ - 'result' => 100, - 'gender' => 1 // Female = 1 -]; - -$result = $calculator->calculate($formula, $variables); -// Result: 110 -``` - -### Complex Formula - -```php -// Pythagorean theorem -$formula = "(({a} ** 2 + {b} ** 2) ** 0.5)"; -$variables = [ - 'a' => 3, - 'b' => 4 -]; - -$result = $calculator->calculate($formula, $variables); -// Result: 5 -``` - -### Using calculateFromDefinition - -```php -$calcDef = [ - 'FormulaCode' => '{result} * {factor} + {gender}', - 'Factor' => 2 -]; - -$testValues = [ - 'result' => 10, - 'gender' => 1 // Female -]; - -$result = $calculator->calculateFromDefinition($calcDef, $testValues); -// Result: 21 (10 * 2 + 1) -``` - ---- - -## Formula Validation - -Validate formulas before storing them: - -```php -$validation = $calculator->validate("{result} / {factor}"); -// Returns: ['valid' => true, 'error' => null] - -$validation = $calculator->validate("{result} /"); -// Returns: ['valid' => false, 'error' => 'Error message'] -``` - -### Extract Variables - -Get a list of variables used in a formula: - -```php -$variables = $calculator->extractVariables("{result} * {factor} + {age}"); -// Returns: ['result', 'factor', 'age'] -``` - ---- - -## Error Handling - -The service throws exceptions for invalid formulas or missing variables: - -```php -try { - $result = $calculator->calculate("{result} / 0"); -} catch (\Exception $e) { - // Handle division by zero or other errors - log_message('error', $e->getMessage()); -} -``` - -Common errors: - -- **Invalid formula syntax**: Malformed expressions -- **Missing variable**: Variable placeholder not provided in data array -- **Non-numeric value**: Variables must be numeric -- **Division by zero**: Mathematical error - ---- - -## Best Practices - -1. **Always validate formulas** before storing in database -2. **Use placeholder syntax** `{variable_name}` for clarity -3. **Handle exceptions** in production code -4. **Test edge cases** like zero values and boundary conditions -5. **Document formulas** with comments in your code - ---- - -## References - -- [Symfony ExpressionLanguage](https://symfony.com/doc/current/components/expression_language.html) -- `app/Services/CalculatorService.php` diff --git a/docs/test-rule-engine.md b/docs/test-rule-engine.md deleted file mode 100644 index bcf1968..0000000 --- a/docs/test-rule-engine.md +++ /dev/null @@ -1,421 +0,0 @@ -# Test Rule Engine Documentation - -## Overview - -The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches. - -Rules are authored using a domain specific language stored in `ruledef.ConditionExpr`. Before the platform executes any rule, the DSL must be compiled into JSON and stored in `ConditionExprCompiled`, and each rule must be linked to the tests it should influence via `testrule`. - -### Execution Flow - -1. Write or edit the DSL in `ConditionExpr`. -2. POST the expression to `POST /api/rule/compile` to validate syntax and produce compiled JSON. -3. Save the compiled payload into `ConditionExprCompiled` and persist the rule in `ruledef`. -4. Link the rule to one or more tests through `testrule.TestSiteID` (rules only run for linked tests). -5. When the configured event fires (`test_created` or `result_updated`), the engine evaluates `ConditionExprCompiled` and runs the resulting `then` or `else` actions. - -> **Note:** The rule engine currently fires only for `test_created` and `result_updated`. Other event codes can exist in the database but are not triggered by the application unless additional `RuleEngineService::run(...)` calls are added. - -## Event Triggers - -| Event Code | Status | Trigger Point | -|------------|--------|----------------| -| `test_created` | Active | Fired after a new test row is persisted; the handler calls `RuleEngineService::run('test_created', ...)` to evaluate test-scoped rules | -| `result_updated` | Active | Fired whenever a test result is saved or updated so result-dependent rules run immediately | - -Other event codes remain in the database for future workflows, but only `test_created` and `result_updated` are executed by the current application flow. - -## Rule Structure - -``` -Rule -├── Event Trigger (when to run) -├── Conditions (when to match) -└── Actions (what to do) -``` - -The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rule/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime. - -## Syntax Guide - -### Basic Format - -``` -if(condition; then-action; else-action) -``` - -### Logical Operators - -- Use `&&` for AND (all sub-conditions must match). -- Use `||` for OR (any matching branch satisfies the rule). -- Surround mixed logic with parentheses for clarity and precedence. - -### Multi-Action Syntax - -Actions within any branch are separated by `:` and evaluated in order. Every `then` and `else` branch must end with an action; use `nothing` when no further work is required. - -``` -if(sex('M'); result_set(0.5):test_insert('HBA1C'); nothing) -``` - -### Multiple Rules - -Create each rule as its own `ruledef` row; do not chain expressions with commas. The `testrule` table manages rule-to-test mappings, so multiple rules can attach to the same test. Example: - -1. Insert `RULE_MALE_RESULT` and `RULE_SENIOR_COMMENT` in `ruledef`. -2. Add two `testrule` rows linking each rule to the appropriate `TestSiteID`. - -Each rule compiles and runs independently when its trigger fires and the test is linked. - -## Available Functions - -### Conditions - -| Function | Description | Example | -|----------|-------------|---------| -| `sex('M'|'F')` | Match patient sex | `sex('M')` | -| `priority('R'|'S'|'U')` | Match order priority | `priority('S')` | -| `age > 18` | Numeric age comparisons (`>`, `<`, `>=`, `<=`) | `age >= 18 && age <= 65` | -| `requested('CODE')` | Check whether the order already requested a test (queries `patres`) | `requested('GLU')` | - -### Logical Operators - -| Operator | Meaning | Example | -|----------|---------|---------| -| `&&` | AND (all truthy) | `sex('M') && age > 40` | -| `||` | OR (any truthy) | `sex('M') || age > 65` | -| `()` | Group expressions | `(sex('M') && age > 40) || priority('S')` | - -## Actions - -| Action | Description | Example | -|--------|-------------|---------| -| `result_set(value)` | (deprecated) Write to `patres.Result` for the current context test | `result_set(0.5)` | -| `result_set('CODE', value)` | Target a specific test by `TestSiteCode`, allowing multiple tests to be updated in one rule | `result_set('tesA', 0.5)` | -| `test_insert('CODE')` | Insert a test row by `TestSiteCode` if it doesn’t already exist for the order | `test_insert('HBA1C')` | -| `test_delete('CODE')` | Remove a previously requested test from the current order when the rule deems it unnecessary | `test_delete('INS')` | -| `comment_insert('text')` | Insert an order comment (`ordercom`) describing priority or clinical guidance | `comment_insert('Male patient - review')` | -| `nothing` | Explicit no-op to terminate an action chain | `nothing` | - -> **Note:** `set_priority()` was removed. Use `comment_insert()` for priority notes without altering billing. - -## Runtime Requirements - -1. **Compiled expression required:** Rules without `ConditionExprCompiled` are ignored (see `RuleEngineService::run`). -2. **Order context:** `context.order.InternalOID` must exist for any action that writes to `patres` or `ordercom`. -3. **TestSiteID:** `result_set()` needs `testSiteID` (either provided in context or from `order.TestSiteID`). When you provide a `TestSiteCode` as the first argument (`result_set('tesA', value)`), the engine resolves that code before writing the result. `test_insert()` resolves a `TestSiteID` via the `TestSiteCode` in `TestDefSiteModel`, and `test_delete()` removes the matching `TestSiteID` rows when needed. -4. **Requested check:** `requested('CODE')` inspects `patres` rows for the same `OrderID` and `TestSiteCode`. - -## Examples - -``` -if(sex('M'); result_set('tesA', 0.5):result_set('tesB', 1.2); result_set('tesA', 0.6):result_set('tesB', 1.0)) -``` -Sets both `tesA`/`tesB` results together per branch. - -``` -if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing) -``` -Adds new tests when glucose is already requested. - -``` -if(sex('M') && age > 40; result_set(1.2); result_set(1.0)) -``` - -``` -if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0)) -``` - -``` -if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL')) -``` - -``` -if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing) -``` - -``` -if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL')) -``` - -``` -if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin request removed'); nothing) -``` - -## API Endpoints - -All endpoints live under `/api/rule` (singular) and accept JSON. Responses use the standard `{ status, message, data }` envelope. These endpoints require auth (bearer token). - -### List Rules - -```http -GET /api/rule?EventCode=test_created&TestSiteID=12&search=glucose -``` - -Query Params: - -- `EventCode` (optional) filter by event code. -- `TestSiteID` (optional) filter rules linked to a test site. -- `search` (optional) partial match against `RuleName`. - -Response: - -```json -{ - "status": "success", - "message": "fetch success", - "data": [ - { - "RuleID": 1, - "RuleCode": "RULE_001", - "RuleName": "Sex-based result", - "EventCode": "test_created", - "ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))", - "ConditionExprCompiled": "{...}" - } - ] -} -``` - -### Get Rule - -```http -GET /api/rule/1 -``` - -Response includes `linkedTests`: - -```json -{ - "status": "success", - "message": "fetch success", - "data": { - "RuleID": 1, - "RuleCode": "RULE_001", - "RuleName": "Sex-based result", - "EventCode": "test_created", - "ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))", - "ConditionExprCompiled": "{...}", - "linkedTests": [1, 2] - } -} -``` - -### Create Rule - -```http -POST /api/rule -Content-Type: application/json - -{ - "RuleCode": "RULE_001", - "RuleName": "Sex-based result", - "EventCode": "test_created", - "ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))", - "ConditionExprCompiled": "", - "TestSiteIDs": [1, 2] -} -``` - -Response: - -```json -{ - "status": "success", - "message": "Rule created successfully", - "data": { - "RuleID": 1 - } -} -``` - -### Update Rule - -```http -PATCH /api/rule/1 -Content-Type: application/json - -{ - "RuleName": "Sex-based result v2", - "ConditionExpr": "if(sex('M'); result_set(0.7); result_set(0.6))", - "ConditionExprCompiled": "", - "TestSiteIDs": [1, 3] -} -``` - -Response: - -```json -{ - "status": "success", - "message": "Rule updated successfully", - "data": { - "RuleID": 1 - } -} -``` - -### Delete Rule - -```http -DELETE /api/rule/1 -``` - -Response: - -```json -{ - "status": "success", - "message": "Rule deleted successfully", - "data": { - "RuleID": 1 - } -} -``` - -### Compile DSL - -Validates the DSL and returns a compiled JSON structure that should be persisted in `ConditionExprCompiled`. - -```http -POST /api/rule/compile -Content-Type: application/json - -{ - "expr": "if(sex('M'); result_set(0.5); result_set(0.6))" -} -``` - -Response: - -```json -{ - "status": "success", - "data": { - "raw": "if(sex('M'); result_set(0.5); result_set(0.6))", - "compiled": { - "conditionExpr": "sex('M')", - "then": ["result_set(0.5)"], - "else": ["result_set(0.6)"] - }, - "conditionExprCompiled": "{...}" - } -} -``` - -### Evaluate Expression (Validation) - -This endpoint evaluates an expression against a runtime context. It does not compile DSL or persist the result. - -```http -POST /api/rule/validate -Content-Type: application/json - -{ - "expr": "order[\"Age\"] > 18", - "context": { - "order": { - "Age": 25 - } - } -} -``` - -Response: - -```json -{ - "status": "success", - "data": { - "valid": true, - "result": true - } -} -``` - -## Database Schema - -### Tables - -- **ruledef** – stores rule metadata, raw DSL, and compiled JSON. -- **testrule** – mapping table that links rules to tests via `TestSiteID`. -- **ruleaction** – deprecated. Actions are now embedded in `ConditionExprCompiled`. - -### Key Columns - -| Column | Table | Description | -|--------|-------|-------------| -| `EventCode` | ruledef | The trigger event (typically `test_created` or `result_updated`). | -| `ConditionExpr` | ruledef | Raw DSL expression (semicolon syntax). | -| `ConditionExprCompiled` | ruledef | JSON payload consumed at runtime (`then`, `else`, etc.). | -| `ActionType` / `ActionParams` | ruleaction | Deprecated; actions live in compiled JSON now. | - -## Best Practices - -1. Always run `POST /api/rule/compile` before persisting a rule so `ConditionExprCompiled` exists. -2. Link each rule to the relevant tests via `testrule.TestSiteID`—rules are scoped to linked tests. -3. Use multi-action (`:`) to bundle several actions in a single branch; finish the branch with `nothing` if no further work is needed. -4. Prefer `comment_insert()` over the removed `set_priority()` action when documenting priority decisions. -5. Group complex boolean logic with parentheses for clarity when mixing `&&` and `||`. -6. Use `requested('CODE')` responsibly; it performs a database lookup on `patres` so avoid invoking it in high-frequency loops without reason. - -## Migration Guide - -### Syntax Changes (v2.0) - -The DSL moved from ternary (`condition ? action : action`) to semicolon syntax. Existing rules must be migrated via the provided script. - -| Old Syntax | New Syntax | -|------------|------------| -| `if(condition ? action : action)` | `if(condition; action; action)` | - -#### Migration Examples - -``` -# BEFORE -if(sex('M') ? result_set(0.5) : result_set(0.6)) - -# AFTER -if(sex('M'); result_set(0.5); result_set(0.6)) -``` - -``` -# BEFORE -if(sex('F') ? set_priority('S') : nothing) - -# AFTER -if(sex('F'); comment_insert('Female patient - review priority'); nothing) -``` - -#### Migration Process - -Run the migration which: - -1. Converts ternary syntax to semicolon syntax. -2. Recompiles every expression into JSON so the engine consumes `ConditionExprCompiled` directly. -3. Eliminates reliance on the `ruleaction` table. - -```bash -php spark migrate -``` - -## Troubleshooting - -### Rule Not Executing - -1. Ensure the rule has a compiled payload (`ConditionExprCompiled`). -2. Confirm the rule is linked to the relevant `TestSiteID` in `testrule`. -3. Verify the `EventCode` matches the currently triggered event (`test_created` or `result_updated`). -4. Check that `EndDate IS NULL` for both `ruledef` and `testrule` (soft deletes disable execution). -5. Use `/api/rule/compile` to validate the DSL and view errors. - -### Invalid Expression - -1. POST the expression to `/api/rule/compile` to get a detailed compilation error. -2. If using `/api/rule/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it. - -### Runtime Errors - -- `RESULT_SET requires context.order.InternalOID` or `testSiteID`: include those fields in the context passed to `RuleEngineService::run()`. -- `TEST_INSERT` failures mean the provided `TestSiteCode` does not exist or the rule attempted to insert a duplicate test; check `testdefsite` and existing `patres` rows. -- `COMMENT_INSERT requires comment`: ensure the action provides text. diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index bd8ea34..ee67029 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -4000,9 +4000,6 @@ paths: schema: type: object properties: - TestCode: - type: string - description: Test Code (required) - maps to HostTestCode or ClientTestCode HostType: type: string description: Host type code @@ -4031,8 +4028,6 @@ paths: type: string ClientTestName: type: string - required: - - TestCode responses: '201': description: Test mapping created @@ -4135,9 +4130,6 @@ paths: schema: type: object properties: - TestCode: - type: string - description: Test Code - maps to HostTestCode or ClientTestCode HostType: type: string HostID: diff --git a/public/paths/testmap.yaml b/public/paths/testmap.yaml index a90dd2c..91d3db5 100644 --- a/public/paths/testmap.yaml +++ b/public/paths/testmap.yaml @@ -45,13 +45,10 @@ requestBody: required: true content: - application/json: - schema: - type: object + application/json: + schema: + type: object properties: - TestCode: - type: string - description: Test Code (required) - maps to HostTestCode or ClientTestCode HostType: type: string description: Host type code @@ -80,8 +77,6 @@ type: string ClientTestName: type: string - required: - - TestCode responses: '201': description: Test mapping created @@ -185,9 +180,6 @@ schema: type: object properties: - TestCode: - type: string - description: Test Code - maps to HostTestCode or ClientTestCode HostType: type: string HostID: diff --git a/tests/feature/Test/TestCreateVariantsTest.php b/tests/feature/Test/TestCreateVariantsTest.php index cd76fbe..ce27428 100644 --- a/tests/feature/Test/TestCreateVariantsTest.php +++ b/tests/feature/Test/TestCreateVariantsTest.php @@ -290,9 +290,7 @@ class TestCreateVariantsTest extends CIUnitTestCase $show->assertStatus(200); $data = json_decode($show->getJSON(), true)['data']; - $this->assertNotEmpty($data['testmap']); - $this->assertSame('HIS', $data['testmap'][0]['HostType']); - $this->assertSame('SITE', $data['testmap'][0]['ClientType']); + $this->assertArrayNotHasKey('testmap', $data); } public function testPatchTechnicalWithFlatTestMapPayload(): void @@ -324,9 +322,7 @@ class TestCreateVariantsTest extends CIUnitTestCase $show->assertStatus(200); $data = json_decode($show->getJSON(), true)['data']; - $this->assertNotEmpty($data['testmap']); - $this->assertSame('HIS', $data['testmap'][0]['HostType']); - $this->assertSame('SITE', $data['testmap'][0]['ClientType']); + $this->assertArrayNotHasKey('testmap', $data); } public function testCreateCalculatedTestWithoutReferenceOrMap(): void