# 06 — VetClinic Sync

LabCore can optionally link lab results to pets in the **VetClinic** module:
raise an order from a pet's profile, get the results back on that pet's
profile. The integration is **soft** — LabCore runs perfectly fine without
VetClinic; when VetClinic is present it lights up.

## When sync is active

Three conditions must be true:

1. The VetClinic module is installed (`vc_pets` table exists and the
   `\Modules\VetClinic\Entities\VcPet` class is loadable).
2. LabCore's per-business `vetclinic_sync` setting is `1` (the default).
3. The `lab_test_sessions.business_id` is set (every agent-ingested
   session inherits its agent's `business_id`).

Toggle in **Lab → Settings**:

```
[x] Enable VetClinic sync (raise lab orders against pets, show results on
    the pet profile)
```

Disabling it is a per-business switch — the `lab_settings(business_id, key,
value)` row with `key='vetclinic_sync'`. Leaving sync off makes LabCore a
fully standalone lab system.

## How the linkage flows

```
                          ┌───────────────┐
   from pet's profile ──> │  lab_worklist │ ── carries vc_pet_id, contact_id
                          └───────┬───────┘
                                  │  agent picks up entry / analyzer queries
                                  ▼
   raw message in agent ──> /api/lab/results
                                  │
                                  ▼
   ┌─────────────────────────────────────────┐
   │  lab_test_sessions                       │
   │   ├── uuid                                │
   │   ├── sample_no, patient, results...     │
   │   └── vc_pet_id, contact_id   ◄── inherited from the worklist entry by
   └─────────────────────────────────────────┘    VetClinicSyncService::applyWorklistLinkage()
                                  │
                                  ▼
   pet profile (`pets/show.blade.php`) ── "Lab Tests" panel renders sessions
                                          for that vc_pet_id
```

The glue lives in
[`VetClinicSyncService`](../../Modules/LabCore/Entities/Services/) (alongside
the NX600Lab version of the same idea, which is preserved for backward
compatibility).

## Raising a lab order from a pet

The cloud-side bridge endpoint is `POST /lab/vetclinic/pet-order`
(part of the LabCore VetClinicBridge), called from a button on the pet's
profile. The button is added by the LabCore "pet lab panel" partial which
the VetClinic pet-show view includes guarded:

```blade
@if(\Illuminate\Support\Facades\View::exists('labCore::bridge.pet_lab_panel'))
    @include('labCore::bridge.pet_lab_panel', ['pet' => $pet])
@endif
```

(NX600Lab uses the parallel guard `'nx600Lab::bridge.pet_lab_panel'`, so
multiple lab modules coexist on the pet profile as separate panels.)

> **Note:** The pet-side bridge UI is scaffolded but not yet wired in V1
> — NX600Lab's pet-lab panel is the working reference. The LabCore
> equivalent is on the Phase 5 list (after Phase 3 agent + Phase 4 UI
> shipped). Until then, raise worklist entries manually in **Lab →
> Worklist** and set the `vc_pet_id` directly when seeding test data.

## Pet linkage logic

When a result message arrives via `/api/lab/results`, the
`ResultIngestionService` stores the session but with no pet link yet. A
later step (handled by `VetClinicSyncService::applyWorklistLinkage()`)
walks the `lab_worklist` table looking for a match:

1. **Primary match** — the worklist entry whose `sample_no` matches the
   incoming session's `sample_no` and whose `vc_pet_id` is set. Inherit
   `business_id`, `contact_id`, `vc_pet_id` from it.
2. **Fallback match** — same patient_id, in case the analyzer chose a
   different sample number than the one in the worklist (some firmware
   does this when a sample is rescanned).
3. No match → session stays unlinked. It still shows in **Lab → Results**
   and gets a `vendor` but no pet linkage.

This mirrors NX600Lab's `VetClinicSyncService::applyWorklistLinkage()` —
proven pattern, copied for the shared engine.

## Field translation

When raising a worklist entry from a pet, the bridge converts the
VetClinic pet record into the worklist fields the analyzer expects:

| VetClinic field | Mapped to | Notes |
|---|---|---|
| `pets.petname` | `lab_worklist.patient_name` | Truncated to 80 chars |
| `pets.serial_no` or `microchip_no` or `P{id}` | `lab_worklist.patient_id` | Stable identifier the analyzer prints on the report |
| `pets.species` | `lab_worklist.species` | Text label |
| `pets.gender` | `lab_worklist.sex` | `male` → `Male`, `female` → `Female`, else `Unknown` |
| `pets.dob` | derived `age` | Whole years; `999` when unknown |
| `pets.contact_id` | `lab_worklist.contact_id` | Pet owner |
| `pets.id` | `lab_worklist.vc_pet_id` | Back-link |

For vendors that need numeric species codes (NX600), the
`speciesCodeForPet()` helper consults the per-business species map (kept
in NX600's `NxModuleSetting::speciesMap()`). AbaxisLab/ASTM uses the text
label directly.

## Permissions

- Raising a worklist entry from a pet's profile requires
  `vetclinic.access_pet` (VetClinic) and `lab.manage_worklist` (LabCore).
- Viewing results on the pet profile requires `vetclinic.access_pet` and
  `lab.view_results`.

## Disabling sync at runtime

Two ways:

1. **Per business in the UI** — Lab → Settings → uncheck "Enable VetClinic
   sync". Existing linked sessions keep their `vc_pet_id` but new sessions
   won't get linked.
2. **Globally via .env** — `LABCORE_VETCLINIC_SYNC=false` flips the
   default for newly-created businesses, but doesn't affect existing
   `lab_settings` rows.

## Troubleshooting linkage

| Symptom | Likely cause |
|---|---|
| Result lands in **Lab → Results** but `vc_pet_id` is null | No matching worklist entry. Was the worklist raised before the analyzer ran? Does `sample_no` match? |
| Pet profile shows no Lab Tests panel | LabCore module not installed, or pet-lab-panel partial not wired (V1 pending) |
| Wrong pet linked | Two worklist entries with the same `sample_no` — the more recent one wins. Soft-delete duplicates. |
