""" 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()