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:
parent
28affce99f
commit
c73e90bc70
@ -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:
|
||||||
|
|||||||
@ -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__)
|
||||||
|
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user