Files
smoothschedule/smoothschedule/schedule/example_automation.py
poduck 3fef0d5749 feat: Add comprehensive plugin documentation and advanced template system
Added complete plugin documentation with visual mockups and expanded template
variable system with CONTEXT, DATE helpers, and default values.

Backend Changes:
- Extended template_parser.py to support all new template types
- Added PROMPT with default values: {{PROMPT:var|desc|default}}
- Added CONTEXT variables: {{CONTEXT:business_name}}, {{CONTEXT:owner_email}}
- Added DATE helpers: {{DATE:today}}, {{DATE:+7d}}, {{DATE:monday}}
- Implemented date expression evaluation for relative dates
- Updated compile_template to handle all template types
- Added context parameter for business data auto-fill

Frontend Changes:
- Created comprehensive HelpPluginDocs.tsx with Stripe-style API docs
- Added visual mockup of plugin configuration form
- Documented all template types with examples and benefits
- Added Command Reference section with allowed/blocked Python commands
- Documented all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Added URL whitelisting requirements and approval process
- Created Platform Staff management page with edit modal
- Added can_approve_plugins and can_whitelist_urls permissions

Platform Staff Features:
- List all platform_manager and platform_support users
- Edit user details with role-based permissions
- Superusers can edit anyone
- Platform managers can only edit platform_support users
- Permission cascade: users can only grant permissions they have
- Real-time updates via React Query cache invalidation

Documentation Highlights:
- 4 template types: PROMPT, CONTEXT, DATE, and automatic validation
- Visual form mockup showing exactly what users see
- All allowed control flow (if/elif/else, for, while, try/except, etc.)
- All allowed built-in functions (len, range, min, max, etc.)
- All blocked operations (import, exec, eval, class/function defs)
- Complete HTTP API reference with examples
- URL whitelisting process: contact pluginaccess@smoothschedule.com

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:54:07 -05:00

527 lines
18 KiB
Python

"""
Example: Smart Client Re-engagement Automation
This demonstrates a real-world automation that businesses would love:
Automatically win back customers who haven't booked in a while.
"""
from typing import Any, Dict
from django.utils import timezone
from django.core.mail import send_mail
from django.conf import settings
from django.db.models import Max, Count, Q
from datetime import timedelta
import logging
from .plugins import BasePlugin, register_plugin, PluginExecutionError
logger = logging.getLogger(__name__)
@register_plugin
class ClientReengagementPlugin(BasePlugin):
"""
Automatically re-engage customers who haven't booked recently.
This plugin:
1. Finds customers who haven't booked in X days
2. Sends personalized re-engagement emails
3. Offers optional discount codes
4. Tracks engagement metrics
"""
name = "client_reengagement"
display_name = "Client Re-engagement Campaign"
description = "Automatically reach out to customers who haven't booked recently with personalized offers"
category = "marketing"
config_schema = {
'days_inactive': {
'type': 'integer',
'required': False,
'default': 60,
'description': 'Target customers who haven\'t booked in this many days (default: 60)',
},
'email_subject': {
'type': 'string',
'required': False,
'default': 'We Miss You! Come Back Soon',
'description': 'Email subject line',
},
'email_message': {
'type': 'text',
'required': False,
'default': '''Hi {customer_name},
We noticed it's been a while since your last visit on {last_visit_date}. We'd love to see you again!
As a valued customer, we're offering you {discount}% off your next appointment.
Book now and use code: {promo_code}
Looking forward to seeing you soon!
{business_name}''',
'description': 'Email message template (variables: customer_name, last_visit_date, business_name, discount, promo_code)',
},
'discount_percentage': {
'type': 'integer',
'required': False,
'default': 15,
'description': 'Discount percentage to offer (default: 15%)',
},
'promo_code_prefix': {
'type': 'string',
'required': False,
'default': 'COMEBACK',
'description': 'Prefix for generated promo codes (default: COMEBACK)',
},
'max_customers_per_run': {
'type': 'integer',
'required': False,
'default': 50,
'description': 'Maximum customers to contact per execution (prevents spam)',
},
'exclude_already_contacted': {
'type': 'boolean',
'required': False,
'default': True,
'description': 'Skip customers who were already contacted by this campaign',
},
'minimum_past_visits': {
'type': 'integer',
'required': False,
'default': 1,
'description': 'Only target customers with at least this many past visits',
},
}
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""Execute the re-engagement campaign"""
from .models import Event
from smoothschedule.users.models import User
business = context.get('business')
if not business:
raise PluginExecutionError("No business context available")
# Get configuration
days_inactive = self.config.get('days_inactive', 60)
max_customers = self.config.get('max_customers_per_run', 50)
min_visits = self.config.get('minimum_past_visits', 1)
# Calculate cutoff date
cutoff_date = timezone.now() - timedelta(days=days_inactive)
# Find inactive customers
inactive_customers = self._find_inactive_customers(
cutoff_date=cutoff_date,
min_visits=min_visits,
max_count=max_customers
)
if not inactive_customers:
return {
'success': True,
'message': 'No inactive customers found',
'data': {
'customers_contacted': 0,
'emails_sent': 0,
}
}
# Send re-engagement emails
results = {
'customers_contacted': 0,
'emails_sent': 0,
'emails_failed': 0,
'customers': [],
}
for customer_data in inactive_customers:
try:
success = self._send_reengagement_email(
customer_data=customer_data,
business=business
)
if success:
results['emails_sent'] += 1
results['customers_contacted'] += 1
results['customers'].append({
'email': customer_data['email'],
'last_visit': customer_data['last_visit'].isoformat(),
'days_since_visit': customer_data['days_since_visit'],
})
else:
results['emails_failed'] += 1
except Exception as e:
logger.error(f"Failed to send re-engagement email to {customer_data['email']}: {e}")
results['emails_failed'] += 1
return {
'success': True,
'message': f"Contacted {results['customers_contacted']} inactive customers",
'data': results
}
def _find_inactive_customers(self, cutoff_date, min_visits, max_count):
"""Find customers who haven't booked recently"""
from .models import Event, Participant
from smoothschedule.users.models import User
from django.contrib.contenttypes.models import ContentType
# Get customer content type
customer_ct = ContentType.objects.get_for_model(User)
# Find customers with their last visit date
# This query finds all customers who participated in events
customer_participants = Participant.objects.filter(
role=Participant.Role.CUSTOMER,
content_type=customer_ct,
).values('object_id').annotate(
last_event_date=Max('event__end_time'),
total_visits=Count('event', filter=Q(event__status__in=['COMPLETED', 'PAID']))
).filter(
last_event_date__lt=cutoff_date, # Last visit before cutoff
total_visits__gte=min_visits, # Minimum number of visits
).order_by('last_event_date')[:max_count]
# Get customer details
inactive_customers = []
for participant_data in customer_participants:
try:
customer = User.objects.get(id=participant_data['object_id'])
# Skip if no email
if not customer.email:
continue
days_since_visit = (timezone.now() - participant_data['last_event_date']).days
inactive_customers.append({
'id': customer.id,
'email': customer.email,
'name': customer.get_full_name() or customer.username,
'last_visit': participant_data['last_event_date'],
'days_since_visit': days_since_visit,
'total_visits': participant_data['total_visits'],
})
except User.DoesNotExist:
continue
return inactive_customers
def _send_reengagement_email(self, customer_data, business):
"""Send personalized re-engagement email to a customer"""
# Generate promo code
promo_prefix = self.config.get('promo_code_prefix', 'COMEBACK')
promo_code = f"{promo_prefix}{customer_data['id']}"
# Format email message
email_template = self.config.get('email_message', self.config_schema['email_message']['default'])
email_body = email_template.format(
customer_name=customer_data['name'],
last_visit_date=customer_data['last_visit'].strftime('%B %d, %Y'),
business_name=business.name if business else 'Our Business',
discount=self.config.get('discount_percentage', 15),
promo_code=promo_code,
)
subject = self.config.get('email_subject', 'We Miss You! Come Back Soon')
try:
send_mail(
subject=subject,
message=email_body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[customer_data['email']],
fail_silently=False,
)
logger.info(f"Sent re-engagement email to {customer_data['email']} (promo: {promo_code})")
return True
except Exception as e:
logger.error(f"Failed to send email to {customer_data['email']}: {e}")
return False
@register_plugin
class LowShowRateAlertPlugin(BasePlugin):
"""
Alert business owners when no-show rate is unusually high.
Helps businesses identify issues early and take corrective action.
"""
name = "low_show_rate_alert"
display_name = "No-Show Rate Alert"
description = "Alert when customer no-show rate exceeds threshold"
category = "monitoring"
config_schema = {
'threshold_percentage': {
'type': 'integer',
'required': False,
'default': 20,
'description': 'Alert if no-show rate exceeds this percentage (default: 20%)',
},
'lookback_days': {
'type': 'integer',
'required': False,
'default': 7,
'description': 'Analyze appointments from the last N days (default: 7)',
},
'alert_emails': {
'type': 'list',
'required': True,
'description': 'Email addresses to notify',
},
'min_appointments': {
'type': 'integer',
'required': False,
'default': 10,
'description': 'Minimum appointments needed before alerting (avoid false positives)',
},
}
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""Check no-show rate and alert if too high"""
from .models import Event
lookback_days = self.config.get('lookback_days', 7)
threshold = self.config.get('threshold_percentage', 20)
min_appointments = self.config.get('min_appointments', 10)
# Calculate date range
start_date = timezone.now() - timedelta(days=lookback_days)
# Get appointment statistics
total_appointments = Event.objects.filter(
start_time__gte=start_date,
start_time__lt=timezone.now(),
).count()
if total_appointments < min_appointments:
return {
'success': True,
'message': f'Not enough data ({total_appointments} appointments, need {min_appointments})',
'data': {'appointments': total_appointments}
}
canceled_count = Event.objects.filter(
start_time__gte=start_date,
start_time__lt=timezone.now(),
status='CANCELED',
).count()
no_show_rate = (canceled_count / total_appointments) * 100
if no_show_rate >= threshold:
# Send alert
business = context.get('business')
self._send_alert(
business=business,
no_show_rate=no_show_rate,
canceled_count=canceled_count,
total_appointments=total_appointments,
lookback_days=lookback_days
)
return {
'success': True,
'message': f'ALERT: No-show rate is {no_show_rate:.1f}% (threshold: {threshold}%)',
'data': {
'no_show_rate': round(no_show_rate, 2),
'canceled': canceled_count,
'total': total_appointments,
'alert_sent': True,
}
}
else:
return {
'success': True,
'message': f'No-show rate is healthy: {no_show_rate:.1f}%',
'data': {
'no_show_rate': round(no_show_rate, 2),
'canceled': canceled_count,
'total': total_appointments,
'alert_sent': False,
}
}
def _send_alert(self, business, no_show_rate, canceled_count, total_appointments, lookback_days):
"""Send alert email to business owners"""
alert_emails = self.config.get('alert_emails', [])
subject = f"⚠️ High No-Show Rate Alert - {business.name if business else 'Your Business'}"
message = f"""
Alert: Your no-show rate is unusually high!
No-Show Rate: {no_show_rate:.1f}%
Period: Last {lookback_days} days
Canceled Appointments: {canceled_count} out of {total_appointments}
Recommended Actions:
1. Review your confirmation process
2. Send appointment reminders 24 hours before
3. Implement a cancellation policy
4. Follow up with customers who no-showed
This is an automated alert from your scheduling system.
"""
try:
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=alert_emails,
fail_silently=False,
)
logger.info(f"Sent no-show alert to {len(alert_emails)} recipient(s)")
except Exception as e:
logger.error(f"Failed to send no-show alert: {e}")
@register_plugin
class PeakHoursAnalyzerPlugin(BasePlugin):
"""
Analyze booking patterns to identify peak hours and make staffing recommendations.
"""
name = "peak_hours_analyzer"
display_name = "Peak Hours Analyzer"
description = "Analyze booking patterns and recommend optimal staffing"
category = "analytics"
config_schema = {
'analysis_days': {
'type': 'integer',
'required': False,
'default': 30,
'description': 'Analyze data from the last N days (default: 30)',
},
'report_emails': {
'type': 'list',
'required': True,
'description': 'Email addresses to receive the analysis report',
},
'group_by': {
'type': 'choice',
'choices': ['hour', 'day_of_week', 'both'],
'required': False,
'default': 'both',
'description': 'How to group the analysis',
},
}
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze booking patterns and send report"""
from .models import Event
from collections import defaultdict
analysis_days = self.config.get('analysis_days', 30)
start_date = timezone.now() - timedelta(days=analysis_days)
# Get all appointments in the period
events = Event.objects.filter(
start_time__gte=start_date,
status__in=['COMPLETED', 'PAID', 'SCHEDULED']
).values_list('start_time', flat=True)
# Analyze by hour
hourly_counts = defaultdict(int)
weekday_counts = defaultdict(int)
for event_time in events:
hour = event_time.hour
weekday = event_time.strftime('%A')
hourly_counts[hour] += 1
weekday_counts[weekday] += 1
# Find peak hours
peak_hours = sorted(hourly_counts.items(), key=lambda x: x[1], reverse=True)[:5]
peak_days = sorted(weekday_counts.items(), key=lambda x: x[1], reverse=True)[:3]
# Generate report
report = self._generate_report(
peak_hours=peak_hours,
peak_days=peak_days,
total_appointments=len(events),
analysis_days=analysis_days,
business=context.get('business')
)
# Send report
self._send_report(report)
return {
'success': True,
'message': 'Peak hours analysis completed',
'data': {
'peak_hours': [f"{h}:00" for h, _ in peak_hours],
'peak_days': [d for d, _ in peak_days],
'total_analyzed': len(events),
}
}
def _generate_report(self, peak_hours, peak_days, total_appointments, analysis_days, business):
"""Generate human-readable report"""
report = f"""
Peak Hours Analysis Report
Business: {business.name if business else 'Your Business'}
Period: Last {analysis_days} days
Total Appointments: {total_appointments}
TOP 5 BUSIEST HOURS:
"""
for hour, count in peak_hours:
percentage = (count / total_appointments * 100) if total_appointments > 0 else 0
time_label = f"{hour}:00 - {hour+1}:00"
report += f" {time_label}: {count} appointments ({percentage:.1f}%)\n"
report += f"\nTOP 3 BUSIEST DAYS:\n"
for day, count in peak_days:
percentage = (count / total_appointments * 100) if total_appointments > 0 else 0
report += f" {day}: {count} appointments ({percentage:.1f}%)\n"
report += f"""
RECOMMENDATIONS:
• Ensure adequate staffing during peak hours
• Consider offering promotions during slower periods
• Use this data to optimize your schedule
• Review this analysis monthly to spot trends
This is an automated report from your scheduling system.
"""
return report
def _send_report(self, report):
"""Send the analysis report via email"""
recipients = self.config.get('report_emails', [])
try:
send_mail(
subject="📊 Peak Hours Analysis Report",
message=report,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipients,
fail_silently=False,
)
logger.info(f"Sent peak hours report to {len(recipients)} recipient(s)")
except Exception as e:
logger.error(f"Failed to send peak hours report: {e}")