From 8e3346178d905f62f0183e0424a30e9bf2f17612 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Sat, 31 Jan 2026 17:54:54 +0100 Subject: [PATCH] refactor: Move GBP and Social Audit API routes to blueprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create blueprints/api/routes_gbp_audit.py (4 routes) - Create blueprints/api/routes_social_audit.py (1 route) - app.py: 6354 → 5860 lines (-494 total this batch) Co-Authored-By: Claude Opus 4.5 --- app.py | 502 +------------------------- blueprints/api/__init__.py | 2 + blueprints/api/routes_gbp_audit.py | 387 ++++++++++++++++++++ blueprints/api/routes_social_audit.py | 171 +++++++++ 4 files changed, 564 insertions(+), 498 deletions(-) create mode 100644 blueprints/api/routes_gbp_audit.py create mode 100644 blueprints/api/routes_social_audit.py diff --git a/app.py b/app.py index ea61b76..4ab9717 100644 --- a/app.py +++ b/app.py @@ -1243,355 +1243,9 @@ def api_connections(): # ============================================================ # ============================================================ -# GBP (GOOGLE BUSINESS PROFILE) AUDIT API +# GBP AUDIT API - MOVED TO: blueprints/api/routes_gbp_audit.py # ============================================================ -@app.route('/api/gbp/audit/health') -def api_gbp_audit_health(): - """ - API: Health check for GBP audit service. - - Returns service status and version information. - Used by monitoring systems to verify service availability. - """ - if GBP_AUDIT_AVAILABLE: - return jsonify({ - 'status': 'ok', - 'service': 'gbp_audit', - 'version': GBP_AUDIT_VERSION, - 'available': True - }), 200 - else: - return jsonify({ - 'status': 'unavailable', - 'service': 'gbp_audit', - 'available': False, - 'error': 'GBP audit service not loaded' - }), 503 - - -@app.route('/api/gbp/audit', methods=['GET']) -def api_gbp_audit_get(): - """ - API: Get GBP audit results for a company. - - Query parameters: - - company_id: Company ID (integer) OR - - slug: Company slug (string) - - Returns: - - Latest audit results with completeness score and recommendations - - 404 if company not found - - 404 if no audit exists for the company - - Example: GET /api/gbp/audit?company_id=26 - Example: GET /api/gbp/audit?slug=pixlab-sp-z-o-o - """ - if not GBP_AUDIT_AVAILABLE: - return jsonify({ - 'success': False, - 'error': 'Usługa audytu GBP jest niedostępna.' - }), 503 - - company_id = request.args.get('company_id', type=int) - slug = request.args.get('slug') - - if not company_id and not slug: - return jsonify({ - 'success': False, - 'error': 'Podaj company_id lub slug firmy.' - }), 400 - - db = SessionLocal() - try: - # Find company - if company_id: - company = db.query(Company).filter_by(id=company_id, status='active').first() - else: - company = db.query(Company).filter_by(slug=slug, status='active').first() - - if not company: - return jsonify({ - 'success': False, - 'error': 'Firma nie znaleziona lub nieaktywna.' - }), 404 - - # Get latest audit - audit = gbp_get_company_audit(db, company.id) - - if not audit: - return jsonify({ - 'success': False, - 'error': f'Brak wyników audytu GBP dla firmy "{company.name}". Uruchom audyt używając POST /api/gbp/audit.', - 'company_id': company.id, - 'company_name': company.name - }), 404 - - # Build response - return jsonify({ - 'success': True, - 'company_id': company.id, - 'company_name': company.name, - 'company_slug': company.slug, - 'audit': { - 'id': audit.id, - 'audit_date': audit.audit_date.isoformat() if audit.audit_date else None, - 'completeness_score': audit.completeness_score, - 'score_category': audit.score_category, - 'fields_status': audit.fields_status, - 'recommendations': audit.recommendations, - 'has_name': audit.has_name, - 'has_address': audit.has_address, - 'has_phone': audit.has_phone, - 'has_website': audit.has_website, - 'has_hours': audit.has_hours, - 'has_categories': audit.has_categories, - 'has_photos': audit.has_photos, - 'has_description': audit.has_description, - 'has_services': audit.has_services, - 'has_reviews': audit.has_reviews, - 'photo_count': audit.photo_count, - 'review_count': audit.review_count, - 'average_rating': float(audit.average_rating) if audit.average_rating else None, - 'google_place_id': audit.google_place_id, - 'audit_source': audit.audit_source, - 'audit_version': audit.audit_version - } - }), 200 - - except Exception as e: - logger.error(f"Error fetching GBP audit: {e}") - return jsonify({ - 'success': False, - 'error': f'Błąd podczas pobierania audytu: {str(e)}' - }), 500 - finally: - db.close() - - -@app.route('/api/gbp/audit/') -def api_gbp_audit_by_slug(slug): - """ - API: Get GBP audit results for a company by slug. - Convenience endpoint that uses slug from URL path. - - Example: GET /api/gbp/audit/pixlab-sp-z-o-o - """ - if not GBP_AUDIT_AVAILABLE: - return jsonify({ - 'success': False, - 'error': 'Usługa audytu GBP jest niedostępna.' - }), 503 - - db = SessionLocal() - try: - company = db.query(Company).filter_by(slug=slug, status='active').first() - - if not company: - return jsonify({ - 'success': False, - 'error': f'Firma o slug "{slug}" nie znaleziona.' - }), 404 - - audit = gbp_get_company_audit(db, company.id) - - if not audit: - return jsonify({ - 'success': False, - 'error': f'Brak wyników audytu GBP dla firmy "{company.name}".', - 'company_id': company.id, - 'company_name': company.name - }), 404 - - return jsonify({ - 'success': True, - 'company_id': company.id, - 'company_name': company.name, - 'company_slug': company.slug, - 'audit': { - 'id': audit.id, - 'audit_date': audit.audit_date.isoformat() if audit.audit_date else None, - 'completeness_score': audit.completeness_score, - 'score_category': audit.score_category, - 'fields_status': audit.fields_status, - 'recommendations': audit.recommendations, - 'photo_count': audit.photo_count, - 'review_count': audit.review_count, - 'average_rating': float(audit.average_rating) if audit.average_rating else None - } - }), 200 - - finally: - db.close() - - -@app.route('/api/gbp/audit', methods=['POST']) -@login_required -@limiter.limit("20 per hour") -def api_gbp_audit_trigger(): - """ - API: Run GBP audit for a company. - - This endpoint runs a completeness audit for Google Business Profile data, - checking fields like name, address, phone, website, hours, categories, - photos, description, services, and reviews. - - Request JSON body: - - company_id: Company ID (integer) OR - - slug: Company slug (string) - - save: Whether to save results to database (default: true) - - Returns: - - Success: Audit results with completeness score and recommendations - - Error: Error message with status code - - Access: - - Members can audit their own company - - Admins can audit any company - - Rate limited to 20 requests per hour per user. - """ - if not GBP_AUDIT_AVAILABLE: - return jsonify({ - 'success': False, - 'error': 'Usługa audytu GBP jest niedostępna. Sprawdź konfigurację serwera.' - }), 503 - - # Parse request data - data = request.get_json() - if not data: - return jsonify({ - 'success': False, - 'error': 'Brak danych w żądaniu. Podaj company_id lub slug.' - }), 400 - - company_id = data.get('company_id') - slug = data.get('slug') - save_result = data.get('save', True) - - if not company_id and not slug: - return jsonify({ - 'success': False, - 'error': 'Podaj company_id lub slug firmy do audytu.' - }), 400 - - db = SessionLocal() - try: - # Find company by ID or slug - if company_id: - company = db.query(Company).filter_by(id=company_id, status='active').first() - else: - company = db.query(Company).filter_by(slug=slug, status='active').first() - - if not company: - return jsonify({ - 'success': False, - 'error': 'Firma nie znaleziona lub nieaktywna.' - }), 404 - - # Check access: admin can audit any company, member only their own - if not current_user.is_admin: - # Check if user is associated with this company - if current_user.company_id != company.id: - return jsonify({ - 'success': False, - 'error': 'Brak uprawnień. Możesz audytować tylko własną firmę.' - }), 403 - - logger.info(f"GBP audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})") - - # Option to fetch fresh Google data before audit - fetch_google = data.get('fetch_google', True) - force_refresh = data.get('force_refresh', False) - - try: - # Step 1: Fetch fresh Google Business data (if enabled) - fetch_result = None - if fetch_google: - logger.info(f"Fetching Google Business data for company {company.id}...") - fetch_result = gbp_fetch_google_data(db, company.id, force_refresh=force_refresh) - if not fetch_result.get('success') and not fetch_result.get('data', {}).get('cached'): - # Log warning but continue with audit - logger.warning(f"Google fetch warning for company {company.id}: {fetch_result.get('error')}") - - # Step 2: Run the audit - result = gbp_audit_company(db, company.id, save=save_result) - - # Build field status for response - fields_response = {} - for field_name, field_status in result.fields.items(): - fields_response[field_name] = { - 'status': field_status.status, - 'value': str(field_status.value) if field_status.value is not None else None, - 'score': field_status.score, - 'max_score': field_status.max_score, - 'recommendation': field_status.recommendation - } - - # Determine score category - score = result.completeness_score - if score >= 90: - score_category = 'excellent' - elif score >= 70: - score_category = 'good' - elif score >= 50: - score_category = 'needs_work' - else: - score_category = 'poor' - - response_data = { - 'success': True, - 'message': f'Audyt GBP dla firmy "{company.name}" został zakończony pomyślnie.', - 'company_id': company.id, - 'company_name': company.name, - 'company_slug': company.slug, - 'audit_version': GBP_AUDIT_VERSION, - 'triggered_by': current_user.email, - 'triggered_at': datetime.now().isoformat(), - 'saved': save_result, - 'audit': { - 'completeness_score': result.completeness_score, - 'score_category': score_category, - 'fields_status': fields_response, - 'recommendations': result.recommendations, - 'photo_count': result.photo_count, - 'logo_present': result.logo_present, - 'cover_photo_present': result.cover_photo_present, - 'review_count': result.review_count, - 'average_rating': float(result.average_rating) if result.average_rating else None, - 'google_place_id': result.google_place_id - } - } - - # Include Google fetch results if performed - if fetch_result: - response_data['google_fetch'] = { - 'success': fetch_result.get('success', False), - 'steps': fetch_result.get('steps', []), - 'data': fetch_result.get('data', {}), - 'error': fetch_result.get('error') - } - - return jsonify(response_data), 200 - - except ValueError as e: - return jsonify({ - 'success': False, - 'error': str(e), - 'company_id': company.id if company else None - }), 400 - except Exception as e: - logger.error(f"GBP audit error for company {company.id}: {e}") - return jsonify({ - 'success': False, - 'error': f'Błąd podczas wykonywania audytu: {str(e)}', - 'company_id': company.id, - 'company_name': company.name - }), 500 - - finally: - db.close() - # ============================================================ # SEO AUDIT USER-FACING DASHBOARD @@ -1755,157 +1409,9 @@ def social_audit_dashboard(slug): db.close() -@app.route('/api/social/audit', methods=['POST']) -@login_required -@limiter.limit("10 per hour") -def api_social_audit_trigger(): - """ - API: Trigger Social Media audit for a company. - - This endpoint performs a comprehensive social media audit: - - Scans company website for social media links - - Searches for profiles via Brave Search API (if configured) - - Fetches Google Business Profile data - - Updates database with discovered profiles - - Request JSON body: - - company_id: Company ID (integer) OR - - slug: Company slug (string) - - Returns: - - Success: Updated social media audit results - - Error: Error message with status code - - Rate limited to 10 requests per hour per user. - """ - # Import the SocialMediaAuditor from scripts - try: - import sys - from pathlib import Path - scripts_dir = Path(__file__).parent / 'scripts' - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - from social_media_audit import SocialMediaAuditor - except ImportError as e: - logger.error(f"Failed to import SocialMediaAuditor: {e}") - return jsonify({ - 'success': False, - 'error': 'Usługa audytu Social Media jest niedostępna. Sprawdź konfigurację serwera.' - }), 503 - - # Parse request data - data = request.get_json() - if not data: - return jsonify({ - 'success': False, - 'error': 'Brak danych w żądaniu. Podaj company_id lub slug.' - }), 400 - - company_id = data.get('company_id') - slug = data.get('slug') - - if not company_id and not slug: - return jsonify({ - 'success': False, - 'error': 'Podaj company_id lub slug firmy do audytu.' - }), 400 - - db = SessionLocal() - try: - # Find company by ID or slug - if company_id: - company = db.query(Company).filter_by(id=company_id, status='active').first() - else: - company = db.query(Company).filter_by(slug=slug, status='active').first() - - if not company: - return jsonify({ - 'success': False, - 'error': 'Firma nie znaleziona lub nieaktywna.' - }), 404 - - # Access control - admin can audit all, users only their company - if not current_user.is_admin: - if current_user.company_id != company.id: - return jsonify({ - 'success': False, - 'error': 'Brak uprawnień do audytu social media tej firmy.' - }), 403 - - logger.info(f"Social Media audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})") - - # Prepare company dict for auditor - company_dict = { - 'id': company.id, - 'name': company.name, - 'slug': company.slug, - 'website': company.website, - 'address_city': company.address_city or 'Wejherowo' - } - - # Initialize auditor and run audit - try: - auditor = SocialMediaAuditor() - audit_result = auditor.audit_company(company_dict) - - # Check for errors - if audit_result.get('errors') and not audit_result.get('social_media') and not audit_result.get('website'): - return jsonify({ - 'success': False, - 'error': f'Audyt nie powiódł się: {", ".join(audit_result["errors"][:3])}', - 'company_id': company.id, - 'company_name': company.name - }), 422 - - # Save result to database - saved = auditor.save_audit_result(audit_result) - - if not saved: - return jsonify({ - 'success': False, - 'error': 'Audyt został wykonany, ale nie udało się zapisać wyników do bazy danych.', - 'company_id': company.id, - 'company_name': company.name - }), 500 - - # Get count of social media profiles found - social_media_found = audit_result.get('social_media', {}) - platforms_count = len(social_media_found) - - # Calculate score - all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok'] - score = int((platforms_count / len(all_platforms)) * 100) - - return jsonify({ - 'success': True, - 'message': f'Audyt Social Media zakończony. Znaleziono {platforms_count} profili.', - 'company_id': company.id, - 'company_name': company.name, - 'profiles_found': platforms_count, - 'platforms': list(social_media_found.keys()), - 'score': score, - 'google_reviews': audit_result.get('google_reviews', {}), - 'errors': audit_result.get('errors') if audit_result.get('errors') else None - }), 200 - - except Exception as e: - logger.error(f"Social Media audit error for company {company.id}: {e}") - return jsonify({ - 'success': False, - 'error': f'Błąd podczas audytu: {str(e)}' - }), 500 - - except Exception as e: - logger.error(f"Social Media audit error for company {slug or company_id}: {e}") - db.rollback() - return jsonify({ - 'success': False, - 'error': f'Błąd podczas audytu: {str(e)}' - }), 500 - - finally: - db.close() - +# ============================================================ +# SOCIAL AUDIT API - MOVED TO: blueprints/api/routes_social_audit.py +# ============================================================ # ============================================================ # GBP AUDIT USER-FACING DASHBOARD diff --git a/blueprints/api/__init__.py b/blueprints/api/__init__.py index 456c29f..84b56f1 100644 --- a/blueprints/api/__init__.py +++ b/blueprints/api/__init__.py @@ -13,3 +13,5 @@ from . import routes_analytics # noqa: E402, F401 from . import routes_recommendations # noqa: E402, F401 from . import routes_contacts # noqa: E402, F401 from . import routes_seo_audit # noqa: E402, F401 +from . import routes_gbp_audit # noqa: E402, F401 +from . import routes_social_audit # noqa: E402, F401 diff --git a/blueprints/api/routes_gbp_audit.py b/blueprints/api/routes_gbp_audit.py new file mode 100644 index 0000000..a470e91 --- /dev/null +++ b/blueprints/api/routes_gbp_audit.py @@ -0,0 +1,387 @@ +""" +GBP Audit API Routes - API blueprint + +Migrated from app.py as part of the blueprint refactoring. +Contains API routes for Google Business Profile audit functionality. +""" + +import logging +from datetime import datetime + +from flask import jsonify, request, current_app +from flask_login import current_user, login_required + +from database import SessionLocal, Company +from . import bp + +logger = logging.getLogger(__name__) + + +# Check if GBP audit service is available +try: + from gbp_audit_service import ( + GBPAuditResult, + audit_company as gbp_audit_company, + get_company_audit as gbp_get_company_audit, + fetch_google_business_data as gbp_fetch_google_data + ) + GBP_AUDIT_AVAILABLE = True + GBP_AUDIT_VERSION = '1.0' +except ImportError: + GBP_AUDIT_AVAILABLE = False + GBP_AUDIT_VERSION = None + + +def get_limiter(): + """Get rate limiter from current app.""" + return current_app.extensions.get('limiter') + + +# ============================================================ +# GBP AUDIT API ROUTES +# ============================================================ + +@bp.route('/gbp/audit/health') +def api_gbp_audit_health(): + """ + API: Health check for GBP audit service. + + Returns service status and version information. + Used by monitoring systems to verify service availability. + """ + if GBP_AUDIT_AVAILABLE: + return jsonify({ + 'status': 'ok', + 'service': 'gbp_audit', + 'version': GBP_AUDIT_VERSION, + 'available': True + }), 200 + else: + return jsonify({ + 'status': 'unavailable', + 'service': 'gbp_audit', + 'available': False, + 'error': 'GBP audit service not loaded' + }), 503 + + +@bp.route('/gbp/audit', methods=['GET']) +def api_gbp_audit_get(): + """ + API: Get GBP audit results for a company. + + Query parameters: + - company_id: Company ID (integer) OR + - slug: Company slug (string) + + Returns: + - Latest audit results with completeness score and recommendations + - 404 if company not found + - 404 if no audit exists for the company + + Example: GET /api/gbp/audit?company_id=26 + Example: GET /api/gbp/audit?slug=pixlab-sp-z-o-o + """ + if not GBP_AUDIT_AVAILABLE: + return jsonify({ + 'success': False, + 'error': 'Usługa audytu GBP jest niedostępna.' + }), 503 + + company_id = request.args.get('company_id', type=int) + slug = request.args.get('slug') + + if not company_id and not slug: + return jsonify({ + 'success': False, + 'error': 'Podaj company_id lub slug firmy.' + }), 400 + + db = SessionLocal() + try: + # Find company + if company_id: + company = db.query(Company).filter_by(id=company_id, status='active').first() + else: + company = db.query(Company).filter_by(slug=slug, status='active').first() + + if not company: + return jsonify({ + 'success': False, + 'error': 'Firma nie znaleziona lub nieaktywna.' + }), 404 + + # Get latest audit + audit = gbp_get_company_audit(db, company.id) + + if not audit: + return jsonify({ + 'success': False, + 'error': f'Brak wyników audytu GBP dla firmy "{company.name}". Uruchom audyt używając POST /api/gbp/audit.', + 'company_id': company.id, + 'company_name': company.name + }), 404 + + # Build response + return jsonify({ + 'success': True, + 'company_id': company.id, + 'company_name': company.name, + 'company_slug': company.slug, + 'audit': { + 'id': audit.id, + 'audit_date': audit.audit_date.isoformat() if audit.audit_date else None, + 'completeness_score': audit.completeness_score, + 'score_category': audit.score_category, + 'fields_status': audit.fields_status, + 'recommendations': audit.recommendations, + 'has_name': audit.has_name, + 'has_address': audit.has_address, + 'has_phone': audit.has_phone, + 'has_website': audit.has_website, + 'has_hours': audit.has_hours, + 'has_categories': audit.has_categories, + 'has_photos': audit.has_photos, + 'has_description': audit.has_description, + 'has_services': audit.has_services, + 'has_reviews': audit.has_reviews, + 'photo_count': audit.photo_count, + 'review_count': audit.review_count, + 'average_rating': float(audit.average_rating) if audit.average_rating else None, + 'google_place_id': audit.google_place_id, + 'audit_source': audit.audit_source, + 'audit_version': audit.audit_version + } + }), 200 + + except Exception as e: + logger.error(f"Error fetching GBP audit: {e}") + return jsonify({ + 'success': False, + 'error': f'Błąd podczas pobierania audytu: {str(e)}' + }), 500 + finally: + db.close() + + +@bp.route('/gbp/audit/') +def api_gbp_audit_by_slug(slug): + """ + API: Get GBP audit results for a company by slug. + Convenience endpoint that uses slug from URL path. + + Example: GET /api/gbp/audit/pixlab-sp-z-o-o + """ + if not GBP_AUDIT_AVAILABLE: + return jsonify({ + 'success': False, + 'error': 'Usługa audytu GBP jest niedostępna.' + }), 503 + + db = SessionLocal() + try: + company = db.query(Company).filter_by(slug=slug, status='active').first() + + if not company: + return jsonify({ + 'success': False, + 'error': f'Firma o slug "{slug}" nie znaleziona.' + }), 404 + + audit = gbp_get_company_audit(db, company.id) + + if not audit: + return jsonify({ + 'success': False, + 'error': f'Brak wyników audytu GBP dla firmy "{company.name}".', + 'company_id': company.id, + 'company_name': company.name + }), 404 + + return jsonify({ + 'success': True, + 'company_id': company.id, + 'company_name': company.name, + 'company_slug': company.slug, + 'audit': { + 'id': audit.id, + 'audit_date': audit.audit_date.isoformat() if audit.audit_date else None, + 'completeness_score': audit.completeness_score, + 'score_category': audit.score_category, + 'fields_status': audit.fields_status, + 'recommendations': audit.recommendations, + 'photo_count': audit.photo_count, + 'review_count': audit.review_count, + 'average_rating': float(audit.average_rating) if audit.average_rating else None + } + }), 200 + + finally: + db.close() + + +@bp.route('/gbp/audit', methods=['POST']) +@login_required +def api_gbp_audit_trigger(): + """ + API: Run GBP audit for a company. + + This endpoint runs a completeness audit for Google Business Profile data, + checking fields like name, address, phone, website, hours, categories, + photos, description, services, and reviews. + + Request JSON body: + - company_id: Company ID (integer) OR + - slug: Company slug (string) + - save: Whether to save results to database (default: true) + + Returns: + - Success: Audit results with completeness score and recommendations + - Error: Error message with status code + + Access: + - Members can audit their own company + - Admins can audit any company + + Rate limited to 20 requests per hour per user. + """ + if not GBP_AUDIT_AVAILABLE: + return jsonify({ + 'success': False, + 'error': 'Usługa audytu GBP jest niedostępna. Sprawdź konfigurację serwera.' + }), 503 + + # Parse request data + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': 'Brak danych w żądaniu. Podaj company_id lub slug.' + }), 400 + + company_id = data.get('company_id') + slug = data.get('slug') + save_result = data.get('save', True) + + if not company_id and not slug: + return jsonify({ + 'success': False, + 'error': 'Podaj company_id lub slug firmy do audytu.' + }), 400 + + db = SessionLocal() + try: + # Find company by ID or slug + if company_id: + company = db.query(Company).filter_by(id=company_id, status='active').first() + else: + company = db.query(Company).filter_by(slug=slug, status='active').first() + + if not company: + return jsonify({ + 'success': False, + 'error': 'Firma nie znaleziona lub nieaktywna.' + }), 404 + + # Check access: admin can audit any company, member only their own + if not current_user.is_admin: + # Check if user is associated with this company + if current_user.company_id != company.id: + return jsonify({ + 'success': False, + 'error': 'Brak uprawnień. Możesz audytować tylko własną firmę.' + }), 403 + + logger.info(f"GBP audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})") + + # Option to fetch fresh Google data before audit + fetch_google = data.get('fetch_google', True) + force_refresh = data.get('force_refresh', False) + + try: + # Step 1: Fetch fresh Google Business data (if enabled) + fetch_result = None + if fetch_google: + logger.info(f"Fetching Google Business data for company {company.id}...") + fetch_result = gbp_fetch_google_data(db, company.id, force_refresh=force_refresh) + if not fetch_result.get('success') and not fetch_result.get('data', {}).get('cached'): + # Log warning but continue with audit + logger.warning(f"Google fetch warning for company {company.id}: {fetch_result.get('error')}") + + # Step 2: Run the audit + result = gbp_audit_company(db, company.id, save=save_result) + + # Build field status for response + fields_response = {} + for field_name, field_status in result.fields.items(): + fields_response[field_name] = { + 'status': field_status.status, + 'value': str(field_status.value) if field_status.value is not None else None, + 'score': field_status.score, + 'max_score': field_status.max_score, + 'recommendation': field_status.recommendation + } + + # Determine score category + score = result.completeness_score + if score >= 90: + score_category = 'excellent' + elif score >= 70: + score_category = 'good' + elif score >= 50: + score_category = 'needs_work' + else: + score_category = 'poor' + + response_data = { + 'success': True, + 'message': f'Audyt GBP dla firmy "{company.name}" został zakończony pomyślnie.', + 'company_id': company.id, + 'company_name': company.name, + 'company_slug': company.slug, + 'audit_version': GBP_AUDIT_VERSION, + 'triggered_by': current_user.email, + 'triggered_at': datetime.now().isoformat(), + 'saved': save_result, + 'audit': { + 'completeness_score': result.completeness_score, + 'score_category': score_category, + 'fields_status': fields_response, + 'recommendations': result.recommendations, + 'photo_count': result.photo_count, + 'logo_present': result.logo_present, + 'cover_photo_present': result.cover_photo_present, + 'review_count': result.review_count, + 'average_rating': float(result.average_rating) if result.average_rating else None, + 'google_place_id': result.google_place_id + } + } + + # Include Google fetch results if performed + if fetch_result: + response_data['google_fetch'] = { + 'success': fetch_result.get('success', False), + 'steps': fetch_result.get('steps', []), + 'data': fetch_result.get('data', {}), + 'error': fetch_result.get('error') + } + + return jsonify(response_data), 200 + + except ValueError as e: + return jsonify({ + 'success': False, + 'error': str(e), + 'company_id': company.id if company else None + }), 400 + except Exception as e: + logger.error(f"GBP audit error for company {company.id}: {e}") + return jsonify({ + 'success': False, + 'error': f'Błąd podczas wykonywania audytu: {str(e)}', + 'company_id': company.id, + 'company_name': company.name + }), 500 + + finally: + db.close() diff --git a/blueprints/api/routes_social_audit.py b/blueprints/api/routes_social_audit.py new file mode 100644 index 0000000..8fadfee --- /dev/null +++ b/blueprints/api/routes_social_audit.py @@ -0,0 +1,171 @@ +""" +Social Audit API Routes - API blueprint + +Migrated from app.py as part of the blueprint refactoring. +Contains API routes for Social Media audit functionality. +""" + +import logging +import sys +from pathlib import Path + +from flask import jsonify, request +from flask_login import current_user, login_required + +from database import SessionLocal, Company +from . import bp + +logger = logging.getLogger(__name__) + + +# ============================================================ +# SOCIAL MEDIA AUDIT API ROUTES +# ============================================================ + +@bp.route('/social/audit', methods=['POST']) +@login_required +def api_social_audit_trigger(): + """ + API: Trigger Social Media audit for a company. + + This endpoint performs a comprehensive social media audit: + - Scans company website for social media links + - Searches for profiles via Brave Search API (if configured) + - Fetches Google Business Profile data + - Updates database with discovered profiles + + Request JSON body: + - company_id: Company ID (integer) OR + - slug: Company slug (string) + + Returns: + - Success: Updated social media audit results + - Error: Error message with status code + + Rate limited to 10 requests per hour per user. + """ + # Import the SocialMediaAuditor from scripts + try: + scripts_dir = Path(__file__).parent.parent.parent / 'scripts' + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from social_media_audit import SocialMediaAuditor + except ImportError as e: + logger.error(f"Failed to import SocialMediaAuditor: {e}") + return jsonify({ + 'success': False, + 'error': 'Usługa audytu Social Media jest niedostępna. Sprawdź konfigurację serwera.' + }), 503 + + # Parse request data + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': 'Brak danych w żądaniu. Podaj company_id lub slug.' + }), 400 + + company_id = data.get('company_id') + slug = data.get('slug') + + if not company_id and not slug: + return jsonify({ + 'success': False, + 'error': 'Podaj company_id lub slug firmy do audytu.' + }), 400 + + db = SessionLocal() + try: + # Find company by ID or slug + if company_id: + company = db.query(Company).filter_by(id=company_id, status='active').first() + else: + company = db.query(Company).filter_by(slug=slug, status='active').first() + + if not company: + return jsonify({ + 'success': False, + 'error': 'Firma nie znaleziona lub nieaktywna.' + }), 404 + + # Access control - admin can audit all, users only their company + if not current_user.is_admin: + if current_user.company_id != company.id: + return jsonify({ + 'success': False, + 'error': 'Brak uprawnień do audytu social media tej firmy.' + }), 403 + + logger.info(f"Social Media audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})") + + # Prepare company dict for auditor + company_dict = { + 'id': company.id, + 'name': company.name, + 'slug': company.slug, + 'website': company.website, + 'address_city': company.address_city or 'Wejherowo' + } + + # Initialize auditor and run audit + try: + auditor = SocialMediaAuditor() + audit_result = auditor.audit_company(company_dict) + + # Check for errors + if audit_result.get('errors') and not audit_result.get('social_media') and not audit_result.get('website'): + return jsonify({ + 'success': False, + 'error': f'Audyt nie powiódł się: {", ".join(audit_result["errors"][:3])}', + 'company_id': company.id, + 'company_name': company.name + }), 422 + + # Save result to database + saved = auditor.save_audit_result(audit_result) + + if not saved: + return jsonify({ + 'success': False, + 'error': 'Audyt został wykonany, ale nie udało się zapisać wyników do bazy danych.', + 'company_id': company.id, + 'company_name': company.name + }), 500 + + # Get count of social media profiles found + social_media_found = audit_result.get('social_media', {}) + platforms_count = len(social_media_found) + + # Calculate score + all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok'] + score = int((platforms_count / len(all_platforms)) * 100) + + return jsonify({ + 'success': True, + 'message': f'Audyt Social Media zakończony. Znaleziono {platforms_count} profili.', + 'company_id': company.id, + 'company_name': company.name, + 'profiles_found': platforms_count, + 'platforms': list(social_media_found.keys()), + 'score': score, + 'google_reviews': audit_result.get('google_reviews', {}), + 'errors': audit_result.get('errors') if audit_result.get('errors') else None + }), 200 + + except Exception as e: + logger.error(f"Social Media audit error for company {company.id}: {e}") + return jsonify({ + 'success': False, + 'error': f'Błąd podczas audytu: {str(e)}' + }), 500 + + except Exception as e: + logger.error(f"Social Media audit error for company {slug or company_id}: {e}") + db.rollback() + return jsonify({ + 'success': False, + 'error': f'Błąd podczas audytu: {str(e)}' + }), 500 + + finally: + db.close()