## Context

Contract rendering pipeline (same shape in `Contract` and `ContractType`):

```
template (CKEditor HTML, contains ${code})
   │
   ├─ Field::templateReplace()        ${salary_information} → <ul><li>{payslip name}: ${salary_x}</li>…</ul>
   │                                   (payslip name inserted raw via __() replacement)
   │
   ├─ collect values
   │    default fields  → Field::getDefaultValueByCode()   (DB: profile, employee, contract…)
   │    custom fields   → ContractField.value / loadFieldFromText()
   │    overriding      → request overriding_fields / ContractOverridingField
   │
   ├─ Field::transformValueByKey()    date → d/m/Y, salary → number_format, term/gender maps
   │
   ├─ str_replace(${code} → value, template)      ◀── values inserted RAW (the bug)
   ├─ preg_replace('/font-family.+?;/', '')
   │
   └─ view layouts.pdf_download  {!! $content !!}  → dompdf
```

Callers:
- `Contract::getPreviewByTemplate()` ← `updatePreview()` (stores `contracts.preview`; signed → `updateStaticFile()` PDF) and `getPDFByRequest()` with `template` (`ContractController::previewByAccount`, `ContractController` preview endpoints).
- `ContractType::getPreviewByTemplate()` ← `getPDFByRequest()` (contract type preview using `Contract::getFake()`).
- Stored `preview` is also returned by the API and rendered as HTML by the frontend.

There is no global input sanitization middleware; values are stored exactly as entered (verified: `contract_position` is `nullable|string|max:191`, nothing strips tags).

## Goals / Non-Goals

**Goals:**
- Every substituted value renders as literal text in preview HTML and PDF.
- One shared code path so `Contract` and `ContractType` cannot drift.
- Output for values without special characters is byte-for-byte unchanged.

**Non-Goals:**
- Sanitizing the template HTML itself (it is admin-authored rich text; trusted).
- Regenerating stored previews or static PDFs of existing contracts.
- Fixing the pre-existing quirk that `str_replace` with arrays is sequential (a value containing another `${code}` could get substituted again) and that `preg_replace('/font-family.+?;/')` runs over values too. Note them; do not change behavior here.
- Rendering newlines in values as `<br>`.
- Validating/rejecting `<`/`>` on input (`contract_position` etc. may legitimately contain them).

## Decisions

### D1: Escape at substitution time, not at input time
Escape in the rendering step, right after `transformValueByKey()` and before `str_replace`.
- *Alternative: strip/escape on save* — rejected: corrupts stored data (the employee screen, export and filter would show `&lt;b&gt;`), and does not cover existing rows.
- *Alternative: escape inside `transformValueByKey()`* — rejected: that function's job is formatting; mixing in HTML encoding makes it unsafe to reuse for non-HTML outputs.

### D2: Use Laravel `e()` (htmlspecialchars, ENT_QUOTES, UTF-8, double_encode = true)
- `double_encode = true` is intentional: a stored literal `&amp;` must display as `&amp;` (spec scenario "Value that looks like an HTML entity").
- `e(null)` returns `''`, matching the current behaviour for empty values.
- Vietnamese characters are untouched (UTF-8, no `htmlentities`).
- Cast non-string values (numbers from `number_format` are already strings; salary raw ints, Carbon not expected after transform) — `e()` handles scalars; ensure the value is a scalar/string before calling.

### D3: Escape all value sources, including `overriding_fields`
Overriding values are plain text entered per contract (validation is `''`, frontend sends input values). Treating them as text is consistent and closes the injection path.
- *Risk*: if the frontend ever sends rich text in an overriding value, it will now show tags literally. See Open Questions.

### D4: Shared helper on `Field`
Add a small static helper on `Field` (e.g. `Field::renderValue($code, $value)` = `e(transformValueByKey(...))`, or a helper that builds the `full_code → escaped value` map) and use it from both `Contract::getPreviewByTemplate()` and `ContractType::getPreviewByTemplate()`, replacing the duplicated transform loop. Exact name/shape is left to the implementer; requirement is one code path.

### D5: Escape payslip item name in `templateReplace()`
`salary_information` builds an HTML fragment with `__($itemTemplate, ['item_name' => $payslipItem->name, 'item_value_code' => $childFullCode])`. `item_name` is data → pass `e($payslipItem->name)`. `item_value_code` is the `${code}` placeholder text, not a value; leave it as is so `str_replace` still matches it, and its value gets escaped later by D1.

## Risks / Trade-offs

- [Existing data that was intentionally HTML, e.g. someone typed `<b>Giám đốc</b>` into a field to get bold] → it will now show tags literally. Mitigation: acceptable and correct; formatting belongs in the template. Check quickly on prod data before deploy: `SELECT contract_position FROM employees WHERE contract_position REGEXP '[<>&]'` (and same for custom field / overriding values) to know who is affected.
- [Stored `preview` of unsigned contracts still holds the old unescaped HTML until regenerated] → regenerated on next update/refresh; acceptable. Optional one-off: re-run `updatePreview()` for unsigned contracts whose fields contain `<>&` — out of scope unless requested.
- [Signed contracts' static PDFs keep the old rendering] → intentional; signed documents must not change.
- [`ContractType::getPreviewByTemplate()` uses `Contract::getFake()` data] → fake data rarely has special chars; covered by shared helper anyway.

## Migration Plan

Code-only change, no migration. Deploy normally. Rollback = revert the commit.

## Open Questions

- Does the frontend ever send rich text (HTML) in `overriding_fields[*].value`? Assumed **no** (plain inputs). If yes, overriding values need a separate decision (e.g. sanitize with an allowlist instead of escape).
- Should unsigned contracts' stored `preview` be regenerated in bulk after deploy? Default: no.
