clqms-be/app/Controllers/Test/TestMapController.php

529 lines
19 KiB
PHP
Executable File

<?php
namespace App\Controllers\Test;
use App\Traits\PatchValidationTrait;
use App\Traits\ResponseTrait;
use App\Controllers\BaseController;
use App\Libraries\ValueSet;
use App\Models\Test\TestMapModel;
use App\Models\Test\TestMapDetailModel;
class TestMapController extends BaseController {
use ResponseTrait;
use PatchValidationTrait;
protected $db;
protected $rules;
protected $patchRules;
protected $model;
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() {
$this->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();
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 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);
}
}