Major updates to order system: - Add specimen and test data to order responses (index, show, create, update, status update) - Implement getOrderSpecimens() and getOrderTests() private methods in OrderTestController - Support 'include=details' query parameter for expanded order data - Update OrderTestModel with enhanced query capabilities and transaction handling API documentation: - Update OpenAPI specs for orders, patient-visits, and tests - Add new schemas for order specimens and tests - Regenerate bundled API documentation Database: - Add Requestable field to TestDefSite migration - Create OrderSeeder and ClearOrderDataSeeder for test data - Update DBSeeder to include order seeding Patient visits: - Add filtering by PatientID, PatientName, and date ranges - Include LastLocation in visit queries Testing: - Add OrderCreateTest with comprehensive test coverage Documentation: - Update AGENTS.md with improved controller structure examples
8.7 KiB
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
# 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 <name>
php spark make:model <name>
php spark make:controller <name>
# Database migrations
php spark migrate
php spark migrate:rollback
Code Style Guidelines
PHP Standards
- PHP Version: 8.1+
- PSR-4 Autoloading:
App\maps toapp/,Config\maps toapp/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
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
use App\Traits\ResponseTrait;
use Firebase\JWT\JWT;
Controller Structure
Controllers handle HTTP requests and delegate business logic to Models. They should NOT contain database queries.
<?php
namespace App\Controllers;
use App\Traits\ResponseTrait;
class ExampleController extends Controller
{
use ResponseTrait;
protected $model;
public function __construct()
{
$this->model = new \App\Models\ExampleModel();
}
public function index()
{
$data = $this->model->findAll();
return $this->respond(['status' => 'success', 'data' => $data], 200);
}
public function create()
{
$data = $this->request->getJSON(true);
$result = $this->model->createWithRelations($data);
return $this->respond(['status' => 'success', 'data' => $result], 201);
}
}
Response Format
All API responses use standardized format:
// 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
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
$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
BaseModelfor automatic UTC date handling - Use
checkDbError()for database error detection
<?php
namespace App\Models;
class PatientModel extends BaseModel
{
protected $table = 'patients';
protected $primaryKey = 'PatientID';
protected $allowedFields = ['NameFirst', 'NameLast', ...];
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}");
}
}
}
Testing Guidelines
<?php
namespace Tests\Feature\Patients;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
use Faker\Factory;
class PatientCreateTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected $endpoint = 'api/patient';
public function testCreatePatientSuccess()
{
$faker = Factory::create('id_ID');
$payload = [...];
$result = $this->withBodyFormat('json')->post($this->endpoint, $payload);
$result->assertStatus(201);
}
}
Test Naming: test<Action><Scenario><ExpectedResult> (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
$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
authfilter 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/<resource>.yaml(e.g.,patients.yaml,orders.yaml) - Schemas:
public/components/schemas/<resource>.yaml - Main file:
public/api-docs.yaml(for tags and schema references)
After updating YAML files, regenerate the bundled documentation:
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
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)
database.default.hostname = localhost
database.default.database = clqms01
database.default.username = root
database.default.password = adminsakti
database.default.DBDriver = MySQLi
JWT Secret (.env)
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.