# 08 — Developer Guide

How to extend LabCore. Mostly: adding a new analyzer vendor module.

## Conceptual model

LabCore's core abstraction is the **driver**:

```
                ┌────────────────────────────────────────────┐
                │  LabCore engine                             │
                │   - lab_* tables                            │
                │   - /api/lab/* endpoints                    │
                │   - UI (worklist, results, devices, …)      │
                │   - ResultIngestionService                  │
                │   - DriverRegistry                          │
                └────────────┬───────────────────────────────┘
                             │ resolves by `vendor`
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
        AbaxisDriver   Nx600Driver   <your driver>
        (ASTM)         (NX600 proto) (whatever)
```

Each driver implements the `LabDeviceDriver` contract and is registered by
its module's service provider. The cloud has no other knowledge of a
vendor's idiosyncrasies — everything routes through the driver.

The contract:
[Modules/LabCore/Entities/Contracts/LabDeviceDriver.php](../../Modules/LabCore/Entities/Contracts/LabDeviceDriver.php)

```php
interface LabDeviceDriver
{
    public function vendor(): string;          // 'abaxis', 'nx600', ...
    public function label(): string;           // 'Abaxis VetScan'
    public function models(): array;           // ['VetScan VS2', ...]
    public function commProtocols(): array;    // ['astm-e1394']
    public function connectionTypes(): array;  // ['tcp', 'serial', ...]
    public function defaultAnalyteMap(): array; // catalog for seeding
    public function parse(string $raw, array $context = []): ?array;
    public function normalize(array $parsed): array; // -> session + results
}
```

`parse()` returns the vendor's own structured representation (or null for
non-result payloads). `normalize()` maps that representation onto the
canonical `lab_test_sessions` + `lab_test_results` shape — see
[AbaxisDriver](../../Modules/AbaxisLab/Entities/Drivers/AbaxisDriver.php)
or [Nx600Driver](../../Modules/NX600Lab/Entities/Drivers/Nx600Driver.php)
for full examples.

## Adding a new vendor module

End-to-end recipe — say you're adding **Mindray** (BC-5000 hematology):

### 1. Scaffold the module

```bash
php artisan module:make MindrayLab
```

The Nwidart generator creates the standard layout. You'll cull most of it;
LabCore vendor modules are tiny.

### 2. Declare the dependency

`Modules/MindrayLab/module.json`:

```json
{
    "name": "MindrayLab",
    "alias": "mindray-lab",
    "description": "Mindray hematology vendor for LabCore",
    "active": 1,
    "providers": ["Modules\\MindrayLab\\Providers\\MindrayLabServiceProvider"],
    "files": ["start.php"],
    "requires": ["LabCore"]
}
```

The `requires` array is what tells the module system to load LabCore first
and refuse to enable MindrayLab without LabCore present.

### 3. Implement the driver

`Modules/MindrayLab/Entities/Drivers/MindrayDriver.php`:

```php
<?php
namespace Modules\MindrayLab\Entities\Drivers;

use Modules\LabCore\Entities\Contracts\LabDeviceDriver;
use Modules\LabCore\Entities\Services\Astm\AstmRecordParser;

class MindrayDriver implements LabDeviceDriver
{
    public function vendor(): string         { return 'mindray'; }
    public function label(): string          { return 'Mindray BC-5000'; }
    public function models(): array          { return ['BC-5000', 'BC-3000']; }
    public function commProtocols(): array   { return ['astm-e1394']; }
    public function connectionTypes(): array { return ['tcp', 'serial']; }

    public function defaultAnalyteMap(): array
    {
        // Vendor analyte catalog — seeded into lab_analyte_map at install.
        return [
            ['analyte_code'=>'WBC','canonical_name'=>'White Blood Cells','unit'=>'10^9/L','species'=>null,'ref_lower'=>null,'ref_upper'=>null,'category'=>'hematology','sort_order'=>0],
            // ...
        ];
    }

    public function parse(string $raw, array $context = []): ?array
    {
        // If your vendor speaks ASTM, reuse LabCore's parser:
        $message = (new AstmRecordParser())->parseMessage($raw);
        $hasResults = collect($message['patients'] ?? [])
            ->flatMap(fn($p) => $p['orders'] ?? [])
            ->contains(fn($o) => !empty($o['results']));
        return $hasResults ? $message : null;
        // If your vendor speaks something else (HL7, proprietary binary):
        // implement it here and return a structured array.
    }

    public function normalize(array $parsed): array
    {
        // Walk $parsed and build the canonical shape:
        return [
            'session' => [
                'vendor'       => 'mindray',
                'sample_no'    => '...',
                'patient_id'   => '...',
                'patient_name' => '...',
                // ...session columns from lab_test_sessions
            ],
            'results' => [
                [
                    'analyte_code' => 'WBC',
                    'value'        => '7.4',
                    'unit'         => '10^9/L',
                    'ref_lower'    => '4',
                    'ref_upper'    => '11',
                    'is_abnormal'  => false,
                    // ...result columns from lab_test_results
                ],
                // ...
            ],
        ];
    }
}
```

### 4. Register with LabCore

`Modules/MindrayLab/Providers/MindrayLabServiceProvider.php`:

```php
public function boot()
{
    $this->registerConfig();
    $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
    $this->registerLabCoreDriver();
}

protected function registerLabCoreDriver(): void
{
    if (class_exists(\Modules\LabCore\Entities\Services\DriverRegistry::class)) {
        \Modules\LabCore\Entities\Services\DriverRegistry::register(
            new \Modules\MindrayLab\Entities\Drivers\MindrayDriver()
        );
    }
}
```

The `class_exists` guard means MindrayLab degrades gracefully if LabCore
were ever absent.

### 5. Seed the analyte catalog

A migration that copies the driver's `defaultAnalyteMap()` into
`lab_analyte_map`:

`Modules/MindrayLab/Database/Migrations/YYYY_MM_DD_seed_mindray_analyte_map.php`:

```php
return new class extends Migration {
    public function up(): void
    {
        if (! Schema::hasTable('lab_analyte_map')) return;
        if (DB::table('lab_analyte_map')->where('vendor', 'mindray')->exists()) return;
        $rows = [];
        foreach ((new \Modules\MindrayLab\Entities\Drivers\MindrayDriver())->defaultAnalyteMap() as $r) {
            $rows[] = array_merge($r, [
                'business_id' => null, 'vendor' => 'mindray', 'model' => null,
                'created_at' => now(), 'updated_at' => now(),
            ]);
        }
        if ($rows) DB::table('lab_analyte_map')->insert($rows);
    }
    public function down(): void
    {
        if (Schema::hasTable('lab_analyte_map')) {
            DB::table('lab_analyte_map')->where('vendor', 'mindray')->delete();
        }
    }
};
```

### 6. (Optional) Add an InstallController

If you want it to install via the Modules admin screen UI, copy
[AbaxisLab's InstallController](../../Modules/AbaxisLab/Http/Controllers/InstallController.php)
and rename `AbaxisLab` → `MindrayLab` and the config keys.

### 7. (Optional) Add a simulator

For dev / CI testing, copy
[AbaxisSimulator](../../Modules/AbaxisLab/Console/AbaxisSimulator.php) and
adjust the analyte panels / vendor key. Register it in
`MindrayLabServiceProvider::boot()` under the `runningInConsole` block.

### 8. Enable + install

```bash
# Add to modules_statuses.json:
"MindrayLab": true,

php artisan module:migrate MindrayLab
php artisan tinker --execute='\App\System::addProperty("MindrayLab_version","1.0.0");'
```

Verify:

```bash
php artisan tinker --execute='echo "Vendors: ".implode(",", array_keys(
  Modules\LabCore\Entities\Services\DriverRegistry::all()
)).PHP_EOL;'
# expect: abaxis,mindray,nx600
```

`mindray` should now appear in the Lab → Devices "Vendor" dropdown.

### 9. Field-test

Use the simulator (or a real device) → run a result → confirm session +
results appear in **Lab → Results** with canonical analyte names from your
seeded map.

## Extending the agent for a new transport

The Go agent's ASTM E1381 layer handles the common case. For a vendor with
a different transport (NX600's proprietary protocol, HL7/MLLP, raw text
files, etc.):

1. Add a new package under `labbridge-agent/internal/transport/<name>/`
   that mirrors `astm` — a `Receive()` function that takes a `ReadWriter`
   and returns a `Session` with the assembled payload.
2. Pick a new `comm_protocol` key (e.g. `nx600-type-1`, `hl7-mllp`).
3. In `internal/listener/manager.go`, branch on
   `dev.CommProtocol` (or wrap into a Strategy if it grows) to choose
   which transport's `Receive()` to call.
4. The cloud-side driver's `vendor` stays the same; the `comm_protocol`
   value flows through to it via the agent's POST.

## Calling LabCore from PHP code

Inside the cloud app, you can ingest a raw message programmatically
(useful for tests, batch imports, custom integrations):

```php
use Modules\LabCore\Entities\LabAgent;
use Modules\LabCore\Entities\Services\ResultIngestionService;
use Illuminate\Support\Str;

$agent = LabAgent::first();
$out = app(ResultIngestionService::class)->ingest(
    $agent,
    'abaxis',                  // vendor
    $rawAstmMessage,           // raw bytes (CR-separated records, no STX/ETX)
    (string) Str::uuid(),      // idempotency key
    null,                      // device_id (optional)
);
// $out = ['status' => 'stored', 'session_id' => 4, 'results' => 8]
```

Same idempotency, same analyte mapping, same VetClinic linkage as the
HTTP path. Wrap in a transaction if you're batch-importing.

## Adding a new connection type

Connection types live in `config('labcore.connection_types')`:

```php
'connection_types' => [
    'tcp'        => 'TCP/IP (network)',
    'serial'     => 'Serial RS-232 (COM port)',
    'serial2eth' => 'Serial-to-Ethernet adapter',
],
```

Add a key (say `usb`), then:

1. Allow it in [LabDeviceController::validateDevice()](../../Modules/LabCore/Http/Controllers/LabDeviceController.php).
2. Add a case in the agent's `listener/manager.go` Start() switch.
3. Write the `internal/listener/usb.go` listener.

The cloud UI's dropdown picks up the new key automatically (the device
form iterates `config('labcore.connection_types')`).

## Adding a new API endpoint

Routes: [Modules/LabCore/Routes/api.php](../../Modules/LabCore/Routes/api.php).

Conventions:
- Agent-facing endpoints sit under `Route::prefix('lab')` (which resolves
  to `/api/lab/...`).
- Auth is bearer-token resolved through `AgentController::agent()`.
- Return `response()->json(...)` shapes that match the rest of the API
  (see [05 API reference](05-api-reference.md)) — `status`, `reason`,
  domain ids.

For non-agent (browser / OAuth) endpoints, route under web.php and use
the existing controllers / `lab.*` permissions.

## Tests

The driver layer is straightforward to unit-test — feed `parse()` a
captured raw message, assert the `normalize()` output shape. See
[`Phase 2` verification approach in the LabCore memory](../../C:/Users/hossa/.claude/projects/d--xampp-htdocs-static-purchase/memory/module_lab_core.md)
for the pattern: build a representative ASTM message in PHP, run it
through driver + ingestion service, assert the stored shape.

A formal test suite under `tests/Feature/LabCore/` isn't shipped yet —
add it as the project warrants.

## Where to file issues / propose changes

- Module bugs / vendor-dialect tweaks: Modules/<Vendor>Lab/
- Engine-wide changes (schema, contract, API): Modules/LabCore/
- Agent (Go): labbridge-agent/
- Docs: docs/labcore/

Branch from `main`, PR back. The Phase 1–4 work is under
`feature/labcore-modules`.
