nordabiz/blueprints/admin/routes_krs_api.py
Maciej Pienczyn f2fc1b89ec
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
refactor(rbac): Complete RBAC migration - 154/154 admin routes protected
- Add @role_required to 2 missing routes (krs_api PDF download, zopk milestones)
- Add role-based menu visibility in admin bar (hide Users, Security, Benefits,
  Model Comparison, Debug from OFFICE_MANAGER users)
- Inject SystemRole into Jinja2 context processor for template role checks
- Replace is_admin checkbox with role select dropdown in user creation form
- Migrate routes.py and routes_users_api.py from is_admin to SystemRole-based
  role assignment via set_role()
- Add deprecation notice to is_admin database column
- Add 23 RBAC unit tests (hierarchy, has_role, set_role, permissions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:36:14 +01:00

469 lines
16 KiB
Python

"""
KRS API Routes - Admin blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains API routes for KRS (National Court Register) audits.
"""
import logging
from datetime import datetime, date
from pathlib import Path
from flask import jsonify, request, send_file, current_app
from flask_login import current_user, login_required
from database import (
SessionLocal,
Company,
Person,
CompanyPerson,
CompanyPKD,
CompanyFinancialReport,
KRSAudit,
SystemRole
)
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
# Import limiter from app - will be initialized when app starts
def get_limiter():
"""Get rate limiter from current app."""
return current_app.extensions.get('limiter')
def is_krs_audit_available():
"""Check if KRS audit service is available."""
try:
from krs_audit_service import parse_krs_pdf
return True
except ImportError:
return False
def parse_date_str(date_val):
"""Helper to parse date from string or return date object as-is"""
if date_val is None:
return None
if isinstance(date_val, date):
return date_val
if isinstance(date_val, str):
try:
return datetime.strptime(date_val, '%Y-%m-%d').date()
except Exception:
return None
return None
def _import_krs_person(db, company_id, person_data, role_category, source_document):
"""Helper to import a person from KRS data"""
pesel = person_data.get('pesel')
nazwisko = person_data.get('nazwisko', '')
imiona = person_data.get('imiona', '')
rola = person_data.get('rola', '')
# Find or create Person
person = None
if pesel:
person = db.query(Person).filter_by(pesel=pesel).first()
if not person:
# Try to find by name
person = db.query(Person).filter_by(
nazwisko=nazwisko,
imiona=imiona
).first()
if not person:
person = Person(
pesel=pesel,
nazwisko=nazwisko,
imiona=imiona
)
db.add(person)
db.flush()
# Check if relation already exists
existing_rel = db.query(CompanyPerson).filter_by(
company_id=company_id,
person_id=person.id,
role_category=role_category
).first()
if not existing_rel:
cp = CompanyPerson(
company_id=company_id,
person_id=person.id,
role=rola,
role_category=role_category,
source='ekrs.ms.gov.pl',
source_document=source_document,
fetched_at=datetime.now()
)
# Add shares info for shareholders
if role_category == 'wspolnik':
cp.shares_count = person_data.get('udzialy_liczba')
if person_data.get('udzialy_wartosc'):
cp.shares_value = person_data['udzialy_wartosc']
if person_data.get('udzialy_procent'):
cp.shares_percent = person_data['udzialy_procent']
db.add(cp)
# ============================================================
# KRS AUDIT API ROUTES
# ============================================================
@bp.route('/krs-api/audit', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_trigger():
"""
API: Trigger KRS audit for a company (admin-only).
Parses KRS PDF file and extracts all available data:
- Basic info (KRS, NIP, REGON, name, legal form)
- Capital and shares
- Management board, shareholders, procurators
- PKD codes
- Financial reports
Request JSON body:
- company_id: Company ID (integer)
Returns:
- Success: Audit results saved to database
- Error: Error message with status code
"""
if not is_krs_audit_available():
return jsonify({
'success': False,
'error': 'Usługa audytu KRS jest niedostępna.'
}), 503
from krs_audit_service import parse_krs_pdf
data = request.get_json()
if not data or not data.get('company_id'):
return jsonify({
'success': False,
'error': 'Podaj company_id firmy do audytu.'
}), 400
company_id = data['company_id']
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=company_id, status='active').first()
if not company:
return jsonify({
'success': False,
'error': 'Firma nie znaleziona.'
}), 404
if not company.krs:
return jsonify({
'success': False,
'error': f'Firma "{company.name}" nie ma numeru KRS.'
}), 400
# Find PDF file
pdf_dir = Path('data/krs_pdfs')
pdf_files = list(pdf_dir.glob(f'*{company.krs}*.pdf'))
if not pdf_files:
return jsonify({
'success': False,
'error': f'Nie znaleziono pliku PDF dla KRS {company.krs}. '
f'Pobierz odpis z ekrs.ms.gov.pl i umieść w data/krs_pdfs/'
}), 404
pdf_path = pdf_files[0]
# Create audit record
audit = KRSAudit(
company_id=company.id,
status='parsing',
progress_percent=10,
progress_message='Parsowanie pliku PDF...',
pdf_filename=pdf_path.name,
pdf_path=str(pdf_path)
)
db.add(audit)
db.commit()
# Parse PDF
try:
parsed_data = parse_krs_pdf(str(pdf_path), verbose=True)
# Update audit with parsed data
audit.status = 'completed'
audit.progress_percent = 100
audit.progress_message = 'Audyt zakończony pomyślnie'
audit.extracted_krs = parsed_data.get('krs')
audit.extracted_nazwa = parsed_data.get('nazwa')
audit.extracted_nip = parsed_data.get('nip')
audit.extracted_regon = parsed_data.get('regon')
audit.extracted_forma_prawna = parsed_data.get('forma_prawna')
audit.extracted_data_rejestracji = parse_date_str(parsed_data.get('data_rejestracji'))
audit.extracted_kapital_zakladowy = parsed_data.get('kapital_zakladowy')
audit.extracted_liczba_udzialow = parsed_data.get('liczba_udzialow')
audit.extracted_sposob_reprezentacji = parsed_data.get('sposob_reprezentacji')
audit.zarzad_count = len(parsed_data.get('zarzad', []))
audit.wspolnicy_count = len(parsed_data.get('wspolnicy', []))
audit.prokurenci_count = len(parsed_data.get('prokurenci', []))
audit.pkd_count = 1 if parsed_data.get('pkd_przewazajacy') else 0
audit.pkd_count += len(parsed_data.get('pkd_pozostale', []))
# Convert non-JSON-serializable values for JSONB storage
def make_json_serializable(obj):
from decimal import Decimal
if isinstance(obj, Decimal):
return float(obj)
elif isinstance(obj, (datetime, date)):
return obj.isoformat()
elif isinstance(obj, dict):
return {k: make_json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [make_json_serializable(i) for i in obj]
return obj
audit.parsed_data = make_json_serializable(parsed_data)
audit.pdf_downloaded_at = datetime.now()
# Update company with parsed data
if parsed_data.get('kapital_zakladowy'):
company.capital_amount = parsed_data['kapital_zakladowy']
if parsed_data.get('liczba_udzialow'):
company.capital_shares_count = parsed_data['liczba_udzialow']
if parsed_data.get('wartosc_nominalna_udzialu'):
company.capital_share_value = parsed_data['wartosc_nominalna_udzialu']
if parsed_data.get('data_rejestracji'):
company.krs_registration_date = parse_date_str(parsed_data['data_rejestracji'])
if parsed_data.get('sposob_reprezentacji'):
company.krs_representation_rules = parsed_data['sposob_reprezentacji']
if parsed_data.get('czas_trwania'):
company.krs_duration = parsed_data['czas_trwania']
company.krs_last_audit_at = datetime.now()
company.krs_pdf_path = str(pdf_path)
# Import PKD codes
pkd_main = parsed_data.get('pkd_przewazajacy')
if pkd_main:
existing = db.query(CompanyPKD).filter_by(
company_id=company.id,
pkd_code=pkd_main['kod']
).first()
if not existing:
db.add(CompanyPKD(
company_id=company.id,
pkd_code=pkd_main['kod'],
pkd_description=pkd_main['opis'],
is_primary=True,
source='ekrs'
))
# Also update Company.pkd_code
company.pkd_code = pkd_main['kod']
company.pkd_description = pkd_main['opis']
for pkd in parsed_data.get('pkd_pozostale', []):
existing = db.query(CompanyPKD).filter_by(
company_id=company.id,
pkd_code=pkd['kod']
).first()
if not existing:
db.add(CompanyPKD(
company_id=company.id,
pkd_code=pkd['kod'],
pkd_description=pkd['opis'],
is_primary=False,
source='ekrs'
))
# Import people (zarząd, wspólnicy)
for person_data in parsed_data.get('zarzad', []):
_import_krs_person(db, company.id, person_data, 'zarzad', pdf_path.name)
for person_data in parsed_data.get('wspolnicy', []):
_import_krs_person(db, company.id, person_data, 'wspolnik', pdf_path.name)
for person_data in parsed_data.get('prokurenci', []):
_import_krs_person(db, company.id, person_data, 'prokurent', pdf_path.name)
# Import financial reports
for report in parsed_data.get('sprawozdania_finansowe', []):
existing = db.query(CompanyFinancialReport).filter_by(
company_id=company.id,
period_start=parse_date_str(report.get('okres_od')),
period_end=parse_date_str(report.get('okres_do'))
).first()
if not existing:
db.add(CompanyFinancialReport(
company_id=company.id,
period_start=parse_date_str(report.get('okres_od')),
period_end=parse_date_str(report.get('okres_do')),
filed_at=parse_date_str(report.get('data_zlozenia')),
source='ekrs'
))
db.commit()
logger.info(f"KRS audit completed for {company.name} (KRS: {company.krs})")
return jsonify({
'success': True,
'message': f'Audyt KRS zakończony dla {company.name}',
'company_id': company.id,
'data': {
'krs': parsed_data.get('krs'),
'nazwa': parsed_data.get('nazwa'),
'nip': parsed_data.get('nip'),
'regon': parsed_data.get('regon'),
'kapital': float(parsed_data.get('kapital_zakladowy', 0) or 0),
'liczba_udzialow': parsed_data.get('liczba_udzialow'),
'zarzad_count': len(parsed_data.get('zarzad', [])),
'wspolnicy_count': len(parsed_data.get('wspolnicy', [])),
'prokurenci_count': len(parsed_data.get('prokurenci', [])),
'pkd_count': audit.pkd_count
}
})
except Exception as e:
audit.status = 'error'
audit.progress_percent = 0
audit.error_message = str(e)
db.commit()
logger.error(f"KRS audit failed for {company.name}: {e}")
return jsonify({
'success': False,
'error': f'Błąd parsowania PDF: {str(e)}'
}), 500
finally:
db.close()
@bp.route('/krs-api/audit/batch', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_batch():
"""
API: Trigger batch KRS audit for all companies with KRS numbers.
This runs audits sequentially to avoid overloading the system.
Returns progress updates via the response.
"""
if not is_krs_audit_available():
return jsonify({
'success': False,
'error': 'Usługa audytu KRS jest niedostępna.'
}), 503
from krs_audit_service import parse_krs_pdf
db = SessionLocal()
try:
# Get companies with KRS that haven't been audited recently
companies = db.query(Company).filter(
Company.status == 'active',
Company.krs.isnot(None),
Company.krs != ''
).order_by(Company.name).all()
results = {
'total': len(companies),
'success': 0,
'failed': 0,
'skipped': 0,
'details': []
}
pdf_dir = Path('data/krs_pdfs')
for company in companies:
# Find PDF file
pdf_files = list(pdf_dir.glob(f'*{company.krs}*.pdf'))
if not pdf_files:
results['skipped'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'skipped',
'reason': 'Brak pliku PDF'
})
continue
pdf_path = pdf_files[0]
try:
parsed_data = parse_krs_pdf(str(pdf_path))
# Update company
if parsed_data.get('kapital_zakladowy'):
company.capital_amount = parsed_data['kapital_zakladowy']
if parsed_data.get('liczba_udzialow'):
company.capital_shares_count = parsed_data['liczba_udzialow']
company.krs_last_audit_at = datetime.now()
company.krs_pdf_path = str(pdf_path)
results['success'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'success'
})
except Exception as e:
results['failed'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'error',
'reason': str(e)
})
db.commit()
return jsonify({
'success': True,
'message': f'Audyt zakończony: {results["success"]} sukces, '
f'{results["failed"]} błędów, {results["skipped"]} pominiętych',
'results': results
})
finally:
db.close()
@bp.route('/krs-api/pdf/<int:company_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_pdf_download(company_id):
"""
API: Download/serve KRS PDF file for a company.
"""
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=company_id).first()
if not company:
return jsonify({'error': 'Firma nie znaleziona'}), 404
if not company.krs_pdf_path:
return jsonify({'error': 'Brak pliku PDF'}), 404
pdf_path = Path(company.krs_pdf_path)
if not pdf_path.exists():
return jsonify({'error': 'Plik PDF nie istnieje'}), 404
return send_file(
str(pdf_path),
mimetype='application/pdf',
as_attachment=False,
download_name=pdf_path.name
)
finally:
db.close()