From c73e90bc70e12c1af584e48df19158860536ae54 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Sun, 1 Feb 2026 14:32:36 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20Bia=C5=82a=20Lista=20VAT=20integr?= =?UTF-8?q?ation=20for=20NIP=E2=86=92KRS=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use official Ministry of Finance API (wl-api.mf.gov.pl) to get KRS from NIP - Add KRS field to membership application form - Workflow: NIP → Biała Lista → KRS Open API → full company data - Fallback to CEIDG for JDG (sole proprietorship) - Remove rejestr.io dependency - only official government APIs Co-Authored-By: Claude Opus 4.5 --- blueprints/api/routes_membership.py | 88 +++++++++++++++++++++-------- krs_api_service.py | 73 +++++++++++++----------- templates/membership/apply.html | 42 ++++++++++---- 3 files changed, 134 insertions(+), 69 deletions(-) diff --git a/blueprints/api/routes_membership.py b/blueprints/api/routes_membership.py index ae5a04b..f0c1d1c 100644 --- a/blueprints/api/routes_membership.py +++ b/blueprints/api/routes_membership.py @@ -31,7 +31,13 @@ logger = logging.getLogger(__name__) @login_required def lookup_nip(): """ - Lookup company data by NIP in KRS and CEIDG registries. + Lookup company data by NIP (and optionally KRS) in official registries. + + Workflow: + 1. If KRS provided - directly query KRS Open API + 2. If only NIP - query Biała Lista VAT to get KRS, then KRS Open API + 3. If no KRS found - try CEIDG (for JDG/sole proprietorship) + Returns company info for auto-fill in application form. """ data = request.get_json() @@ -39,14 +45,25 @@ def lookup_nip(): return jsonify({'success': False, 'error': 'Brak danych'}), 400 nip = data.get('nip', '').strip().replace('-', '').replace(' ', '') + krs = data.get('krs', '').strip().replace('-', '').replace(' ', '') if data.get('krs') else None + if not nip or len(nip) != 10: return jsonify({'success': False, 'error': 'NIP musi mieć 10 cyfr'}), 400 - # Check if NIP is numeric if not nip.isdigit(): return jsonify({'success': False, 'error': 'NIP może zawierać tylko cyfry'}), 400 - # Try KRS first + # Option 1: If KRS provided, use it directly + if krs and len(krs) >= 7 and krs.isdigit(): + krs_result = _lookup_krs_by_number(krs) + if krs_result: + return jsonify({ + 'success': True, + 'source': 'KRS', + 'data': krs_result + }) + + # Option 2: Try KRS via NIP (uses Biała Lista VAT → KRS Open API) krs_result = _lookup_krs(nip) if krs_result: return jsonify({ @@ -55,7 +72,7 @@ def lookup_nip(): 'data': krs_result }) - # Try CEIDG + # Option 3: Try CEIDG (for JDG - sole proprietorship) ceidg_result = _lookup_ceidg(nip) if ceidg_result: return jsonify({ @@ -73,31 +90,28 @@ def lookup_nip(): }) +def _lookup_krs_by_number(krs_number): + """Lookup in KRS registry directly by KRS number.""" + try: + from krs_api_service import get_company_from_krs + krs_normalized = krs_number.zfill(10) + result = get_company_from_krs(krs_normalized) + if result: + return _parse_krs_data(result.to_dict()) + except ImportError: + logger.warning("KRS API service not available") + except Exception as e: + logger.error(f"KRS lookup error for KRS {krs_number}: {e}") + return None + + def _lookup_krs(nip): - """Lookup in KRS registry.""" + """Lookup in KRS registry by NIP (via Biała Lista VAT → KRS Open API).""" try: from krs_api_service import krs_api_service result = krs_api_service.search_by_nip(nip) if result: - # Parse address components - address = result.get('adres', {}) - if isinstance(address, str): - address = {'full': address} - - return { - 'name': result.get('nazwa'), - 'krs': result.get('krs'), - 'regon': result.get('regon'), - 'address_postal_code': address.get('kodPocztowy', ''), - 'address_city': address.get('miejscowosc', ''), - 'address_street': address.get('ulica', ''), - 'address_number': address.get('nrDomu', ''), - 'founded_date': result.get('data_rejestracji'), - 'business_type': _detect_business_type_from_krs(result), - 'email': result.get('email'), - 'website': result.get('strona_www'), - 'raw': result - } + return _parse_krs_data(result) except ImportError: logger.warning("KRS API service not available") except Exception as e: @@ -105,6 +119,32 @@ def _lookup_krs(nip): return None +def _parse_krs_data(result): + """Parse KRS data into standardized format.""" + # Parse address components + address = result.get('adres', {}) + if isinstance(address, str): + address = {'full': address} + + # Handle kontakt_krs for email/website + kontakt = result.get('kontakt_krs', {}) or {} + + return { + 'name': result.get('nazwa'), + 'krs': result.get('krs'), + 'regon': result.get('regon'), + 'address_postal_code': address.get('kod_pocztowy') or address.get('kodPocztowy', ''), + 'address_city': address.get('miejscowosc', ''), + 'address_street': address.get('ulica', ''), + 'address_number': address.get('nr_domu') or address.get('nrDomu', ''), + 'founded_date': result.get('daty', {}).get('rejestracji') if isinstance(result.get('daty'), dict) else result.get('data_rejestracji'), + 'business_type': _detect_business_type_from_krs(result), + 'email': kontakt.get('email') or result.get('email'), + 'website': kontakt.get('www') or result.get('strona_www'), + 'raw': result + } + + def _lookup_ceidg(nip): """Lookup in CEIDG registry.""" try: diff --git a/krs_api_service.py b/krs_api_service.py index b9b814c..7fedf85 100644 --- a/krs_api_service.py +++ b/krs_api_service.py @@ -444,25 +444,27 @@ def format_address(krs_data: KRSCompanyData) -> str: # KRS API Service Class (for search_by_nip compatibility) # ============================================================ +# Biała Lista VAT API (Ministry of Finance) - returns KRS from NIP +BIALA_LISTA_API_URL = "https://wl-api.mf.gov.pl/api/search/nip" +BIALA_LISTA_TIMEOUT = 10 + + class KRSApiService: """ KRS API Service class providing unified interface for KRS lookups. - Note: KRS Open API doesn't support direct NIP lookup. - This class uses rejestr.io unofficial API as a fallback. + Uses official government APIs: + 1. Biała Lista VAT (wl-api.mf.gov.pl) - to get KRS from NIP + 2. KRS Open API (api-krs.ms.gov.pl) - to get full company data from KRS """ - REJESTR_IO_URL = "https://rejestr.io/api/v2/org" - REJESTR_IO_TIMEOUT = 10 - def search_by_nip(self, nip: str) -> Optional[Dict[str, Any]]: """ - Search KRS by NIP number. + Search KRS by NIP number using official government APIs. - Since KRS Open API doesn't support NIP lookup, this method: - 1. First tries rejestr.io API (unofficial but reliable) - 2. Falls back to checking our database for KRS number - 3. Then fetches full data from KRS Open API + Workflow: + 1. Query Biała Lista VAT API (Ministry of Finance) to get KRS from NIP + 2. Fetch full data from KRS Open API using the KRS number Args: nip: NIP number (10 digits) @@ -471,6 +473,7 @@ class KRSApiService: Dictionary with company data or None if not found """ import logging + from datetime import date logger = logging.getLogger(__name__) # Clean NIP @@ -478,19 +481,19 @@ class KRSApiService: if not nip or len(nip) != 10 or not nip.isdigit(): return None - # Try rejestr.io first (supports NIP lookup) - krs_number = self._get_krs_from_rejestr_io(nip) + # Step 1: Get KRS from Biała Lista VAT API + krs_number = self._get_krs_from_biala_lista(nip) if not krs_number: - # Try our database + # Fallback: check our database krs_number = self._get_krs_from_database(nip) if not krs_number: logger.info(f"No KRS found for NIP {nip}") return None - # Fetch full data from KRS Open API - logger.info(f"Found KRS {krs_number} for NIP {nip}, fetching details") + # Step 2: Fetch full data from official KRS Open API + logger.info(f"Found KRS {krs_number} for NIP {nip}, fetching from KRS Open API") krs_data = get_company_from_krs(krs_number) if not krs_data: @@ -499,39 +502,43 @@ class KRSApiService: # Return as dict with expected format return krs_data.to_dict() - def _get_krs_from_rejestr_io(self, nip: str) -> Optional[str]: - """Try to get KRS number from rejestr.io API.""" + def _get_krs_from_biala_lista(self, nip: str) -> Optional[str]: + """ + Get KRS number from Biała Lista VAT API (Ministry of Finance). + Official API: https://wl-api.mf.gov.pl/ + """ import logging + from datetime import date logger = logging.getLogger(__name__) try: - # rejestr.io API endpoint for NIP lookup - url = f"{self.REJESTR_IO_URL}" - params = {"nip": nip} + today = date.today().strftime('%Y-%m-%d') + url = f"{BIALA_LISTA_API_URL}/{nip}?date={today}" - response = requests.get(url, params=params, timeout=self.REJESTR_IO_TIMEOUT) + response = requests.get(url, timeout=BIALA_LISTA_TIMEOUT) if response.status_code == 200: data = response.json() - # Handle different response formats - if isinstance(data, list) and data: - krs = data[0].get('krs') - if krs: - return str(krs).zfill(10) - elif isinstance(data, dict): - krs = data.get('krs') - if krs: - return str(krs).zfill(10) + subject = data.get('result', {}).get('subject', {}) + + if subject: + krs = subject.get('krs') + if krs: + logger.info(f"Biała Lista: Found KRS {krs} for NIP {nip}") + return str(krs).zfill(10) + else: + logger.debug(f"Biała Lista: NIP {nip} found but no KRS (likely JDG)") + else: + logger.debug(f"Biała Lista: NIP {nip} not found") - logger.debug(f"rejestr.io: No KRS found for NIP {nip}") return None except Exception as e: - logger.debug(f"rejestr.io lookup failed: {e}") + logger.warning(f"Biała Lista API error for NIP {nip}: {e}") return None def _get_krs_from_database(self, nip: str) -> Optional[str]: - """Check our database for KRS number.""" + """Check our database for KRS number (fallback).""" import logging logger = logging.getLogger(__name__) diff --git a/templates/membership/apply.html b/templates/membership/apply.html index 7cbbd6b..7bdd6e9 100644 --- a/templates/membership/apply.html +++ b/templates/membership/apply.html @@ -129,6 +129,12 @@ margin-top: var(--spacing-xs); } + .form-hint-inline { + font-size: var(--font-size-sm); + color: var(--text-secondary); + font-weight: 400; + } + .nip-lookup { display: flex; gap: var(--spacing-sm); @@ -367,17 +373,28 @@

Pobierz dane z rejestru

-
- -
+
+
+ - +
10 cyfr, bez myślników
-
Wpisz 10-cyfrowy NIP bez myślników
+
+ + +
7-10 cyfr (opcjonalne dla JDG)
+
+
+ +
+ +
Podaj NIP. Dla spółek (Sp. z o.o., SA) podaj też KRS aby pobrać dane z rejestru.
-
@@ -687,6 +703,7 @@ {% block extra_js %} {% if step == 1 %} const nipInput = document.getElementById('nipInput'); +const krsInput = document.getElementById('krsInput'); const btnLookup = document.getElementById('btnLookup'); const registryPreview = document.getElementById('registryPreview'); const registrySource = document.getElementById('registrySource'); @@ -694,6 +711,7 @@ const registryData = document.getElementById('registryData'); btnLookup.addEventListener('click', async function() { const nip = nipInput.value.replace(/[\s-]/g, ''); + const krs = krsInput ? krsInput.value.replace(/[\s-]/g, '') : ''; if (nip.length !== 10 || !/^\d+$/.test(nip)) { alert('NIP musi mieć 10 cyfr'); @@ -701,13 +719,13 @@ btnLookup.addEventListener('click', async function() { } btnLookup.disabled = true; - btnLookup.innerHTML = ' Sprawdzam...'; + btnLookup.innerHTML = ' Sprawdzam w rejestrach...'; try { const response = await fetch('/api/membership/lookup-nip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nip: nip }) + body: JSON.stringify({ nip: nip, krs: krs || null }) }); const result = await response.json(); @@ -742,10 +760,10 @@ btnLookup.addEventListener('click', async function() { } } catch (error) { console.error('Lookup error:', error); - alert('Błąd podczas sprawdzania NIP'); + alert('Błąd podczas sprawdzania w rejestrach'); } finally { btnLookup.disabled = false; - btnLookup.innerHTML = 'Sprawdź w rejestrze'; + btnLookup.innerHTML = 'Sprawdź w rejestrze (KRS lub CEIDG)'; } }); {% endif %}