diff --git a/TODO.md b/TODO.md deleted file mode 100755 index 5c81e0b..0000000 --- a/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ -## Remaining Work - -1. `PatVisitController::updateADT` needs to accept or infer `InternalPVID` so the ADT patch tests no longer error out. -2. Implement or expose `POST /api/result` and align `ResultController` responses with what `ResultPatchTest` expects (success + 400/404 handling). -3. For each patch test controller (Contact, Location, Organization modules, Specimen masters, Test/TestMap variants, Rule, User, etc.), ensure the update action validates payloads, rejects empty bodies with 400, returns 404 for absent IDs, and responds with 200/201 on success. -4. Once controllers are fixed, rerun `./vendor/bin/phpunit` to confirm the patch suite passes end-to-end. diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 73f716d..4020092 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -315,13 +315,13 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Test\TestsController::index'); $routes->get('(:num)', 'Test\TestsController::show/$1'); $routes->post('/', 'Test\TestsController::create'); - $routes->patch('(:any)', 'Test\TestsController::update/$1'); - $routes->group('testmap', function ($routes) { - $routes->get('/', 'Test\TestMapController::index'); - $routes->get('(:num)', 'Test\TestMapController::show/$1'); - $routes->post('/', 'Test\TestMapController::create'); - $routes->patch('(:any)', 'Test\TestMapController::update/$1'); - $routes->delete('/', 'Test\TestMapController::delete'); + $routes->patch('(:segment)', 'Test\TestsController::update/$1'); + $routes->group('testmap', function ($routes) { + $routes->get('/', 'Test\TestMapController::index'); + $routes->get('(:num)', 'Test\TestMapController::show/$1'); + $routes->post('/', 'Test\TestMapController::create'); + $routes->patch('(:segment)', 'Test\TestMapController::update/$1'); + $routes->delete('/', 'Test\TestMapController::delete'); // Filter routes $routes->get('by-testcode/(:any)', 'Test\TestMapController::showByTestCode/$1'); @@ -331,7 +331,7 @@ $routes->group('api', function ($routes) { $routes->get('/', 'Test\TestMapDetailController::index'); $routes->get('(:num)', 'Test\TestMapDetailController::show/$1'); $routes->post('/', 'Test\TestMapDetailController::create'); - $routes->patch('(:any)', 'Test\TestMapDetailController::update/$1'); + $routes->patch('(:segment)', 'Test\TestMapDetailController::update/$1'); $routes->delete('/', 'Test\TestMapDetailController::delete'); $routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1'); $routes->post('batch', 'Test\TestMapDetailController::batchCreate'); diff --git a/app/Controllers/Test/TestMapController.php b/app/Controllers/Test/TestMapController.php index 795331d..bd52cb1 100755 --- a/app/Controllers/Test/TestMapController.php +++ b/app/Controllers/Test/TestMapController.php @@ -235,9 +235,9 @@ class TestMapController extends BaseController { } if ($this->isDetailOpsPayload($detailsPayload)) { - $newItems = $this->normalizeDetailList($detailsPayload['new'] ?? []); + $newItems = $this->normalizeDetailList($detailsPayload['new'] ?? [], 'details.new'); if ($newItems === null) { return null; } - $editItems = $this->normalizeDetailList($detailsPayload['edit'] ?? []); + $editItems = $this->normalizeDetailList($detailsPayload['edit'] ?? [], 'details.edit'); if ($editItems === null) { return null; } $deletedIds = $this->normalizeDetailIds($detailsPayload['deleted'] ?? []); if ($deletedIds === null) { return null; } @@ -246,13 +246,13 @@ class TestMapController extends BaseController { } if ($this->isListPayload($detailsPayload)) { - $items = $this->normalizeDetailList($detailsPayload); + $items = $this->normalizeDetailList($detailsPayload, 'details'); if ($items === null) { return null; } return ['new' => $items, 'edit' => [], 'deleted' => []]; } if ($this->isAssocArray($detailsPayload)) { - $items = $this->normalizeDetailList([$detailsPayload]); + $items = $this->normalizeDetailList([$detailsPayload], 'details'); if ($items === null) { return null; } return ['new' => $items, 'edit' => [], 'deleted' => []]; } @@ -386,21 +386,25 @@ class TestMapController extends BaseController { return array_keys($payload) !== range(0, count($payload) - 1); } - private function normalizeDetailList(mixed $value): ?array + private function normalizeDetailList(mixed $value, string $fieldPath): ?array { if ($value === null) { return []; } if (!is_array($value)) { - $this->failValidationErrors('Details must be provided as an array of objects.'); + $this->failValidationErrors("{$fieldPath} must be an array of objects."); return null; } + if ($value !== [] && $this->isAssocArray($value)) { + $value = [$value]; + } + $results = []; foreach ($value as $index => $item) { if (!is_array($item)) { - $this->failValidationErrors("details[{$index}] must be an object."); + $this->failValidationErrors("{$fieldPath}[{$index}] must be an object."); return null; } $results[] = $item; diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index 838cafc..157c28a 100755 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -4199,21 +4199,6 @@ paths: type: integer description: Test Map ID requestBody: - '200': - description: Test mapping updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: success - message: - type: string - data: - type: integer - description: Updated TestMapID required: true content: application/json: @@ -4229,12 +4214,53 @@ paths: ClientID: type: string details: - type: object - description: Apply detail-level changes together with the header update - properties: - new: - type: array - description: New detail records to insert + description: | + Detail payload supports either a flat array/object (treated as new rows) + or an operations object with `new`, `edit`, and `deleted` arrays. + oneOf: + - type: object + properties: + new: + type: array + description: New detail records to insert + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + edit: + type: array + description: Existing detail records to update + items: + type: object + properties: + TestMapDetailID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + deleted: + type: array + description: TestMapDetailIDs to soft delete + items: + type: integer + - type: array + description: Shortcut format for creating new details only items: type: object properties: @@ -4248,30 +4274,35 @@ paths: type: string ClientTestName: type: string - edit: - type: array - description: Existing detail records to update - items: - type: object - properties: - TestMapDetailID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - deleted: - type: array - description: TestMapDetailIDs to soft delete - items: - type: integer - responses: null + - type: object + description: Shortcut format for creating a single new detail + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Test mapping updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: integer + description: Updated TestMapID /api/test/testmap/by-testcode/{testCode}: get: tags: diff --git a/public/paths/testmap.yaml b/public/paths/testmap.yaml index 9418675..e9a6479 100755 --- a/public/paths/testmap.yaml +++ b/public/paths/testmap.yaml @@ -189,12 +189,53 @@ ClientID: type: string details: - type: object - description: Apply detail-level changes together with the header update - properties: - new: - type: array - description: New detail records to insert + description: | + Detail payload supports either a flat array/object (treated as new rows) + or an operations object with `new`, `edit`, and `deleted` arrays. + oneOf: + - type: object + properties: + new: + type: array + description: New detail records to insert + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + edit: + type: array + description: Existing detail records to update + items: + type: object + properties: + TestMapDetailID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + deleted: + type: array + description: TestMapDetailIDs to soft delete + items: + type: integer + - type: array + description: Shortcut format for creating new details only items: type: object properties: @@ -208,30 +249,20 @@ type: string ClientTestName: type: string - edit: - type: array - description: Existing detail records to update - items: - type: object - properties: - TestMapDetailID: - type: integer - HostTestCode: - type: string - HostTestName: - type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string - deleted: - type: array - description: TestMapDetailIDs to soft delete - items: - type: integer - responses: + - type: object + description: Shortcut format for creating a single new detail + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: '200': description: Test mapping updated content: diff --git a/tests/feature/Test/TestMapDetailPatchTest.php b/tests/feature/Test/TestMapDetailPatchTest.php index 41c1628..3022e5e 100755 --- a/tests/feature/Test/TestMapDetailPatchTest.php +++ b/tests/feature/Test/TestMapDetailPatchTest.php @@ -12,6 +12,7 @@ class TestMapDetailPatchTest extends CIUnitTestCase protected string $token; protected string $endpoint = 'api/test/testmap/detail'; + protected string $mapEndpoint = 'api/test/testmap'; protected function setUp(): void { @@ -36,9 +37,23 @@ class TestMapDetailPatchTest extends CIUnitTestCase private function createTestMapDetail(array $data = []): array { + $mapResponse = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->mapEndpoint, [ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 1, + ]); + $mapResponse->assertStatus(201); + $mapID = json_decode($mapResponse->getJSON(), true)['data']; + $payload = array_merge([ - 'TestMapDetailCode' => 'TMD_' . uniqid(), - 'TestMapDetailName' => 'Test Map Detail ' . uniqid(), + 'TestMapID' => $mapID, + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ClientTestCode' => '2', + 'ClientTestName' => 'Hemoglobin', ], $data); $response = $this->withHeaders($this->authHeaders()) @@ -47,7 +62,11 @@ class TestMapDetailPatchTest extends CIUnitTestCase $response->assertStatus(201); $decoded = json_decode($response->getJSON(), true); - return $decoded['data']; + $detailID = $decoded['data']; + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$detailID}"); + $show->assertStatus(200); + return json_decode($show->getJSON(), true)['data']; } public function testPartialUpdateTestMapDetailSuccess() @@ -57,7 +76,7 @@ class TestMapDetailPatchTest extends CIUnitTestCase $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestMapDetailName' => 'Updated Detail']); + ->call('patch', "{$this->endpoint}/{$id}", ['ClientTestName' => 'Updated Detail']); $patch->assertStatus(200); $patchData = json_decode($patch->getJSON(), true); @@ -67,15 +86,15 @@ class TestMapDetailPatchTest extends CIUnitTestCase $show->assertStatus(200); $showData = json_decode($show->getJSON(), true)['data']; - $this->assertEquals('Updated Detail', $showData['TestMapDetailName']); - $this->assertEquals($detail['TestMapDetailCode'], $showData['TestMapDetailCode']); + $this->assertEquals('Updated Detail', $showData['ClientTestName']); + $this->assertEquals($detail['HostTestCode'], $showData['HostTestCode']); } public function testPartialUpdateTestMapDetailNotFound() { $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['TestMapDetailName' => 'Updated']); + ->call('patch', "{$this->endpoint}/999999", ['ClientTestName' => 'Updated']); $patch->assertStatus(404); } @@ -84,7 +103,7 @@ class TestMapDetailPatchTest extends CIUnitTestCase { $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['TestMapDetailName' => 'Updated']); + ->call('patch', "{$this->endpoint}/invalid", ['ClientTestName' => 'Updated']); $patch->assertStatus(400); } @@ -108,14 +127,14 @@ class TestMapDetailPatchTest extends CIUnitTestCase $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestMapDetailCode' => 'NEW_' . uniqid()]); + ->call('patch', "{$this->endpoint}/{$id}", ['HostTestCode' => 'HBA1C']); $patch->assertStatus(200); $showData = json_decode($this->withHeaders($this->authHeaders()) ->call('get', "{$this->endpoint}/{$id}") ->getJSON(), true)['data']; - $this->assertNotEquals($detail['TestMapDetailCode'], $showData['TestMapDetailCode']); - $this->assertEquals($detail['TestMapDetailName'], $showData['TestMapDetailName']); + $this->assertNotEquals($detail['HostTestCode'], $showData['HostTestCode']); + $this->assertEquals($detail['ClientTestName'], $showData['ClientTestName']); } } diff --git a/tests/feature/Test/TestMapPatchTest.php b/tests/feature/Test/TestMapPatchTest.php index 5d056fb..5e7274c 100755 --- a/tests/feature/Test/TestMapPatchTest.php +++ b/tests/feature/Test/TestMapPatchTest.php @@ -37,8 +37,6 @@ class TestMapPatchTest extends CIUnitTestCase private function createTestMap(array $data = []): array { $payload = array_merge([ - 'TestMapCode' => 'TM_' . uniqid(), - 'TestMapName' => 'Test Map ' . uniqid(), 'HostType' => 'SITE', 'HostID' => 1, 'ClientType' => 'SITE', @@ -49,7 +47,6 @@ class TestMapPatchTest extends CIUnitTestCase ->withBodyFormat('json') ->call('post', $this->endpoint, $payload); - fwrite(STDERR, 'Create response: ' . $response->getStatusCode() . ' ' . $response->getBody() . PHP_EOL); $response->assertStatus(201); $created = json_decode($response->getJSON(), true); $id = $created['data']; @@ -66,7 +63,7 @@ class TestMapPatchTest extends CIUnitTestCase $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestMapName' => 'Updated TestMap']); + ->call('patch', "{$this->endpoint}/{$id}", ['ClientType' => 'WST']); $patch->assertStatus(200); $patchData = json_decode($patch->getJSON(), true); @@ -76,15 +73,15 @@ class TestMapPatchTest extends CIUnitTestCase $show->assertStatus(200); $showData = json_decode($show->getJSON(), true)['data']; - $this->assertEquals('Updated TestMap', $showData['TestMapName']); - $this->assertEquals($testMap['TestMapCode'], $showData['TestMapCode']); + $this->assertEquals('WST', $showData['ClientType']); + $this->assertEquals((string) $testMap['HostID'], (string) $showData['HostID']); } public function testPartialUpdateTestMapNotFound() { $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['TestMapName' => 'Updated']); + ->call('patch', "{$this->endpoint}/999999", ['ClientType' => 'WST']); $patch->assertStatus(404); } @@ -93,7 +90,7 @@ class TestMapPatchTest extends CIUnitTestCase { $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['TestMapName' => 'Updated']); + ->call('patch', "{$this->endpoint}/invalid", ['ClientType' => 'WST']); $patch->assertStatus(400); } @@ -117,28 +114,28 @@ class TestMapPatchTest extends CIUnitTestCase $patch = $this->withHeaders($this->authHeaders()) ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['TestMapCode' => 'NEW_' . uniqid()]); + ->call('patch', "{$this->endpoint}/{$id}", ['HostID' => 2]); $patch->assertStatus(200); $showData = json_decode($this->withHeaders($this->authHeaders()) ->call('get', "{$this->endpoint}/{$id}") ->getJSON(), true)['data']; - $this->assertNotEquals($testMap['TestMapCode'], $showData['TestMapCode']); - $this->assertEquals($testMap['TestMapName'], $showData['TestMapName']); + $this->assertEquals('2', (string) $showData['HostID']); + $this->assertEquals((string) $testMap['ClientID'], (string) $showData['ClientID']); } public function testCreateTestMapWithDetails() { $details = [ [ - 'HostTestCode' => 'HB_' . uniqid(), + 'HostTestCode' => 'HB', 'HostTestName' => 'Hemoglobin', 'ClientTestCode' => '2', 'ClientTestName' => 'Hemoglobin', ], [ - 'HostTestCode' => 'HCT_' . uniqid(), + 'HostTestCode' => 'HCT', 'HostTestName' => 'Hematocrit', 'ClientTestCode' => '3', 'ClientTestName' => 'Hematocrit', @@ -161,13 +158,13 @@ class TestMapPatchTest extends CIUnitTestCase { $initialDetails = [ [ - 'HostTestCode' => 'HB_' . uniqid(), + 'HostTestCode' => 'HB', 'HostTestName' => 'Hemoglobin', 'ClientTestCode' => '2', 'ClientTestName' => 'Hemoglobin', ], [ - 'HostTestCode' => 'HCT_' . uniqid(), + 'HostTestCode' => 'HCT', 'HostTestName' => 'Hematocrit', 'ClientTestCode' => '3', 'ClientTestName' => 'Hematocrit', @@ -199,10 +196,10 @@ class TestMapPatchTest extends CIUnitTestCase ], 'new' => [ [ - 'HostTestCode' => 'MCV_' . uniqid(), - 'HostTestName' => 'MCV', - 'ClientTestCode' => '4', - 'ClientTestName' => 'MCV', + 'HostTestCode' => 'MCV', + 'HostTestName' => 'MCV', + 'ClientTestCode' => '4', + 'ClientTestName' => 'MCV', ], ], 'deleted' => [$deleteDetail['TestMapDetailID']], @@ -237,7 +234,7 @@ class TestMapPatchTest extends CIUnitTestCase 'ClientID' => 3, 'details' => [ [ - 'HostTestCode' => 'PLT_' . uniqid(), + 'HostTestCode' => 'PLT', 'HostTestName' => 'Platelet', 'ClientTestCode' => '5', 'ClientTestName' => 'Platelet', diff --git a/todo.md b/todo.md deleted file mode 100644 index 6d184fa..0000000 --- a/todo.md +++ /dev/null @@ -1,5 +0,0 @@ -### TestMap detail sync fix -- Investigate why `TestMapController::create` and `patch` still reject payloads (400) despite passing required fields; log output hints validation errors. -- Complete detail operation helpers (new/edit/deleted) so frontend payload works end-to-end and rerun feature tests. -- Update tests once endpoints behave (remove stderr logging) and verify `phpunit tests/feature/Test/TestMapPatchTest.php` passes. -- Confirm OpenAPI docs reflect final behavior and bundle output already up-to-date.