| """ |
| Profile Manager - Save/Load/Update Business Profiles |
| |
| Handles persistence of business profiles to JSON files, |
| enabling manual editing and incremental updates. |
| """ |
| import os |
| import json |
| import shutil |
| from pathlib import Path |
| from datetime import datetime |
| from typing import Optional, Dict, Any, List |
|
|
| from backend.utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| |
| PROFILES_DIR = Path(__file__).parent.parent.parent / "storage" / "profiles" |
|
|
|
|
| class ProfileManager: |
| """ |
| Manages business profile persistence. |
| |
| Features: |
| - Save profiles as JSON files |
| - Load profiles by job_id |
| - Update individual services within a profile |
| - List all saved profiles |
| - Export profile for download |
| """ |
| |
| def __init__(self, storage_dir: Optional[str] = None): |
| self.storage_dir = Path(storage_dir) if storage_dir else PROFILES_DIR |
| self.storage_dir.mkdir(parents=True, exist_ok=True) |
| logger.info(f"ProfileManager initialized: {self.storage_dir}") |
| |
| def save_profile(self, job_id: str, profile_data: Dict[str, Any]) -> str: |
| """ |
| Save a business profile to JSON file. |
| |
| Args: |
| job_id: Unique job identifier |
| profile_data: Profile dictionary |
| |
| Returns: |
| Path to saved file |
| """ |
| filepath = self.storage_dir / f"{job_id}_profile.json" |
| |
| |
| profile_data['_metadata'] = { |
| 'saved_at': datetime.now().isoformat(), |
| 'job_id': job_id, |
| 'version': profile_data.get('_metadata', {}).get('version', 0) + 1, |
| 'last_edited_by': 'manual_ui' |
| } |
| |
| |
| if filepath.exists(): |
| backup_path = self.storage_dir / f"{job_id}_profile.backup.json" |
| shutil.copy2(filepath, backup_path) |
| logger.info(f"Created backup: {backup_path}") |
| |
| with open(filepath, 'w', encoding='utf-8') as f: |
| json.dump(profile_data, f, indent=2, ensure_ascii=False, default=str) |
| |
| logger.info(f"Profile saved: {filepath}") |
| return str(filepath) |
| |
| def load_profile(self, job_id: str) -> Optional[Dict[str, Any]]: |
| """ |
| Load a business profile from JSON file. |
| |
| Args: |
| job_id: Unique job identifier |
| |
| Returns: |
| Profile dictionary or None |
| """ |
| filepath = self.storage_dir / f"{job_id}_profile.json" |
| |
| if not filepath.exists(): |
| logger.warning(f"Profile not found: {filepath}") |
| return None |
| |
| with open(filepath, 'r', encoding='utf-8') as f: |
| profile_data = json.load(f) |
| |
| logger.info(f"Profile loaded: {filepath}") |
| return profile_data |
| |
| def update_service(self, job_id: str, service_index: int, service_data: Dict[str, Any]) -> bool: |
| """ |
| Update a specific service in the profile. |
| |
| Args: |
| job_id: Unique job identifier |
| service_index: Index of service to update |
| service_data: Updated service data |
| |
| Returns: |
| True if successful |
| """ |
| profile = self.load_profile(job_id) |
| if not profile: |
| logger.error(f"Cannot update service: profile not found for {job_id}") |
| return False |
| |
| services = profile.get('services', []) |
| if service_index >= len(services): |
| logger.error(f"Service index {service_index} out of range (total: {len(services)})") |
| return False |
| |
| services[service_index] = service_data |
| profile['services'] = services |
| |
| self.save_profile(job_id, profile) |
| logger.info(f"Service {service_index} updated for job {job_id}") |
| return True |
| |
| def add_service(self, job_id: str, service_data: Dict[str, Any]) -> bool: |
| """ |
| Add a new service to the profile. |
| |
| Args: |
| job_id: Unique job identifier |
| service_data: New service data |
| |
| Returns: |
| True if successful |
| """ |
| profile = self.load_profile(job_id) |
| if not profile: |
| logger.error(f"Cannot add service: profile not found for {job_id}") |
| return False |
| |
| services = profile.get('services', []) |
| service_data['service_id'] = f"svc_{len(services)}" |
| services.append(service_data) |
| profile['services'] = services |
| |
| self.save_profile(job_id, profile) |
| logger.info(f"New service added to job {job_id} (total: {len(services)})") |
| return True |
| |
| def delete_service(self, job_id: str, service_index: int) -> bool: |
| """ |
| Delete a service from the profile. |
| |
| Args: |
| job_id: Unique job identifier |
| service_index: Index of service to delete |
| |
| Returns: |
| True if successful |
| """ |
| profile = self.load_profile(job_id) |
| if not profile: |
| return False |
| |
| services = profile.get('services', []) |
| if service_index >= len(services): |
| return False |
| |
| removed = services.pop(service_index) |
| |
| |
| for i, svc in enumerate(services): |
| svc['service_id'] = f"svc_{i}" |
| |
| profile['services'] = services |
| self.save_profile(job_id, profile) |
| logger.info(f"Service '{removed.get('name', 'unknown')}' deleted from job {job_id}") |
| return True |
| |
| def list_profiles(self) -> List[Dict[str, Any]]: |
| """ |
| List all saved profiles. |
| |
| Returns: |
| List of profile summaries |
| """ |
| profiles = [] |
| for filepath in self.storage_dir.glob("*_profile.json"): |
| if filepath.name.endswith('.backup.json'): |
| continue |
| |
| try: |
| with open(filepath, 'r', encoding='utf-8') as f: |
| data = json.load(f) |
| |
| profiles.append({ |
| 'job_id': data.get('_metadata', {}).get('job_id', filepath.stem.replace('_profile', '')), |
| 'business_name': data.get('business_info', {}).get('name', 'Unknown'), |
| 'business_type': data.get('business_type', 'unknown'), |
| 'services_count': len(data.get('services', [])), |
| 'products_count': len(data.get('products', [])), |
| 'saved_at': data.get('_metadata', {}).get('saved_at', 'Unknown'), |
| 'filepath': str(filepath) |
| }) |
| except Exception as e: |
| logger.warning(f"Failed to read profile {filepath}: {e}") |
| |
| return sorted(profiles, key=lambda x: x.get('saved_at', ''), reverse=True) |
| |
| def export_profile(self, job_id: str) -> Optional[str]: |
| """ |
| Export profile as formatted JSON string (for download). |
| |
| Args: |
| job_id: Unique job identifier |
| |
| Returns: |
| JSON string or None |
| """ |
| profile = self.load_profile(job_id) |
| if not profile: |
| return None |
| |
| |
| export_data = {k: v for k, v in profile.items() if not k.startswith('_')} |
| |
| return json.dumps(export_data, indent=2, ensure_ascii=False, default=str) |
| |
| def calculate_completeness(self, profile: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Calculate profile completeness scores. |
| |
| Args: |
| profile: Profile dictionary |
| |
| Returns: |
| Completeness analysis |
| """ |
| scores = {} |
| |
| |
| bi = profile.get('business_info', {}) |
| bi_fields = ['name', 'description', 'category'] |
| bi_contact = bi.get('contact', {}) |
| bi_filled = sum(1 for f in bi_fields if bi.get(f)) |
| bi_filled += sum(1 for f in ['phone', 'email', 'website'] if bi_contact.get(f)) |
| scores['business_info'] = bi_filled / 6 |
| |
| |
| services = profile.get('services', []) |
| if services: |
| svc_scores = [] |
| for svc in services: |
| svc_score = 0 |
| total_fields = 13 |
| |
| if svc.get('name'): svc_score += 1 |
| if svc.get('description') and len(svc.get('description', '')) > 20: svc_score += 1 |
| if svc.get('category'): svc_score += 1 |
| if svc.get('pricing') and svc['pricing'].get('base_price'): svc_score += 1 |
| if svc.get('details') and svc['details'].get('duration'): svc_score += 1 |
| if svc.get('itinerary') and len(svc.get('itinerary', [])) > 0: svc_score += 1 |
| if svc.get('inclusions') and len(svc.get('inclusions', [])) > 0: svc_score += 1 |
| if svc.get('exclusions') and len(svc.get('exclusions', [])) > 0: svc_score += 1 |
| if svc.get('cancellation_policy'): svc_score += 1 |
| if svc.get('payment_policy'): svc_score += 1 |
| if svc.get('travel_info'): svc_score += 1 |
| if svc.get('faqs') and len(svc.get('faqs', [])) > 0: svc_score += 1 |
| if svc.get('tags') and len(svc.get('tags', [])) > 1: svc_score += 1 |
| |
| svc_scores.append(svc_score / total_fields) |
| |
| scores['services'] = sum(svc_scores) / len(svc_scores) |
| scores['services_detail'] = svc_scores |
| else: |
| scores['services'] = 0.0 |
| |
| |
| scores['overall'] = (scores['business_info'] * 0.3 + scores.get('services', 0) * 0.7) |
| |
| return scores |
|
|