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
|
||||
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:
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
@ -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 @@
|
||||
<div class="form-section">
|
||||
<h2>Pobierz dane z rejestru</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label>NIP <span class="required">*</span></label>
|
||||
<div class="nip-lookup">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>NIP <span class="required">*</span></label>
|
||||
<input type="text" class="form-control" id="nipInput" name="nip"
|
||||
value="{{ application.nip or '' }}"
|
||||
placeholder="0000000000" maxlength="10" pattern="\d{10}">
|
||||
<button type="button" class="btn-lookup" id="btnLookup">
|
||||
Sprawdź w rejestrze
|
||||
</button>
|
||||
<div class="form-hint">10 cyfr, bez myślników</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 id="registryPreview" class="registry-preview" style="display: none;">
|
||||
@ -428,7 +445,6 @@
|
||||
</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="registry_source" value="{{ application.registry_source or 'manual' }}">
|
||||
</div>
|
||||
@ -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 = '<span class="loading-spinner"></span> Sprawdzam...';
|
||||
btnLookup.innerHTML = '<span class="loading-spinner"></span> 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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user