update-nzok · diff

git:20260825.2979aec to git:20261017.cf6ec25

2 added, 2 removed. Audit A to A.

---
name: update-nzok
description: Refresh the НЗОК (National Health Insurance Fund) health-pack data (data/budget/nzok/) — the per-hospital БМП payments, gross drug-reimbursement by INN, and monthly B1 cash-execution that feed the health sector pack on /awarder/121858220. Each generator fetches the latest file from nhif.bg directly (no manual download). Also covers the per-hospital drug UNIT prices (НЗОК Справка 5 / ПЛС2), the МЗ quarterly hospital financial indicators (ЕЕОФ), the clinical-activity corpus (cases + ЗОЛ per clinical pathway per hospital — the case-mix denominator), and — via the opt-in `--revenue` step — private-hospital annual revenue recovered from the filed ГФО in the Търговски регистър (portal.registryagency.bg) plus the public-vs-private comparison band. Use when the daily watch report flags `nzok_hospital_bmp`, `nzok_drug_quarterly`, `nzok_drug_unit_prices`, `nzok_execution_b1`, `mh_eeof_quarterly`, `nzok_activities`, or `nzok_nrd_tariffs` (a new НРД/amendment may re-set the КП/АПр/КПр prices) as changed, when the user asks to refresh НЗОК / NHIF / health-fund / hospital-financials / private-hospital-revenue data, periodically (e.g. after the Sept ГФО filing season) to fill new private-hospital revenue via `--revenue`, or after a fresh git clone if data/budget/nzok/*.json is missing.
allowed-tools:
- Read
- Bash
- Edit
- Write
---
# Update НЗОК skill
- Refreshes `data/budget/nzok/` — the data behind the **health sector pack** on `/awarder/121858220`. НЗОК's public-procurement footprint (~€79M) is ~1.5% of its ~€5.5bn budget; the pack fuses the budget law + the non-ЗОП payment streams (hospital reimbursements, drug reimbursement, cash execution) with the contract ledger. Design: `docs/plans/nzok-health-pack-v1.md`; ingest details: `scripts/nzok/README.md`; memory `[[project_nzok_health_pack]]`.
+ Refreshes `data/budget/nzok/` — the data behind the **health sector pack** on `/awarder/121858220`. НЗОК's public-procurement footprint (~€79M) is ~1.5% of its ~€5.5bn budget; the pack fuses the budget law + the non-ЗОП payment streams (hospital reimbursements, drug reimbursement, cash execution) with the contract ledger. Ingest details: `scripts/nzok/README.md`; memory `[[project_nzok_health_pack]]`.
**Source:** `nhif.bg` — fetched directly over plain HTTPS (no Cloudflare, unlike the NSSI B1 flow which needs a manual download). All four generators auto-discover and download the newest file.
## When to run
| Trigger | Action |
|---|---|
| Watcher flags `nzok_hospital_bmp` changed (new monthly hospital-payments PDF) | `npm run data:nzok -- --hospitals` |
| Watcher flags `nzok_drug_quarterly` changed (new quarter / annual drug XLS) | `npm run data:nzok -- --drugs` |
| Watcher flags `nzok_execution_b1` changed (new monthly B1 execution XLS) | `npm run data:nzok -- --execution` |
| Two or three flagged together | `npm run data:nzok` (runs all steps) |
| User asks "refresh НЗОК / health-fund data" | `npm run data:nzok` |
| User asks to refresh private-hospital revenue / ГФО / "публични срещу частни болници"; **or periodically** to fill new ГФО filings (annual, Sept season) | `npm run data:nzok -- --revenue` (opt-in, **heavy Gemini OCR**; needs `GEMINI_API_KEY` + local PG; resumable — only fills empty cells) |
| Fresh clone, `data/budget/nzok/*.json` missing | `npm run data:nzok` |
The budget law (`--budget`, `budget.json`) is **hard-keyed** from the annual ЗБНЗОК — it has no watcher source of its own. Re-run it in **two** cases:
1. a new fiscal year's law is added to `scripts/budget/nzok/__write_budget.ts`; **or**
2. an existing year moves from **проект to обн. закон** — the `dv_laws` watcher's describe-line names `ЗБНЗОК` when the law is promulgated. This is a *user-visible number changing on a live page*, not a new row: FY2026 went €5,537,996,900 (Надзор draft) → €5,256,677,200 (ДВ бр. 68 от 28.07.2026), a €281.3M correction on the `/awarder/121858220` bridge tile. Flip `basis: "draft"` → `"law"` and re-key every line.
**Key every line from its чл. 1 ал. 2 CODE, not its label,** and set `expectedResidualK` when the law states the residual's components separately. The generator's overshoot throw only catches *over*-keying; an under-keyed table reconciles perfectly and silently inflates the reserve bar. Both 2026 transcription errors (a dropped 1.1.3.6, and `hospital` taken from sub-line 1.1.3.7.1 instead of the parent 1.1.3.7) were invisible to Σ-equals-total and are caught only by that residual assertion. Verify with:
```bash
npx tsx scripts/budget/nzok/__smoke_budget.ts
```
**`data/budget` is bucket-served in production** (`useBudget` fetches through `dataUrl()`), so committing `budget.json` is not enough — the corrected number reaches the live site only after a sync. Include `data-changes.json`: it is bucket-served from the data root, so `-- budget` alone publishes the corrected figure while leaving the changelog entry that explains it invisible.
```bash
npm run bucket:sync:paths -- budget data-changes.json
```
**Derived analytics need no separate trigger.** The drug-savings leaderboard (060), the report-card + decile-fan and reporting-coverage fns (056/058) and the pathway tree (059's serving fn over the activities corpus) are read-only functions over the drug-prices / ЕЕОФ / activities corpora, applied by those same three loaders — so they refresh automatically whenever `nzok_drug_unit_prices`, `mh_eeof_quarterly` or `nzok_activities` flips and its loader re-runs. The only genuinely new source is the **pathway tariffs** (opt-in, BG egress — see the dedicated section below).
## Procedure
1. Run the ingest for the flagged subset (or all):
```bash
npm run data:nzok -- --hospitals --drugs --execution
```
Each generator fetches from nhif.bg, parses (reconciliation-asserted for hospital payments), and rewrites the matching `data/budget/nzok/*.json`. Note: `--execution` also writes `execution_history.json` (every B1 month on the page, for the plan-vs-actual pace chart), and `--drugs` also computes the full-year-vs-full-year `growth` block (fastest-rising / falling / newly-reimbursed molecules from the two latest annual files) inside `drug_reimbursement.json`.
2. **Reload the hospital-payments Postgres table** — the pack's hospital tile and the `/company/:eik` reimbursement tile are DB-served from `nzok_hospital_payments`, NOT the static JSON, so on a new monthly БМП file this is what actually updates the live tiles:
```bash
npm run db:load:nzok-hospital:pg # local — verify row/month counts
npm run db:load:nzok-hospital:pg:cloud # Cloud SQL via the proxy on :5434
npm run db:dump:cloud # refresh the GCS snapshot
```
The loader is idempotent (TRUNCATE+reload in one txn, changelog-deduped) and applies **both** migration 045 (the table) and 047 (the `nzok_hospital_payments_trends` / `nzok_hospital_momentum_by_eik` functions that serve the momentum tile + the `/company/:eik` spend-growth percentile), so the trend endpoints refresh with the table. Skip when only `--execution` changed (that stays static-JSON-served). ⚠️ `--drugs` is
NOT static any more — the per-INN quarterly series is served from Postgres, so it needs
its own load and publish:
```bash
npm run db:load:nzok-drug-quarterly:pg
npm run db:load:nzok-drug-quarterly:pg:cloud # nothing does this automatically
```
Then rebuild the **hospital map geo crosswalk** (`nzok_hospital_geo`, migration 075) that the health-pack marker map at the top of `/awarder/121858220` is drawn from. It re-resolves one HQ point per hospital EIK from `awarder_seats` + `data/settlements.json`, so a new monthly БМП file (new/renamed facilities) means new points:
```bash
npm run db:load:nzok-hospital-map:pg # local — verify geolocated count
npm run db:load:nzok-hospital-map:pg:cloud # Cloud SQL via the proxy on :5434
```
Must run **after** `db:load:nzok-hospital:pg` (it reads `nzok_hospital_payments`) and after `db:load:awarder-seats:pg` (the geo bridge). The metric fold (`nzok_hospital_map()`) is live over the payments / drug-overpay / activities tables, so the crosswalk only needs a re-run when the facility universe or its seats change — not when a metric moves.
**If the loader reports RESTATED rows it has already written `data-changes.json` — commit and sync it.** This is the one thing `recordIngestBatch` cannot do: it keys on `(reg_no, period)`, so a TRUNCATE+reload of months already in `ingest_first_seen` itemises **nothing** — the rows are not new — and `/data/updates` shows silence while published per-hospital euros change. The loader diffs the previous vintage inside its own transaction and, when anything moved, appends the entry itself (`dedupeSameDay`, so a re-run replaces rather than duplicates).
⚠️ It writes the entry rather than printing a command because the signal is **one-shot**: once the load commits, the corrected figures ARE the previous vintage and the next run reports nothing. `db:refresh` runs this loader with `--tolerate-offline`, so the run that consumes a correction may be unattended.
`data/data-changes.json` is git-tracked **and** bucket-served, so it needs both:
```bash
git add data/data-changes.json # commit it with the rest of step 4
npm run bucket:sync:paths -- budget data-changes.json
```
- ⚠️ The FIRST load after the parser-hardening work will report a large restatement — the Tier 1 corrections were measured at ~€1.67M across 11 months, including a sign flip on ДКЦ Св. София ЕООД that published a clawback as income for six months, plus ~33 rows whose month figure becomes 0 ("unknown") instead of a fabricated fragment. Expect a large `added` count too: the 24 months the old asserts withheld now load. That is the correction landing, not a defect — see `docs/plans/nzok-hospital-parser-hardening-v1.md`. (The loader counts ROWS, not months; the month figures here come from the plan.)
+ ⚠️ The FIRST load after the parser-hardening work will report a large restatement — the Tier 1 corrections were measured at ~€1.67M across 11 months, including a sign flip on ДКЦ Св. София ЕООД that published a clawback as income for six months, plus ~33 rows whose month figure becomes 0 ("unknown") instead of a fabricated fragment. Expect a large `added` count too: the 24 months the old asserts withheld now load. That is the correction landing, not a defect. (The loader counts ROWS, not months.)
3. Sanity-check the console output. Hospital payments print, in decreasing order of urgency: any **skipped** month (expected 0 — a skip is a month the site does not have); any **restated** rows (money already published changing value — the loader writes the `data-changes.json` entry, you commit and sync it); any month that **repeats** its predecessor with a zero month column (published as filed, see RC-5); and finally the months that loaded with something **unverified** — blocks disagreeing with НЗОК's own printed facility count, or printing no subtotal at all. The count figure is descriptive — watch it for a TREND against the baseline the loader prints, because a rise means НЗОК's documents changed shape. The **no-subtotal** figure beside it is not descriptive: money in a block that prints no subtotal is reconciled by nothing but the whole-file 0.5% ratio, which is the check that let €1,672,123 through before this work. Drugs print the €total + top INN + oncology group L; execution prints revenue + expenditure YTD.
Legacy note: this step used to say "reconcile the printed facility count + national total against the file's own Общо РЗОК grand total". That whole-file check is now the BACKSTOP rather than the check — each РЗОК block is reconciled against its own printed subtotal, and the facility count is reported rather than asserted. Nothing is left for you to reconcile by eye; the loader either accepts a month or names the block that failed.
4. Commit the changed `data/budget/nzok/*.json` — **and `data/data-changes.json` if the loader wrote a restatement entry** (+ `data/db/procurement.lock.json` if you pushed).
5. **`bucket:sync data/budget/nzok/`** — the budget/drug/execution JSONs are served from the GCS bucket; without the sync those tiles aren't live. If step 4 committed `data/data-changes.json`, sync it too (`npm run bucket:sync:paths -- budget data-changes.json`) — it is bucket-served from the data root, so the corrected figures otherwise publish while the note explaining them does not.
6. Stamp `state/ingest/update-nzok.json` with `lastSuccessfulIngest` (current UTC ISO) + a one-line `summary` so `process-watch-report` won't re-trigger.
## Notes
- **xlsx**: the bundled build has file access disabled — the generators read via `xlsx.read(fs.readFileSync(path), { type: "buffer", codepage: 1251 })`.
- Raw downloads cache under `raw_data/nzok/` (gitignored) — safe to delete.
- The ИАМН рег.№→EIK crosswalk is **built** (opt-in via `npm run data:nzok -- --crosswalk`, needs local PG; 265/381 facilities matched = 93% of YTD €). It is near-static — re-run + re-audit (0 false positives is the bar) only when extending `MANUAL_OVERRIDES` in `scripts/nzok/write_hospital_eik.ts`. See `scripts/nzok/README.md`.
## Payment streams (migration 050)
НЗОК pays a hospital through THREE monthly reports, all listed on the same
`nhif.bg/bg/hospitals/bmp/{year}` page. A facility's НЗОК income is their sum:
| stream | report | FY2025 | facilities |
|---|---|---|---|
| `bmp` | Заплатени здравноосигурителни плащания за БМП | €2.271bn | 388 |
| `drugs` | Заплатени средства за ЛП в условията на БМП | €0.799bn | 48 |
| `devices` | Заплатени средства за МИ, прилагани в БМП | €0.059bn | 107 |
Before 050 only `bmp` was ingested and every per-hospital figure understated the
facility. `scripts/nzok/parse_hospital_payments.ts` takes a `stream` argument;
`bmp` keeps a strict reading while `drugs`/`devices` allow single-amount rows and
negative amounts (clawbacks), which they legitimately contain.
The `nzok_hospital_bmp` watcher fingerprints **all three** links. The trend and
momentum functions stay pinned to `stream = 'bmp'` — the drugs/devices series is
shorter, so folding them into a 2023→ time series would print a step change that
is an artefact of ingest coverage.
**After ANY parser change, check the latest-period TOTAL per stream, not just the
facility count.** A misparse once shipped inside the 0.5% reconciliation tolerance.
## New datasets
### Per-hospital drug unit prices — `npm run data:nzok -- --drug-prices`
Source: `nhif.bg/bg/nzok/medicine/5` → `Справка 5_ПЛС2_MM.YYYY.xls` (monthly).
Carries `Опаковки`, `Брой в опаковка`, `Реимбурсна сума` and the `МКБ` code, so a
unit price is derivable — the annual "Брутни разходи по INN" file has no quantity
column, which is why this was long recorded as blocked.
Writes `data/budget/nzok/drug_unit_prices.json` (gitignored, regenerable), then
`npm run db:load:nzok-drug-prices:pg` loads migrations
052, 054, 060 **and** 065 — the loader applies all four schema files, so the serving
functions behind the `/molecule/:inn` + pack pages (`nzok_drug_molecule_detail`,
`nzok_drug_pack_detail`), the risk aggregates, and the **drug-savings leaderboard**
(`nzok_drug_savings_overview`, migration 060 — the national "€X avoidable overpay
if every hospital paid the peer median" tile) reach the DB with the corpus.
**Publish — nothing does this automatically:**
```bash
npm run db:load:nzok-drug-prices:pg:cloud
```
(or a surgical `apply_functions.ts 052_nzok_drug_unit_prices.sql 054_nzok_risk.sql
060_nzok_drug_savings.sql 065_nzok_ownership.sql` with the cloud `DATABASE_URL` when
only the FUNCTIONS changed — **060 has no other applier**, so omitting it leaves
`nzok_drug_savings_overview()` on its previous body with nothing erroring). Skip the
publish entirely and the `/api/db` routes degrade to null in production while local is
correct.
Compare at **pack identity** (`Национален №`), never at INN: PEMETREXED alone
spans five packs whose per-unit medians run €17–€66. A **5-pack volume floor**
applies. Dispersion is not wrongdoing — volume, delivery period and contract terms
all move a unit price. The defensible claim is *persistent* dispersion.
### Quarterly hospital financials (ЕЕОФ) — `npm run data:nzok -- --eeof`
Source: МЗ, "Финансови показатели на лечебни заведения за болнична помощ", one
XLSX per quarter under Наредба № 5 от 2019, **2019-Q2 →** (26 quarters).
Revenue, expense, total and overdue liabilities, beds, occupancy, length of stay,
cost per patient. Money columns are published in **хил. лева**.
Writes `data/budget/nzok/hospital_financials.json` (gitignored, regenerable), then
`npm run db:load:nzok-financials:pg` loads migrations 051, **056 and 058** — the
loader applies all three, so the **report-card + decile-fan** distribution fns
(`nzok_financials_measures_by_eik` / `nzok_financials_measure_fan`, migration 056 —
each ratio measure vs the national median + the OpenPrescribing decile fan) and the
**reporting-coverage** fn (`nzok_financials_coverage_by_eik`, migration 058 — which
quarters a hospital reported, so a gap isn't misread as a drop) reach the DB with
the corpus.
The workbook's third sheet (`НЗОК`) is keyed by `Рег.№ ЛЗ` and carries БМП +
devices + drugs per quarter — an **independent parity reference** for the three
payment streams, loaded into `nzok_eeof_nzok_parity`.
Per-patient indicators are emitted raw and are **never ranked**: a specialised
centre spends multiples of a general hospital's per-patient figure because of its
case mix. Ranking without a case-mix denominator (the clinical-pathway corpus,
below) would rank specialties, not stewardship.
**Publish — nothing does this automatically:**
```bash
npm run db:load:nzok-financials:pg:cloud
```
This loader is the sole applier of 051, 056 and 058, so skipping it leaves the
report-card and decile-fan functions absent on Cloud SQL — not stale, missing —
while every local check passes.
Known gap: 8 municipal blocks (2019-Q4 → 2021-Q3) are skipped on load — a parser
artefact collapses hospital identity to bare oblast labels in those quarters, so
their rows can neither be keyed nor matched to an EIK. They are listed on every
load. The remaining 43 blocks (3,635 rows) load cleanly.
### Clinical-activity corpus — `npm run data:nzok -- --activities`
Source: НЗОК, "Брой отчетени дейности по КП/АПр/КПр и брой ЗОЛ по код на лечебно
заведение и код на диагноза", 12 monthly XLSX per year at
`nhif.bg/bg/hospitalcare-report/activities/{year}`. ~104k rows/month at the
(facility × procedure × primary-ICD × secondary-ICD) grain; `parse_activities.ts`
folds each file to (facility, procedure) and `write_activities.ts` sums the year
to the annual matrix in `data/budget/nzok/activities.json` (~4.5 MB, **gitignored
and regenerable** — like `hospital_financials.json` / `drug_unit_prices.json`, so
a fresh clone must run this writer before `db:load:nzok-activities:pg`) plus a
national monthly cases/ЗОЛ trend. The small **`activities_overview.json`** companion
(~8 KB, national headline + trend + top procedures) IS committed — it feeds the AI
tool and any static reader.
This is the **case-mix denominator** the rest of the pack lacked. The source
carries the procedure CODE only — no name, no НРД price — so `procType` is derived
from the code's first letter (P→КП, A→АПр, K→КПр). Cases are volume, not money; the
НРД pathway-price join that turns them into spend is the separate `--pathway-tariffs`
ingest (migration 059, below).
### Procedure code→name nomenclature — `npm run data:nzok -- --procedure-names`
The activity feed's codes (P###/A##/K##) are unreadable on their own, so
`write_procedure_names.ts` builds `data/budget/nzok/procedures.json`
(`{ code: bgName }`) from НЗОК's НРД **Приложение 17** (клинични пътеки) + **18**
(амбулаторни процедури) + the клинични-процедури list on nhif.bg. The activity
tiles (awarder + `/company/:eik`) join it client-side via `resolveProcedureName`
(`src/lib/nzokProcedures.ts`), which strips the A99 / B1 / B2 / E billing modifiers
to the base name and falls back to the bare code when a code isn't mapped. **Run
from a BG egress** (nhif.bg is IP-gated) and needs `pdftotext` if the appendices are
PDFs. VERIFY the resolved appendix links on first run — the exact nhif.bg markup
couldn't be checked from the dev box (see the script header). The committed
`procedures.json` is the full generated nomenclature (~427 names); the writer
replaces it wholesale on each run.
`npm run db:load:nzok-activities:pg` (publish with
`npm run db:load:nzok-activities:pg:cloud`) loads migrations 053
**and 059's serving functions** — the `nzok_activities` table +
`nzok_activity_monthly` trend, and the jsonb fns: `nzok_activities_overview()`
(national top procedures + trend + the cases-per-bed outlier), `nzok_activities_by_eik()`
(a hospital's case-mix + national share), and the migration-059 fns
`nzok_activity_by_procedure_spend()` (the **pathway tree**: which hospitals bill one
КП, ranked by cases, with implied spend once tariffs are loaded) +
`nzok_casemix_expected_vs_actual()` (both volume-only / NULL until the tariff table is
populated). The loader attaches EIK and bed counts by a strong name fold against the
payments + ЕЕОФ crosswalks (private hospitals included via payments); unmatched → NULL.
### Clinical-pathway tariffs — `npm run data:nzok -- --pathway-tariffs` (INGESTED 2026-08-04)
The price factor that turns the volume-only activity corpus into a **spend** reading
and unlocks the **case-mix expected-vs-actual** signal (the STAR-PU / MSPB idea:
Σ list-tariff × cases vs actual БМП paid). **Source = the НРД CONTRACT BODY, not an
annex**: the prices are the чл. 368/369/370 tables (КП/КПр/АПр), re-tabled as
чл. 368б/369б/370б by each amendment — the 2025 prices live in Договор
№ РД-НС-01-2-3 от 22 май 2025 г. on nhif.bg/bg/nrd/2023-2025/medical. (This section
used to claim a price annex and BG-egress IP-gating; both were wrong — no such
annex exists and nhif.bg serves 200 from non-BG egress, verified 2026-08-04.)
The parser sections the contract text at the чл. 368/369/370 markers; iterate with
`--dump` / `--from-dump` if a future document shifts layout:
```
# value flags (--page/--annex/--nrd-year) need DIRECT invocation — the npm wrapper
# only forwards --dump/--from-dump/--bgn and rejects unknown flags:
tsx scripts/nzok/write_pathway_tariffs.ts --annex "<contract-or-amendment-pdf-url>" --dump --nrd-year 2025 --bgn
tsx scripts/nzok/write_pathway_tariffs.ts --from-dump --nrd-year 2025 --bgn # iterate the parser
npm run db:load:nzok-tariffs:pg # applies migration 059, loads tariffs
npm run db:load:nzok-tariffs:pg:cloud # publish — nothing does this automatically
```
Writes `data/budget/nzok/pathway_tariffs.json` (`{ code: priceEur }`, gitignored).
The loader is empty-safe — migration 059 is **also applied by the activities loader**
so its serving fns exist on a fresh DB even before tariffs land. First load
(НРД 2025, 410 codes: 352 КП + 51 АПр + 7 КПр) resolved 059's scope caveat by
measurement: national expected (КП+АПр+КПр) €2,137M vs actual БМП €2,108M — the
БМП stream covers all three types, so the case-mix pairing is correct as written.
The `nzok_nrd_tariffs` watcher flags a new НРД/amendment PDF on the era page (the
parse still needs a human pass — prices may be re-tabled under new чл. numbers).
Money: 2026+ НРД is EUR-native; pass `--bgn` for pre-2026 documents (÷ 1.95583).
The **cases-per-bed outlier** is pathway-internal and same-type-grouped (УМБАЛ vs
УМБАЛ, one procedure), with floors (≥50 cases, ≥20 beds, ≥4 peers). It is a
signpost, not a verdict — day-case pathways, referral concentration and bed
accounting all move the ratio legitimately. It only covers facilities with an
ЕЕОФ bed count (~82); the national volume + case-mix layers cover all ~463.
### Private-hospital ГФО revenue + public/private band — `npm run data:nzok -- --revenue` (opt-in, heavy OCR)
Public/state hospitals report quarterly to МЗ (ЕЕОФ → `hospital_financials.json`);
private hospitals do **not**, but as commercial companies they file an annual ГФО
with the Търговски регистър. `--revenue` recovers each private hospital's annual
revenue (2019–2024) so the whole hospital universe has a comparable multi-year
financial picture, and the EC-lawsuit "% publicly funded" number gets a real
denominator. Two generators run in order:
- `write_hospital_revenue.ts` → **`hospital_revenue.json`** (127 hospitals, ~611
hospital-years). Discovers each ГФО `ActID` from the TR daily open-data feed
already on disk (`raw_data/tr/daily/*.json`) → `curl`s the CC-BY PDF from
`portal.registryagency.bg/CR/api/Documents/{ActID}` (node fetch is WAF-blocked;
curl passes) → Gemini Vision OCR of the ОПР (код 18000 „Общо приходи“ / 15100) →
sanity gate dropping a read where same-year НЗОК > 1.15× revenue. **Resumable** —
a re-run only fills cells still empty, so refreshing FY2023/24 as hospitals file
late is cheap.
- `write_public_private.ts` → **`public_private.json`** — the "Частни болници и
обществените поръчки" band blob: ownership split of НЗОК money + the 50%-threshold
stats + who runs ЗОП tenders. Joins ownership + reimbursement-by-EIK + revenue +
the PG contracts corpus.
Prereqs: `GEMINI_API_KEY` in `.env.local` + the **local Postgres** (the tender
counts + the sanity gate). Upstream source is the Търговски регистър (the same
`egov_commerce` daily feed behind `tr:daily-refresh`), but the ГФО filings are
**annual/late** and the OCR is expensive — so this is a **periodic manual refresh**
(run after the Sept filing season, or whenever the private-hospital financials are
being refreshed), NOT a daily-watcher auto-run. Ingest details: `scripts/nzok/README.md`.
Then for production: `bucket:sync data/budget/nzok/`.