clqms-be/AGENTS.md
mahdahar 42006e1af9 feat: implement comprehensive order management with specimens and tests
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
2026-03-03 13:51:27 +07:00

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 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

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 BaseModel for 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 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:

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.