feat: Add Biała Lista VAT integration for NIP→KRS lookup

- 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 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-01 14:32:36 +01:00
parent 28affce99f
commit c73e90bc70
3 changed files with 134 additions and 69 deletions

View File

@ -31,7 +31,13 @@ logger = logging.getLogger(__name__)
@login_required @login_required
def lookup_nip(): 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. Returns company info for auto-fill in application form.
""" """
data = request.get_json() data = request.get_json()
@ -39,14 +45,25 @@ def lookup_nip():
return jsonify({'success': False, 'error': 'Brak danych'}), 400 return jsonify({'success': False, 'error': 'Brak danych'}), 400
nip = data.get('nip', '').strip().replace('-', '').replace(' ', '') 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: if not nip or len(nip) != 10:
return jsonify({'success': False, 'error': 'NIP musi mieć 10 cyfr'}), 400 return jsonify({'success': False, 'error': 'NIP musi mieć 10 cyfr'}), 400
# Check if NIP is numeric
if not nip.isdigit(): if not nip.isdigit():
return jsonify({'success': False, 'error': 'NIP może zawierać tylko cyfry'}), 400 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) krs_result = _lookup_krs(nip)
if krs_result: if krs_result:
return jsonify({ return jsonify({
@ -55,7 +72,7 @@ def lookup_nip():
'data': krs_result 'data': krs_result
}) })
# Try CEIDG # Option 3: Try CEIDG (for JDG - sole proprietorship)
ceidg_result = _lookup_ceidg(nip) ceidg_result = _lookup_ceidg(nip)
if ceidg_result: if ceidg_result:
return jsonify({ 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): def _lookup_krs(nip):
"""Lookup in KRS registry.""" """Lookup in KRS registry by NIP (via Biała Lista VAT → KRS Open API)."""
try: try:
from krs_api_service import krs_api_service from krs_api_service import krs_api_service
result = krs_api_service.search_by_nip(nip) result = krs_api_service.search_by_nip(nip)
if result: if result:
# Parse address components return _parse_krs_data(result)
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
}
except ImportError: except ImportError:
logger.warning("KRS API service not available") logger.warning("KRS API service not available")
except Exception as e: except Exception as e:
@ -105,6 +119,32 @@ def _lookup_krs(nip):
return None 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): def _lookup_ceidg(nip):
"""Lookup in CEIDG registry.""" """Lookup in CEIDG registry."""
try: try:

View File

@ -444,25 +444,27 @@ def format_address(krs_data: KRSCompanyData) -> str:
# KRS API Service Class (for search_by_nip compatibility) # 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: class KRSApiService:
""" """
KRS API Service class providing unified interface for KRS lookups. KRS API Service class providing unified interface for KRS lookups.
Note: KRS Open API doesn't support direct NIP lookup. Uses official government APIs:
This class uses rejestr.io unofficial API as a fallback. 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]]: 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: Workflow:
1. First tries rejestr.io API (unofficial but reliable) 1. Query Biała Lista VAT API (Ministry of Finance) to get KRS from NIP
2. Falls back to checking our database for KRS number 2. Fetch full data from KRS Open API using the KRS number
3. Then fetches full data from KRS Open API
Args: Args:
nip: NIP number (10 digits) nip: NIP number (10 digits)
@ -471,6 +473,7 @@ class KRSApiService:
Dictionary with company data or None if not found Dictionary with company data or None if not found
""" """
import logging import logging
from datetime import date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Clean NIP # Clean NIP
@ -478,19 +481,19 @@ class KRSApiService:
if not nip or len(nip) != 10 or not nip.isdigit(): if not nip or len(nip) != 10 or not nip.isdigit():
return None return None
# Try rejestr.io first (supports NIP lookup) # Step 1: Get KRS from Biała Lista VAT API
krs_number = self._get_krs_from_rejestr_io(nip) krs_number = self._get_krs_from_biala_lista(nip)
if not krs_number: if not krs_number:
# Try our database # Fallback: check our database
krs_number = self._get_krs_from_database(nip) krs_number = self._get_krs_from_database(nip)
if not krs_number: if not krs_number:
logger.info(f"No KRS found for NIP {nip}") logger.info(f"No KRS found for NIP {nip}")
return None return None
# Fetch full data from KRS Open API # Step 2: Fetch full data from official KRS Open API
logger.info(f"Found KRS {krs_number} for NIP {nip}, fetching details") logger.info(f"Found KRS {krs_number} for NIP {nip}, fetching from KRS Open API")
krs_data = get_company_from_krs(krs_number) krs_data = get_company_from_krs(krs_number)
if not krs_data: if not krs_data:
@ -499,39 +502,43 @@ class KRSApiService:
# Return as dict with expected format # Return as dict with expected format
return krs_data.to_dict() return krs_data.to_dict()
def _get_krs_from_rejestr_io(self, nip: str) -> Optional[str]: def _get_krs_from_biala_lista(self, nip: str) -> Optional[str]:
"""Try to get KRS number from rejestr.io API.""" """
Get KRS number from Biała Lista VAT API (Ministry of Finance).
Official API: https://wl-api.mf.gov.pl/
"""
import logging import logging
from datetime import date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
# rejestr.io API endpoint for NIP lookup today = date.today().strftime('%Y-%m-%d')
url = f"{self.REJESTR_IO_URL}" url = f"{BIALA_LISTA_API_URL}/{nip}?date={today}"
params = {"nip": nip}
response = requests.get(url, params=params, timeout=self.REJESTR_IO_TIMEOUT) response = requests.get(url, timeout=BIALA_LISTA_TIMEOUT)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
# Handle different response formats subject = data.get('result', {}).get('subject', {})
if isinstance(data, list) and data:
krs = data[0].get('krs') if subject:
if krs: krs = subject.get('krs')
return str(krs).zfill(10) if krs:
elif isinstance(data, dict): logger.info(f"Biała Lista: Found KRS {krs} for NIP {nip}")
krs = data.get('krs') return str(krs).zfill(10)
if krs: else:
return str(krs).zfill(10) 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 return None
except Exception as e: 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 return None
def _get_krs_from_database(self, nip: str) -> Optional[str]: 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 import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -129,6 +129,12 @@
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);
} }
.form-hint-inline {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-weight: 400;
}
.nip-lookup { .nip-lookup {
display: flex; display: flex;
gap: var(--spacing-sm); gap: var(--spacing-sm);
@ -367,17 +373,28 @@
<div class="form-section"> <div class="form-section">
<h2>Pobierz dane z rejestru</h2> <h2>Pobierz dane z rejestru</h2>
<div class="form-row">
<div class="form-group"> <div class="form-group">
<label>NIP <span class="required">*</span></label> <label>NIP <span class="required">*</span></label>
<div class="nip-lookup">
<input type="text" class="form-control" id="nipInput" name="nip" <input type="text" class="form-control" id="nipInput" name="nip"
value="{{ application.nip or '' }}" value="{{ application.nip or '' }}"
placeholder="0000000000" maxlength="10" pattern="\d{10}"> placeholder="0000000000" maxlength="10" pattern="\d{10}">
<button type="button" class="btn-lookup" id="btnLookup"> <div class="form-hint">10 cyfr, bez myślników</div>
Sprawdź w rejestrze
</button>
</div> </div>
<div class="form-hint">Wpisz 10-cyfrowy NIP bez myślników</div> <div class="form-group">
<label>KRS <span class="form-hint-inline">(dla spółek)</span></label>
<input type="text" class="form-control" id="krsInput" name="krs_number"
value="{{ application.krs_number or '' }}"
placeholder="0000000000" maxlength="10" pattern="\d{7,10}">
<div class="form-hint">7-10 cyfr (opcjonalne dla JDG)</div>
</div>
</div>
<div class="form-group">
<button type="button" class="btn-lookup" id="btnLookup" style="width: 100%;">
Sprawdź w rejestrze (KRS lub CEIDG)
</button>
<div class="form-hint">Podaj NIP. Dla spółek (Sp. z o.o., SA) podaj też KRS aby pobrać dane z rejestru.</div>
</div> </div>
<div id="registryPreview" class="registry-preview" style="display: none;"> <div id="registryPreview" class="registry-preview" style="display: none;">
@ -428,7 +445,6 @@
</div> </div>
</div> </div>
<input type="hidden" name="krs_number" value="{{ application.krs_number or '' }}">
<input type="hidden" name="regon" value="{{ application.regon or '' }}"> <input type="hidden" name="regon" value="{{ application.regon or '' }}">
<input type="hidden" name="registry_source" value="{{ application.registry_source or 'manual' }}"> <input type="hidden" name="registry_source" value="{{ application.registry_source or 'manual' }}">
</div> </div>
@ -687,6 +703,7 @@
{% block extra_js %} {% block extra_js %}
{% if step == 1 %} {% if step == 1 %}
const nipInput = document.getElementById('nipInput'); const nipInput = document.getElementById('nipInput');
const krsInput = document.getElementById('krsInput');
const btnLookup = document.getElementById('btnLookup'); const btnLookup = document.getElementById('btnLookup');
const registryPreview = document.getElementById('registryPreview'); const registryPreview = document.getElementById('registryPreview');
const registrySource = document.getElementById('registrySource'); const registrySource = document.getElementById('registrySource');
@ -694,6 +711,7 @@ const registryData = document.getElementById('registryData');
btnLookup.addEventListener('click', async function() { btnLookup.addEventListener('click', async function() {
const nip = nipInput.value.replace(/[\s-]/g, ''); const nip = nipInput.value.replace(/[\s-]/g, '');
const krs = krsInput ? krsInput.value.replace(/[\s-]/g, '') : '';
if (nip.length !== 10 || !/^\d+$/.test(nip)) { if (nip.length !== 10 || !/^\d+$/.test(nip)) {
alert('NIP musi mieć 10 cyfr'); alert('NIP musi mieć 10 cyfr');
@ -701,13 +719,13 @@ btnLookup.addEventListener('click', async function() {
} }
btnLookup.disabled = true; btnLookup.disabled = true;
btnLookup.innerHTML = '<span class="loading-spinner"></span> Sprawdzam...'; btnLookup.innerHTML = '<span class="loading-spinner"></span> Sprawdzam w rejestrach...';
try { try {
const response = await fetch('/api/membership/lookup-nip', { const response = await fetch('/api/membership/lookup-nip', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nip: nip }) body: JSON.stringify({ nip: nip, krs: krs || null })
}); });
const result = await response.json(); const result = await response.json();
@ -742,10 +760,10 @@ btnLookup.addEventListener('click', async function() {
} }
} catch (error) { } catch (error) {
console.error('Lookup error:', error); console.error('Lookup error:', error);
alert('Błąd podczas sprawdzania NIP'); alert('Błąd podczas sprawdzania w rejestrach');
} finally { } finally {
btnLookup.disabled = false; btnLookup.disabled = false;
btnLookup.innerHTML = 'Sprawdź w rejestrze'; btnLookup.innerHTML = 'Sprawdź w rejestrze (KRS lub CEIDG)';
} }
}); });
{% endif %} {% endif %}