db = \Config\Database::connect(); $this->model = new TestMapModel; $this->modelDetail = new TestMapDetailModel; $this->rules = [ 'HostID' => 'required|integer', 'ClientID' => 'required|integer', ]; $this->patchRules = [ 'HostID' => '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() { $rows = $this->model->getUniqueGroupings(); $rows = $this->applyIndexFilters($rows); 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->getByIdWithNames($id); 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]; $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); } public function create() { $input = $this->request->getJSON(true); $detailsPayload = null; if (array_key_exists('details', $input)) { $detailsPayload = $this->resolveDetailOperations($input['details']); if ($detailsPayload === null) { return; } } $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['created'])) { if (!$this->insertDetailRows($id, $detailsPayload['created'])) { $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) { $input = $this->requirePatchPayload($this->request->getJSON(true)); if ($input === null) { return; } $detailsPayload = null; if (array_key_exists('details', $input)) { $detailsPayload = $this->resolveDetailOperations($input['details']); if ($detailsPayload === null) { return; } } $id = $this->requirePatchId($TestMapID, 'TestMapID'); if ($id === null) { return; } $existing = $this->model->where('TestMapID', $id)->where('EndDate', null)->first(); if (!$existing) { return $this->respond([ 'status' => 'failed', 'message' => 'Test map not found', 'data' => [] ], 404); } if (isset($input['TestMapID']) && (string) $input['TestMapID'] !== (string) $id) { return $this->failValidationErrors('TestMapID in URL does not match body.'); } $validationInput = array_intersect_key($headerInput = array_intersect_key($input, array_flip($this->headerFields)), $this->patchRules); if (!empty($validationInput) && !$this->validateData($validationInput, $this->patchRules)) { return $this->failValidationErrors($this->validator->getErrors()); } $input['TestMapID'] = $id; $this->db->transStart(); try { 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); } 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) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function showByTestCode($testCode = null) { if (!$testCode) { return $this->failValidationErrors('TestCode is required.'); } $rows = $this->model->getMappingsByTestCode($testCode); 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); } private function sanitizeTopLevelPayload(array $row): array { unset($row['TestCode'], $row['testcode']); return $row; } private function applyIndexFilters(array $rows): array { $hostFilter = trim((string) $this->request->getGet('host')); $clientFilter = trim((string) $this->request->getGet('client')); if ($hostFilter === '' && $clientFilter === '') { return $rows; } return array_values(array_filter($rows, function (array $row) use ($hostFilter, $clientFilter): bool { if ($hostFilter !== '' && !$this->matchesSearch($row, 'Host', $hostFilter)) { return false; } if ($clientFilter !== '' && !$this->matchesSearch($row, 'Client', $clientFilter)) { return false; } return true; })); } private function matchesSearch(array $row, string $prefix, string $filter): bool { $haystacks = [ (string) ($row[$prefix . 'Name'] ?? ''), (string) ($row[$prefix . 'ID'] ?? ''), (string) ($row[$prefix . 'Type'] ?? ''), ]; $needle = strtolower($filter); foreach ($haystacks as $value) { if ($value !== '' && str_contains(strtolower($value), $needle)) { return true; } } return false; } 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)) { $createdItems = $this->normalizeDetailList($detailsPayload['created'] ?? [], 'details.created'); if ($createdItems === null) { return null; } $editedItems = $this->normalizeDetailList($detailsPayload['edited'] ?? [], 'details.edited'); if ($editedItems === null) { return null; } $deletedIds = $this->normalizeDetailIds($detailsPayload['deleted'] ?? []); if ($deletedIds === null) { return null; } return ['created' => $createdItems, 'edited' => $editedItems, 'deleted' => $deletedIds]; } if ($this->isListPayload($detailsPayload)) { $items = $this->normalizeDetailList($detailsPayload, 'details'); if ($items === null) { return null; } return ['created' => $items, 'edited' => [], 'deleted' => []]; } if ($this->isAssocArray($detailsPayload)) { $items = $this->normalizeDetailList([$detailsPayload], 'details'); if ($items === null) { return null; } return ['created' => $items, 'edited' => [], 'deleted' => []]; } $this->failValidationErrors('details must be an array of objects or contain created/edited/deleted arrays.'); return null; } private function applyDetailOperations(int $testMapID, array $operations): bool { if (!empty($operations['edited']) && !$this->updateDetails($testMapID, $operations['edited'])) { return false; } if (!empty($operations['deleted']) && !$this->softDeleteDetails($testMapID, $operations['deleted'])) { return false; } if (!empty($operations['created']) && !$this->insertDetailRows($testMapID, $operations['created'])) { 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.created' => $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.edited[{$index}].TestMapDetailID is required and must be an integer."); return false; } if (array_key_exists('TestMapID', $detail) && (int) $detail['TestMapID'] !== $testMapID) { $this->failValidationErrors("details.edited[{$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), ['created', 'edited', '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, string $fieldPath): ?array { if ($value === null) { return []; } if (!is_array($value)) { $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("{$fieldPath}[{$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() { $items = $this->request->getJSON(true); if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); } $results = ['success' => [], 'failed' => []]; $this->db->transStart(); foreach ($items as $index => $item) { if (!$this->validateData($item, $this->rules)) { $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; continue; } try { $id = $this->model->insert($item); $results['success'][] = ['index' => $index, 'TestMapID' => $id]; } catch (\Exception $e) { $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; } } $this->db->transComplete(); return $this->respond([ 'status' => empty($results['failed']) ? 'success' : 'partial', 'message' => 'Batch create completed', 'data' => $results ], 200); } public function batchUpdate() { $items = $this->request->getJSON(true); if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); } $results = ['success' => [], 'failed' => []]; $this->db->transStart(); foreach ($items as $index => $item) { $id = $item['TestMapID'] ?? null; if (!$id) { $results['failed'][] = ['index' => $index, 'error' => 'TestMapID required']; continue; } if (!$this->validateData($item, $this->rules)) { $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; continue; } try { $this->model->update($id, $item); $results['success'][] = ['index' => $index, 'TestMapID' => $id]; } catch (\Exception $e) { $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; } } $this->db->transComplete(); return $this->respond([ 'status' => empty($results['failed']) ? 'success' : 'partial', 'message' => 'Batch update completed', 'data' => $results ], 200); } public function batchDelete() { $ids = $this->request->getJSON(true); if (!is_array($ids)) { return $this->failValidationErrors('Expected array of TestMapIDs'); } $results = ['success' => [], 'failed' => []]; $this->db->transStart(); foreach ($ids as $id) { try { $row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first(); if (empty($row)) { $results['failed'][] = ['TestMapID' => $id, 'error' => 'Not found or already deleted']; continue; } $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]); $results['success'][] = $id; } catch (\Exception $e) { $results['failed'][] = ['TestMapID' => $id, 'error' => $e->getMessage()]; } } $this->db->transComplete(); return $this->respond([ 'status' => empty($results['failed']) ? 'success' : 'partial', 'message' => 'Batch delete completed', 'data' => $results ], 200); } }