romania-mcp-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v7
12
+ - uses: actions/setup-python@v7
13
+ with:
14
+ python-version: "3.12"
15
+
16
+ - name: check tag matches version
17
+ run: |
18
+ tag="${GITHUB_REF_NAME#v}"
19
+ py=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
20
+ js=$(python -c "import json; print(json.load(open('server.json'))['version'])")
21
+ echo "tag=$tag pyproject=$py server.json=$js"
22
+ [ "$tag" = "$py" ] && [ "$tag" = "$js" ]
23
+
24
+ - run: pip install -e . pytest build
25
+ - run: pytest
26
+ - run: python -m build
27
+ - uses: actions/upload-artifact@v7
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ pypi:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - uses: actions/download-artifact@v8
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+ - uses: pypa/gh-action-pypi-publish@release/v1
44
+
45
+ mcp-registry:
46
+ needs: pypi
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ id-token: write
50
+ contents: read
51
+ steps:
52
+ - uses: actions/checkout@v7
53
+
54
+ - name: install mcp-publisher
55
+ run: |
56
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
57
+
58
+ - run: ./mcp-publisher login github-oidc
59
+
60
+ # pypi can take a moment before the new version shows up in its api
61
+ - name: publish
62
+ run: |
63
+ for i in 1 2 3 4 5; do
64
+ ./mcp-publisher publish && exit 0
65
+ sleep 20
66
+ done
67
+ exit 1
@@ -0,0 +1,21 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python: ["3.10", "3.12", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v7
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - run: pip install -e . pytest
21
+ - run: pytest -v
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .env
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release: `shop_check`, `company_lookup`, `company_financials`, `court_cases`, `exchange_rate`, `iban_check`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Vind-Gardoș
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: romania-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP server for Romanian public data: ANAF company status, financial statements, court cases, BNR exchange rates, online shop checks
5
+ Project-URL: Repository, https://github.com/robyroro/romania-mcp
6
+ Project-URL: Author, https://stratagency.ro/en/robert-vind-gardos
7
+ Author: Robert Vind-Gardoș
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: anaf,bnr,claude,cui,e-factura,mcp,portal-just,romania
11
+ Classifier: Natural Language :: Romanian
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: httpx>=0.27
16
+ Requires-Dist: mcp>=2.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # romania-mcp
20
+
21
+ [![tests](https://github.com/robyroro/romania-mcp/actions/workflows/tests.yml/badge.svg)](https://github.com/robyroro/romania-mcp/actions/workflows/tests.yml)
22
+ [![PyPI](https://img.shields.io/pypi/v/romania-mcp-server)](https://pypi.org/project/romania-mcp-server/)
23
+
24
+ MCP server for Romanian public data. Lets Claude (or any MCP client) check a company at ANAF, read its financial statements, find its court cases, get BNR exchange rates, validate IBANs, and put all of it together to tell you if an online shop looks like a scam. No API keys, everything comes from official public sources.
25
+
26
+ > **Pe scurt:** verifici o firmă după CUI (activă, radiată, inactivă fiscal, plătitoare de TVA), îi vezi bilanțurile, dosarele din instanță și cursul BNR, direct din Claude. Și îl poți întreba „e țeapă magazinul ăsta?”.
27
+
28
+ ## Tools
29
+
30
+ | tool | what it does | source |
31
+ |---|---|---|
32
+ | `shop_check` | is this online shop legit? reads the CUI off the site, checks the company, its financials, insolvency cases and how old the site is, returns red flags / warnings / good signs | everything below + Wayback Machine |
33
+ | `company_lookup` | status of up to 500 companies by CUI: name, address, Reg. Com. number, CAEN, deregistered, VAT registered, VAT on collection, split VAT, fiscally inactive, e-Factura registry. optionally as of a past date | ANAF |
34
+ | `company_financials` | filed balance sheets year by year: revenue, profit/loss, employees, debts, equity, cash | ANAF |
35
+ | `court_cases` | cases in every Romanian court by party name or case number: stage, parties and roles, latest outcome, next hearing, insolvency as debtor | Portal Just |
36
+ | `exchange_rate` | BNR reference rate for any day since 2005, with conversion | BNR |
37
+ | `iban_check` | is an IBAN well formed, which Romanian bank issued it, is it a Treasury account | offline |
38
+
39
+ ## Install
40
+
41
+ Needs Python 3.10+.
42
+
43
+ ```
44
+ pip install romania-mcp-server
45
+ ```
46
+
47
+ or if you use uv you don't need to install anything, just point the client at `uvx` (see below).
48
+
49
+ ### Claude Desktop
50
+
51
+ Add this to `claude_desktop_config.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "romania": {
57
+ "command": "uvx",
58
+ "args": ["romania-mcp-server"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ If you installed with pip, `"command": "romania-mcp-server"` with no args works too.
65
+
66
+ ### Claude Code
67
+
68
+ ```
69
+ claude mcp add romania -- uvx romania-mcp-server
70
+ ```
71
+
72
+ Cursor, Windsurf etc. take the same JSON as Claude Desktop.
73
+
74
+ ## Example prompts
75
+
76
+ - "e țeapă magazinul ăsta? https://..."
77
+ - "here are my 40 suppliers' CUIs, which ones are fiscally inactive or deregistered?"
78
+ - "was RO18547290 VAT registered on 15.03.2016?"
79
+ - "how did 14399840 do in the last 5 years? revenue, profit, employees"
80
+ - "does Foo Trade SRL have any insolvency cases? when's the next hearing?"
81
+ - "cât fac 2.450 EUR în lei la cursul BNR din 14 martie 2025?"
82
+ - "is this IBAN right? RO49 AAAA 1B31 0075 9384 0000"
83
+
84
+ ## How `shop_check` decides
85
+
86
+ It's a list of signals, not a verdict from above:
87
+
88
+ - **red flags** (risk = high): no CUI anywhere on the site, CUI unknown to ANAF, company deregistered or fiscally inactive, company is the debtor in an insolvency case
89
+ - **warnings** (two or more = medium): company or site younger than a few months, never archived by the Wayback Machine, no financial statements, zero revenue or employees, negative equity, no ANPC link, the company name ANAF returns never appears on the site (scam shops sometimes copy a real company's CUI)
90
+ - **good signs**: company and site have been around for years, real revenue and employees, ANPC links present
91
+
92
+ It can't see reviews, delivery times or whether the products exist, so a "low" doesn't mean the shop is good, just that the company behind it checks out.
93
+
94
+ ## Caching
95
+
96
+ Responses are cached in sqlite at `~/.cache/romania-mcp/cache.sqlite3` so ANAF and friends don't get asked the same thing twice: company status 6h, financials 24h, court cases 3h, current exchange rates 1h, past years' rates 30 days.
97
+
98
+ ```
99
+ ROMANIA_MCP_CACHE=off # disable
100
+ ROMANIA_MCP_CACHE_PATH=/tmp/x.db
101
+ ```
102
+
103
+ ## Notes
104
+
105
+ - ANAF only searches by CUI, not by company name. Name search would need the ONRC dataset, maybe later.
106
+ - ANAF asks for at most one request per second. The server spaces requests out, so 500 CUIs take about 5 seconds and 5 years of financials about 5.
107
+ - Financial statements for a year are filed by mid next year, and some companies only have data from around 2020 on.
108
+ - Portal Just's API only answers over plain http (their https is broken), and caps a search at 1000 cases.
109
+ - `.ro` domains have no public RDAP and ROTLD's whois terms forbid automated queries, so for `.ro` shops the site age comes from the Wayback Machine only.
110
+ - `iban_check` knows the main Romanian banks. A valid IBAN doesn't prove who owns the account.
111
+
112
+ ## Development
113
+
114
+ ```
115
+ git clone https://github.com/robyroro/romania-mcp
116
+ cd romania-mcp
117
+ pip install -e . pytest
118
+ pytest
119
+ ```
120
+
121
+ Tests don't hit the network, HTTP calls are mocked.
122
+
123
+ To poke at it with the MCP inspector:
124
+
125
+ ```
126
+ npx @modelcontextprotocol/inspector romania-mcp
127
+ ```
128
+
129
+ ## Be reasonable
130
+
131
+ All of this is public data, but court records name real people. Use it to vet companies you do business with, not to dig into individuals.
132
+
133
+ ## License
134
+
135
+ MIT © [Robert Vind-Gardoș](https://stratagency.ro/en/robert-vind-gardos) ([@robyroro](https://github.com/robyroro))
136
+
137
+ <!-- mcp-name: io.github.robyroro/romania-mcp -->
@@ -0,0 +1,119 @@
1
+ # romania-mcp
2
+
3
+ [![tests](https://github.com/robyroro/romania-mcp/actions/workflows/tests.yml/badge.svg)](https://github.com/robyroro/romania-mcp/actions/workflows/tests.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/romania-mcp-server)](https://pypi.org/project/romania-mcp-server/)
5
+
6
+ MCP server for Romanian public data. Lets Claude (or any MCP client) check a company at ANAF, read its financial statements, find its court cases, get BNR exchange rates, validate IBANs, and put all of it together to tell you if an online shop looks like a scam. No API keys, everything comes from official public sources.
7
+
8
+ > **Pe scurt:** verifici o firmă după CUI (activă, radiată, inactivă fiscal, plătitoare de TVA), îi vezi bilanțurile, dosarele din instanță și cursul BNR, direct din Claude. Și îl poți întreba „e țeapă magazinul ăsta?”.
9
+
10
+ ## Tools
11
+
12
+ | tool | what it does | source |
13
+ |---|---|---|
14
+ | `shop_check` | is this online shop legit? reads the CUI off the site, checks the company, its financials, insolvency cases and how old the site is, returns red flags / warnings / good signs | everything below + Wayback Machine |
15
+ | `company_lookup` | status of up to 500 companies by CUI: name, address, Reg. Com. number, CAEN, deregistered, VAT registered, VAT on collection, split VAT, fiscally inactive, e-Factura registry. optionally as of a past date | ANAF |
16
+ | `company_financials` | filed balance sheets year by year: revenue, profit/loss, employees, debts, equity, cash | ANAF |
17
+ | `court_cases` | cases in every Romanian court by party name or case number: stage, parties and roles, latest outcome, next hearing, insolvency as debtor | Portal Just |
18
+ | `exchange_rate` | BNR reference rate for any day since 2005, with conversion | BNR |
19
+ | `iban_check` | is an IBAN well formed, which Romanian bank issued it, is it a Treasury account | offline |
20
+
21
+ ## Install
22
+
23
+ Needs Python 3.10+.
24
+
25
+ ```
26
+ pip install romania-mcp-server
27
+ ```
28
+
29
+ or if you use uv you don't need to install anything, just point the client at `uvx` (see below).
30
+
31
+ ### Claude Desktop
32
+
33
+ Add this to `claude_desktop_config.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "romania": {
39
+ "command": "uvx",
40
+ "args": ["romania-mcp-server"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ If you installed with pip, `"command": "romania-mcp-server"` with no args works too.
47
+
48
+ ### Claude Code
49
+
50
+ ```
51
+ claude mcp add romania -- uvx romania-mcp-server
52
+ ```
53
+
54
+ Cursor, Windsurf etc. take the same JSON as Claude Desktop.
55
+
56
+ ## Example prompts
57
+
58
+ - "e țeapă magazinul ăsta? https://..."
59
+ - "here are my 40 suppliers' CUIs, which ones are fiscally inactive or deregistered?"
60
+ - "was RO18547290 VAT registered on 15.03.2016?"
61
+ - "how did 14399840 do in the last 5 years? revenue, profit, employees"
62
+ - "does Foo Trade SRL have any insolvency cases? when's the next hearing?"
63
+ - "cât fac 2.450 EUR în lei la cursul BNR din 14 martie 2025?"
64
+ - "is this IBAN right? RO49 AAAA 1B31 0075 9384 0000"
65
+
66
+ ## How `shop_check` decides
67
+
68
+ It's a list of signals, not a verdict from above:
69
+
70
+ - **red flags** (risk = high): no CUI anywhere on the site, CUI unknown to ANAF, company deregistered or fiscally inactive, company is the debtor in an insolvency case
71
+ - **warnings** (two or more = medium): company or site younger than a few months, never archived by the Wayback Machine, no financial statements, zero revenue or employees, negative equity, no ANPC link, the company name ANAF returns never appears on the site (scam shops sometimes copy a real company's CUI)
72
+ - **good signs**: company and site have been around for years, real revenue and employees, ANPC links present
73
+
74
+ It can't see reviews, delivery times or whether the products exist, so a "low" doesn't mean the shop is good, just that the company behind it checks out.
75
+
76
+ ## Caching
77
+
78
+ Responses are cached in sqlite at `~/.cache/romania-mcp/cache.sqlite3` so ANAF and friends don't get asked the same thing twice: company status 6h, financials 24h, court cases 3h, current exchange rates 1h, past years' rates 30 days.
79
+
80
+ ```
81
+ ROMANIA_MCP_CACHE=off # disable
82
+ ROMANIA_MCP_CACHE_PATH=/tmp/x.db
83
+ ```
84
+
85
+ ## Notes
86
+
87
+ - ANAF only searches by CUI, not by company name. Name search would need the ONRC dataset, maybe later.
88
+ - ANAF asks for at most one request per second. The server spaces requests out, so 500 CUIs take about 5 seconds and 5 years of financials about 5.
89
+ - Financial statements for a year are filed by mid next year, and some companies only have data from around 2020 on.
90
+ - Portal Just's API only answers over plain http (their https is broken), and caps a search at 1000 cases.
91
+ - `.ro` domains have no public RDAP and ROTLD's whois terms forbid automated queries, so for `.ro` shops the site age comes from the Wayback Machine only.
92
+ - `iban_check` knows the main Romanian banks. A valid IBAN doesn't prove who owns the account.
93
+
94
+ ## Development
95
+
96
+ ```
97
+ git clone https://github.com/robyroro/romania-mcp
98
+ cd romania-mcp
99
+ pip install -e . pytest
100
+ pytest
101
+ ```
102
+
103
+ Tests don't hit the network, HTTP calls are mocked.
104
+
105
+ To poke at it with the MCP inspector:
106
+
107
+ ```
108
+ npx @modelcontextprotocol/inspector romania-mcp
109
+ ```
110
+
111
+ ## Be reasonable
112
+
113
+ All of this is public data, but court records name real people. Use it to vet companies you do business with, not to dig into individuals.
114
+
115
+ ## License
116
+
117
+ MIT © [Robert Vind-Gardoș](https://stratagency.ro/en/robert-vind-gardos) ([@robyroro](https://github.com/robyroro))
118
+
119
+ <!-- mcp-name: io.github.robyroro/romania-mcp -->
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["robyroro"]
4
+ }
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "romania-mcp-server"
3
+ version = "0.1.0"
4
+ description = "MCP server for Romanian public data: ANAF company status, financial statements, court cases, BNR exchange rates, online shop checks"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Robert Vind-Gardoș" }]
8
+ requires-python = ">=3.10"
9
+ keywords = ["mcp", "romania", "anaf", "cui", "bnr", "portal-just", "e-factura", "claude"]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Natural Language :: Romanian",
13
+ "Topic :: Office/Business :: Financial :: Accounting",
14
+ ]
15
+ dependencies = [
16
+ "mcp>=2.0",
17
+ "httpx>=0.27",
18
+ ]
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/robyroro/romania-mcp"
22
+ Author = "https://stratagency.ro/en/robert-vind-gardos"
23
+
24
+ [project.scripts]
25
+ romania-mcp = "romania_mcp.server:main"
26
+ # so `uvx romania-mcp-server` works without --from
27
+ romania-mcp-server = "romania_mcp.server:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/romania_mcp"]
31
+
32
+ [dependency-groups]
33
+ dev = ["pytest>=8"]
34
+
35
+ [build-system]
36
+ requires = ["hatchling"]
37
+ build-backend = "hatchling.build"
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.robyroro/romania-mcp",
4
+ "title": "Romania",
5
+ "description": "Romanian public data: ANAF company status, financials, court cases, BNR rates, shop checks",
6
+ "repository": {
7
+ "url": "https://github.com/robyroro/romania-mcp",
8
+ "source": "github"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "pypi",
14
+ "identifier": "romania-mcp-server",
15
+ "version": "0.1.0",
16
+ "runtimeHint": "uvx",
17
+ "transport": {
18
+ "type": "stdio"
19
+ }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
@@ -0,0 +1,219 @@
1
+ """ANAF public web services: the VAT payer registry (company status) and filed balance sheets."""
2
+
3
+ import asyncio
4
+ import time
5
+ from datetime import date
6
+
7
+ from .cache import cached, get_cache
8
+ from .common import fold
9
+
10
+ TVA_URL = "https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva"
11
+ BILANT_URL = "https://webservicesp.anaf.ro/bilant"
12
+ MAX_BATCH = 100 # ANAF's limit per request
13
+
14
+ # ANAF asks for at most one request per second
15
+ MIN_INTERVAL = 1.0
16
+ _next_slot = 0.0
17
+
18
+
19
+ async def _throttle():
20
+ global _next_slot
21
+ now = time.monotonic()
22
+ wait = _next_slot - now
23
+ _next_slot = max(now, _next_slot) + MIN_INTERVAL
24
+ if wait > 0:
25
+ await asyncio.sleep(wait)
26
+
27
+
28
+ # --- CUI ---
29
+
30
+ CUI_KEY = "753217532"
31
+
32
+
33
+ def cui_checksum_ok(digits):
34
+ body, control = digits[:-1], int(digits[-1])
35
+ total = sum(int(d) * int(k) for d, k in zip(body, CUI_KEY[-len(body):]))
36
+ return total * 10 % 11 % 10 == control
37
+
38
+
39
+ def clean_cui(value):
40
+ """'RO 14399840', 'ro14399840', 14399840 -> 14399840. ValueError if the control digit is off."""
41
+ s = str(value).strip().upper().replace(" ", "").removeprefix("RO")
42
+ if not s.isdigit() or not 2 <= len(s) <= 10:
43
+ raise ValueError(f"not a CUI: {value!r}")
44
+ if not cui_checksum_ok(s):
45
+ raise ValueError(f"{value!r} isn't a valid CUI, the control digit doesn't match")
46
+ return int(s)
47
+
48
+
49
+ # --- company status (VAT registry) ---
50
+
51
+ def _date(value):
52
+ return value or None
53
+
54
+
55
+ def parse_company(item):
56
+ g = item.get("date_generale", {})
57
+ vat = item.get("inregistrare_scop_Tva", {})
58
+ vat_cash = item.get("inregistrare_RTVAI", {})
59
+ inactive = item.get("stare_inactiv", {})
60
+ split = item.get("inregistrare_SplitTVA", {})
61
+ status = g.get("stare_inregistrare") or ""
62
+
63
+ out = {
64
+ "cui": g.get("cui"),
65
+ "name": g.get("denumire"),
66
+ "registration_number": g.get("nrRegCom") or None,
67
+ "registered_on": _date(g.get("data_inregistrare")),
68
+ "status": status,
69
+ "deregistered": status.upper().startswith("RADIERE"),
70
+ "address": g.get("adresa"),
71
+ "county": item.get("adresa_sediu_social", {}).get("sdenumire_Judet") or None,
72
+ "caen": g.get("cod_CAEN") or None,
73
+ "legal_form": g.get("forma_juridica") or None,
74
+ "phone": g.get("telefon") or None,
75
+ "tax_office": g.get("organFiscalCompetent") or None,
76
+ "vat_registered": bool(vat.get("scpTVA")),
77
+ "vat_periods": [
78
+ {
79
+ "from": _date(p.get("data_inceput_ScpTVA")),
80
+ "to": _date(p.get("data_sfarsit_ScpTVA")),
81
+ "note": p.get("mesaj_ScpTVA") or None,
82
+ }
83
+ for p in vat.get("perioade_TVA") or []
84
+ if p.get("data_inceput_ScpTVA")
85
+ ],
86
+ "vat_on_collection": bool(vat_cash.get("statusTvaIncasare")),
87
+ "split_vat": bool(split.get("statusSplitTVA")),
88
+ "fiscally_inactive": bool(inactive.get("statusInactivi")),
89
+ "inactive_since": _date(inactive.get("dataInactivare")),
90
+ "reactivated_on": _date(inactive.get("dataReactivare")),
91
+ "e_factura_registry": bool(g.get("statusRO_e_Factura")),
92
+ "as_of": g.get("data"),
93
+ }
94
+ out["warnings"] = company_warnings(out)
95
+ return out
96
+
97
+
98
+ def company_warnings(c):
99
+ out = []
100
+ if c["deregistered"]:
101
+ out.append(f"company is deregistered ({c['status']})")
102
+ if c["fiscally_inactive"]:
103
+ out.append(
104
+ f"declared fiscally inactive since {c['inactive_since']}: buyers generally can't "
105
+ "deduct expenses or VAT on its invoices (Cod fiscal art. 11)"
106
+ )
107
+ if "SUSPENDARE" in c["status"].upper():
108
+ out.append(f"activity is suspended ({c['status']})")
109
+ return out
110
+
111
+
112
+ async def lookup(client, cuis, on=None):
113
+ """Status of any number of CUIs (batched by 100). Returns (found, not_found)."""
114
+ on = on or date.today().isoformat()
115
+ cache = get_cache()
116
+ found = {}
117
+ todo = []
118
+ for cui in dict.fromkeys(cuis):
119
+ hit = cache.get(f"anaf:{cui}:{on}")
120
+ if hit is not None:
121
+ found[cui] = hit
122
+ else:
123
+ todo.append(cui)
124
+
125
+ not_found = []
126
+ for i in range(0, len(todo), MAX_BATCH):
127
+ batch = todo[i:i + MAX_BATCH]
128
+ await _throttle()
129
+ r = await client.post(TVA_URL, json=[{"cui": c, "data": on} for c in batch])
130
+ r.raise_for_status()
131
+ body = r.json()
132
+ for item in body.get("found") or []:
133
+ company = parse_company(item)
134
+ found[company["cui"]] = company
135
+ cache.set(f"anaf:{company['cui']}:{on}", company, 6 * 3600)
136
+ not_found += body.get("notFound") or []
137
+
138
+ return [found[c] for c in dict.fromkeys(cuis) if c in found], not_found
139
+
140
+
141
+ # --- balance sheets ---
142
+
143
+ # what the summary calls each line, matched on the folded ANAF label
144
+ LABELS = {
145
+ "cifra de afaceri neta": "revenue",
146
+ "venituri totale": "total_income",
147
+ "cheltuieli totale": "total_expenses",
148
+ "profit brut": "gross_profit",
149
+ "pierdere bruta": "gross_loss",
150
+ "profit net": "net_profit",
151
+ "pierdere neta": "net_loss",
152
+ "numar mediu de salariati": "employees",
153
+ "active imobilizate": "fixed_assets",
154
+ "active circulante": "current_assets",
155
+ "stocuri": "inventory",
156
+ "creante": "receivables",
157
+ "casa si conturi la banci": "cash",
158
+ "cheltuieli in avans": "prepaid_expenses",
159
+ "datorii": "debts",
160
+ "venituri in avans": "deferred_income",
161
+ "provizioane": "provisions",
162
+ "capitaluri": "equity",
163
+ "capital subscris varsat": "share_capital",
164
+ "patrimoniul regiei": "public_patrimony",
165
+ }
166
+
167
+
168
+ def _label_key(label):
169
+ s = fold(label).strip()
170
+ s = s.removesuffix(", din care:").strip()
171
+ s = s.removesuffix("- total").strip()
172
+ return s
173
+
174
+
175
+ def parse_bilant(data):
176
+ """One year of filed financials, None if nothing was filed (or the CUI is unknown)."""
177
+ items = data.get("i") or []
178
+ if not items:
179
+ return None
180
+ out = {"year": data.get("an")}
181
+ other = {}
182
+ for i in sorted(items, key=lambda i: int(i["indicator"].lstrip("I") or 0)):
183
+ key = LABELS.get(_label_key(i["val_den_indicator"]))
184
+ if key:
185
+ out[key] = i["val_indicator"]
186
+ else:
187
+ other[i["val_den_indicator"].strip()] = i["val_indicator"]
188
+ if "net_profit" in out or "net_loss" in out:
189
+ out["net_result"] = out.get("net_profit", 0) - out.get("net_loss", 0)
190
+ if other:
191
+ # banks, insurers and NGOs file a different layout, pass it through as is
192
+ out["other"] = other
193
+ return out
194
+
195
+
196
+ async def bilant(client, cui, year):
197
+ async def fetch():
198
+ await _throttle()
199
+ r = await client.get(BILANT_URL, params={"an": year, "cui": cui})
200
+ r.raise_for_status()
201
+ data = r.json()
202
+ parsed = parse_bilant(data)
203
+ meta = {"name": data.get("deni") or None, "caen": data.get("caen") or None,
204
+ "caen_description": data.get("den_caen") or None}
205
+ return {"meta": meta, "year": parsed or {"year": year, "filed": False}}
206
+
207
+ # filed statements barely change, a day is plenty
208
+ return await cached(f"bilant:{cui}:{year}", 24 * 3600, fetch)
209
+
210
+
211
+ async def financials(client, cui, years):
212
+ meta = {}
213
+ out = []
214
+ for year in years:
215
+ res = await bilant(client, cui, year)
216
+ if res["meta"]["name"] and not meta:
217
+ meta = res["meta"]
218
+ out.append(res["year"])
219
+ return {"cui": cui, **meta, "currency": "RON", "years": out}