- 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>
599 lines
21 KiB
Python
599 lines
21 KiB
Python
"""
|
|
Membership API Routes
|
|
======================
|
|
|
|
API endpoints for membership application system:
|
|
- NIP lookup in KRS/CEIDG registries
|
|
- Draft save/load
|
|
- Application submission
|
|
- Company data requests
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from flask import jsonify, request
|
|
from flask_login import current_user, login_required
|
|
|
|
from database import (
|
|
SessionLocal, MembershipApplication, CompanyDataRequest, Company
|
|
)
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================
|
|
# NIP LOOKUP (KRS/CEIDG)
|
|
# ============================================================
|
|
|
|
@bp.route('/membership/lookup-nip', methods=['POST'])
|
|
@login_required
|
|
def lookup_nip():
|
|
"""
|
|
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()
|
|
if not data:
|
|
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
|
|
|
|
if not nip.isdigit():
|
|
return jsonify({'success': False, 'error': 'NIP może zawierać tylko cyfry'}), 400
|
|
|
|
# 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({
|
|
'success': True,
|
|
'source': 'KRS',
|
|
'data': krs_result
|
|
})
|
|
|
|
# Option 3: Try CEIDG (for JDG - sole proprietorship)
|
|
ceidg_result = _lookup_ceidg(nip)
|
|
if ceidg_result:
|
|
return jsonify({
|
|
'success': True,
|
|
'source': 'CEIDG',
|
|
'data': ceidg_result
|
|
})
|
|
|
|
# Not found in any registry
|
|
return jsonify({
|
|
'success': True,
|
|
'source': 'manual',
|
|
'data': None,
|
|
'message': 'Firma nie została znaleziona w KRS ani CEIDG. Wypełnij dane ręcznie.'
|
|
})
|
|
|
|
|
|
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 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:
|
|
return _parse_krs_data(result)
|
|
except ImportError:
|
|
logger.warning("KRS API service not available")
|
|
except Exception as e:
|
|
logger.error(f"KRS lookup error for NIP {nip}: {e}")
|
|
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:
|
|
from ceidg_api_service import fetch_ceidg_by_nip
|
|
result = fetch_ceidg_by_nip(nip)
|
|
if result:
|
|
address = result.get('adresDzialalnosci', {})
|
|
|
|
return {
|
|
'name': result.get('firma'),
|
|
'regon': result.get('regon'),
|
|
'address_postal_code': address.get('kodPocztowy', ''),
|
|
'address_city': address.get('miejscowosc', ''),
|
|
'address_street': address.get('ulica', ''),
|
|
'address_number': address.get('budynek', ''),
|
|
'founded_date': result.get('dataRozpoczeciaDzialalnosci'),
|
|
'business_type': 'jdg',
|
|
'email': result.get('email'),
|
|
'website': result.get('stronaWWW'),
|
|
'raw': result
|
|
}
|
|
except ImportError:
|
|
logger.warning("CEIDG API service not available")
|
|
except Exception as e:
|
|
logger.error(f"CEIDG lookup error for NIP {nip}: {e}")
|
|
return None
|
|
|
|
|
|
def _detect_business_type_from_krs(data):
|
|
"""Detect business type from KRS data."""
|
|
legal_form = data.get('forma_prawna', '').lower()
|
|
|
|
if 'akcyjna' in legal_form and 'komandytowo' in legal_form:
|
|
return 'spolka_komandytowo_akcyjna'
|
|
elif 'akcyjna' in legal_form:
|
|
return 'spolka_akcyjna'
|
|
elif 'z ograniczoną odpowiedzialnością' in legal_form or 'z o.o.' in legal_form:
|
|
if 'komandytowa' in legal_form:
|
|
return 'sp_z_oo_komandytowa'
|
|
return 'sp_z_oo'
|
|
elif 'komandytowa' in legal_form:
|
|
return 'spolka_komandytowa'
|
|
elif 'partnerska' in legal_form:
|
|
return 'spolka_partnerska'
|
|
elif 'jawna' in legal_form:
|
|
return 'spolka_jawna'
|
|
elif 'cywilna' in legal_form:
|
|
return 'spolka_cywilna'
|
|
|
|
return 'inna'
|
|
|
|
|
|
# ============================================================
|
|
# MEMBERSHIP APPLICATION DRAFT
|
|
# ============================================================
|
|
|
|
@bp.route('/membership/draft', methods=['GET'])
|
|
@login_required
|
|
def get_draft():
|
|
"""Get current draft application."""
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status.in_(['draft', 'changes_requested'])
|
|
).first()
|
|
|
|
if not application:
|
|
return jsonify({'success': True, 'draft': None})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'draft': _serialize_application(application)
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/membership/draft', methods=['POST'])
|
|
@login_required
|
|
def save_draft():
|
|
"""Save draft application data."""
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'success': False, 'error': 'Brak danych'}), 400
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Get or create draft
|
|
application = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status.in_(['draft', 'changes_requested'])
|
|
).first()
|
|
|
|
if not application:
|
|
application = MembershipApplication(
|
|
user_id=current_user.id,
|
|
company_name=data.get('company_name', ''),
|
|
nip=data.get('nip', ''),
|
|
email=data.get('email', current_user.email or ''),
|
|
status='draft'
|
|
)
|
|
db.add(application)
|
|
|
|
# Update fields
|
|
_update_application_from_data(application, data)
|
|
application.updated_at = datetime.now()
|
|
|
|
db.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Zapisano',
|
|
'application_id': application.id
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error saving draft: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _update_application_from_data(app, data):
|
|
"""Update application fields from request data."""
|
|
# Step 1 fields
|
|
if 'company_name' in data:
|
|
app.company_name = data['company_name'].strip()
|
|
if 'nip' in data:
|
|
app.nip = data['nip'].strip().replace('-', '').replace(' ', '')
|
|
if 'address_postal_code' in data:
|
|
app.address_postal_code = data['address_postal_code'].strip()
|
|
if 'address_city' in data:
|
|
app.address_city = data['address_city'].strip()
|
|
if 'address_street' in data:
|
|
app.address_street = data['address_street'].strip()
|
|
if 'address_number' in data:
|
|
app.address_number = data['address_number'].strip()
|
|
if 'delegate_1' in data:
|
|
app.delegate_1 = data['delegate_1'].strip()
|
|
if 'delegate_2' in data:
|
|
app.delegate_2 = data['delegate_2'].strip()
|
|
if 'delegate_3' in data:
|
|
app.delegate_3 = data['delegate_3'].strip()
|
|
if 'krs_number' in data:
|
|
app.krs_number = data['krs_number'].strip()
|
|
if 'regon' in data:
|
|
app.regon = data['regon'].strip()
|
|
if 'registry_source' in data:
|
|
app.registry_source = data['registry_source']
|
|
if 'registry_data' in data:
|
|
app.registry_data = data['registry_data']
|
|
|
|
# Step 2 fields
|
|
if 'website' in data:
|
|
app.website = data['website'].strip()
|
|
if 'email' in data:
|
|
app.email = data['email'].strip()
|
|
if 'phone' in data:
|
|
app.phone = data['phone'].strip()
|
|
if 'short_name' in data:
|
|
app.short_name = data['short_name'].strip()
|
|
if 'description' in data:
|
|
app.description = data['description'].strip()
|
|
if 'founded_date' in data and data['founded_date']:
|
|
try:
|
|
app.founded_date = datetime.strptime(data['founded_date'], '%Y-%m-%d').date()
|
|
except ValueError:
|
|
pass
|
|
if 'employee_count' in data and data['employee_count']:
|
|
try:
|
|
app.employee_count = int(data['employee_count'])
|
|
except ValueError:
|
|
pass
|
|
if 'show_employee_count' in data:
|
|
app.show_employee_count = bool(data['show_employee_count'])
|
|
if 'annual_revenue' in data:
|
|
app.annual_revenue = data['annual_revenue'].strip() if data['annual_revenue'] else None
|
|
if 'related_companies' in data:
|
|
app.related_companies = data['related_companies'] if data['related_companies'] else None
|
|
|
|
# Step 3 fields
|
|
if 'sections' in data:
|
|
app.sections = data['sections'] if data['sections'] else []
|
|
if 'sections_other' in data:
|
|
app.sections_other = data['sections_other'].strip() if data['sections_other'] else None
|
|
if 'business_type' in data:
|
|
app.business_type = data['business_type']
|
|
if 'business_type_other' in data:
|
|
app.business_type_other = data['business_type_other'].strip() if data['business_type_other'] else None
|
|
if 'consent_email' in data:
|
|
app.consent_email = bool(data['consent_email'])
|
|
if 'consent_email_address' in data:
|
|
app.consent_email_address = data['consent_email_address'].strip() if data['consent_email_address'] else None
|
|
if 'consent_sms' in data:
|
|
app.consent_sms = bool(data['consent_sms'])
|
|
if 'consent_sms_phone' in data:
|
|
app.consent_sms_phone = data['consent_sms_phone'].strip() if data['consent_sms_phone'] else None
|
|
|
|
|
|
def _serialize_application(app):
|
|
"""Serialize application to dict."""
|
|
return {
|
|
'id': app.id,
|
|
'status': app.status,
|
|
'company_name': app.company_name,
|
|
'nip': app.nip,
|
|
'address_postal_code': app.address_postal_code,
|
|
'address_city': app.address_city,
|
|
'address_street': app.address_street,
|
|
'address_number': app.address_number,
|
|
'delegate_1': app.delegate_1,
|
|
'delegate_2': app.delegate_2,
|
|
'delegate_3': app.delegate_3,
|
|
'krs_number': app.krs_number,
|
|
'regon': app.regon,
|
|
'registry_source': app.registry_source,
|
|
'website': app.website,
|
|
'email': app.email,
|
|
'phone': app.phone,
|
|
'short_name': app.short_name,
|
|
'description': app.description,
|
|
'founded_date': app.founded_date.isoformat() if app.founded_date else None,
|
|
'employee_count': app.employee_count,
|
|
'show_employee_count': app.show_employee_count,
|
|
'annual_revenue': app.annual_revenue,
|
|
'related_companies': app.related_companies,
|
|
'sections': app.sections,
|
|
'sections_other': app.sections_other,
|
|
'business_type': app.business_type,
|
|
'business_type_other': app.business_type_other,
|
|
'consent_email': app.consent_email,
|
|
'consent_email_address': app.consent_email_address,
|
|
'consent_sms': app.consent_sms,
|
|
'consent_sms_phone': app.consent_sms_phone,
|
|
'declaration_accepted': app.declaration_accepted,
|
|
'created_at': app.created_at.isoformat() if app.created_at else None,
|
|
'updated_at': app.updated_at.isoformat() if app.updated_at else None,
|
|
'submitted_at': app.submitted_at.isoformat() if app.submitted_at else None
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# MEMBERSHIP APPLICATION SUBMIT
|
|
# ============================================================
|
|
|
|
@bp.route('/membership/submit', methods=['POST'])
|
|
@login_required
|
|
def submit_application():
|
|
"""Submit draft application for review."""
|
|
db = SessionLocal()
|
|
try:
|
|
application = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status.in_(['draft', 'changes_requested'])
|
|
).first()
|
|
|
|
if not application:
|
|
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
|
|
|
# Validate
|
|
errors = _validate_application(application)
|
|
if errors:
|
|
return jsonify({'success': False, 'errors': errors}), 400
|
|
|
|
# Submit
|
|
application.status = 'submitted'
|
|
application.submitted_at = datetime.now()
|
|
application.declaration_accepted_at = datetime.now()
|
|
application.declaration_ip_address = request.remote_addr
|
|
db.commit()
|
|
|
|
logger.info(f"Membership application submitted: user={current_user.id}, app={application.id}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Deklaracja została wysłana do rozpatrzenia',
|
|
'application_id': application.id
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error submitting application: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _validate_application(app):
|
|
"""Validate application before submission."""
|
|
errors = []
|
|
|
|
if not app.company_name:
|
|
errors.append('Nazwa firmy jest wymagana')
|
|
if not app.nip or len(app.nip) != 10:
|
|
errors.append('NIP jest wymagany (10 cyfr)')
|
|
if not app.email:
|
|
errors.append('Email jest wymagany')
|
|
if not app.delegate_1:
|
|
errors.append('Przynajmniej jeden delegat jest wymagany')
|
|
if not app.sections:
|
|
errors.append('Wybierz przynajmniej jedną sekcję tematyczną')
|
|
if not app.consent_email:
|
|
errors.append('Zgoda na kontakt email jest wymagana')
|
|
if not app.declaration_accepted:
|
|
errors.append('Musisz zaakceptować oświadczenie')
|
|
|
|
return errors
|
|
|
|
|
|
# ============================================================
|
|
# MEMBERSHIP STATUS
|
|
# ============================================================
|
|
|
|
@bp.route('/membership/status', methods=['GET'])
|
|
@login_required
|
|
def get_status():
|
|
"""Get application status for current user."""
|
|
db = SessionLocal()
|
|
try:
|
|
applications = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id
|
|
).order_by(MembershipApplication.created_at.desc()).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'applications': [
|
|
{
|
|
'id': app.id,
|
|
'status': app.status,
|
|
'status_label': app.status_label,
|
|
'company_name': app.company_name,
|
|
'created_at': app.created_at.isoformat() if app.created_at else None,
|
|
'submitted_at': app.submitted_at.isoformat() if app.submitted_at else None,
|
|
'reviewed_at': app.reviewed_at.isoformat() if app.reviewed_at else None,
|
|
'review_comment': app.review_comment
|
|
}
|
|
for app in applications
|
|
]
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# COMPANY DATA REQUEST
|
|
# ============================================================
|
|
|
|
@bp.route('/company/data-request', methods=['POST'])
|
|
@login_required
|
|
def create_data_request():
|
|
"""Create a company data update request."""
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'success': False, 'error': 'Brak danych'}), 400
|
|
|
|
if not current_user.company_id:
|
|
return jsonify({'success': False, 'error': 'Nie masz przypisanej firmy'}), 400
|
|
|
|
nip = data.get('nip', '').strip().replace('-', '').replace(' ', '')
|
|
if not nip or len(nip) != 10:
|
|
return jsonify({'success': False, 'error': 'NIP musi mieć 10 cyfr'}), 400
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Check for existing pending request
|
|
existing = db.query(CompanyDataRequest).filter(
|
|
CompanyDataRequest.user_id == current_user.id,
|
|
CompanyDataRequest.company_id == current_user.company_id,
|
|
CompanyDataRequest.status == 'pending'
|
|
).first()
|
|
|
|
if existing:
|
|
return jsonify({'success': False, 'error': 'Masz już oczekujące zgłoszenie'}), 400
|
|
|
|
# Fetch registry data
|
|
registry_data = None
|
|
registry_source = None
|
|
|
|
krs_result = _lookup_krs(nip)
|
|
if krs_result:
|
|
registry_data = krs_result
|
|
registry_source = 'KRS'
|
|
else:
|
|
ceidg_result = _lookup_ceidg(nip)
|
|
if ceidg_result:
|
|
registry_data = ceidg_result
|
|
registry_source = 'CEIDG'
|
|
|
|
# Create request
|
|
data_request = CompanyDataRequest(
|
|
request_type=data.get('request_type', 'update_data'),
|
|
user_id=current_user.id,
|
|
company_id=current_user.company_id,
|
|
nip=nip,
|
|
registry_source=registry_source,
|
|
fetched_data=registry_data,
|
|
user_note=data.get('user_note', '').strip() if data.get('user_note') else None
|
|
)
|
|
|
|
db.add(data_request)
|
|
db.commit()
|
|
|
|
logger.info(f"Company data request created: user={current_user.id}, company={current_user.company_id}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Zgłoszenie zostało wysłane do rozpatrzenia',
|
|
'request_id': data_request.id,
|
|
'registry_data': registry_data
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error creating data request: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/data-request/status', methods=['GET'])
|
|
@login_required
|
|
def get_data_request_status():
|
|
"""Get company data request status."""
|
|
if not current_user.company_id:
|
|
return jsonify({'success': True, 'requests': []})
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
requests = db.query(CompanyDataRequest).filter(
|
|
CompanyDataRequest.user_id == current_user.id
|
|
).order_by(CompanyDataRequest.created_at.desc()).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'requests': [
|
|
{
|
|
'id': req.id,
|
|
'request_type': req.request_type,
|
|
'request_type_label': req.request_type_label,
|
|
'status': req.status,
|
|
'status_label': req.status_label,
|
|
'nip': req.nip,
|
|
'registry_source': req.registry_source,
|
|
'created_at': req.created_at.isoformat() if req.created_at else None,
|
|
'reviewed_at': req.reviewed_at.isoformat() if req.reviewed_at else None,
|
|
'review_comment': req.review_comment
|
|
}
|
|
for req in requests
|
|
]
|
|
})
|
|
finally:
|
|
db.close()
|