2026-03-16 07:24:50 +07:00
|
|
|
# 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 <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 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
|
|
|
|
|
<?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
|
|
|
|
|
<?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:
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
<?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
|
|
|
|
|
<?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
|
|
|
|
|
```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/<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:
|
|
|
|
|
```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.*
|