| name | update-local-taxes |
| description | Refresh the per-município local-tax rates (data/local_taxes/index.json) — five ИПИ indicators across all 265 общини (property tax on legal entities, property-transfer tax, vehicle tax 74-110 kW, retail patent tax ≤100 m², taxi patent tax) plus optional per-município naredba blocks (residential ТБО + basis flag, property tax for individuals where the TAX naredba is reachable, tourist tax, dog tax) for the oblast capitals. Use when the daily watch report flags `ipi_local_taxes` or `municipal_naredba` as changed, when the user asks to refresh local taxes / местни данъци / municipal-tax rates, after a new fiscal year of naredbi (typically December-January), or after a fresh git clone if data/local_taxes/index.json is missing or has an empty `scoresByObshtina`. |
| allowed-tools | ["Read","Bash","Edit","Write","WebFetch"] |
Update local taxes skill
Refreshes data/local_taxes/index.json — Bulgaria's per-município local-tax rates. Powers the My-Area "Местни данъци" tile.
The file has two tiers that merge into one structure:
| Tier | Source | Coverage | Cadence | Cost |
|---|
| A — ИПИ aggregator | 265obshtini.bg | All 265 общини × 5 tax indicators | Annual (Q3 publication) | Fully automated |
| B — Per-município naredba | each município's FEES + TAX naredbi | 10 wired, full 10/10 coverage on all four slots: ТБО basis flag, property tax for individuals (Sofia 1.875‰, Plovdiv 1.8‰, Varna 2.0‰, Burgas 1.75‰ pinned, Стара Загора 1.7‰, Razgrad 3‰, Samokov 2.5‰, Мъглиж 3.5‰, Балчик 2.5‰, Петрич 3‰), tourist tax (extractor walks every "[1-5] звезда/звезди" anchor — handles Varna which starts at 2-star — with a 200-char rate window; prefers EUR over BGN-converted; range / single-rate fallbacks), dog tax (extractor walks every "за притежаване на куче" / "такса куче" anchor with three rate-pattern fallbacks: anchored "в размер на X лв/евро", dual inline "X лв./Y евро", and tabular no-unit pair "куче NN.NN NN.NN" where the second number is EUR). Other oblast capitals land as parsers are added; scripts/local_taxes/probe_obshtini_all.ts enumerates obshtini.bg coverage for the remaining 256 municípios (it tries three slug variants — concatenated / underscore / hyphenated — since obshtini.bg subdomains are inconsistent; name-slug auto-discovery surfaces ~10 matchable tenants, the rest use non-name slugs needing manual lookup). | Annual (Dec→Jan adoption) | Per-município parser via createObshtiniBgNaredbaParser factory when both naredbi are on the obshtini.bg platform; bespoke for direct PDFs / DOCX / legacy .doc (Burgas — lib/fetch_doc.ts shells out to macOS textutil; Linux operators swap in antiword) |
Tier B fills in the resident-side taxes ИПИ doesn't cover — most importantly the residential garbage-fee basis (промил / users / area / volume), which is the comparability metadata that lets the tile honestly show ТБО across municipalities.
When to run
| Trigger | Action |
|---|
Daily watcher describes ИПИ — Местни данъци (265 общини) with a year tick or row-count change | Step A1 |
Daily watcher describes Общински наредби за местни данъци with a N município(s) re-uploaded line | Step B1 — pass the named municípios via --naredba <code>,<code> |
| User asks "refresh local taxes", "update местни данъци", "ИПИ tax rates" | Step A1 (+ Step B1 if oblast-capital naredbi changed) |
Fresh clone with empty scoresByObshtina | Step A1 (Tier B is incremental — naredba blocks land as parsers run) |
| New fiscal year (December rollover) | Step A1 once ИПИ publishes (typically August/September of T+1); Step B1 once each município adopts its new naredba (late Dec → late Jan) |
Step A1 — refresh ИПИ tier (5 CSVs, all 265 общини)
npx tsx scripts/local_taxes/build_index.ts
Expected output:
fetching property_tax_legal (id=615)…
property_tax_legal: 265/265 municípios mapped · latest year 2025 · avg 2.539
fetching transfer_tax (id=616)…
transfer_tax: 265/265 municípios mapped · latest year 2025 · avg 2.758
fetching vehicle_tax_74_110kw (id=617)…
vehicle_tax_74_110kw: 265/265 municípios mapped · latest year 2025 · avg 1.498
fetching patent_tax_retail (id=618)…
patent_tax_retail: 265/265 municípios mapped · latest year 2025 · avg 8.332
fetching patent_tax_taxi (id=360)…
patent_tax_taxi: 265/265 municípios mapped · latest year 2023 · avg 302.015
wrote data/local_taxes/index.json · ipi: 265 municípios · naredba: 0 (preserved) · total unmatched cell-rows: 0
The build is idempotent and merges with any existing naredba blocks Tier B parsers wrote — re-running Tier A never wipes Tier B output.
If unmatched > 0 appears in the output, the script will print the unique município names that didn't resolve. Add the missing alias in scripts/local_taxes/lib/match_obshtina.ts — most are case-only mismatches between ИПИ's title-cased spelling and the canonical first-word-only-capitalised Bulgarian convention.
Step B1 — refresh per-município naredba tier (Tier B)
npx tsx scripts/local_taxes/run_naredba.ts
npx tsx scripts/local_taxes/run_naredba.ts SOF00,PDV01
npx tsx scripts/local_taxes/run_naredba.ts --force SOF00
The watcher's describe-line names exactly which munícipios' source URLs flipped (N naredba(s) re-uploaded: SOF00, PDV01) — run the dispatcher with that subset.
What ships per município:
- ТБО basis flag (промил / users / area / volume) — required; without it cross-município ТБО comparisons are noise.
- ТБО rate — surfaced when the naredba carries it inline; many municípios (Sofia in particular) defer the per-year rate to a separate annual council decision, in which case the
rate field stays absent and a Bulgarian note explains why.
- Tourist tax — value + unit ("BGN/нощувка"), when the source document is the TAX naredba (туристически данък lives there, not in the FEES naredba).
- Dog tax — value + unit ("BGN/година"), same caveat.
Adding a new município:
- Write
scripts/local_taxes/parsers/<obshtina>.ts exporting a NaredbaParser (see parsers/sof.ts as the template — fetches a PDF via fetchNaredbaPdf, extracts text, calls buildNaredbaBlock).
- Push it onto
NAREDBA_PARSERS in scripts/local_taxes/parsers/index.ts.
- Re-run the dispatcher with just that código to verify the basis flag landed correctly.
The dispatcher merges into data/local_taxes/index.json preserving the Tier A ipi blocks, and writes a per-município watermark at state/ingest/local_taxes_<obshtina>.json so future runs can short-circuit when the source PDF hasn't changed (sourceHash matches).
OCR fallback (Gemini Vision) is not wired yet — the current parsers assume the upstream PDF carries a real text layer. When we hit a município with image-only naredbi (Сливен-style protocols), we'll add a --ocr flag mirroring the council-minutes pattern.
Step 2 — verify
jq '.latestYear, .indicators | length, (.scoresByObshtina | length)' data/local_taxes/index.json
Spot-check a known município (Sofia uses SOF00 synthetic code):
jq '.scoresByObshtina.SOF00.ipi' data/local_taxes/index.json
Step 3 — commit + stamp
git add scripts/local_taxes/ data/local_taxes/ scripts/watch/sources/{ipi_local_taxes.ts,index.ts}
git commit -m "local-taxes: refresh ИПИ tier (5 indicators · 265 municípios · latest YYYY)"
npx tsx scripts/stamp-ingest.ts update-local-taxes \
--summary "ИПИ tier: 265/265 municípios · 5 indicators · latest YYYY · avg Y.YY%"
Append to the public data-changes log only when on-disk data actually moved:
if [ -n "$(git diff --stat data/local_taxes/)" ]; then
npx tsx scripts/append-data-change.ts update-local-taxes \
--summary "ИПИ tier: 265/265 municípios · 5 indicators · latest YYYY" \
--source "ИПИ — Местни данъци (265 общини)"
fi
Known limitations
- Residential ТБО not in ИПИ. The most-asked-about tax (garbage fee on private homes) varies by basis (промил / брой ползватели / РЗП / количество) and isn't part of the ИПИ aggregator. Tier B parsers will surface it per município once Tier B ships.
- No intra-município zoning. Property tax and ТБО can vary by zone within a município under ЗМДТ Art. 15, but each município defines its own zones in prose; no national EKATTE→zone map exists. Tier B captures only the município-level rate (or the city-centre / "zone 1" rate if multiple zones exist).
- Vehicle tax band is 74-110 kW only. ИПИ tracks one slice of the ЗМДТ tariff grid as the representative comparison point. Other engine-power bands are out of scope.
- Taxi patent tax tracked only through 2023. ИПИ paused this indicator after the 2024 amendments to ЗМДТ Art. 61з. We surface the 2023 value as the latest available and don't try to back-fill 2024-2025.
- Манual paste not required. Unlike LISI, the ИПИ CSV endpoint is a stable HTTP download. Tier A runs unattended.
What this skill does NOT do
- Does not touch state/municipal_budgets/. Local-tax rates are an input to the budget, not a slice of it.
- Does not build per-município sidecars. The file is one ~30-40 KB blob; per-município shards aren't needed at this size.
- Does not enrich with collection-rate data. ИПИ also publishes "collected % of assessed" indicators (separate ids 619-620); deferred — the tile design doesn't yet have a place for them.
File map
Tier A — ИПИ pipeline
| Path | Purpose |
|---|
scripts/local_taxes/ipi.ts | Indicator catalogue (5 ids, labels, units, direction, EUR conversion) |
scripts/local_taxes/build_index.ts | Tier A build script — re-fetches 5 CSVs, normalises names, ranks, writes |
scripts/local_taxes/lib/match_obshtina.ts | Município-name → obshtina-code resolver (manual aliases + oblast disambiguation) |
Tier B — naredba pipeline
| Path | Purpose |
|---|
scripts/local_taxes/types.ts | Canonical NaredbaBlock, NaredbaParser, NaredbaParserResult (imported by build_index.ts and mirrored in src/data/local_taxes/useLocalTaxes.tsx) |
scripts/local_taxes/run_naredba.ts | Tier B dispatcher — runs one or all parsers, writes per-município shards + watermarks |
scripts/local_taxes/lib/extract_naredba.ts | Extractors: detectTboBasis, extractResidentialTboRate, extractPropertyTaxIndividualsRate, extractTouristTax, extractDogTax, buildNaredbaBlock |
scripts/local_taxes/lib/fetch_pdf.ts | Direct PDF fetch + pdftotext (Sofia FEES, Varna FEES) |
scripts/local_taxes/lib/fetch_docx.ts | DOCX fetch + XML strip (Burgas FEES) |
scripts/local_taxes/lib/fetch_doc.ts | Legacy .doc fetch + macOS textutil shell-out (Burgas TAX); swap in antiword on Linux |
scripts/local_taxes/lib/fetch_obshtini_bg.ts | obshtini.bg JSON-API fetcher (Sofia/Plovdiv/Varna/Razgrad/Samokov/Мъглиж/Балчик/Петрич TAX + most FEES) |
scripts/local_taxes/lib/obshtini_bg_naredba.ts | createObshtiniBgNaredbaParser factory for the multi-source obshtini.bg pattern |
scripts/local_taxes/parsers/{sof,var,bgs,pdv,raz,sfo,mgl,blc,ptr}.ts | 9 per-município parsers (3 bespoke for multi-platform, 6 thin factory wrappers) |
scripts/local_taxes/parsers/index.ts | Registry — push new parsers here |
scripts/local_taxes/probe_obshtini_all.ts | Discovery utility — transliterates every BG município and probes the obshtini.bg API for naredbi |
Watch + state
| Path | Purpose |
|---|
scripts/watch/sources/ipi_local_taxes.ts | Tier A watcher — fingerprints all 5 CSVs jointly + tracks the max year seen |
scripts/watch/sources/municipal_naredba.ts | Tier B watcher — HEAD-probes each parser's url + secondaryUrls |
state/ingest/local_taxes_<code>.json | Per-município watermark (sourceHash + lastSuccessfulIngest) |
state/watch/municipal_naredba.json | Watcher fingerprint state |
Output + consumption
| Path | Purpose |
|---|
data/local_taxes/index.json | Tier A output — indicator catalogue + ranking denominators + national averages |
data/local_taxes/<obshtina>.json | Per-município shard — ipi block (5 indicators) + optional naredba block (Tier B) |
src/data/local_taxes/useLocalTaxes.tsx | React Query hook + frontend NaredbaBlock mirror (Sofia район S2xxx and SOF46 → SOF00 fallback) |
src/screens/myarea/MyAreaLocalTaxesTile.tsx | "Местни данъци" rate-comparison tile (ИПИ rows + ТБО basis + tourist + dog) |
src/screens/myarea/MyAreaTaxReceiptTile.tsx | "Where do my taxes go" tile — local-tax estimate block using the same data |