# Hướng dẫn Validation - Bộ Hướng dẫn Triển khai Core FHIR cho Việt Nam v0.5.0

## Hướng dẫn Validation

# Hướng dẫn kiểm tra hợp lệ — Validation Guidance

Mô hình kiểm tra hợp lệ của VN Core theo ba tầng:

* **Tầng 1 — Mức profile**: FHIRPath invariant tự động kiểm tra khi kiểm tra hợp lệ resource
* **Tầng 2 — Mức máy chủ**: Logic kiểm tra phức tạp cần triển khai trên máy chủ FHIR
* **Tầng 3 — Mức tài liệu**: Quy tắc nghiệp vụ mà bên triển khai cần tuân thủ

Nguyên tắc kiểm soát căn cứ: mọi validate có thể làm hồ sơ bị từ chối phải có căn cứ rõ trong văn bản pháp lý/chuẩn dữ liệu hoặc phải được ghi là quyết định conformance của VN Core. Các kiểm tra chỉ nhằm giữ ví dụ sạch, phát hiện mapping sai hoặc kiểm tra dữ liệu hỗ trợ được giữ ở mức warning/example regression, không được coi là lý do từ chối pháp lý nếu chưa có rule tương ứng.

| | |
| :--- | :--- |
| `normative-data-standard` | Văn bản/chuẩn dữ liệu quy định trực tiếp field, format, required hoặc required-if. |
| `legal-business-rule` | Luật/nghị định/thông tư quy định quyền lợi, phạm vi, mức hưởng, giám định hoặc từ chối thanh toán. |
| `profile-conformance` | Quyết định của VN Core khi biểu diễn nhóm dữ liệu pháp lý bằng resource FHIR. |
| `technical-quality-guardrail` | Kiểm tra chất lượng dữ liệu, fixture hoặc mapping; không mặc nhiên là lỗi pháp lý. |
| `source-exception` | Nguồn/validator có mâu thuẫn, thiếu text sạch hoặc nghi typo nên chưa encode hard error. |

Ma trận chi tiết đang được duy trì trong Wiki tại `wiki/mappings/bhyt-output-data/validation-traceability.md`. Riêng cụm thiết bị y tế có ma trận severity riêng tại `wiki/mappings/device-udi/device-validation-traceability.md` để tách hard error, warning và deferred/package.

## Tầng 1 — FHIRPath Invariants (tự động)

Các invariant sau được nhúng trong profile và sẽ tự động kiểm tra khi validate resource bằng FHIR validator.

### vn-cccd-format (VNCorePatient)

| | |
| :--- | :--- |
| **Context** | `Patient.identifier`(slice CCCD) |
| **Severity** | error |
| **Mô tả** | Số CCCD phải đúng 12 chữ số |
| **FHIRPath** | `value.empty() or value.matches('[0-9]{12}')` |
| **Căn cứ** | Luật Căn cước 2023, Điều 20 |

### vn-addr-province (VNCoreAddress)

| | |
| :--- | :--- |
| **Context** | `Address`(VNCoreAddress profile) |
| **Severity** | warning |
| **Mô tả** | Địa chỉ Việt Nam (country = 'VN') phải có extension:province |
| **FHIRPath** | `country.empty() or country != 'VN' or extension.where(url = '...vn-ext-province').exists()` |
| **Mẫu tham chiếu** | CH Core invariant`ch-addr-2` |

-------

### BHYT XML1-XML12 logical model invariants

Các invariant dưới đây nằm trên logical model payload QĐ 3176/OHP, không phải trên base clinical profiles. Chúng chỉ encode các điều kiện xuất hiện có thể kiểm tra trong cùng một dòng logical model.

| | | |
| :--- | :--- | :--- |
| `bhyt-xml1-ly-do-vnt-required` | `BHYTXML1SummaryLM` | `LY_DO_VNT`bắt buộc khi`MA_LOAI_KCB`là`3`,`4`, hoặc`9`. |
| `bhyt-xml3-service-code-required` | `BHYTXML3ServiceLM` | `MA_DICH_VU`bắt buộc khi không có`MA_VAT_TU`hoặc`MA_NHOM`không phải`10`. |
| `bhyt-xml3-supply-code-required` | `BHYTXML3ServiceLM` | `MA_VAT_TU`bắt buộc khi không có`MA_DICH_VU`hoặc`MA_NHOM=10`. |
| `bhyt-xml3-bed-group-requires-bed` | `BHYTXML3ServiceLM` | `MA_GIUONG`bắt buộc với`MA_NHOM``14`,`15`,`16`. |
| `bhyt-xml3-result-date-required` | `BHYTXML3ServiceLM` | `NGAY_KQ`bắt buộc trừ nhóm ngày giường`14`,`15`,`16`. |
| `bhyt-xml7-abortion-fields-required` | `BHYTXML7DischargePaperLM` | Trường đình chỉ thai bắt buộc khi`MA_DINH_CHI_THAI=1`. |
| `bhyt-xml11-abortion-fields-required` | `BHYTXML11SickLeaveLM` | Trường đình chỉ thai bắt buộc khi`MA_DINH_CHI_THAI=1`. |

Không đưa vào FHIRPath các rule cần đối chiếu nhiều XML, kiểm tra thời điểm hiện tại, tính ngày, tính tổng tiền hoặc tra danh mục ngoài. Các rule này thuộc Tầng 2.

Ghi chú pháp lý/kỹ thuật: `XML6.NGAYKD_HIV` đang được giữ optional trong logical model vì validator local đánh dấu bắt buộc nhưng mô tả rule có nêu ngoại lệ điều trị phơi nhiễm. Không khóa cứng `1..1` cho đến khi có nguồn pháp lý sạch xác nhận.

-------

### Device invariants

Các rule thiết bị y tế chỉ được hard-error khi là profile conformance đúng context. `VNCoreDevice.type` là `0..1 MS` để nhận dữ liệu legacy/BHYT; `VNCoreImplantableDevice` siết riêng cho thiết bị cấy ghép.

| | | | |
| :--- | :--- | :--- | :--- |
| `vn-implant-udi-carrier-content` | `VNCoreImplantableDevice` | warning | Nếu có`udiCarrier`, nên có ít nhất`carrierHRF`hoặc`carrierAIDC`. |

Xem thêm `wiki/mappings/device-udi/device-validation-traceability.md` trong Wiki khi thêm rule mới.

-------

## Tầng 2 — Kiểm tra ở phía máy chủ (bên triển khai thực hiện)

Các quy tắc sau **KHÔNG thể** kiểm tra bằng FHIRPath invariant vì yêu cầu logic phức tạp hoặc tra cứu dữ liệu ngoài. Implementer PHẢI triển khai trên FHIR server (custom OperationOutcome).

### CCCD: Chữ số thứ 4 phải phù hợp giới tính

**Quy tắc**: Chữ số thứ 4 (vị trí index 3) của số CCCD mã hóa giới tính và thế kỷ sinh:

| | | |
| :--- | :--- | :--- |
| 0 | Nam | 19xx |
| 1 | Nữ | 19xx |
| 2 | Nam | 20xx |
| 3 | Nữ | 20xx |
| 4 | Nam | 21xx |
| 5 | Nữ | 21xx |

**Kiểm tra**: `Patient.gender` phải nhất quán với chữ số thứ 4 của `identifier[CCCD].value`:

* `gender = male` → chữ số thứ 4 phải chẵn (0, 2, 4, 6, 8)
* `gender = female` → chữ số thứ 4 phải lẻ (1, 3, 5, 7, 9)

**Severity đề xuất**: warning (có thể có trường hợp đặc biệt như chuyển giới chưa cập nhật CCCD)

**Pseudocode**:

```
cccd_value = patient.identifier.where(system = '.../sid/cccd').value
if cccd_value is not null and cccd_value.length == 12:
    digit4 = int(cccd_value[3])
    if patient.gender == 'male' and digit4 % 2 != 0:
        warning("Chữ số thứ 4 CCCD không phù hợp giới tính nam")
    if patient.gender == 'female' and digit4 % 2 != 1:
        warning("Chữ số thứ 4 CCCD không phù hợp giới tính nữ")

```

### CCCD: Chữ số 5-6 phải phù hợp năm sinh

**Quy tắc**: Chữ số 5-6 (vị trí index 4-5) là 2 chữ số cuối của năm sinh.

**Kiểm tra**: Nếu `Patient.birthDate` có giá trị, 2 chữ số cuối năm sinh phải khớp vị trí 5-6 trong CCCD.

**Pseudocode**:

```
cccd_value = patient.identifier.where(system = '.../sid/cccd').value
if cccd_value is not null and patient.birthDate is not null:
    birth_year_last2 = patient.birthDate.year % 100  # e.g. 85, 90, 01
    cccd_year = int(cccd_value[4:6])
    if birth_year_last2 != cccd_year:
        warning("Năm sinh trong CCCD không khớp Patient.birthDate")

```

**Severity đề xuất**: warning

### CCCD: 3 chữ số đầu phải là prefix hợp lệ theo danh mục BCA

**Quy tắc**: 3 chữ số đầu tiên của CCCD là mã prefix nơi đăng ký khai sinh theo danh mục BCA, **KHÔNG phải** mã ĐVHC hiện hành trong `VNProvinceCS`.

**Kiểm tra**: Server tra cứu `VNCitizenIdBirthplacePrefixCS` để xác nhận 3 chữ số đầu là prefix BCA hợp lệ.

> **Lưu ý**: Mã tỉnh BCA (3 chữ số) khác với mã tỉnh TCTK (2 chữ số) dùng trong VNProvinceCS. Đây là bảng hỗ trợ validation cho số định danh cá nhân/CCCD đã cấp, không dùng để mã hóa địa chỉ.

**Severity đề xuất**: warning

### Địa chỉ: Xã/phường phải thuộc tỉnh/TP đã chọn

**Quy tắc**: Nếu cả `extension:province` và `extension:ward` đều có giá trị, mã xã phải thuộc về tỉnh đã chọn theo QĐ 19/2025/QĐ-TTg.

**Kiểm tra**: Server tra cứu bảng ĐVHC để xác nhận mã xã (5 chữ số) thuộc tỉnh (2 chữ số).

**Pseudocode**:

```
province_code = address.extension[province].valueCoding.code  # e.g. "01" (Hà Nội)
ward_code = address.extension[ward].valueCoding.code           # e.g. "00008" (Phường Ngọc Hà)

# Tra bảng ĐVHC: ward_code phải thuộc province_code
if not dvhc_lookup(ward_code).province == province_code:
    error("Mã xã/phường không thuộc tỉnh/TP đã chọn")

```

**Severity đề xuất**: error (sai ĐVHC là lỗi dữ liệu nghiêm trọng)

**Mẫu tham chiếu**: CH Core invariant `ch-addr-2` kiểm tra canton code bằng `memberOf()` — nhưng chỉ kiểm tra membership đơn cấp. Validation đa cấp (xã thuộc tỉnh) yêu cầu server-side.

### BHYT: identifier[BHYT] format CCCD nên khớp CCCD bệnh nhân

**Quy tắc**: Từ 15/8/2025 (NĐ 188/2025/NĐ-CP), số thẻ BHYT có thể sử dụng số CCCD 12 chữ số (duy trì song song với format BHXH 10 số và legacy 15 ký tự). Nếu `Coverage.identifier[BHYT].value` là CCCD format (12 chữ số), nó nên khớp với `Patient.identifier[CCCD].value` của beneficiary.

**Pseudocode**:

```
bhyt_value = coverage.identifier.where(system = '.../sid/bhyt').value
if bhyt_value is not null and bhyt_value.matches('[0-9]{12}'):
    patient = resolve(coverage.beneficiary)
    cccd = patient.identifier.where(system = '.../sid/cccd').value
    if cccd is not null and bhyt_value != cccd:
        warning("identifier[BHYT] (CCCD) không khớp CCCD bệnh nhân")

```

**Severity đề xuất**: warning (có thể dùng CCCD người thân cho trẻ em)

### Liên thông hồ sơ BHYT: SO_CCCD bắt buộc, trừ trường hợp bất khả kháng hợp lệ

**Quy tắc**: Khi xuất dữ liệu phục vụ Cổng giám định BHXH theo QĐ 3176/QĐ-BYT, `SO_CCCD` phải có dữ liệu. Chỉ được để trống nếu hồ sơ ghi nhận rõ mã lý do bất khả kháng hợp lệ.

**Cách model trong VN Core**:

* `Patient.identifier[CCCD]` vẫn là slice bắt buộc ở tầng hồ sơ lâm sàng
* Nếu chưa có số CCCD khi gửi BHYT, phải dùng `data-absent-reason` trên `identifier[CCCD].value`
* Đồng thời PHẢI có `Patient.extension[vn-ext-force-majeure-reason]`

**Pseudocode**:

```
cccd = patient.identifier.where(system = 'http://fhir.hl7.org.vn/core/sid/cccd')
reason = patient.extension.where(url = 'http://fhir.hl7.org.vn/core/StructureDefinition/vn-ext-force-majeure-reason')

if cccd.value.empty():
    if reason.empty():
        error("Thiếu SO_CCCD nhưng không có mã lý do bất khả kháng hợp lệ")

```

**Severity đề xuất**: error

### Liên thông hồ sơ BHYT: MA_LK phải tồn tại và nhất quán trên toàn bộ hồ sơ gửi

**Quy tắc**: `MA_LK` là khóa liên kết hồ sơ bắt buộc cho cùng một đợt KCB. Mã này phải xuất hiện trên `Claim.identifier` và ở Bundle hồ sơ thanh toán phải chỉ ra cùng một giá trị cho mọi resource nghiệp vụ liên quan.

**Cách model trong VN Core**:

* `Claim.identifier[MA_LK]` là nơi lưu canonical record linkage key
* `VNCoreBHYTSubmissionBundle.identifier` mang cùng giá trị
* Server triển khai nên kiểm tra tất cả resource trong Bundle trỏ về cùng một hồ sơ thanh toán

**Severity đề xuất**: error

### Liên thông hồ sơ BHYT: mã phản hồi gateway không được thay MA_LK

**Quy tắc**: `XML1_ID` hoặc mã phản hồi tương đương từ Cổng tiếp nhận dữ liệu là định danh của kết quả tiếp nhận/giám định. Mã này phải nằm ở `ClaimResponse.identifier[gatewayResponseId]` hoặc metadata phản hồi, không được dùng làm `Claim.identifier[MA_LK]` hoặc `BHYTSubmissionBundle.identifier`.

**Cách model trong VN Core**:

* `Claim.identifier[MALK]` và `VNCoreBHYTSubmissionBundle.identifier` giữ khóa liên kết hồ sơ.
* `VNCoreClaimResponse.identifier[gatewayResponseId]` giữ `XML1_ID`/mã phản hồi gateway nếu có.
* Server triển khai nên cảnh báo nếu `XML1_ID` được ghi vào system `http://fhir.hl7.org.vn/core/sid/ma-lk`.

**Severity đề xuất**: warning khi import dữ liệu cũ, error khi submit/gateway layer tạo mới.

### Liên thông hồ sơ BHYT: ngày giờ xuất cổng phải theo format yyyyMMddHHmm

**Quy tắc**: Các trường ngày giờ xuất/gateway như `NGAY_VAO`, `NGAY_RA`, `NGAY_SINH`, `NGAY_YL`, `NGAY_KQ`, timestamp export XML phải tuân thủ định dạng `yyyyMMddHHmm`.

**Áp dụng trong VN Core**:

* FHIR native vẫn dùng `date`, `dateTime`, `instant`
* Lớp export/gateway phải chuyển đổi sang chuỗi `yyyyMMddHHmm`
* Validator server phải chặn payload JSON/XML export nếu format sai

**Pseudocode**:

```
export_value = transform_fhir_datetime(resource_datetime)
if not export_value.matches('^[0-9]{12}$'):
    error("Ngày giờ xuất BHYT phải theo định dạng yyyyMMddHHmm")

```

**Severity đề xuất**: error

-------

## Tầng 3 — Quy tắc nghiệp vụ (documentation only)

Các quy tắc sau mang tính nghiệp vụ, không bắt buộc kiểm tra tự động nhưng bên triển khai nên lưu ý.

### Timeline lâm sàng

* `Encounter.period` không được overlap cho cùng bệnh nhân (trừ Encounter lồng nhau)
* `Condition.recordedDate` nên nằm trong `Encounter.period` của encounter tham chiếu
* `Observation.effectiveDateTime` nên nằm trong `Encounter.period`

### LOINC code phải phù hợp đơn vị

* LOINC có property type (MCnc = mg/dL, SCnc = mmol/L). Code LOINC phải phù hợp đơn vị đo trong `Observation.valueQuantity.unit`
* Ví dụ: LOINC `14771-0` (SCnc) → mmol/L, KHÔNG dùng LOINC `1558-6` (MCnc) cho mmol/L

### Encounter.diagnosis nhất quán

* Mỗi `Condition` tham chiếu encounter qua `Condition.encounter` → nên có entry tương ứng trong `Encounter.diagnosis`
* `Encounter.diagnosis.use` nên phân biệt rõ: AD (chẩn đoán nhập viện), DD (chẩn đoán phân biệt), CM (bệnh kèm)

### Organization semantics nhất quán

* Không dùng `vn-ext-org-rank` để biểu diễn `chưa xếp hạng`.
* Không dùng `vn-ext-org-level` để thay cho `vn-ext-legacy-technical-line`.
* Nếu cơ sở là y tế dự phòng/TTYT/kiểm nghiệm/kiểm định, ưu tiên `vn-ext-health-unit-rank` thay vì `vn-ext-org-rank`.
* Nếu dữ liệu là hồ sơ cũ theo TT 43/2013/TT-BYT, lưu tuyến 1-4 ở `vn-ext-legacy-technical-line`; không kéo ngược semantics này vào cấp quản lý hành chính hiện hành.

-------

## Tham khảo — Best Practices quốc tế

| | | |
| :--- | :--- | :--- |
| **CH Core**(Thụy Sĩ) | FHIRPath`memberOf()`cho canton code | `ch-addr-2`: canton phải thuộc VS |
| **US Core**(Mỹ) | Bindings`extensible`+ guidance page | Không dùng invariant cho SSN format |
| **AU Core**(Úc) | FHIRPath regex cho IHI (16 digits) | Invariant check digit trên Individual Healthcare Identifier |
| **JP Core**(Nhật) | Server-side cho address hierarchy | Không dùng FHIRPath cho prefecture-city validation |

VN Core áp dụng mô hình kết hợp: FHIRPath cho validation đơn giản (format, membership), server-side cho cross-field logic (CCCD-gender, xã-tỉnh hierarchy, `MA_LK`, `SO_CCCD`, định dạng export `yyyyMMddHHmm`).

### Thực thi trong repo

VN Core hiện kèm sẵn script để biến một phần Tier 2 thành executable checks:

```
./scripts/validate-tier2.sh

```

Script này là regression/check chất lượng dữ liệu cho repo. Một số check là căn cứ bắt buộc theo QĐ 3176/export gateway, một số check chỉ là warning-tier guardrail để giữ ví dụ và danh mục hỗ trợ không bị sai. Không dùng toàn bộ kết quả script này như bộ luật từ chối hồ sơ thật nếu chưa triển khai traceability tương ứng.

Script này hiện kiểm tra:

* CCCD khớp giới tính và năm sinh trên các patient examples chuẩn — `technical-quality-guardrail`.
* Prefix 3 số đầu CCCD thuộc `vn-citizen-id-birthplace-prefix-cs` — validation-support, không thay thế xác thực CSDL dân cư.
* Xã/phường thuộc tỉnh đã chọn dựa trên property `province` của `vn-ward-cs` — cần bảng ĐVHC theo thời điểm hiệu lực.
* `Coverage.identifier[BHYT]` dạng CCCD khớp CCCD beneficiary — warning-tier vì có ca trẻ em/người phụ thuộc/thẻ tạm.
* `subscriberId` khớp `identifier[BHYT]` — profile consistency khi cả hai cùng được khai báo.
* `SO_CCCD` thiếu chỉ hợp lệ khi có `force-majeure-reason` — lỗi bắt buộc trong profile/gateway hiện tại.
* `MA_LK` nhất quán trong `VNCoreBHYTSubmissionBundle` — lỗi bắt buộc cho hồ sơ gửi.
* `exportDateTime` âm tính khi không theo `yyyyMMddHHmm` — lỗi bắt buộc ở lớp export/gateway.
* Semantics của `Organization` không trộn hạng KCB, hạng đơn vị không phải cơ sở KCB, cấp quản lý hành chính, và tuyến kỹ thuật lịch sử trên các example chuẩn — profile/example guardrail.

Danh mục prefix CCCD của Bộ Công an nay đã được đóng gói ở mức validation-support trong IG dưới dạng `vn-citizen-id-birthplace-prefix-cs`, tách riêng với `VNProvinceCS`.

-------

## Liên hệ với các trang khác

| | |
| :--- | :--- |
| Tra cứu căn cứ pháp lý cho các quy tắc kiểm tra hợp lệ | [Cơ sở pháp lý](legal-basis.md) |
| Nắm yêu cầu bảo mật và quyền riêng tư áp dụng cho dữ liệu y tế | [Bảo mật và quyền riêng tư](security.md) |
| Diễn giải Must Support và cách xử lý khi thiếu dữ liệu | [Hướng dẫn Must Support](must-support-guidance.md) |
| Xác định nghĩa vụ tuân thủ theo vai trò triển khai | [Tuân thủ theo vai trò triển khai](conformance-by-actor.md) |

-------

## English Summary

VN Core uses a 3-tier validation model. Tier 1 uses profile invariants for automatic checks, Tier 2 covers server-side rules for cross-field and export logic, and Tier 3 records business rules that implementers should enforce in workflow and QA. The repo also ships executable Tier 2 checks for the current baseline.

