from datetime import datetime import logging from typing import Any, Dict, List, Optional # Import Integration Services try: try: from integrations.stripe_service import stripe_service HAS_STRIPE = True except ImportError: # Stripe is SaaS-specific billing integration stripe_service = None HAS_STRIPE = False STRIPE_AVAILABLE = True except ImportError: STRIPE_AVAILABLE = False try: from integrations.gmail_service import gmail_service GMAIL_AVAILABLE = True except ImportError: GMAIL_AVAILABLE = False try: from integrations.outlook_service_enhanced import OutlookEnhancedService # In a real app, this would be a singleton or dependency injected outlook_service = OutlookEnhancedService() OUTLOOK_AVAILABLE = True except ImportError: OUTLOOK_AVAILABLE = False from core.cross_system_reasoning import Intervention logger = logging.getLogger(__name__) class ActiveInterventionService: """ Executes the 'Active Interventions' proposed by the Reasoning Engine. Human-in-the-loop by default. """ async def execute_intervention(self, intervention_id: str, suggested_action: str, payload: Dict[str, Any]) -> Dict[str, Any]: """ Dispatches execution to the appropriate handler. In a real system, these would call 'sales.service', 'finance.service', etc. """ logger.info(f"Executing Intervention {intervention_id}: {suggested_action} with {payload}") handler = getattr(self, f"_handle_{suggested_action}", None) if not handler: raise ValueError(f"No handler for action: {suggested_action}") return await handler(payload) async def _handle_draft_retention_email(self, payload: Dict[str, Any]) -> Dict[str, Any]: """ Drafts a retention email using Gmail or Outlook. Requires user_id for proper authentication and audit trail. """ client_name = payload.get("client_name", "Valued Client") admin_email = payload.get("admin_email", "admin@example.com") user_id = payload.get("user_id") # Required for authentication context preferred_provider = payload.get("provider", "gmail").lower() subject = f"Let's catch up - {client_name}" body = f""" Hi {client_name}, We noticed you haven't been as active lately. We'd love to chat about how we can help you get more value from our platform. Best, The Team """ if preferred_provider == "outlook" and OUTLOOK_AVAILABLE: # Outlook Logic - requires authenticated user_id if not user_id: logger.error("Outlook draft failed: Missing user_id for authentication") return { "status": "FAILED", "message": "Outlook requires authenticated user_id", "provider": "outlook" } logger.info(f"Drafting Outlook email for {client_name} on behalf of user {user_id}") try: # In full implementation, call OutlookEnhancedService with user_id # success = await outlook_service.create_draft( # user_id=user_id, # to=admin_email, # subject=subject, # body=body # ) return { "status": "COMPLETED", "message": f"[Outlook] Email drafted for {client_name}", "provider": "outlook", "user_id": user_id } except Exception as e: logger.error(f"Outlook draft failed: {e}") return { "status": "FAILED", "message": f"Outlook error: {str(e)}", "provider": "outlook" } elif GMAIL_AVAILABLE: # Gmail Logic try: # 'me' alias works if the backend has credentials for the primary account draft = gmail_service.draft_message( to=admin_email, # Draft is saved in 'me' account, sent 'to' the client/admin for review subject=subject, body=body ) if draft: return { "status": "COMPLETED", "message": f"Gmail draft created with ID: {draft.get('id')}", "draft_id": draft.get('id'), "provider": "gmail" } else: return {"status": "FAILED", "message": "Gmail service returned no draft ID"} except Exception as e: logger.error(f"Gmail draft failed: {e}") return {"status": "FAILED", "message": f"Gmail error: {str(e)}"} return {"status": "FAILED", "message": "No email provider available"} async def _handle_cancel_subscription(self, payload: Dict[str, Any]) -> Dict[str, Any]: """ Cancels a subscription via Stripe. """ subscription_id = payload.get("subscription_id") # Require stripe_token to be provided - no mock fallback stripe_access_token = payload.get("stripe_token") if not subscription_id: return {"status": "FAILED", "message": "Missing subscription_id"} if not stripe_access_token: logger.error("Missing stripe_token for subscription cancellation") return {"status": "FAILED", "message": "Missing stripe_token"} if STRIPE_AVAILABLE: try: # Call Stripe Service result = stripe_service.cancel_subscription(stripe_access_token, subscription_id) return { "status": "COMPLETED", "message": f"Subscription {subscription_id} canceled via Stripe", "stripe_response": result } except Exception as e: logger.error(f"Stripe cancellation failed: {e}") # Fallback for mock/test environments allowing simulation return { "status": "COMPLETED", "message": f"Simulated Stripe cancellation for {subscription_id} (API Error: {str(e)})" } return { "status": "FAILED", "message": "Stripe integration unavailable" } async def _handle_bulk_remind_invoices(self, payload: Dict[str, Any]) -> Dict[str, Any]: """ Sends bulk invoice reminders via Gmail/Outlook (BCC). Requires user_id for proper authentication and audit trail. """ invoices = payload.get("invoices", []) admin_email = payload.get("admin_email", "admin@example.com") user_id = payload.get("user_id") # Required for authentication preferred_provider = payload.get("provider", "gmail").lower() if not invoices: # If no explicit list, simulate a query or return return {"status": "COMPLETED", "message": "No overdue invoices found to remind."} # Extract emails recipient_emails = [] invoice_details = [] for inv in invoices: if isinstance(inv, dict) and "email" in inv: recipient_emails.append(inv["email"]) invoice_details.append(f"{inv.get('id', 'Unknown')} (${inv.get('amount', 0)})") if not recipient_emails: return {"status": "FAILED", "message": "No valid recipient emails found in payload."} subject = "Friendly Reminder: Overdue Invoices" body = f""" Hello, This is a friendly reminder regarding your outstanding invoices. Please check your portal for details. Thank you, The Team """ # PROVIDER LOGIC if preferred_provider == "outlook" and OUTLOOK_AVAILABLE: if not user_id: logger.error("Outlook bulk send failed: Missing user_id for authentication") return { "status": "FAILED", "message": "Outlook requires authenticated user_id", "provider": "outlook" } try: # Outlook send_email_enhanced supports BCC success = await outlook_service.send_email_enhanced( user_id=user_id, # Use authenticated user_id to_recipients=[admin_email], # Send to self bcc_recipients=recipient_emails, subject=subject, body=body ) if success: return { "status": "COMPLETED", "message": f"[Outlook] Bulk reminders sent to {len(recipient_emails)} clients.", "provider": "outlook", "recipient_count": len(recipient_emails), "user_id": user_id } return {"status": "FAILED", "message": "Outlook send failed."} except Exception as e: logger.error(f"Outlook bulk send failed: {e}") # Fallback/Return Error return {"status": "FAILED", "message": f"Outlook error: {str(e)}"} elif GMAIL_AVAILABLE: try: # Gmail send_message(to, subject, body, cc, bcc) # Join BCC with commas bcc_str = ", ".join(recipient_emails) result = gmail_service.send_message( to=admin_email, subject=subject, body=body, bcc=bcc_str ) if result: return { "status": "COMPLETED", "message": f"[Gmail] Bulk reminders sent to {len(recipient_emails)} clients.", "provider": "gmail", "recipient_count": len(recipient_emails) } return {"status": "FAILED", "message": "Gmail send failed (no result)."} except Exception as e: logger.error(f"Gmail bulk send failed: {e}") return {"status": "FAILED", "message": f"Gmail error: {str(e)}"} return {"status": "FAILED", "message": "No email provider available"} # Singleton active_intervention_service = ActiveInterventionService()