todo : fixing testmap detail
This commit is contained in:
parent
9946978487
commit
84cfff2201
@ -17,6 +17,10 @@ class TestMapController extends BaseController {
|
|||||||
protected $patchRules;
|
protected $patchRules;
|
||||||
protected $model;
|
protected $model;
|
||||||
protected $modelDetail;
|
protected $modelDetail;
|
||||||
|
protected array $headerFields = ['HostType', 'HostID', 'ClientType', 'ClientID'];
|
||||||
|
protected array $detailFields = ['HostTestCode', 'HostTestName', 'ConDefID', 'ClientTestCode', 'ClientTestName'];
|
||||||
|
protected array $detailRules;
|
||||||
|
protected array $detailPatchRules;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
@ -29,7 +33,17 @@ class TestMapController extends BaseController {
|
|||||||
$this->patchRules = [
|
$this->patchRules = [
|
||||||
'HostID' => 'permit_empty|integer',
|
'HostID' => 'permit_empty|integer',
|
||||||
'ClientID' => 'permit_empty|integer',
|
'ClientID' => 'permit_empty|integer',
|
||||||
|
'HostType' => 'permit_empty|string',
|
||||||
|
'ClientType' => 'permit_empty|string',
|
||||||
];
|
];
|
||||||
|
$this->detailRules = [
|
||||||
|
'HostTestCode' => 'permit_empty|max_length[10]',
|
||||||
|
'HostTestName' => 'permit_empty|max_length[100]',
|
||||||
|
'ConDefID' => 'permit_empty|integer',
|
||||||
|
'ClientTestCode' => 'permit_empty|max_length[10]',
|
||||||
|
'ClientTestName' => 'permit_empty|max_length[100]',
|
||||||
|
];
|
||||||
|
$this->detailPatchRules = $this->detailRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index() {
|
||||||
@ -63,16 +77,42 @@ class TestMapController extends BaseController {
|
|||||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create() {
|
public function create() {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); }
|
$detailsPayload = null;
|
||||||
try {
|
if (array_key_exists('details', $input)) {
|
||||||
$id = $this->model->insert($input);
|
$detailsPayload = $this->resolveDetailOperations($input['details']);
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data created successfully", 'data' => $id ]);
|
if ($detailsPayload === null) { return; }
|
||||||
} catch (\Exception $e) {
|
}
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
||||||
}
|
$headerInput = array_intersect_key($input, array_flip($this->headerFields));
|
||||||
}
|
if (!$this->validateData($headerInput, $this->rules)) {
|
||||||
|
log_message('error', 'TestMap create validation failed: ' . json_encode($this->validator->getErrors()));
|
||||||
|
return $this->failValidationErrors($this->validator->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transStart();
|
||||||
|
try {
|
||||||
|
$id = $this->model->insert($headerInput);
|
||||||
|
|
||||||
|
if ($detailsPayload !== null && !empty($detailsPayload['new'])) {
|
||||||
|
if (!$this->insertDetailRows($id, $detailsPayload['new'])) {
|
||||||
|
$this->db->transRollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transComplete();
|
||||||
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->failServerError('Something went wrong while saving the test map.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respondCreated([ 'status' => 'success', 'message' => "data created successfully", 'data' => $id ]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->db->transRollback();
|
||||||
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function update($TestMapID = null) {
|
public function update($TestMapID = null) {
|
||||||
$input = $this->requirePatchPayload($this->request->getJSON(true));
|
$input = $this->requirePatchPayload($this->request->getJSON(true));
|
||||||
@ -80,6 +120,14 @@ class TestMapController extends BaseController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$detailsPayload = null;
|
||||||
|
if (array_key_exists('details', $input)) {
|
||||||
|
$detailsPayload = $this->resolveDetailOperations($input['details']);
|
||||||
|
if ($detailsPayload === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$id = $this->requirePatchId($TestMapID, 'TestMapID');
|
$id = $this->requirePatchId($TestMapID, 'TestMapID');
|
||||||
if ($id === null) {
|
if ($id === null) {
|
||||||
return;
|
return;
|
||||||
@ -94,47 +142,64 @@ class TestMapController extends BaseController {
|
|||||||
return $this->failValidationErrors('TestMapID in URL does not match body.');
|
return $this->failValidationErrors('TestMapID in URL does not match body.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$validationInput = array_intersect_key($input, $this->patchRules);
|
$validationInput = array_intersect_key($headerInput = array_intersect_key($input, array_flip($this->headerFields)), $this->patchRules);
|
||||||
if (!empty($validationInput) && !$this->validateData($validationInput, $this->patchRules)) {
|
if (!empty($validationInput) && !$this->validateData($validationInput, $this->patchRules)) {
|
||||||
return $this->failValidationErrors($this->validator->getErrors());
|
return $this->failValidationErrors($this->validator->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
$input['TestMapID'] = $id;
|
$input['TestMapID'] = $id;
|
||||||
|
$this->db->transStart();
|
||||||
try {
|
try {
|
||||||
$this->model->update($id,$input);
|
if (!empty($headerInput)) {
|
||||||
|
$this->model->update($id, $headerInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($detailsPayload !== null && !$this->applyDetailOperations($id, $detailsPayload)) {
|
||||||
|
$this->db->transRollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transComplete();
|
||||||
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->failServerError('Something went wrong while updating the test map.');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 200);
|
return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->db->transRollback();
|
||||||
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
$input = $this->request->getJSON(true);
|
||||||
|
$id = $input["TestMapID"] ?? null;
|
||||||
|
if (!$id) { return $this->failValidationErrors('TestMapID is required.'); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
|
||||||
|
if (empty($row)) { return $this->respond([ 'status' => 'failed', 'message' => "Data not found or already deleted.", 'data' => null ], 404); }
|
||||||
|
|
||||||
|
$this->db->transStart();
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$this->model->update($id, ['EndDate' => $timestamp]);
|
||||||
|
|
||||||
|
$this->modelDetail->where('TestMapID', $id)
|
||||||
|
->where('EndDate', null)
|
||||||
|
->set('EndDate', $timestamp)
|
||||||
|
->update();
|
||||||
|
|
||||||
|
$this->db->transComplete();
|
||||||
|
|
||||||
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->failServerError('Something went wrong while deleting the test map.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond([ 'status' => 'success', 'message' => "data deleted successfully", 'data' => $id ], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete() {
|
|
||||||
$input = $this->request->getJSON(true);
|
|
||||||
$id = $input["TestMapID"] ?? null;
|
|
||||||
if (!$id) { return $this->failValidationErrors('TestMapID is required.'); }
|
|
||||||
|
|
||||||
try {
|
|
||||||
$row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
|
|
||||||
if (empty($row)) { return $this->respond([ 'status' => 'failed', 'message' => "Data not found or already deleted.", 'data' => null ], 404); }
|
|
||||||
|
|
||||||
$this->db->transStart();
|
|
||||||
|
|
||||||
// Soft delete the testmap
|
|
||||||
$this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]);
|
|
||||||
|
|
||||||
// Soft delete all related details
|
|
||||||
$this->modelDetail->where('TestMapID', $id)
|
|
||||||
->where('EndDate', null)
|
|
||||||
->set('EndDate', date('Y-m-d H:i:s'))
|
|
||||||
->update();
|
|
||||||
|
|
||||||
$this->db->transComplete();
|
|
||||||
|
|
||||||
return $this->respond([ 'status' => 'success', 'message' => "data deleted successfully", 'data' => $id ], 200);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function showByTestCode($testCode = null) {
|
public function showByTestCode($testCode = null) {
|
||||||
if (!$testCode) { return $this->failValidationErrors('TestCode is required.'); }
|
if (!$testCode) { return $this->failValidationErrors('TestCode is required.'); }
|
||||||
@ -157,6 +222,214 @@ class TestMapController extends BaseController {
|
|||||||
unset($row['TestCode'], $row['testcode']);
|
unset($row['TestCode'], $row['testcode']);
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveDetailOperations(mixed $detailsPayload): ?array
|
||||||
|
{
|
||||||
|
if ($detailsPayload === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($detailsPayload)) {
|
||||||
|
$this->failValidationErrors('details must be an array or object.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isDetailOpsPayload($detailsPayload)) {
|
||||||
|
$newItems = $this->normalizeDetailList($detailsPayload['new'] ?? []);
|
||||||
|
if ($newItems === null) { return null; }
|
||||||
|
$editItems = $this->normalizeDetailList($detailsPayload['edit'] ?? []);
|
||||||
|
if ($editItems === null) { return null; }
|
||||||
|
$deletedIds = $this->normalizeDetailIds($detailsPayload['deleted'] ?? []);
|
||||||
|
if ($deletedIds === null) { return null; }
|
||||||
|
|
||||||
|
return ['new' => $newItems, 'edit' => $editItems, 'deleted' => $deletedIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isListPayload($detailsPayload)) {
|
||||||
|
$items = $this->normalizeDetailList($detailsPayload);
|
||||||
|
if ($items === null) { return null; }
|
||||||
|
return ['new' => $items, 'edit' => [], 'deleted' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isAssocArray($detailsPayload)) {
|
||||||
|
$items = $this->normalizeDetailList([$detailsPayload]);
|
||||||
|
if ($items === null) { return null; }
|
||||||
|
return ['new' => $items, 'edit' => [], 'deleted' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->failValidationErrors('details must be an array of objects or contain new/edit/deleted arrays.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyDetailOperations(int $testMapID, array $operations): bool
|
||||||
|
{
|
||||||
|
if (!empty($operations['edit']) && !$this->updateDetails($testMapID, $operations['edit'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($operations['deleted']) && !$this->softDeleteDetails($testMapID, $operations['deleted'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($operations['new']) && !$this->insertDetailRows($testMapID, $operations['new'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function insertDetailRows(int $testMapID, array $items): bool
|
||||||
|
{
|
||||||
|
if (empty($items)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prepared = [];
|
||||||
|
foreach ($items as $index => $item) {
|
||||||
|
if (!$this->validateData($item, $this->detailRules)) {
|
||||||
|
$this->failValidationErrors(['details.new' => $this->validator->getErrors()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$prepared[] = array_merge(['TestMapID' => $testMapID], $item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->modelDetail->insertBatch($prepared);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateDetails(int $testMapID, array $items): bool
|
||||||
|
{
|
||||||
|
foreach ($items as $index => $detail) {
|
||||||
|
$detailID = $detail['TestMapDetailID'] ?? null;
|
||||||
|
if (!$detailID || !ctype_digit((string) $detailID)) {
|
||||||
|
$this->failValidationErrors("details.edit[{$index}].TestMapDetailID is required and must be an integer.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('TestMapID', $detail) && (int) $detail['TestMapID'] !== $testMapID) {
|
||||||
|
$this->failValidationErrors("details.edit[{$index}] must belong to TestMap {$testMapID}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->modelDetail->where('TestMapDetailID', $detailID)
|
||||||
|
->where('TestMapID', $testMapID)
|
||||||
|
->where('EndDate', null)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($existing)) {
|
||||||
|
$this->failValidationErrors("Detail record {$detailID} not found for this test map.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updateData = array_intersect_key($detail, array_flip($this->detailFields));
|
||||||
|
if ($updateData === []) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->validateData($updateData, $this->detailPatchRules)) {
|
||||||
|
$this->failValidationErrors($this->validator->getErrors());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->modelDetail->update($detailID, $updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function softDeleteDetails(int $testMapID, array $ids): bool
|
||||||
|
{
|
||||||
|
if (empty($ids)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->modelDetail->select('TestMapDetailID')
|
||||||
|
->whereIn('TestMapDetailID', $ids)
|
||||||
|
->where('TestMapID', $testMapID)
|
||||||
|
->where('EndDate', null)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
$foundIds = array_column($existing, 'TestMapDetailID');
|
||||||
|
$missing = array_diff($ids, $foundIds);
|
||||||
|
if (!empty($missing)) {
|
||||||
|
$this->failValidationErrors('Some detail IDs do not exist or belong to another test map: ' . implode(', ', $missing));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->modelDetail->whereIn('TestMapDetailID', $ids)
|
||||||
|
->where('TestMapID', $testMapID)
|
||||||
|
->where('EndDate', null)
|
||||||
|
->set('EndDate', date('Y-m-d H:i:s'))
|
||||||
|
->update();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isDetailOpsPayload(array $payload): bool
|
||||||
|
{
|
||||||
|
return (bool) array_intersect(array_keys($payload), ['new', 'edit', 'deleted']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isListPayload(array $payload): bool
|
||||||
|
{
|
||||||
|
if ($payload === []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return array_keys($payload) === range(0, count($payload) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAssocArray(array $payload): bool
|
||||||
|
{
|
||||||
|
if ($payload === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return array_keys($payload) !== range(0, count($payload) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDetailList(mixed $value): ?array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$this->failValidationErrors('Details must be provided as an array of objects.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($value as $index => $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
$this->failValidationErrors("details[{$index}] must be an object.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$results[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDetailIds(mixed $value): ?array
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$value = [$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($value as $index => $item) {
|
||||||
|
if (!ctype_digit((string) $item)) {
|
||||||
|
$this->failValidationErrors("details.deleted[{$index}] must be an integer.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$results[] = (int) $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($results));
|
||||||
|
}
|
||||||
|
|
||||||
public function batchCreate() {
|
public function batchCreate() {
|
||||||
$items = $this->request->getJSON(true);
|
$items = $this->request->getJSON(true);
|
||||||
|
|||||||
@ -4089,7 +4089,7 @@ paths:
|
|||||||
description: Client identifier
|
description: Client identifier
|
||||||
details:
|
details:
|
||||||
type: array
|
type: array
|
||||||
description: Optional detail records to create
|
description: Optional detail records to create alongside the header
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -4199,21 +4199,6 @@ paths:
|
|||||||
type: integer
|
type: integer
|
||||||
description: Test Map ID
|
description: Test Map ID
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
HostType:
|
|
||||||
type: string
|
|
||||||
HostID:
|
|
||||||
type: string
|
|
||||||
ClientType:
|
|
||||||
type: string
|
|
||||||
ClientID:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
'200':
|
||||||
description: Test mapping updated
|
description: Test mapping updated
|
||||||
content:
|
content:
|
||||||
@ -4229,6 +4214,64 @@ paths:
|
|||||||
data:
|
data:
|
||||||
type: integer
|
type: integer
|
||||||
description: Updated TestMapID
|
description: Updated TestMapID
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
HostType:
|
||||||
|
type: string
|
||||||
|
HostID:
|
||||||
|
type: string
|
||||||
|
ClientType:
|
||||||
|
type: string
|
||||||
|
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
|
||||||
|
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
|
||||||
|
responses: null
|
||||||
/api/test/testmap/by-testcode/{testCode}:
|
/api/test/testmap/by-testcode/{testCode}:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
description: Client identifier
|
description: Client identifier
|
||||||
details:
|
details:
|
||||||
type: array
|
type: array
|
||||||
description: Optional detail records to create
|
description: Optional detail records to create alongside the header
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -188,7 +188,50 @@
|
|||||||
type: string
|
type: string
|
||||||
ClientID:
|
ClientID:
|
||||||
type: string
|
type: string
|
||||||
responses:
|
details:
|
||||||
|
type: object
|
||||||
|
description: Apply detail-level changes together with the header update
|
||||||
|
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
|
||||||
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Test mapping updated
|
description: Test mapping updated
|
||||||
content:
|
content:
|
||||||
|
|||||||
@ -39,15 +39,24 @@ class TestMapPatchTest extends CIUnitTestCase
|
|||||||
$payload = array_merge([
|
$payload = array_merge([
|
||||||
'TestMapCode' => 'TM_' . uniqid(),
|
'TestMapCode' => 'TM_' . uniqid(),
|
||||||
'TestMapName' => 'Test Map ' . uniqid(),
|
'TestMapName' => 'Test Map ' . uniqid(),
|
||||||
|
'HostType' => 'SITE',
|
||||||
|
'HostID' => 1,
|
||||||
|
'ClientType' => 'SITE',
|
||||||
|
'ClientID' => 1,
|
||||||
], $data);
|
], $data);
|
||||||
|
|
||||||
$response = $this->withHeaders($this->authHeaders())
|
$response = $this->withHeaders($this->authHeaders())
|
||||||
->withBodyFormat('json')
|
->withBodyFormat('json')
|
||||||
->call('post', $this->endpoint, $payload);
|
->call('post', $this->endpoint, $payload);
|
||||||
|
|
||||||
|
fwrite(STDERR, 'Create response: ' . $response->getStatusCode() . ' ' . $response->getBody() . PHP_EOL);
|
||||||
$response->assertStatus(201);
|
$response->assertStatus(201);
|
||||||
$decoded = json_decode($response->getJSON(), true);
|
$created = json_decode($response->getJSON(), true);
|
||||||
return $decoded['data'];
|
$id = $created['data'];
|
||||||
|
|
||||||
|
$show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}");
|
||||||
|
$show->assertStatus(200);
|
||||||
|
return json_decode($show->getJSON(), true)['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPartialUpdateTestMapSuccess()
|
public function testPartialUpdateTestMapSuccess()
|
||||||
@ -118,4 +127,133 @@ class TestMapPatchTest extends CIUnitTestCase
|
|||||||
$this->assertNotEquals($testMap['TestMapCode'], $showData['TestMapCode']);
|
$this->assertNotEquals($testMap['TestMapCode'], $showData['TestMapCode']);
|
||||||
$this->assertEquals($testMap['TestMapName'], $showData['TestMapName']);
|
$this->assertEquals($testMap['TestMapName'], $showData['TestMapName']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateTestMapWithDetails()
|
||||||
|
{
|
||||||
|
$details = [
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'HB_' . uniqid(),
|
||||||
|
'HostTestName' => 'Hemoglobin',
|
||||||
|
'ClientTestCode' => '2',
|
||||||
|
'ClientTestName' => 'Hemoglobin',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'HCT_' . uniqid(),
|
||||||
|
'HostTestName' => 'Hematocrit',
|
||||||
|
'ClientTestCode' => '3',
|
||||||
|
'ClientTestName' => 'Hematocrit',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$testMap = $this->createTestMap([
|
||||||
|
'HostType' => 'SITE',
|
||||||
|
'HostID' => 1,
|
||||||
|
'ClientType' => 'SITE',
|
||||||
|
'ClientID' => 2,
|
||||||
|
'details' => $details,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertCount(2, $testMap['details']);
|
||||||
|
$this->assertEquals('2', $testMap['details'][0]['ClientTestCode']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPatchTestMapDetailOperations()
|
||||||
|
{
|
||||||
|
$initialDetails = [
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'HB_' . uniqid(),
|
||||||
|
'HostTestName' => 'Hemoglobin',
|
||||||
|
'ClientTestCode' => '2',
|
||||||
|
'ClientTestName' => 'Hemoglobin',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'HCT_' . uniqid(),
|
||||||
|
'HostTestName' => 'Hematocrit',
|
||||||
|
'ClientTestCode' => '3',
|
||||||
|
'ClientTestName' => 'Hematocrit',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$testMap = $this->createTestMap([
|
||||||
|
'HostType' => 'SITE',
|
||||||
|
'HostID' => 1,
|
||||||
|
'ClientType' => 'SITE',
|
||||||
|
'ClientID' => 1,
|
||||||
|
'details' => $initialDetails,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$existingDetails = $testMap['details'];
|
||||||
|
$editDetail = $existingDetails[0];
|
||||||
|
$deleteDetail = $existingDetails[1];
|
||||||
|
|
||||||
|
$patch = $this->withHeaders($this->authHeaders())
|
||||||
|
->withBodyFormat('json')
|
||||||
|
->call('patch', "{$this->endpoint}/{$testMap['TestMapID']}", [
|
||||||
|
'ClientType' => 'WST',
|
||||||
|
'details' => [
|
||||||
|
'edit' => [
|
||||||
|
[
|
||||||
|
'TestMapDetailID' => $editDetail['TestMapDetailID'],
|
||||||
|
'ClientTestName' => 'Hemoglobin Updated',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'new' => [
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'MCV_' . uniqid(),
|
||||||
|
'HostTestName' => 'MCV',
|
||||||
|
'ClientTestCode' => '4',
|
||||||
|
'ClientTestName' => 'MCV',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'deleted' => [$deleteDetail['TestMapDetailID']],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$patch->assertStatus(200);
|
||||||
|
$patchData = json_decode($patch->getJSON(), true);
|
||||||
|
$this->assertEquals('success', $patchData['status']);
|
||||||
|
|
||||||
|
$show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$testMap['TestMapID']}");
|
||||||
|
$show->assertStatus(200);
|
||||||
|
$showData = json_decode($show->getJSON(), true)['data'];
|
||||||
|
|
||||||
|
$this->assertEquals('WST', $showData['ClientType']);
|
||||||
|
$this->assertCount(2, $showData['details']);
|
||||||
|
$detailIds = array_column($showData['details'], 'TestMapDetailID');
|
||||||
|
$this->assertContains($editDetail['TestMapDetailID'], $detailIds);
|
||||||
|
$this->assertNotContains($deleteDetail['TestMapDetailID'], $detailIds);
|
||||||
|
|
||||||
|
$updatedDetails = array_values(array_filter($showData['details'], static fn ($row) => $row['TestMapDetailID'] === $editDetail['TestMapDetailID']));
|
||||||
|
$this->assertNotEmpty($updatedDetails);
|
||||||
|
$this->assertEquals('Hemoglobin Updated', $updatedDetails[0]['ClientTestName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteTestMapRemovesDetails()
|
||||||
|
{
|
||||||
|
$testMap = $this->createTestMap([
|
||||||
|
'HostType' => 'SITE',
|
||||||
|
'HostID' => 2,
|
||||||
|
'ClientType' => 'SITE',
|
||||||
|
'ClientID' => 3,
|
||||||
|
'details' => [
|
||||||
|
[
|
||||||
|
'HostTestCode' => 'PLT_' . uniqid(),
|
||||||
|
'HostTestName' => 'Platelet',
|
||||||
|
'ClientTestCode' => '5',
|
||||||
|
'ClientTestName' => 'Platelet',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$delete = $this->withHeaders($this->authHeaders())
|
||||||
|
->withBodyFormat('json')
|
||||||
|
->call('delete', $this->endpoint, ['TestMapID' => $testMap['TestMapID']]);
|
||||||
|
|
||||||
|
$delete->assertStatus(200);
|
||||||
|
|
||||||
|
$details = $this->withHeaders($this->authHeaders())
|
||||||
|
->call('get', "{$this->endpoint}/detail/by-testmap/{$testMap['TestMapID']}");
|
||||||
|
$details->assertStatus(200);
|
||||||
|
$this->assertEquals([], json_decode($details->getJSON(), true)['data']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
todo.md
Normal file
5
todo.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
### 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.
|
||||||
Loading…
x
Reference in New Issue
Block a user