# AGENTS.md - Code Guidelines for CLQMS > **CLQMS (Clinical Laboratory Quality Management System)** - A headless REST API backend for clinical laboratory workflows built with CodeIgniter 4. --- ## Build, Test & Lint Commands ```bash # Run all tests ./vendor/bin/phpunit # Run a specific test file ./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php # Run a specific test method ./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php # Run tests with coverage ./vendor/bin/phpunit --coverage-html build/logs/html # Run tests by suite ./vendor/bin/phpunit --testsuite App # Generate scaffolding php spark make:migration php spark make:model php spark make:controller # Database migrations php spark migrate php spark migrate:rollback ``` --- ## Code Style Guidelines ### PHP Standards - **PHP Version**: 8.1+ - **PSR-4 Autoloading**: `App\` maps to `app/`, `Config\` maps to `app/Config/` - **PSR-12 Coding Style** (follow where applicable) ### Naming Conventions | Element | Convention | Example | |---------|-----------|---------| | Classes | PascalCase | `PatientController` | | Methods | camelCase | `createPatient()` | | Properties | snake_case (legacy) / camelCase (new) | `$patient_id` / `$patientId` | | Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | | Tables | snake_case | `patient_visits` | | Columns | PascalCase (legacy) | `PatientID`, `NameFirst` | | JSON fields | PascalCase | `"PatientID": "123"` | ### Imports & Namespaces - Fully qualified namespaces at the top - Group imports: Framework first, then App, then external - Alphabetical order within groups ```php db = \Config\Database::connect(); $this->model = new \App\Models\ExampleModel(); } public function index() { /* ... */ } public function create() { /* ... */ } } ``` ### Response Format All API responses use standardized format: ```php // Success return $this->respond([ 'status' => 'success', 'message' => 'Operation completed', 'data' => $data ], 200); // Error return $this->respond([ 'status' => 'failed', 'message' => 'Error description', 'data' => [] ], 400); ``` **Note**: Custom `ResponseTrait` automatically converts empty strings to `null`. ### Error Handling - Use try-catch for JWT and external calls - Log errors: `log_message('error', $message)` - Return structured error responses with appropriate HTTP status codes ```php try { $decoded = JWT::decode($token, new Key($key, 'HS256')); } catch (\Firebase\JWT\ExpiredException $e) { return $this->respond(['status' => 'failed', 'message' => 'Token expired'], 401); } catch (\Exception $e) { return $this->respond(['status' => 'failed', 'message' => 'Invalid token'], 401); } ``` ### Database Operations - Use CodeIgniter Query Builder or Model methods - Use `helper('utc')` for UTC date conversion - Wrap multi-table operations in transactions ```php $this->db->transStart(); // ... database operations $this->db->transComplete(); if ($this->db->transStatus() === false) { return $this->respond(['status' => 'error', 'message' => 'Transaction failed'], 500); } ``` ### Model Patterns - Extend `BaseModel` for automatic UTC date handling - Use `checkDbError()` for database error detection ```php error(); if (!empty($error['code'])) { throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}"); } } } ``` ### Testing Guidelines ```php withBodyFormat('json')->post($this->endpoint, $payload); $result->assertStatus(201); } } ``` **Test Naming**: `test` (e.g., `testCreatePatientValidationFail`) **Test Status Codes**: 200 (GET/PATCH), 201 (POST), 400 (Validation), 401 (Unauthorized), 404 (Not Found), 500 (Server Error) ### API Design - **Base URL**: `/api/` - **Authentication**: JWT token via HttpOnly cookie - **Content-Type**: `application/json` - **Methods**: GET (read), POST (create), PATCH (partial update), DELETE (delete) ### Routes Pattern ```php $routes->group('api/patient', function ($routes) { $routes->get('/', 'Patient\PatientController::index'); $routes->post('/', 'Patient\PatientController::create'); $routes->get('(:num)', 'Patient\PatientController::show/$1'); $routes->patch('/', 'Patient\PatientController::update'); $routes->delete('/', 'Patient\PatientController::delete'); }); ``` ### Security - Use `auth` filter for protected routes - Sanitize user inputs - Use parameterized queries - Store secrets in `.env`, never commit --- ## Project-Specific Conventions ### API Documentation Sync **CRITICAL**: When updating any controller, you MUST also update the corresponding OpenAPI YAML documentation: - **Paths**: `public/paths/.yaml` (e.g., `patients.yaml`, `orders.yaml`) - **Schemas**: `public/components/schemas/.yaml` - **Main file**: `public/api-docs.yaml` (for tags and schema references) **After updating YAML files**, regenerate the bundled documentation: ```bash node public/bundle-api-docs.js ``` This produces `public/api-docs.bundled.yaml` which is used by Swagger UI/Redoc. ### Controller-to-YAML Mapping | Controller | YAML Path File | YAML Schema File | |-----------|----------------|------------------| | `PatientController` | `paths/patients.yaml` | `components/schemas/patient.yaml` | | `PatVisitController` | `paths/patient-visits.yaml` | `components/schemas/patient-visit.yaml` | | `OrderTestController` | `paths/orders.yaml` | `components/schemas/orders.yaml` | | `SpecimenController` | `paths/specimen.yaml` | `components/schemas/specimen.yaml` | | `TestsController` | `paths/tests.yaml` | `components/schemas/tests.yaml` | | `AuthController` | `paths/authentication.yaml` | `components/schemas/authentication.yaml` | | `ResultController` | `paths/results.yaml` | `components/schemas/*.yaml` | | `EdgeController` | `paths/edge-api.yaml` | `components/schemas/edge-api.yaml` | | `LocationController` | `paths/locations.yaml` | `components/schemas/master-data.yaml` | | `ValueSetController` | `paths/valuesets.yaml` | `components/schemas/valuesets.yaml` | | `ContactController` | `paths/contact.yaml` | (inline schemas) | ### Legacy Field Naming Database uses PascalCase columns: `PatientID`, `NameFirst`, `Birthdate`, `CreatedAt`, `UpdatedAt` ### ValueSet System ```php use App\Libraries\Lookups; $genders = Lookups::get('gender'); $label = Lookups::getLabel('gender', '1'); // Returns 'Female' $options = Lookups::getOptions('gender'); $labeled = Lookups::transformLabels($data, ['Sex' => 'gender']); ``` ### Nested Data Handling For entities with nested data (PatIdt, PatCom, PatAtt): - Extract nested arrays before filtering - Use transactions for multi-table operations - Handle empty/null arrays appropriately --- ## Environment Configuration ### Database (`.env`) ```ini database.default.hostname = localhost database.default.database = clqms01 database.default.username = root database.default.password = adminsakti database.default.DBDriver = MySQLi ``` ### JWT Secret (`.env`) ```ini JWT_SECRET = '5pandaNdutNdut' ``` --- ## Additional Notes - **API-Only**: No view layer - headless REST API - **Frontend Agnostic**: Any client can consume these APIs - **Stateless**: JWT-based authentication per request - **UTC Dates**: All dates stored in UTC, converted for display *© 2025 5Panda Team. Engineering Precision in Clinical Diagnostics.*