ReceptionistAI SMS Follow-up System

Response:
```python
# RECEPTIONISTAI/api/sms_followup.py
"""
SMS Follow-up System for ReceptionistAI
Handles automated SMS communications for Australian businesses including:
- Missed call follow-ups with Australian timezone support
- Appointment confirmations and reminders
- Template-based personalization
- Opt-out compliance (STOP keyword handling)
- Delivery tracking via Telnyx webhooks
"""

import asyncio
import json
import logging
import re
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional, List, Dict, Any, Set
from zoneinfo import ZoneInfo

import httpx
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Request, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jinja2 import Template, Environment, BaseLoader, UndefinedError
from pydantic import BaseModel, Field, validator, constr
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, Enum as SQLEnum, select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.dialects.postgresql import UUID, JSONB
import uuid

# Configuration
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/receptionistai"
TELNYX_API_KEY = "YOUR_TELNYX_API_KEY"
TELNYX_PHONE_NUMBER = "+61412345678"  # Must be Telnyx Australian number
WEBHOOK_BASE_URL = "https://api.receptionistai.com.au/webhooks"
MAX_SMS_LENGTH = 1600  # Telnyx supports concatenated messages
MAX_RETRIES = 3
RETRY_DELAY_SECONDS = 300  # 5 minutes

# Australian Timezone Mapping
AUSTRALIAN_TIMEZONES: Dict[str, str] = {
    'NSW': 'Australia/Sydney',      # AEST/AEDT (UTC+10/+11)
    'VIC': 'Australia/Melbourne',   # AEST/AEDT
    'QLD': 'Australia/Brisbane',    # AEST (UTC+10)
    'SA': 'Australia/Adelaide',     # ACST/ACDT (UTC+9:30/+10:30)
    'NT': 'Australia/Darwin',       # ACST (UTC+9:30)
    'WA': 'Australia/Perth',        # AWST (UTC+8)
    'TAS': 'Australia/Hobart',      # AEST/AEDT
    'ACT': 'Australia/Canberra',    # AEST/AEDT
}

# Opt-out keywords (case-insensitive)
OPT_OUT_KEYWORDS: Set[str] = {"STOP", "UNSUBSCRIBE", "CANCEL", "OPT OUT", "OPT-OUT", "END", "QUIT"}

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Database setup
Base = declarative_base()
engine = create_async_engine(DATABASE_URL, echo=False, future=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

# Pydantic Types
class AustralianPhoneNumber(str):
    """Validator for Australian phone numbers in E.164 format"""

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v: str) -> str:
        if not v:
            raise ValueError("Phone number cannot be empty")

        # Remove whitespace, dashes, parentheses, and dots
        cleaned = re.sub(r'[\s\-\(\)\.]', '', v)

        # Convert local format to E.164
        if cleaned.startswith('0'):
            cleaned = '+61' + cleaned[1:]
        elif cleaned.startswith('61'):
            cleaned = '+' + cleaned
        elif not cleaned.startswith('+61'):
            raise ValueError("Australian number must start with +61, 61, or 0")

        # Validate length (+61 + 9 digits = 12 chars total)
        if len(cleaned) != 12:
            raise ValueError(f"Invalid Australian phone number length: {len(cleaned)} characters")

        # Validate area code (2,3,7,8 for landline, 4 for mobile)
        area_code = cleaned[3]
        if area_code == '4':
            # Mobile number
            pass
        elif area_code in '2378':
            # Landline
            pass
        else:
            raise ValueError(f"Invalid Australian area code: {area_code}")

        return cleaned

    @classmethod
    def format_national(cls, number: str) -> str:
        """Format as (0x) xxxx xxxx for display"""
        num = cls.validate(number)
        local = '0' + num[3:]  # Remove +61, add 0
        if len(local) == 10:  # Mobile or landline
            return f"{local[:4]} {local[4:7]} {local[7:]}"
        return local

# Enums
class SMSStatus(str, Enum):
    PENDING = "pending"
    QUEUED = "queued"
    SENDING = "sending"
    SENT = "sent"
    DELIVERED = "delivered"
    FAILED = "failed"
    OPTED_OUT = "opted_out"
    PERMANENT_FAILURE = "permanent_failure"

class MessageType(str, Enum):
    MISSED_CALL = "missed_call"
    APPOINTMENT_CONFIRMATION = "appointment_confirmation"
    APPOINTMENT_REMINDER = "appointment_reminder"
    FOLLOW_UP = "follow_up"
    CUSTOM = "custom"

# Pydantic Models
class SMSTemplateCreate(BaseModel):
    name: constr(min_length=1, max_length=100)
    content: constr(min_length=1, max_length=1600)
    message_type: MessageType
    business_id: uuid.UUID
    description: Optional[str] = None

class SMSRequest(BaseModel):
    to_number: AustralianPhoneNumber
    template_name: Optional[str] = None
    custom_message: Optional[constr(min_length=1, max_length=1600)] = None


ReceptionistAI Competitor Feature Matrix


ReceptionistAI Call Transfer Handler


ReceptionistAI Post-Call Processing Pipeline

Response:
```python
"""
RECEPTIONISTAI/api/post_call_pipeline.py

Post-call processing pipeline for ReceptionistAI.
Handles transcription, entity extraction, lead scoring, and notifications
for Australian business calls with proper timezone and phone number handling.
"""

import asyncio
import hashlib
import hmac
import logging
import tempfile
import uuid
from datetime import datetime, timedelta
from decimal import Decimal
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from zoneinfo import ZoneInfo

import httpx
import phonenumbers
from fastapi import (
    BackgroundTasks,
    Depends,
    FastAPI,
    Header,
    HTTPException,
    Request,
    status,
)
from pydantic import BaseModel, Field, validator, constr
from sqlalchemy import (
    Boolean,
    Column,
    DateTime,
    Float,
    ForeignKey,
    Integer,
    JSON,
    String,
    Text,
    select,
)
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker

# Configuration
class Config:
    DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost/receptionistai"
    TELNYX_API_KEY: str = "YOUR_TELNYX_API_KEY"
    TELNYX_WEBHOOK_SECRET: str = "YOUR_TELNYX_WEBHOOK_SECRET"
    OPENAI_API_KEY: str = "YOUR_OPENAI_API_KEY"

    # Timeouts and retries
    WEBHOOK_TIMEOUT: int = 30
    MAX_RETRY_ATTEMPTS: int = 3
    TRANSCRIPTION_TIMEOUT: int = 120

    # Australian timezone mapping (state/territory codes)
    AU_TIMEZONES: Dict[str, str] = {
        "NSW": "Australia/Sydney",
        "VIC": "Australia/Melbourne",
        "QLD": "Australia/Brisbane",  # No daylight saving
        "WA": "Australia/Perth",      # UTC+8
        "SA": "Australia/Adelaide",   # UTC+9:30
        "TAS": "Australia/Hobart",
        "ACT": "Australia/Canberra",
        "NT": "Australia/Darwin",     # UTC+9:30, no daylight saving
    }

config = Config()

# Setup logging with correlation IDs
class CorrelationIdFilter(logging.Filter):
    def filter(self, record):
        record.correlation_id = getattr(record, "correlation_id", "unknown")
        return True

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - [%(correlation_id)s] - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
logger.addFilter(CorrelationIdFilter())

# Database Models
Base = declarative_base()

class ProcessingStatus(str, Enum):
    PENDING = "pending"
    TRANSCRIBING = "transcribing"
    EXTRACTING = "extracting"
    SCORING = "scoring"
    NOTIFYING = "notifying"
    COMPLETED = "completed"
    FAILED = "failed"

class UrgencyLevel(str, Enum):
    CRITICAL = "critical"  # Emergency, legal threats, safety issues
    HIGH = "high"          # Hot lead, complaint, urgent booking
    MEDIUM = "medium"      # General inquiry
    LOW = "low"            # Spam, wrong number

class IntentType(str, Enum):
    SALES_INQUIRY = "sales_inquiry"
    SUPPORT_REQUEST = "support_request"
    BOOKING_APPOINTMENT = "booking_appointment"
    COMPLAINT = "complaint"
    BILLING_QUERY = "billing_query"
    JOB_APPLICATION = "job_application"
    GENERAL = "general"

class CallRecord(Base):
    __tablename__ = "call_records"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    telnyx_call_control_id = Column(String(100), unique=True, index=True, nullable=False)
    business_id = Column(String(36), index=True, nullable=False)

    # Caller Information (Australian format)
    caller_number_raw = Column(String(30))
    caller_number_e164 = Column(String(20), index=True)  # +61 format
    caller_state = Column(String(3))  # For timezone determination

    # Call Metadata
    recording_url = Column(String(500))
    recording_duration_sec = Column(Integer)
    direction = Column(String(10))  # inbound/outbound
    started_at = Column(DateTime(timezone=True))
    ended_at = Column(DateTime(timezone=True))

    # Processing Status
    status = Column(String(20), default=ProcessingStatus.PENDING)
    correlation_id = Column(String(36), index=True)
    error_message = Column(Text)
    retry_count = Column(Integer, default=0)
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("UTC")))
    updated_at = Column(DateTime(timezone=True), onupdate=lambda: datetime.now(ZoneInfo("UTC")))

    # Relationships
    transcript = relationship("CallTranscript", back_populates="call", uselist=False)
    entities = relationship("ExtractedEntity", back_populates="call", uselist=False)
    lead_score = relationship("LeadScore", back_populates="call", uselist=False)
    webhooks = relationship("WebhookDelivery", back_populates="call")

class CallTranscript(Base):
    __tablename__ = "call_transcripts"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    call_id = Column(String(36), ForeignKey("call_records.id"), unique=True)
    transcript_text = Column(Text, nullable=False)
    confidence_score = Column(Float)
    language_code = Column(String(10), default="en-AU")
    word_count = Column(Integer)
    speaker_segments = Column(JSON)  # Diarization data if available

    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("UTC")))

    call = relationship("CallRecord", back_populates="transcript")

class ExtractedEntity(Base):
    __tablename__ = "extracted_entities"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    call_id = Column(String(36), ForeignKey("call_records.id"), unique=True)

    # Contact Details
    caller_name = Column(String(200))
    caller_email = Column(String(200))
    company_name = Column(String(200))
    abn_acn = Column(String(20))  # Australian Business Number / Company Number

    # Classification
    intent = Column(String(50))
    urgency = Column(String(20))
    sentiment_score = Column(Float)  # -1.0 to 1.0

    # Content
    summary = Column(Text)
    action_items = Column(JSON)
    key_topics = Column(JSON)

    # Raw data
    raw_extraction = Column(JSON)
    extracted_at = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("UTC")))

    call = relationship("CallRecord", back_populates="entities")

class LeadScore(Base):
    __tablename__ = "lead_scores"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    call_id = Column(String(36), ForeignKey("call_records.id"), unique=True)

    score = Column(Float, nullable=False)  # 0-100
    tier = Column(String(20))  # hot, warm, cold, unqualified
    conversion_probability = Column(Float)

    # Scoring factors
    factors = Column(JSON)  # Detailed breakdown
    model_version = Column(String(20))

    calculated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("UTC")))

    call = relationship("CallRecord", back_populates="lead_score")

class WebhookDelivery(Base):
    __tablename__


Genesis Webhook Registry


Genesis API Key Management

Response:
```python
"""
Core API Key Management System for ReceptionistAI
Production-ready implementation with PostgreSQL, async support, and Australian timezone handling.
"""

import hashlib
import secrets
import logging
from datetime import datetime, timedelta, date
from typing import Optional, List, Dict, Any, Tuple, Set
from enum import Enum as PyEnum
from dataclasses import dataclass
import asyncio

from sqlalchemy import (
    select, insert, update, delete, func, and_, or_, text, String, Boolean,
    DateTime, Date, Integer, ForeignKey, Column, Index, UniqueConstraint, JSON
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import declarative_base, relationship, selectinload
from sqlalchemy.dialects.postgresql import UUID, ARRAY, ENUM, JSONB
import uuid
from pydantic import BaseModel, Field, validator, constr
import pytz

# Configure logging
logger = logging.getLogger(__name__)

# Australian Timezones
AU_TIMEZONES = {
    'AEST': 'Australia/Brisbane',  # No DST
    'AEDT': 'Australia/Sydney',    # NSW/VIC/TAS with DST
    'ACST': 'Australia/Adelaide',  # SA with DST
    'ACDT': 'Australia/Adelaide',
    'AWST': 'Australia/Perth',     # No DST
}

# SQLAlchemy Base
Base = declarative_base()

class ApiKeyScope(PyEnum):
    READ = "read"
    WRITE = "write"
    ADMIN = "admin"

class ApiKeyStatus(PyEnum):
    ACTIVE = "active"
    DEPRECATED = "deprecated"  # During rotation grace period
    REVOKED = "revoked"
    EXPIRED = "expired"

class ApiKeyRateLimit(Base):
    """Rate limiting tracking per key - sliding window implementation"""
    __tablename__ = "api_key_rate_limits"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    key_id = Column(UUID(as_uuid=True), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False, index=True)
    window_start = Column(DateTime(timezone=True), nullable=False, index=True)
    request_count = Column(Integer, default=0, nullable=False)
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC))

    __table_args__ = (
        Index('idx_rate_limit_key_window', 'key_id', 'window_start'),
    )

class ApiKeyUsage(Base):
    """Daily aggregated usage statistics"""
    __tablename__ = "api_key_usage"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    key_id = Column(UUID(as_uuid=True), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False)
    usage_date = Column(Date, nullable=False, index=True)
    request_count = Column(Integer, default=0, nullable=False)
    endpoint_breakdown = Column(JSONB, default=dict)  # {"/api/v1/calls": 150, ...}
    error_count = Column(Integer, default=0)
    last_used_at = Column(DateTime(timezone=True))

    __table_args__ = (
        UniqueConstraint('key_id', 'usage_date', name='unique_key_daily_usage'),
        Index('idx_usage_date_key', 'usage_date', 'key_id'),
    )

class ApiKeyAuditLog(Base):
    """Comprehensive audit trail for compliance"""
    __tablename__ = "api_key_audit_log"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    key_id = Column(UUID(as_uuid=True), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False, index=True)
    timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC), index=True)
    action = Column(String(50), nullable=False)  # 'authenticate', 'rotate', 'revoke', 'usage_denied'
    ip_address = Column(String(45))  # IPv6 compatible
    user_agent = Column(String(255))
    success = Column(Boolean, nullable=False)
    details = Column(JSONB)  # Flexible metadata
    australian_timezone = Column(String(10), default='AEST')  # For audit localization

    __table_args__ = (
        Index('idx_audit_timestamp', 'timestamp', 'key_id'),
    )

class ApiKey(Base):
    """Core API Key entity"""
    __tablename__ = "api_keys"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    business_id = Column(UUID(as_uuid=True), nullable=False, index=True)  # Tenant isolation
    key_prefix = Column(String(12), nullable=False, index=True)  # First 8 chars of key for identification
    key_hash = Column(String(64), nullable=False, unique=True, index=True)  # SHA-256 hash
    scopes = Column(ARRAY(String), default=list, nullable=False)  # ['read', 'write']
    status = Column(ENUM(ApiKeyStatus), default=ApiKeyStatus.ACTIVE, nullable=False, index=True)

    # Rate limiting config
    rate_limit_rpm = Column(Integer, default=60)  # Requests per minute

    # Rotation tracking
    rotated_from_key_id = Column(UUID(as_uuid=True), ForeignKey("api_keys.id"), nullable=True)
    rotated_at = Column(DateTime(timezone=True))
    deprecation_deadline = Column(DateTime(timezone=True))  # When deprecated key expires

    # Lifecycle
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(pytz.UTC))
    expires_at = Column(DateTime(timezone=True), nullable=True)
    last_used_at = Column(DateTime(timezone=True))

    # Relationships
    rotated_from = relationship("ApiKey", remote_side=[id], foreign_keys=[rotated_from_key_id])
    usage_records = relationship("ApiKeyUsage", backref="api_key", cascade="all, delete-orphan")
    audit_logs = relationship("ApiKeyAuditLog", backref="api_key", cascade="all, delete-orphan")
    rate_limit_records = relationship("ApiKeyRateLimit", backref="api_key", cascade="all, delete-orphan")

    __table_args__ = (
        Index('idx_business_status', 'business_id', 'status'),
        Index('idx_expiry', 'expires_at', 'status'),
    )

# Pydantic Models for API Layer
class ApiKeyCreate(BaseModel):
    business_id: uuid.UUID
    scopes: List[ApiKeyScope] = Field(default=[ApiKeyScope.READ])
    expires_days: Optional[int] = Field(default=365, ge=1, le=1095)  # Max 3 years
    rate_limit_rpm: int = Field(default=60, ge=10, le=10000)
    description: Optional[str] = Field(default=None, max_length=255)

    @validator('scopes')
    def validate_scopes(cls, v):
        if ApiKeyScope.ADMIN in v and len(v) > 1:
            raise ValueError("Admin scope cannot be combined with other scopes")
        return v

class ApiKeyResponse(BaseModel):
    id: uuid.UUID
    key_prefix: str
    plaintext_key: str  # Only returned once during creation
    scopes: List[str]
    status: ApiKeyStatus
    expires_at: Optional[datetime]
    created_at: datetime
    rate_limit_rpm: int

    class Config:
        orm_mode = True

class ApiKeyValidationResult(BaseModel):
    valid: bool
    key_id: Optional[uuid.UUID]
    business_id: Optional[uuid.UUID]
    scopes: List[str]
    error_message: Optional[str] = None

class UsageStats(BaseModel):
    total_requests: int
    daily_breakdown: Dict[str, int]  # ISO date string -> count
    endpoints: Dict[str, int]
    error_rate: float

class KeyRotationResult(BaseModel):
    old_key_id: uuid.UUID
    new_key_id: uuid.UUID
    new_plaintext_key: str
    deprecation_deadline: datetime

# Custom Exceptions
class ApiKeyManagerError(Exception):
    pass

class RateLimitExceeded(ApiKeyManagerError):
    pass

class ApiKeyExpired(ApiKeyManagerError):
    pass

class ApiKeyRevoked(ApiKeyManagerError):
    pass

class ApiKeyNotFound(ApiKeyManagerError):
    pass

class InsufficientPermissions(ApiKeyManagerError):
    pass

class ApiKeyManager:
    """
    Production-grade API Key Manager with comprehensive security,
    Australian timezone support, and audit compliance.
    """

    KEY_PREFIX = "rak_"  # ReceptionistAI Key
    HASH_ALGORITHM = "sha256"
    RATE_LIMIT_WINDOW_MINUTES = 1

    def __init__(self, db_session: AsyncSession, redis_client: Optional[Any] = None):
        self.db = db_session
        self.redis = redis_client  # Optional Redis for high-performance rate limiting
        self._lock = asyncio.Lock()  # For DB-based rate limiting concurrency

    def _generate_plaintext_key(self) -> str:
        """Generate cryptographically secure API key"""
        # Format: rak_<32_char_random>
        random_component = secrets.token_urlsafe(32)
        return f"{self.KEY_PREFIX}{random_component}"

    def _hash_key(self, plaintext_key: str) -> str:
        """One-way hash for secure storage"""
        return hashlib.sha256(plaintext_key.encode()).hexdigest()

    def _get_key_prefix(self, plaintext_key: str) -> str:
        """Extract prefix for identification (first 12 chars after prefix)"""
        clean_key = plaintext_key.replace(self.KEY_PREFIX, "")
        return clean_key[:12]

    async def generate_key(
        self,
        business_id: uuid.UUID,
        scopes: List[ApiKeyScope],
        expires_days: int = 365,
        rate_limit_rpm: int = 60,
        description: Optional[str] = None,
        rotated_from_key_id: Optional[uuid.UUID] = None
    ) -> ApiKeyResponse:
        """
        Generate a new API key with specified scopes and configuration.

        Args:
            business_id: Tenant/business identifier
            scopes: List of permission scopes
            expires_days: Key validity period
            rate_limit_rpm: Requests per minute limit
            description: Optional metadata
            rotated_from_key_id: If rotating, the parent key ID

        Returns:
            ApiKeyResponse containing the plaintext key (shown only once)
        """
        try:
            plaintext_key = self._generate_plaintext_key()
            key_hash = self._hash_key(plaintext_key)
            prefix = self._get_key_prefix(plaintext_key)

            expires_at = datetime.now(pytz.UTC) + timedelta(days=expires_days)

            # Handle rotation metadata
            rotated_at = None
            deprecation_deadline = None
            if rotated_from_key_id:
                rotated_at = datetime.now(pytz.UTC)
                # Grace period: 7 days for rotation
                deprecation_deadline = rotated_at + timedelta(days=7)

            api_key = ApiKey(
                id=uuid.uuid4(),
                business_id=business_id,
                key_prefix=prefix,
                key_hash=key_hash,
                scopes=[s.value for s in scopes],
                status=ApiKeyStatus.ACTIVE,
                rate_limit_rpm=rate_limit_rpm,
                expires_at=expires_at,
                rotated_from_key_id=rotated_from_key_id,
                rotated_at=rotated_at,
                deprecation_deadline=deprecation_deadline,
            )

            self.db.add(api_key)
            await self.db.commit()
            await self.db.refresh(api_key)

            # Log creation audit
            await self._log_audit_event(
                key_id=api_key.id,
                action='create',
                success=True,
                details={
                    'scopes': [s.value for s in scopes],
                    'description': description,
                    'rotated_from': str(rotated_from_key_id) if rotated_from_key_id else None,
                    'expires_days': expires_days
                }
            )

            logger.info(f"Generated API key {api_key.id} for business {business_id}")

            return ApiKeyResponse(
                id=api_key.id,
                key_prefix=prefix,
                plaintext_key=plaintext_key,  # ONLY TIME THIS IS RETURNED
                scopes=api_key.scopes,
                status=api_key.status,
                expires_at=api_key.expires_at,
                created_at=api_key.created_at,
                rate_limit_rpm=api_key.rate_limit_rpm
            )

        except Exception as e:
            await self.db.rollback()
            logger.error(f"Failed to generate API key: {str(e)}")
            raise ApiKeyManagerError(f"Key generation failed: {str(e)}")

    async def validate_key(self, plaintext_key: str) -> ApiKeyValidationResult:
        """
        Validate an API key and return its permissions.
        Performs constant-time comparison to prevent timing attacks.
        """
        if not plaintext_key.startswith(self.KEY_PREFIX):
            return ApiKeyValidationResult(
                valid=False,
                error_message="Invalid key format",
                key_id=None,
                business_id=None,
                scopes=[]
            )

        key_hash = self._hash_key(plaintext_key)

        try:
            # Query by hash with status check
            query = select(ApiKey).where(
                and_(
                    ApiKey.key_hash == key_hash,
                    ApiKey.status.in_([ApiKeyStatus.ACTIVE, ApiKeyStatus.DEPRECATED])
                )
            )
            result = await self.db.execute(query)
            api_key = result.scalar_one_or_none()

            if not api_key:
                await self._log_audit_event(
                    key_id=None,
                    action='authenticate',
                    success=False,
                    details={'error': 'Key not found', 'prefix': plaintext_key[:16]}
                )
                return ApiKeyValidationResult(
                    valid=False,
                    error_message="Invalid API key",
                    key_id=None,
                    business_id=None,
                    scopes=[]
                )

            # Check expiration
            if api_key.expires_at and api_key.expires_at < datetime.now(pytz.UTC):
                await self._update_key_status(api_key.id, ApiKeyStatus.EXPIRED)
                await self._log_audit_event(
                    key_id=api_key.id,
                    action='authenticate',
                    success=False,
                    details={'error': 'Key expired', 'expired_at': api_key.expires_at.isoformat()}
                )
                raise ApiKeyExpired("API key has expired")

            # Check deprecation deadline
            if api_key.status == ApiKeyStatus.DEPRECATED and api_key.deprecation_deadline:
                if datetime.now(pytz.UTC) > api_key.deprecation_deadline:
                    await self._update_key_status(api_key.id, ApiKeyStatus.REVOKED)
                    raise ApiKeyRevoked("Deprecated key grace period has ended")

            # Update last used
            api_key.last_used_at = datetime.now(pytz.UTC)
            await self.db.commit()

            return ApiKeyValidationResult(
                valid=True,
                key_id=api_key.id,
                business_id=api_key.business_id,
                scopes=api_key.scopes
            )

        except (ApiKeyExpired, ApiKeyRevoked) as e:
            raise e
        except Exception as e:
            logger.error(f"Validation error: {str(e)}")
            return ApiKeyValidationResult(
                valid=False,
                error_message="Validation error",
                key_id=None,
                business_id=None,
                scopes=[]
            )

    async def check_rate_limit(self, key_id: uuid.UUID, increment: bool = True) -> Tuple[bool, int]:
        """
        Check if key has exceeded rate limit. Returns (allowed, current_count).
        Uses PostgreSQL with atomic increment or Redis if available.
        """
        now = datetime.now(pytz.UTC)
        window_start = now - timedelta(minutes=self.RATE_LIMIT_WINDOW_MINUTES)

        try:
            if self.redis:
                # High-performance Redis implementation
                return await self._check_rate_limit_redis(key_id, now, increment)
            else:
                # PostgreSQL implementation
                return await self._check_rate_limit_db(key_id, window_start, now, increment)

        except Exception as e:
            logger.error(f"Rate limit check failed for key {key_id}: {e}")
            # Fail open or closed based on your security posture
            # Here we fail open to prevent service disruption, but log heavily
            return True, 0

    async def _check_rate_limit_db(
        self,
        key_id: uuid.UUID,
        window_start: datetime,
        now: datetime,
        increment: bool
    ) -> Tuple[bool, int]:
        """Database-backed rate limiting with cleanup"""
        async with self._lock:
            # Get current window count
            query = select(func.coalesce(func.sum(ApiKeyRateLimit.request_count), 0)).where(
                and_(
                    ApiKeyRateLimit.key_id == key_id,
                    ApiKeyRateLimit.window_start >= window_start
                )
            )
            result = await self.db.execute(query)
            current_count = result.scalar() or 0

            # Get key's rate limit setting
            key_query = select(ApiKey.rate_limit_rpm).where(ApiKey.id == key_id)
            key_result = await self.db.execute(key_query)
            rate_limit = key_result.scalar() or 60

            if increment and current_count < rate_limit:
                # Insert or update window
                upsert_query = insert(ApiKeyRateLimit).values(
                    id=uuid.uuid4(),
                    key_id=key_id,
                    window_start=now.replace(second=0, microsecond=0),
                    request_count=1
                ).on_conflict_do_update(
                    index_elements=['key_id', 'window_start'],
                    set_=dict(request_count=ApiKeyRateLimit.request_count + 1)
                )
                await self.db.execute(upsert_query)
                await self.db.commit()
                current_count += 1

            allowed = current_count <= rate_limit
            return allowed, current_count

    async def _check_rate_limit_redis(self, key_id: uuid.UUID, now: datetime, increment: bool) -> Tuple[bool, int]:
        """Redis-backed sliding window rate limiting"""
        if not self.redis:
            return True, 0

        key = f"ratelimit:{key_id}"
        pipe = self.redis.pipeline()

        # Remove old entries outside window
        window_ms = self.RATE_LIMIT_WINDOW_MINUTES * 60 * 1000
        pipe.zremrangebyscore(key, 0, now.timestamp() * 1000 - window_ms)

        # Count current
        pipe.zcard(key)

        if increment:
            # Add current request
            pipe.zadd(key, {str(now.timestamp()): now.timestamp() * 1000})
            pipe.pexpire(key, window_ms)

        results = await pipe.execute()
        current_count = results[1]

        # Get limit from DB cache or default
        limit = 60  # Default, should be cached
        return current_count <= limit, current_count

    async def rotate_key(
        self,
        old_key_id: uuid.UUID,
        business_id: uuid.UUID,
        grace_period_days: int = 7
    ) -> KeyRotationResult:
        """
        Rotate an API key: create new key, deprecate old one with grace period.

        Args:
            old_key_id: Key to rotate
            business_id: For authorization verification
            grace_period_days: Days old key remains valid

        Returns:
            KeyRotationResult with new credentials and deadline
        """
        try:
            # Verify old key exists and belongs to business
            old_key_query = select(ApiKey).where(
                and_(
                    ApiKey.id == old_key_id,
                    ApiKey.business_id == business_id,
                    ApiKey.status == ApiKeyStatus.ACTIVE
                )
            )
            result = await self.db.execute(old_key_query)
            old_key = result.scalar_one_or_none()

            if not old_key:
                raise ApiKeyNotFound("Key not found or not active")

            # Create new key with same permissions
            new_key_data = ApiKeyCreate(
                business_id=business_id,
                scopes=[ApiKeyScope(s) for s in old_key.scopes],
                rate_limit_rpm=old_key.rate_limit_rpm,
                expires_days=365  # New full expiration
            )

            new_key_response = await self.generate_key(
                business_id=business_id,
                scopes=[ApiKeyScope(s) for s in old_key.scopes],
                rate_limit_rpm=old_key.rate_limit_rpm,
                rotated_from_key_id=old_key_id
            )

            # Deprecate old key
            deprecation_deadline = datetime.now(pytz.UTC) + timedelta(days=grace_period_days)
            await self._update_key_status(old_key_id, ApiKeyStatus.DEPRECATED)

            update_query = (
                update(ApiKey)
                .where(ApiKey.id == old_key_id)
                .values(deprecation_deadline=deprecation_deadline)
            )
            await self.db.execute(update_query)
            await self.db.commit()

            # Log rotation
            await self._log_audit_event(
                key_id=old_key_id,
                action='rotate',
                success=True,
                details={
                    'new_key_id': str(new_key_response.id),
                    'grace_period_days': grace_period_days
                }
            )

            await self._log_audit_event(
                key_id=new_key_response.id,
                action='create_via_rotation',
                success=True,
                details={'rotated_from': str(old_key_id)}
            )

            return KeyRotationResult(
                old_key_id=old_key_id,
                new_key_id=new_key_response.id,
                new_plaintext_key=new_key_response.plaintext_key,
                deprecation_deadline=deprecation_deadline
            )

        except ApiKeyNotFound:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Key rotation failed: {e}")
            raise ApiKeyManagerError(f"Rotation failed: {str(e)}")

    async def revoke_key(self, key_id: uuid.UUID, business_id: uuid.UUID, reason: str = "") -> bool:
        """
        Immediately revoke an API key.
        """
        try:
            query = select(ApiKey).where(
                and_(
                    ApiKey.id == key_id,
                    ApiKey.business_id == business_id
                )
            )
            result = await self.db.execute(query)
            key = result.scalar_one_or_none()

            if not key:
                raise ApiKeyNotFound("Key not found")

            await self._update_key_status(key_id, ApiKeyStatus.REVOKED)

            await self._log_audit_event(
                key_id=key_id,
                action='revoke',
                success=True,
                details={'reason': reason, 'previous_status': key.status.value}
            )

            logger.info(f"Revoked API key {key_id} for business {business_id}")
            return True

        except ApiKeyNotFound:
            raise
        except Exception as e:
            await self.db.rollback()
            logger.error(f"Key revocation failed: {e}")
            raise ApiKeyManagerError(f"Revocation failed: {str(e)}")

    async def track_usage(
        self,
        key_id: uuid.UUID,
        endpoint: str,
        success: bool = True,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None,
        timezone: str = 'AEST'
    ):
        """
        Track API usage for analytics and billing.
        Aggregates daily to minimize DB writes.
        """
        try:
            today = date.today()
            now = datetime.now(pytz.UTC)

            # Upsert daily usage
            stmt = insert(ApiKeyUsage).values(
                id=uuid.uuid4(),
                key_id=key_id,
                usage_date=today,
                request_count=1,
                endpoint_breakdown={endpoint: 1},
                error_count=0 if success else 1,
                last_used_at=now
            ).on_conflict_do_update(
                index_elements=['key_id', 'usage_date'],
                set_=dict(
                    request_count=ApiKeyUsage.request_count + 1,
                    last_used_at=now,
                    endpoint_breakdown=func.coalesce(ApiKeyUsage.endpoint_breakdown, '{}').op('||')(
                        func.jsonb_build_object(endpoint, func.coalesce(
                            ApiKeyUsage.endpoint_breakdown.op('->>')(endpoint), '0'
                        ).cast(Integer) + 1)
                    ),
                    error_count=ApiKeyUsage.error_count + (0 if success else 1)
                )
            )

            await self.db.execute(stmt)
            await self.db.commit()

        except Exception as e:
            logger.error(f"Failed to track usage for key {key_id}: {e}")
            await self.db.rollback()

    async def get_usage_stats(
        self,
        key_id: uuid.UUID,
        business_id: uuid.UUID,
        days: int = 30,
        timezone: str = 'AEST'
    ) -> UsageStats:
        """
        Retrieve usage statistics for a specific key.
        Converts timestamps to Australian timezone for display.
        """
        try:
            # Verify ownership
            key_query = select(ApiKey).where(
                and_(ApiKey.id == key_id, ApiKey.business_id == business_id)
            )
            result = await self.db.execute(key_query)
            if not result.scalar_one_or_none():
                raise ApiKeyNotFound("Key not found")

            # Get date range
            end_date = date.today()
            start_date = end_date - timedelta(days=days)

            # Aggregate usage
            query = select(ApiKeyUsage).where(
                and_(
                    ApiKeyUsage.key_id == key_id,
                    ApiKeyUsage.usage_date >= start_date,
                    ApiKeyUsage.usage_date <= end_date
                )
            ).order_by(ApiKeyUsage.usage_date)

            result = await self.db.execute(query)
            records = result.scalars().all()

            # Calculate stats
            total_requests = sum(r.request_count for r in records)
            total_errors = sum(r.error_count for r in records)

            daily_breakdown = {}
            endpoint_totals = {}

            tz = pytz.timezone(AU_TIMEZONES.get(timezone, 'Australia/Sydney'))

            for record in records:
                # Convert to AU timezone for display
                local_date = record.usage_date.strftime('%Y-%m-%d')
                daily_breakdown[local_date] = record.request_count

                # Aggregate endpoints
                for ep, count in (record.endpoint_breakdown or {}).items():
                    endpoint_totals[ep] = endpoint_totals.get(ep, 0) + count

            error_rate = (total_errors / total_requests * 100) if total_requests > 0 else 0

            return UsageStats(
                total_requests=total_requests,
                daily_breakdown=daily_breakdown,
                endpoints=endpoint_totals,
                error_rate=round(error_rate, 2)
            )

        except ApiKeyNotFound:
            raise
        except Exception as e:
            logger.error(f"Failed to get usage stats: {e}")
            raise ApiKeyManagerError(f"Stats retrieval failed: {str(e)}")

    async def cleanup_expired_keys(self) -> int:
        """
        Background job: Mark expired keys as expired.
        Returns count of keys updated.
        """
        try:
            now = datetime.now(pytz.UTC)

            query = (
                update(ApiKey)
                .where(
                    and_(
                        ApiKey.expires_at < now,
                        ApiKey.status == ApiKeyStatus.ACTIVE
                    )
                )
                .values(status=ApiKeyStatus.EXPIRED)
            )

            result = await self.db.execute(query)
            await self.db.commit()

            count = result.rowcount
            if count > 0:
                logger.info(f"Auto-expired {count} API keys")
            return count

        except Exception as e:
            await self.db.rollback()
            logger.error(f"Cleanup job failed: {e}")
            return 0

    async def cleanup_old_rate_limits(self, retention_hours: int = 24) -> int:
        """Clean up old rate limit windows"""
        try:
            cutoff = datetime.now(pytz.UTC) - timedelta(hours=retention_hours)
            query = delete(ApiKeyRateLimit).where(ApiKeyRateLimit.window_start < cutoff)
            result = await self.db.execute(query)
            await self.db.commit()
            return result.rowcount
        except Exception as e:
            logger.error(f"Rate limit cleanup failed: {e}")
            return 0

    async def _update_key_status(self, key_id: uuid.UUID, status: ApiKeyStatus):
        """Internal method to update key status"""
        query = (
            update(ApiKey)
            .where(ApiKey.id == key_id)
            .values(status=status)
        )
        await self.db.execute(query)
        await self.db.commit()

    async def _log_audit_event(
        self,
        key_id: Optional[uuid.UUID],
        action: str,
        success: bool,
        details: Optional[Dict] = None,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None
    ):
        """Comprehensive audit logging"""
        try:
            # Determine AU timezone based on time of year (simplified)
            now = datetime.now(pytz.UTC)
            au_tz = 'AEST' if now.astimezone(pytz.timezone('Australia/Sydney')).dst() == timedelta(0) else 'AEDT'

            log_entry = ApiKeyAuditLog(
                id=uuid.uuid4(),
                key_id=key_id,
                action=action,
                success=success,
                details=details or {},
                ip_address=ip_address,
                user_agent=user_agent,
                australian_timezone=au_tz
            )
            self.db.add(log_entry)
            await self.db.commit()
        except Exception as e:
            logger.error(f"Failed to write audit log: {e}")
            await self.db.rollback()

    async def has_scope(self, key_id: uuid.UUID, required_scope: ApiKeyScope) -> bool:
        """Check if key has specific scope (supports admin override)"""
        query = select(ApiKey.scopes).where(ApiKey.id == key_id)
        result = await self.db.execute(query)
        scopes = result.scalar_one_or_none()

        if not scopes:
            return False

        if ApiKeyScope.ADMIN.value in scopes:
            return True

        return required_scope.value in scopes

# Dependency for FastAPI
async def get_api_key_manager(db: AsyncSession) -> ApiKeyManager:
    """
    FastAPI dependency to get API Key Manager instance.
    Usage:
        @router.post("/keys")
        async def create_key(
            data: ApiKeyCreate,
            manager: ApiKeyManager = Depends(get_api_key_manager)
        ):
            return await manager.generate_key(...)
    """
    return ApiKeyManager(db)
```


Genesis Event Bus

Response:
```python
"""
Genesis Event Bus - Internal Communication Infrastructure
PostgreSQL-backed event store with DLQ and replay capabilities.
Optimized for Australian business operations (AEST/ACST/AWST).
"""

import asyncio
import json
import logging
import traceback
import uuid
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import (
    Any,
    Callable,
    Coroutine,
    Dict,
    Generic,
    List,
    Optional,
    Protocol,
    Set,
    TypeVar,
    Union,
    get_type_hints,
)
from zoneinfo import ZoneInfo

import pytz
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import (
    JSON,
    Boolean,
    DateTime,
    Index,
    Integer,
    String,
    Text,
    select,
    insert,
    update,
    delete,
    func,
)
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from sqlalchemy.orm import declarative_base, Mapped, mapped_column

# Configure structured logging
logger = logging.getLogger("genesis.event_bus")
logger.setLevel(logging.INFO)

# Australian Timezone Constants
AUSTRALIA_ZONES = {
    "AEST": ZoneInfo("Australia/Sydney"),      # NSW, VIC, TAS, ACT
    "ACST": ZoneInfo("Australia/Adelaide"),    # SA, NT
    "AWST": ZoneInfo("Australia/Perth"),       # WA
    "AEDT": ZoneInfo("Australia/Sydney"),      # DST variant
    "ACDT": ZoneInfo("Australia/Adelaide"),    # DST variant
}

# Default to Sydney time for internal operations
DEFAULT_TZ = AUSTRALIA_ZONES["AEST"]

class EventType(str, Enum):
    """Genesis domain events"""
    CALL_STARTED = "call.started"
    CALL_ENDED = "call.ended"
    CALL_TRANSFERRED = "call.transferred"
    LEAD_CAPTURED = "lead.captured"
    LEAD_QUALIFIED = "lead.qualified"
    PAYMENT_RECEIVED = "payment.received"
    PAYMENT_FAILED = "payment.failed"
    WIDGET_INSTALLED = "widget.installed"
    WIDGET_UNINSTALLED = "widget.uninstalled"
    SYSTEM_ERROR = "system.error"

class EventPriority(int, Enum):
    """Event processing priority"""
    CRITICAL = 1
    HIGH = 2
    NORMAL = 3
    LOW = 4

T = TypeVar('T')

class EventMetadata(BaseModel):
    """Event metadata with Australian timezone support"""
    correlation_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    causation_id: Optional[str] = None
    timestamp_utc: datetime = Field(default_factory=lambda: datetime.now(pytz.UTC))
    timezone: str = Field(default="Australia/Sydney")
    source_service: str = Field(default="genesis")
    user_id: Optional[str] = None
    ip_address: Optional[str] = None
    retry_count: int = Field(default=0, ge=0)

    @field_validator('timestamp_utc')
    @classmethod
    def ensure_timezone_aware(cls, v: datetime) -> datetime:
        if v.tzinfo is None:
            return pytz.UTC.localize(v)
        return v

    def get_local_time(self) -> datetime:
        """Convert UTC timestamp to Australian local time"""
        tz = ZoneInfo(self.timezone) if self.timezone in [
            "Australia/Sydney", "Australia/Melbourne", "Australia/Brisbane",
            "Australia/Adelaide", "Australia/Perth", "Australia/Darwin", "Australia/Hobart"
        ] else DEFAULT_TZ
        return self.timestamp_utc.astimezone(tz)

class Event(BaseModel, Generic[T]):
    """Generic event envelope"""
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    type: EventType
    payload: T
    metadata: EventMetadata = Field(default_factory=EventMetadata)
    priority: EventPriority = EventPriority.NORMAL

    model_config = ConfigDict(arbitrary_types_allowed=True)

# Database Models (SQLAlchemy)
Base = declarative_base()

class EventRecord(Base):
    """PostgreSQL event store table"""
    __tablename__ = "genesis_events"

    id: Mapped[str] = mapped_column(String(36), primary_key=True)
    event_type: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
    payload: Mapped[dict] = mapped_column(JSON, nullable=False)
    metadata: Mapped[dict] = mapped_column(JSON, nullable=False)
    priority: Mapped[int] = mapped_column(Integer, default=3)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        nullable=False,
        index=True,
        server_default=func.now()
    )
    processed: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
    processing_attempts: Mapped[int] = mapped_column(Integer, default=0)

    # Indexes for efficient querying
    __table_args__ = (
        Index('idx_events_type_created', 'event_type', 'created_at'),
        Index('idx_events_correlation', 'metadata', postgresql_using='gin'),
    )

class DeadLetterRecord(Base):
    """Dead Letter Queue for failed event handlers"""
    __tablename__ = "genesis_dlq"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    event_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
    event_type: Mapped[str] = mapped_column(String(100), nullable=False)
    handler_name: Mapped[str] = mapped_column(String(255), nullable=False)
    error_message: Mapped[str] = mapped_column(Text, nullable=False)
    error_stack: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    failed_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        nullable=False,
        default=func.now()
    )
    retry_count: Mapped[int] = mapped_column(Integer, default=0)
    last_retry_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
    resolved: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
    resolution_notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

    __table_args__ = (
        Index('idx_dlq_unresolved', 'resolved', 'failed_at'),
    )

class HandlerError(Exception):
    """Custom exception for handler failures"""
    def __init__(self, handler_name: str, event_id: str, original_error: Exception):
        self.handler_name = handler_name
        self.event_id = event_id
        self.original_error = original_error
        super().__init__(f"Handler {handler_name} failed for event {event_id}: {str(original_error)}")

# Type definitions for handlers
EventHandler = Callable[[Event[Any]], Coroutine[Any, Any, None]]

@dataclass
class Subscription:
    """Handler subscription metadata"""
    event_types: Set[EventType]
    handler: EventHandler
    name: str
    max_retries: int = 3
    timeout_seconds: float = 30.0
    circuit_breaker_threshold: int = 5  # Failures before opening circuit

class CircuitBreaker:
    """Simple circuit breaker pattern for resilient handlers"""
    def __init__(self, threshold: int = 5, timeout_seconds: float = 60.0):
        self.failure_count = 0
        self.threshold = threshold
        self.timeout = timedelta(seconds=timeout_seconds)
        self.last_failure_time: Optional[datetime] = None
        self.state = "closed"  # closed, open, half-open

    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = datetime.now(pytz.UTC)
        if self.failure_count >= self.threshold:
            self.state = "open"
            logger.warning(f"Circuit breaker opened for {self}")

    def record_success(self):
        self.failure_count = 0
        self.state = "closed"

    def can_execute(self) -> bool:
        if self.state == "closed":
            return True
        if self.state == "open":
            if self.last_failure_time and datetime.now(pytz.UTC) - self.last_failure_time > self.timeout:
                self.state = "half-open"
                return True
            return False
        return True  # half-open allows one trial

class GenesisEventBus:
    """
    Production-grade event bus with PostgreSQL persistence, DLQ, and replay.

    Features:
    - Async publish/subscribe
    - Transactional event persistence (Outbox pattern)
    - Dead Letter Queue with retry logic
    - Event replay for debugging/auditing
    - Circuit breakers for fault isolation
    - Australian timezone support in metadata
    """

    def __init__(
        self,
        database_url: str,
        max_concurrent_handlers: int = 100,
        enable_dlq: bool = True,
        default_timezone: str = "Australia/Sydney"
    ):
        self.database_url = database_url
        self.enable_dlq = enable_dlq
        self.default_timezone = default_timezone

        # Database setup
        self.engine = create_async_engine(
            database_url,
            pool_size=20,
            max_overflow=10,
            pool_pre_ping=True,
            echo=False
        )
        self.session_factory = async_sessionmaker(
            self.engine,
            class_=AsyncSession,
            expire_on_commit=False
        )

        # In-memory handler registry
        self._subscriptions: List[Subscription] = []
        self._circuit_breakers: Dict[str, CircuitBreaker] = {}
        self._semaphore = asyncio.Semaphore(max_concurrent_handlers)

        # Background tasks
        self._background_tasks: Set[asyncio.Task] = set()

        logger.info(f"GenesisEventBus initialized with timezone: {default_timezone}")

    async def initialize_schema(self):
        """Create database tables if they don't exist"""
        async with self.engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)
        logger.info("Event store schema initialized")

    async def close(self):
        """Graceful shutdown"""
        logger.info("Shutting down EventBus...")

        # Wait for background tasks to complete (with timeout)
        if self._background_tasks:
            pending = list(self._background_tasks)
            logger.info(f"Waiting for {len(pending)} background tasks...")
            done, _ = await asyncio.wait(pending, timeout=30.0)
            for task in done:
                try:
                    await task
                except Exception as e:
                    logger.error(f"Background task error during shutdown: {e}")

        await self.engine.dispose()
        logger.info("EventBus shutdown complete")

    def subscribe(
        self,
        event_types: Union[EventType, List[EventType]],
        handler: EventHandler,
        name: Optional[str] = None,
        max_retries: int = 3,
        timeout_seconds: float = 30.0,
        circuit_breaker_threshold: int = 5
    ) -> Subscription:
        """
        Subscribe to event types with a handler function.

        Example:
            @bus.subscribe(EventType.CALL_STARTED)
            async def handle_call(event: Event[CallPayload]):
                await process_call(event.payload)
        """
        if isinstance(event_types, EventType):
            event_types = [event_types]

        event_types_set = set(event_types)
        handler_name = name or handler.__name__

        subscription = Subscription(
            event_types=event_types_set,
            handler=handler,
            name=handler_name,
            max_retries=max_retries,
            timeout_seconds=timeout_seconds,
            circuit_breaker_threshold=circuit_breaker_threshold
        )

        self._subscriptions.append(subscription)
        self._circuit_breakers[handler_name] = CircuitBreaker(circuit_breaker_threshold)

        logger.info(f"Subscribed {handler_name} to events: {[e.value for e in event_types_set]}")
        return subscription

    async def publish(
        self,
        event_type: EventType,
        payload: Any,
        metadata: Optional[EventMetadata] = None,
        priority: EventPriority = EventPriority.NORMAL,
        correlation_id: Optional[str] = None
    ) -> Event[Any]:
        """
        Publish an event to the bus.

        Persists to PostgreSQL first (transactional outbox pattern),
        then dispatches to subscribers asynchronously.
        """
        # Create event envelope
        if metadata is None:
            metadata = EventMetadata(
                timezone=self.default_timezone,
                correlation_id=correlation_id or str(uuid.uuid4())
            )

        if correlation_id and not metadata.correlation_id:
            metadata.correlation_id = correlation_id

        event = Event(
            type=event_type,
            payload=payload,
            metadata=metadata,
            priority=priority
        )

        # Persist to PostgreSQL (transactional)
        async with self.session_factory() as session:
            async with session.begin():
                record = EventRecord(
                    id=event.id,
                    event_type=event.type.value,
                    payload=self._serialize_payload(payload),
                    metadata=metadata.model_dump(),
                    priority=priority.value,
                    created_at=metadata.timestamp_utc,
                    processed=False
                )
                session.add(record)

            await session.commit()

        # Dispatch asynchronously (don't block publisher)
        task = asyncio.create_task(self._dispatch_event(event))
        self._background_tasks.add(task)
        task.add_done_callback(self._background_tasks.discard)

        # Log in Australian time for operational visibility
        local_time = metadata.get_local_time()
        logger.info(
            f"Event published: {event_type.value} [{event.id}] at "
            f"{local_time.strftime('%Y-%m-%d %H:%M:%S %Z')} "
            f"(correlation: {metadata.correlation_id})"
        )

        return event

    async def _dispatch_event(self, event: Event[Any]):
        """Dispatch event to all registered handlers"""
        matching_subs = [
            sub for sub in self._subscriptions
            if event.type in sub.event_types
        ]

        if not matching_subs:
            logger.debug(f"No handlers registered for {event.type.value}")
            return

        # Execute handlers concurrently with semaphore for backpressure
        tasks = []
        for sub in matching_subs:
            task = asyncio.create_task(
                self._execute_handler_with_guard(sub, event)
            )
            tasks.append((sub, task))

        # Wait for all handlers to complete
        results = await asyncio.gather(
            *[t[1] for t in tasks],
            return_exceptions=True
        )

        # Process results and handle failures
        for (sub, _), result in zip(tasks, results):
            if isinstance(result, Exception):
                await self._handle_handler_failure(sub, event, result)

    async def _execute_handler_with_guard(
        self,
        subscription: Subscription,
        event: Event[Any]
    ):
        """Execute handler with circuit breaker, timeout, and semaphore"""
        handler_name = subscription.name

        # Check circuit breaker
        cb = self._circuit_breakers[handler_name]
        if not cb.can_execute():
            raise HandlerError(
                handler_name,
                event.id,
                Exception("Circuit breaker is open")
            )

        async with self._semaphore:
            try:
                # Execute with timeout
                await asyncio.wait_for(
                    subscription.handler(event),
                    timeout=subscription.timeout_seconds
                )
                cb.record_success()

            except asyncio.TimeoutError as e:
                cb.record_failure()
                raise HandlerError(handler_name, event.id, e)
            except Exception as e:
                cb.record_failure()
                raise HandlerError(handler_name, event.id, e)

    async def _handle_handler_failure(
        self,
        subscription: Subscription,
        event: Event[Any],
        error: Exception
    ):
        """Handle handler failure - retry logic and DLQ"""
        handler_name = subscription.name

        # Check if we should retry immediately (exponential backoff could be added here)
        current_retry = event.metadata.retry_count

        if current_retry < subscription.max_retries:
            logger.warning(
                f"Handler {handler_name} failed for {event.id}, "
                f"retrying ({current_retry + 1}/{subscription.max_retries})"
            )
            event.metadata.retry_count += 1

            # Retry after delay
            await asyncio.sleep(2 ** current_retry)  # Exponential backoff
            task = asyncio.create_task(self._execute_handler_with_guard(subscription, event))
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
        else:
            # Send to DLQ
            logger.error(
                f"Handler {handler_name} failed permanently for {event.id}: {error}"
            )
            if self.enable_dlq:
                await self._send_to_dlq(subscription, event, error)

    async def _send_to_dlq(
        self,
        subscription: Subscription,
        event: Event[Any],
        error: Exception
    ):
        """Persist failed event to Dead Letter Queue"""
        async with self.session_factory() as session:
            dlq_entry = DeadLetterRecord(
                event_id=event.id,
                event_type=event.type.value,
                handler_name=subscription.name,
                error_message=str(error),
                error_stack=traceback.format_exc(),
                failed_at=datetime.now(pytz.UTC),
                retry_count=event.metadata.retry_count
            )
            session.add(dlq_entry)
            await session.commit()

        logger.error(f"Event {event.id} sent to DLQ for handler {subscription.name}")

    async def replay_events(
        self,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
        event_types: Optional[List[EventType]] = None,
        correlation_id: Optional[str] = None,
        handler_filter: Optional[str] = None,
        dry_run: bool = False
    ) -> List[Event[Any]]:
        """
        Replay events from the event store for debugging or recovery.

        Args:
            start_time: Replay events after this time (UTC)
            end_time: Replay events before this time (UTC)
            event_types: Filter by specific event types
            correlation_id: Replay events for a specific transaction
            handler_filter: Only replay to specific handler (by name)
            dry_run: If True, don't actually dispatch, just return events

        Returns:
            List of replayed events
        """
        async with self.session_factory() as session:
            query = select(EventRecord).where(EventRecord.processed == True)

            if start_time:
                query = query.where(EventRecord.created_at >= start_time)
            if end_time:
                query = query.where(EventRecord.created_at <= end_time)
            if event_types:
                type_names = [et.value for et in event_types]
                query = query.where(EventRecord.event_type.in_(type_names))
            if correlation_id:
                # Query JSONB metadata for correlation_id
                query = query.where(
                    EventRecord.metadata["correlation_id"].astext == correlation_id
                )

            query = query.order_by(EventRecord.created_at)
            result = await session.execute(query)
            records = result.scalars().all()

            events = []
            for record in records:
                event = self._record_to_event(record)
                events.append(event)

                if not dry_run:
                    # Dispatch to specific handler or all handlers
                    if handler_filter:
                        sub = next(
                            (s for s in self._subscriptions if s.name == handler_filter),
                            None
                        )
                        if sub and event.type in sub.event_types:
                            task = asyncio.create_task(
                                self._execute_handler_with_guard(sub, event)
                            )
                            self._background_tasks.add(task)
                            task.add_done_callback(self._background_tasks.discard)
                    else:
                        task = asyncio.create_task(self._dispatch_event(event))
                        self._background_tasks.add(task)
                        task.add_done_callback(self._background_tasks.discard)

            logger.info(f"Replayed {len(events)} events" + (" (dry run)" if dry_run else ""))
            return events

    async def get_dlq_entries(
        self,
        resolved: Optional[bool] = False,
        handler_name: Optional[str] = None,
        limit: int = 100
    ) -> List[DeadLetterRecord]:
        """Query Dead Letter Queue entries"""
        async with self.session_factory() as session:
            query = select(DeadLetterRecord)

            if resolved is not None:
                query = query.where(DeadLetterRecord.resolved == resolved)
            if handler_name:
                query = query.where(DeadLetterRecord.handler_name == handler_name)

            query = query.order_by(DeadLetterRecord.failed_at.desc()).limit(limit)
            result = await session.execute(query)
            return list(result.scalars().all())

    async def retry_dlq_entry(self, dlq_id: int, session: Optional[AsyncSession] = None) -> bool:
        """
        Retry a specific DLQ entry.
        Returns True if successful, False otherwise.
        """
        close_session = False
        if session is None:
            session = self.session_factory()
            close_session = True

        try:
            # Get DLQ entry
            result = await session.execute(
                select(DeadLetterRecord).where(DeadLetterRecord.id == dlq_id)
            )
            dlq_entry = result.scalar_one_or_none()

            if not dlq_entry or dlq_entry.resolved:
                return False

            # Retrieve original event
            event_result = await session.execute(
                select(EventRecord).where(EventRecord.id == dlq_entry.event_id)
            )
            event_record = event_result.scalar_one_or_none()

            if not event_record:
                logger.error(f"Original event {dlq_entry.event_id} not found for DLQ retry")
                return False

            event = self._record_to_event(event_record)
            event.metadata.retry_count = dlq_entry.retry_count + 1

            # Find handler
            sub = next(
                (s for s in self._subscriptions if s.name == dlq_entry.handler_name),
                None
            )

            if not sub:
                logger.error(f"Handler {dlq_entry.handler_name} not found for retry")
                return False

            # Attempt execution
            try:
                await self._execute_handler_with_guard(sub, event)

                # Mark as resolved
                dlq_entry.resolved = True
                dlq_entry.resolution_notes = "Successfully retried and processed"
                dlq_entry.last_retry_at = datetime.now(pytz.UTC)
                await session.commit()

                logger.info(f"DLQ entry {dlq_id} successfully retried")
                return True

            except Exception as e:
                dlq_entry.retry_count += 1
                dlq_entry.last_retry_at = datetime.now(pytz.UTC)
                dlq_entry.error_message = f"Retry failed: {str(e)}"
                await session.commit()

                logger.error(f"DLQ retry failed for entry {dlq_id}: {e}")
                return False

        finally:
            if close_session:
                await session.close()

    async def purge_old_events(self, days: int = 90):
        """Archive or delete events older than specified days"""
        cutoff = datetime.now(pytz.UTC) - timedelta(days=days)

        async with self.session_factory() as session:
            # Delete old processed events
            result = await session.execute(
                delete(EventRecord).where(
                    EventRecord.created_at < cutoff,
                    EventRecord.processed == True
                )
            )
            deleted_count = result.rowcount

            # Delete old resolved DLQ entries
            dlq_result = await session.execute(
                delete(DeadLetterRecord).where(
                    DeadLetterRecord.failed_at < cutoff,
                    DeadLetterRecord.resolved == True
                )
            )
            dlq_deleted = dlq_result.rowcount

            await session.commit()

            logger.info(
                f"Purged {deleted_count} old events and {dlq_deleted} DLQ entries"
            )

    def _serialize_payload(self, payload: Any) -> dict:
        """Serialize payload to JSON-compatible dict"""
        if isinstance(payload, BaseModel):
            return payload.model_dump()
        elif isinstance(payload, dict):
            return payload
        else:
            return {"data": str(payload)}

    def _record_to_event(self, record: EventRecord) -> Event[Any]:
        """Convert database record to Event object"""
        metadata = EventMetadata(**record.metadata)
        return Event(
            id=record.id,
            type=EventType(record.event_type),
            payload=record.payload,
            metadata=metadata,
            priority=EventPriority(record.priority)
        )

    @asynccontextmanager
    async def transaction(self):
        """Context manager for atomic operations with event publishing"""
        async with self.session_factory() as session:
            async with session.begin():
                yield session

# Convenience decorator
def event_handler(
    bus: GenesisEventBus,
    event_types: Union[EventType, List[EventType]],
    name: Optional[str] = None,
    max_retries: int = 3,
    timeout_seconds: float = 30.0
):
    """Decorator for registering event handlers"""
    def decorator(func: EventHandler) -> EventHandler:
        bus.subscribe(
            event_types=event_types,
            handler=func,
            name=name or func.__name__,
            max_retries=max_retries,
            timeout_seconds=timeout_seconds
        )
        return func
    return decorator

# Example usage and test fixtures
if __name__ == "__main__":
    import asyncio

    async def demo():
        # Initialize with PostgreSQL connection
        bus = GenesisEventBus(
            database_url="postgresql+asyncpg://user:pass@localhost/genesis",
            default_timezone="Australia/Sydney"
        )

        await bus.initialize_schema()

        # Example handler
        @event_handler(bus, [EventType.CALL_STARTED], name="call_logger")
        async def handle_call_started(event: Event[dict]):
            print(f"Call started: {event.payload}")
            # Simulate processing
            await asyncio.sleep(0.1)

        # Publish test event
        await bus.publish(
            event_type=EventType.CALL_STARTED,
            payload={
                "phone_number": "+61400123456",  # Australian format
                "business_id": "biz_123",
                "timestamp_aest": datetime.now(AUSTRALIA_ZONES["AEST"]).isoformat()
            },
            metadata=EventMetadata(
                source_service="voice_gateway",
                timezone="Australia/Sydney"
            )
        )

        # Wait for processing
        await asyncio.sleep(2)

        # Check DLQ
        dlq = await bus.get_dlq_entries()
        print(f"DLQ entries: {len(dlq)}")

        await bus.close()

    asyncio.run(demo())
```


Cold Email - Plumbing Business Owners

Response:
# 5-Email Cold Outreach Sequence: AI Receptionist for Plumbing Businesses

---

## Email 1 – The Hook

**Subject:** You're losing $500 per missed call

Hi {first_name},

Running {business_name} is demanding—we get it.

But here's the painful truth: every missed call = $500+ lost in potential jobs.

That "I'll call them back later" moment? It's costing you thousands.

The good news? There's a simple fix.

Quick question: Do you want to stop missing calls—without hiring more staff?

Cheers,
[Your name]

---

## Email 2 – Problem Agitation

**Subject:** What if you never missed another call?

Hey {first_name},

Quick scenario:

It's 11pm on a Saturday. A burst pipe is flooding a customer's home. They call you—twice—and reach voicemail.

They call your competitor instead. That's a $500+ job gone.

What if your phone was answered 24/7—every call, every time—by an AI receptionist that books jobs directly?

No staff needed. No missed calls. Ever.

Curious how this works?

Cheers,
[Your name]

---

## Email 3 – Solution Introduction

**Subject:** Your AI receptionist works 24/7

Hey {first_name},

Meet your new team member: an AI receptionist that never sleeps.

✓ Answers every call—even at 2am
✓ Books jobs directly into your calendar
✓ Never takes sick days
✓ Never forgets to follow up

Setup takes 5 minutes. That's it.

Pricing starts at $197/mo (was $497)—because we know plumbers need simple tools that work.

See for yourself?

Cheers,
[Your name]

---

## Email 4 – Social Proof

**Subject:** Other plumbers are already using this

Hey {first_name},

We surveyed 50 plumbing businesses in Australia.

The average missed call problem? $2,400/month in lost revenue.

The ones using our AI receptionist? Zero missed calls since day one.

They're saving $2,400+ monthly—and gaining 10+ hours/week of free time.

No contracts. Cancel anytime.

Want to see what you're missing?

Cheers,
[Your name]

---

## Email 5 – Final Close

**Subject:** Last chance to stop missing calls

Hey {first_name},

We'll keep this short.

Missed calls are costing you $500+ per lead. That's thousands monthly.

Our AI receptionist fixes it—in 5 minutes, for $197/mo.

This offer won't last. Let's chat.

Book your free demo here: [link]

Cheers,
[Your name]

---

**Sequence Notes:**
- Send emails 2–4 days apart
- Personalise with first name and business name
- Track reply rates and adjust timing based on engagement
- Use a professional sending domain (avoid spam triggers)


Cold Email - Electrical Contractors

Response:
# 5-Email Cold Outreach Sequence

**Target:** Electrical Contractors in Australia
**Goal:** Book demo calls for electrical business management software
**Price Display:** $197/mo (was $497/mo)

---

## Email 1: The Emergency Call Hook

**Subject:** Lost sleep over after-hours emergency calls?

Hi [First Name],

Running an electrical business in Australia means you're never truly off the clock.

Those 2am calls from customers with no power, exposed wiring, or storm damage—they're part of the job. But answering them personally? That's a choice that's costing you sleep, sanity, and your personal life.

We built a system that handles your after-hours emergency calls automatically—24/7/365. While you sleep, we capture the job details, triage urgency, and book the callout directly into your calendar.

No more lost leads. No more groggy 3am conversations.

Curious how it works?

Cheers,
[Your Name]

**CTA:** Reply "YES" and I'll show you a live demo

---

## Email 2: The Quote Follow-Up

**Subject:** How many quotes have you lost to slow follow-ups?

Hi [First Name],

Here's a hard truth: electrical contractors lose an average of 30% of quote opportunities simply because they follow up too slowly.

You're on the job site, covered in dust, trying to manually write up a quote between callouts. Meanwhile, your competitor sent a professional quote 20 minutes ago.

Our platform sends quotes automatically while you're still on-site. Customer details, job scope, materials, labour—pulled from your mobile and sent to their inbox before you clock off.

Would recovering just 2 lost jobs a month cover the software cost?

Cheers,
[Your Name]

**CTA:** See how it works in 90 seconds—book a time here

---

## Email 3: The Admin Reality Check

**Subject:** This is why you're still doing admin at 9pm

Hi [First Name],

You became an electrician to do electrical work—not data entry.

Yet here you are, at night, entering job details from paper notes, chasing up customer contact info, and manually invoicing for yesterday's callouts.

We've helped over 200 Aussie contractors reclaim 10+ hours weekly by capturing everything on-site: photos, notes, customer details, signatures—all synced automatically to your invoicing system.

Your paperwork done before you leave the job.

That's not a luxury. It's the new standard.

Cheers,
[Your Name]

**CTA:** Book a 15-minute demo—no hard sell

---

## Email 4: The Price Objection Breaker

**Subject:** What if the software paid for itself?

Hi [First Name],

I know what you're thinking: "I can't afford another monthly expense."

Fair enough. But here's the thing—at $197/month (down from $497), this software costs less than a single hourly callout rate.

Now ask yourself:

- How many emergency leads have you missed this year?
- How many quotes did you lose to slow follow-up?
- How many hours of admin could you reclaim weekly?

This isn't an expense. It's an investment that pays for itself within the first month.

We offer a 14-day free trial. No credit card required. See the results first, then decide.

Deal?

Cheers,
[Your Name]

**CTA:** Start your free trial here

---

## Email 5: The Close

**Subject:** Last chance to streamline your electrical business?

Hi [First Name],

I've reached out a few times now because I genuinely believe this system can help your business.

Here's what I'm offering:

✅ After-hours emergency call handling (24/7)
✅ Automatic quote generation
✅ On-site customer detail capture
✅ Reduced admin overhead

**Special offer: $197/month (was $497) — locked in for life**

This isn't a trial offer that's about to expire. It's a locked-in price for as long as you stay a customer.

One last question: What's stopping you from reclaiming your evenings?

Cheers,
[Your Name]

**CTA:** Claim your locked-in rate and start today

---

*Word counts: Email 1 (128), Email 2 (132), Email 3 (138), Email 4 (149), Email 5 (122)*


Cold Email - HVAC Companies

Response:
# 5-Email Cold Outreach Sequence for HVAC Companies

---

## Email 1: The Hook

**Subject:** [Company Name] — missing calls during peak season?

Hi [First Name],

Quick question — how many calls did [Company] miss last summer?

Between installation bookings and emergency callouts, busy season overwhelms even the best HVAC teams. That means lost revenue and frustrated homeowners who call your competitors.

We help HVAC companies across Australia capture every call, book installations directly into your calendar, and dispatch emergency jobs faster.

Want to know your exact missed-call number? It's probably higher than you think.

Cheers,
[Your Name]

**CTA:** Reply with "CALLS" and I'll show you

---

## Email 2: The Solution

**Subject:** How [Similar HVAC Company] handled 340 more calls this summer

Hi [First Name],

[Similar Company] — a 4-person HVAC team in [Their City] — was drowning in calls during peak season. They were losing an estimated $8,200/month in missed emergency jobs.

After implementing our call handling system, they:

- Booked 47 new installation appointments directly into technician calendars
- Reduced emergency dispatch time by 68%
- Captured 340+ calls they previously missed

No extra staff required.

Want to see how it works?

Cheers,
[Your Name]

**CTA:** Book a 10-minute demo here: [Link]

---

## Email 3: Social Proof

**Subject:** What 47 Australian HVAC companies discovered

Hi [First Name],

We work with 47 HVAC companies across Australia — from solo operators to 20-person teams.

Here's what the data shows:

- **Average 23% increase** in booked installations per month
- **$6,400 average monthly revenue** recovered from previously missed calls
- **9-minute average reduction** in emergency response time

The common thread? They all needed one thing: a way to handle more calls without hiring more reception staff.

Happy to share references from companies similar to yours.

Cheers,
[Your Name]

**CTA:** Reply and I'll connect you with a nearby HVAC company

---

## Email 4: Price Objection Handling

**Subject:** [Company Name] — let's talk investment

Hi [First Name],

I get it — you're wondering about cost before seeing value.

Our system starts at $97/month (normally $197/month). That's less than one service call per month.

Most clients break even within 14 days from recovered revenue alone.

We also offer a 30-day pilot — no long-term contract. If it doesn't work for [Company], you walk away.

Let's hop on a quick call and see if it's a fit.

Cheers,
[Your Name]

**CTA:** Book a time here: [Link]

---

## Email 5: Urgency + Seasonal Timing

**Subject:** Winter's coming — last chance to get systems ready

Hi [First Name],

Winter is peak emergency season for HVAC. The teams who prepared last year are already booking their capacity.

Here's my ask: a 15-minute call this week. No pressure — just a conversation about your current call handling and where the gaps are.

If it's not a fit, I'll say so. If it is a fit, you'll wonder how you managed without it.

Next week I'm fully booked with other HVAC companies. Let's talk now.

Cheers,
[Your Name]

**CTA:** Book your slot: [Link]

---

**Quick notes for deployment:**

- Space emails 3-4 days apart
- Personalise with real company names where possible
- Replace [Link] with your actual booking calendar
- Adjust city/state references to match your target territory


Cold Email - Dental Practices

Response:
# Dental Practice Cold Outreach Sequence

---

## Email 1: The Hook – Appointment Booking

**Subject:** [Practice Name], want to fill 15 more appointment slots per week?

Hi [Name],

Your front desk team is likely spending hours each week on the phone—scheduling, confirming, rescheduling.

What if your patients could book their own appointments online 24/7—without calling in?

Our dental booking system automatically sends reminders, reduces no-shows by 40%, and syncs directly with your practice management software.

Would a 10-minute demo work for you this week?

Cheers,
[Your Name]

---

## Email 2: Patient Intake Information

**Subject:** Stop asking patients the same questions 3 times

Hi [Name],

New patients arrive at your practice with paperwork in hand—then your team asks the same questions again at the front desk. Sound familiar?

Our digital intake system lets patients complete health histories, consent forms, and medical questionnaires before they even walk through the door.

No more illegible handwriting. No more repetitive questions. Just clean, organised patient data from day one.

Want to see how it works?

Cheers,
[Your Name]

---

## Email 3: Insurance Verification

**Subject:** The hidden revenue leak in your practice

Hi [Name],

Here's a frustrating scenario: A patient finishes treatment, your team submits the claim, and it gets denied. Hours of work. Zero revenue.

Sound familiar?

Our automated insurance verification system checks patient eligibility before the appointment—catching issues weeks before treatment even begins.

Practices using this have recovered an average of $8,000+ per month in previously denied claims.

Have 15 minutes to discuss?

Cheers,
[Your Name]

---

## Email 4: After-Hours Emergency Line

**Subject:** Your patients need you at 11pm. Do they know how to reach you?

Hi [Name],

Tooth emergencies don't follow business hours. When a patient has severe pain at midnight, they need help—not a voicemail that says "call back tomorrow."

Our 24/7 emergency dental line routes after-hours calls directly to your on-call team, or provides patients with clear instructions for urgent care.

You maintain complete control over availability and protocols.

Interested in reducing after-hours stress for your team?

Cheers,
[Your Name]

---

## Email 5: Front Desk Workload

**Subject:** What if your front desk could focus on patients instead of paperwork?

Hi [Name],

We've spoken about booking, intake, insurance, and after-hours care—each solving a different daily headache.

Together, these tools reduce your front desk workload by an average of 12 hours per week. That's one full staff day reclaimed.

Our complete practice solution is available from ~~$299~~ $199/month—less than the cost of one part-time admin hour per week.

Let's schedule a 15-minute call to see if we're a fit.

Cheers,
[Your Name]

---

Want me to adjust the tone, add personalization variables, or tailor any email to a specific dental segment (e.g., solo practice vs. DSO)?


Cold Email - Real Estate Agents

Response:
# 5-Email Cold Outreach Sequence for Real Estate Agents

---

## Email 1: The Hook

**Subject:** [First Name], what's your response time to buyer inquiries?

Hi [First Name],

Quick question: How fast do you respond to buyer inquiries?

In real estate, the first reply wins the lead. Studies show agents who respond within 5 minutes are 9x more likely to convert.

Yet most agents are stuck in showings, meetings, or—let's be honest—enjoying their weekend when inquiries roll in.

That's the gap we're closing.

We'll be in touch shortly with a better way to capture every buyer—before your competition does.

Cheers,
[Your Name]

---

## Email 2: Never Miss a Lead

**Subject:** The $50,000 question every agent dodges

Hi [First Name],

Here's an uncomfortable truth: You're losing leads right now.

Not because you're bad at sales. Because you can't be everywhere at once.

Our platform ensures you **never miss a buyer inquiry**—whether it's 2pm or 2am. Instant, personalized responses that feel like you wrote them (because the AI learns your style).

Think of it as having a top-performing assistant who never sleeps, never takes holidays, and never drops the ball.

More leads. Less stress. More closings.

Curious?

Cheers,
[Your Name]

---

## Email 3: Scheduling Showings on Autopilot

**Subject:** [First Name], stop playing calendar Tetris

Hi [First Name],

Coordinating property viewings is a nightmare.

Back-and-forth texts. Double-booked slots. Clients ghosting because the process is too hard.

What if your calendar synced automatically with every buyer? They see your real availability, book a time, and get instant confirmation—all without you lifting a finger.

No more scheduling headaches. No more missed showings.

Your time is worth more than calendar management.

Ready to streamline it?

Cheers,
[Your Name]

---

## Email 4: Qualify Leads While You Sleep

**First Name], here's the truth about unqualified leads

Hi [First Name],

Every "just browsing" inquiry eats 15 minutes you'll never get back.

What if every lead that hit your inbox was pre-qualified? Budget confirmed. Timeline clear. Preferences matched to your active listings.

Our system does the heavy lifting:

- **Budget** collected upfront
- **Timeline** (ready now vs. 6 months) identified
- **Preferences** (location, bedrooms, must-haves) captured

You show up to conversations already armed with what matters.

Stop chasing. Start closing.

Let's talk.

Cheers,
[Your Name]

---

## Email 5: The CTA

**Subject:** Your 24/7 competitive edge

Hi [First Name],

Buyers don't shop 9-5. Why should your availability be limited to business hours?

With our platform, you can **show properties 24/7**—through virtual tours, instant self-scheduled viewings, and round-the-clock AI support that answers questions while you rest.

The agents winning today aren't the hardest workers. They're the ones with systems that never stop.

Here's what we offer:

- ~~$897/month~~ **$497/month** for the first 3 months
- Full setup included
- No lock-in contract

Book a 15-minute demo and see how it works.

[Book Here]

Cheers,
[Your Name]

---

**Quick notes for implementation:**

- Personalise with CRM data where possible
- Space emails 2-3 days apart
- A/B test subject lines
- Track opens/clicks to identify warm prospects
- Scale by swapping in different pain points per list segment


Cold Email - Website Agency Partners

Response:
# 5-Email Partnership Sequence for Website Agencies

---

## Email 1: Cold Outreach – The Opportunity

**Subject:** Turn your web builds into recurring revenue

Hi [Name],

Quick question: how much revenue leaks out of your agency every time you finish a website build?

Most web agencies hit a wall after launch. Project fees are great, but there's no ongoing income—until now.

We're offering website agencies a **30% recurring commission** on every client you refer. That's revenue that keeps flowing, month after month, for work you're not even doing.

We've made it simple: white-label option, easy installation, and it adds genuine value to your web packages.

Interested in exploring a partnership?

Cheers,
[Your name]

---

## Email 2: Follow-Up – The "Why Now"

**Subject:** Recurring revenue. No extra work.

Hi [Name],

Following up on my last email—because this one's worth your time.

As a web agency, you're constantly building client websites. But once the site launches, that revenue stream dries up. We're changing that.

Our partner programme gives you a **30% recurring commission** every month for every client you refer. White-label it as your own service. Install it in minutes. Your clients get more value, and you get paid.

No extra projects. No additional staff. Just revenue on autopilot.

Let's chat about how it works.

Cheers,
[Your name]

---

## Email 3: Value Proposition – Addressing Skepticism

**Subject:** Why your clients will actually want this

Hi [Name],

I get it—you're probably thinking "another plugin to upsell."

Here's what makes this different: your clients are already asking for it. They want better analytics, automated reporting, lead tracking—things that make their website work harder for their business.

Our solution delivers exactly that. And here's the beauty part:

- **White-label it completely** – your brand, your client relationships
- **Installs in minutes** – no developer needed
- **Adds real value** – not fluff they'll ignore

You position yourself as the agency that delivers ongoing results, not just pretty websites. That's worth a premium.

Let's jump on a 15-minute call to show you how simple it is.

Cheers,
[Your name]

---

## Email 4: Partnership Details – How It Works

**Subject:** How our 30% commission works (no catches)

Hi [Name],

Thanks for your interest—here's exactly how our partnership works:

**What you get:**
- 30% recurring commission on every referred client—forever
- White-label access—no trace of us to your clients
- Simple installation—no coding required
- Dedicated support for you and your clients

**What you do:**
1. Mention our solution to your existing clients (or add it to new builds)
2. We handle setup, support, and delivery
3. You get paid monthly—no invoicing, no chasing

**The maths:**
If you refer just 5 clients on a $100/month plan, that's $150/month in recurring revenue. Refer 20? That's $600/month. And it grows as your clients grow.

No minimums. No commitments. Just revenue.

Ready to get started?

Cheers,
[Your name]

---

## Email 5: Closing – CTA

**Subject:** Let's turn your next website build into recurring income

Hi [Name],

Let's make this easy.

Our partner programme is built for agencies like yours—small to mid-size, client-focused, looking for ways to grow without scaling your team.

You'll get:
✓ 30% recurring commission (paid monthly)
✓ Full white-label access
✓ Quick-start onboarding

We'll even give you a demo account so you can see it from your clients' perspective.

**Next step:** Book a 15-minute call here → [link]

No pressure. Just a conversation about how this could work for your agency.

Cheers,
[Your name]

---

**Word counts:** Email 1: 148 | Email 2: 149 | Email 3: 147 | Email 4: 295 | Email 5: 147


Cold Email - Legal Firms

Response:
# 5-Email Cold Outreach Sequence for Legal Practices

---

## Email 1: The Hook

**Subject:** Never miss another client call, {First Name}

Hi {First Name},

New clients rarely call twice. When they reach out with a legal matter, they want to feel heard—not routed through a confusing voicemail maze or handled by an overwhelmed receptionist.

We help boutique firms and legal practices present a polished, professional front from the first point of contact—without the overhead of a full-time receptionist.

A quick question: how many potential clients do you estimate slip through the cracks each month due to missed calls or delayed follow-ups?

Best,

{Your Name}
P.S. Happy to share how {Similar Firm} reduced missed inquiries by 67% in their first quarter.

---

## Email 2: The Problem-Solver

**Subject:** What happens when a new client calls after hours?

Hey {First Name},

Here's what we hear from legal practices almost daily:

*"We can't afford to miss calls, but we also can't afford a receptionist 24/7."*

The reality is: legal matters don't keep business hours. A distressed client calling at 9pm deserves the same professional experience as one calling at 9am.

Our service handles:

- **Confidential call screening** – clients feel respected, not interrogated
- **Client intake forms** – structured information gathered before you ever pick up the phone
- **After-hours emergency line** – never miss an urgent matter again

We're not just answering phones. We're protecting your reputation.

Cheers,

{Your Name}

---

## Email 3: The Trust Builder

**Subject:** Your firm deserves a front door as professional as your legal work

Hi {First Name},

Your legal expertise got you here. But how you answer the phone might be costing you clients.

When a potential client calls, they can't see your impressive credentials or your track record. They can only hear how they're treated in those first 30 seconds.

We help law firms project confidence and competence through:

- **Trained intake specialists** who represent your brand voice
- **Custom intake forms** tailored to your practice areas
- **Seamless appointment scheduling** – no back-and-forth, no dropped balls

Your clients should feel they're in capable hands from the very first conversation.

Shall I show you how it works?

{Your Name}

---

## Email 4: The Social Proof

**Subject:** How {Similar Practice} stopped losing clients at the front door

Hey {First Name},

Spoke with a three-partner firm in {City} last month. They were drowning in:

- Calls going to voicemail during client meetings
- Incomplete intake information when they finally called back
- Prospective clients choosing competitors who answered faster

Within 60 days, they had a structured intake system, professional after-hours coverage, and—most importantly—a 40% increase in new client conversions.

The best part? No additional staff. No training burden. Just a seamless experience for their callers.

Curious whether this could move the needle for your practice?

{Your Name}

---

## Email 5: The Close

**Subject:** Last chance to lock in this pricing, {First Name}

Hi {First Name},

We've helped dozens of legal practices transform their client intake. I'd love to show you what that could look like for {Firm Name}.

**Special offer for new legal clients:**

Full intake + call screening + after-hours line: ~~$1,200/month~~ **$897/month** for the first 3 months.

That's less than a junior receptionist—and far more professional.

Let's hop on a 15-minute call this week. No pressure—just a conversation about your current client intake flow.

Book a time here: {Calendar Link}

Cheers,

{Your Name}

---

**Quick notes for customisation:**

- Replace {City} and {Similar Practice} with firm-specific details
- Adjust pricing based on your actual offering
- Add case studies or testimonials if available
- Consider A/B testing subject lines for higher open rates


Cold Email - Automotive Workshops

Response:
# 5-Email Cold Outreach Sequence for Auto Mechanics & Workshops

---

## Email 1: Booking Service Appointments

Hi [Name],

Ever wish booking was easier—without the phone tag and no-shows eating your day?

Our booking system lets customers schedule 24/7, gets reminders automatically, and confirms everything in writing. No chasing. No confusion.

[Workshop Name] reduced missed appointments by 67% in their first month.

Worth a 15-minute chat?

Cheers,
[Your Name]

---

## Email 2: Providing Quotes for Common Services

Hi [Name],

Customers who wait hours for a quote go elsewhere. That's lost revenue.

We help workshops generate fast, accurate quotes for common services—oil changes, brake jobs, logbook services. Send via text or email. Update in seconds.

One mechanic told us: *"I quit doing quotes by hand. Now I send one in under 60 seconds."*

Want the demo?

Cheers,
[Your Name]

---

## Email 3: Handling Warranty Inquiries

Hi [Name],

Warranty claims shouldn't mean paperwork nightmares.

Our system tracks every job, stores photos, logs parts used, and auto-generates warranty documentation. What used to take 20 minutes now takes 2.

Plus—fight rejections with clear audit trails.

Book a quick walkthrough?

Cheers,
[Your Name]

---

## Email 4: Parts Availability Questions

Hi [Name],

"Nothing fits" or "we're still waiting" is the fastest way to lose trust.

Give customers real-time parts visibility. Check stock across suppliers, provide ETAs, and auto-notify when parts arrive.

Your reception desk stops playing phone tag. Customers stop calling. You get more jobs done on time.

Quick demo?

Cheers,
[Your Name]

---

## Email 5: Reducing Interruptions While Working on Vehicles

Hi [Name],

Nothing kills productivity like constant interruptions—customers calling asking "where's my car?", "when's it ready?", "how much?"

We let customers track their job online, get automatic updates, and message you only when it's urgent.

Your guys actually finish vehicles instead of answering the same questions all day.

See how it works in 10 minutes?

Cheers,
[Your Name]

---

*Each email stays under 150 words, leads with value, and ends with a clear call to action. Want me to adjust the tone, add personalization placeholders, or test different subject lines?*