ferrero-opentext/Python-Version/scripts/shared/notifier.py
nickviljoen f28b5221f7 Enhancement: Capture CreativeX score on B1→B2 global masters
Extracts CreativeX score and URL from DAM master metadata during the
B1→B2 download, persists to creativex_scores with new status
'b1-master-cx-score' (dedup by tracking_id), and surfaces the score in
the b1_to_b2_complete and b1_to_b2_partial emails — falling back to
"No CreativeX Score" when the master has no score yet. Skipped
already-downloaded assets backfill from full_metadata JSONB on next pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 11:31:07 +02:00

1243 lines
90 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Notifier - Email and Webhook Notifications
Handles SMTP emails (Mailgun) and outgoing webhooks
Compatible with Python 3.6+
"""
import requests
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Template
logger = logging.getLogger('Notifier')
class Notifier:
def __init__(self, config):
self.config = config
self.enabled = config['notifications']['enabled']
# SMTP configuration
smtp_config = config['notifications'].get('smtp', {})
self.smtp_server = smtp_config.get('server')
self.smtp_port = smtp_config.get('port', 587)
self.smtp_user = smtp_config.get('user')
self.smtp_password = smtp_config.get('password')
self.sender_email = smtp_config.get('sender_email')
# Mailgun API configuration (preferred over SMTP when configured)
mailgun_config = config['notifications'].get('mailgun', {})
self.mailgun_api_key = mailgun_config.get('api_key')
self.mailgun_domain = mailgun_config.get('domain')
self.mailgun_sender = mailgun_config.get('sender_email') or self.sender_email
self.recipients = config['notifications']['recipients']
self.webhook_config = config.get('webhooks', {})
def send_email(self, template_name, recipients, data, attachments=None):
"""
Send email via SMTP (Mailgun)
Args:
template_name: Name of email template
recipients: List of email addresses or single email
data: Template data dict
attachments: List of file paths to attach (optional)
"""
if not self.enabled:
logger.info("Notifications disabled, skipping email")
return
if not self.mailgun_api_key and (not self.smtp_server or not self.smtp_user):
logger.warning("Neither Mailgun API nor SMTP configured, skipping email")
return
try:
# Simple templates (full template system would load from YAML)
templates = {
'a1_to_a2_complete': {
'subject': "✅ Master Assets Downloaded - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ Master Assets Downloaded Successfully</h1>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ asset_count }}
{% if existing_asset_count and existing_asset_count > 0 %}
({{ existing_asset_count }} previously downloaded, <strong>{{ new_asset_count }} new this run</strong>)
{% endif %}
</p>
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A1 → A2</p>
</div>
{% if new_assets is defined %}
{% if new_assets|length > 0 %}
<h3 style="margin-top: 30px; color: #28a745;">🆕 New This Run ({{ new_assets|length }}):</h3>
{% for asset in new_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if existing_assets is defined and existing_assets|length > 0 %}
<h3 style="margin-top: 30px; color: #666;">📁 Previously Downloaded ({{ existing_assets|length }}):</h3>
<div style="border: 1px solid #ddd; padding: 10px 15px; background-color: #f5f5f5; border-radius: 4px;">
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">These files were already in Box from an earlier run and were skipped.</p>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #555;">
{% for asset in existing_assets %}
<li style="margin: 3px 0;">{{ asset.asset_name }} <code style="color: #888; font-size: 11px;">({{ asset.tracking_id }})</code></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% else %}
<h3 style="margin-top: 30px; color: #333;">Processed Assets:</h3>
{% for asset in processed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✓ Complete:</strong> All assets downloaded from DAM and uploaded to Box with tracking IDs.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from A1 to A2. Box Folder: 348304357505 (Local Adaptation)</p>
</div>
"""
},
'a2_to_a3_complete': {
'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ Localized Assets Uploaded Successfully</h1>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }}</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Assets Uploaded:</strong> {{ asset_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A2 → A3</p>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✓ Complete:</strong> All localized assets have been uploaded to DAM.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from A2 to A3.</p>
</div>
"""
},
'a2_to_a3_batch_complete': {
'subject': "A2→A3 Batch Upload Complete - {successful_count}/{total_files} Successful",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: {% if failed_count == 0 %}#28a745{% else %}#ff9800{% endif %}; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">{% if failed_count == 0 %}✅ Batch Upload Complete{% else %}⚠️ Batch Upload Partial{% endif %}</h1>
</div>
<div style="background-color: {% if failed_count == 0 %}#d4edda{% else %}#fff3cd{% endif %}; border-left: 4px solid {% if failed_count == 0 %}#28a745{% else %}#ffc107{% endif %}; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Total Files:</strong> {{ total_files }}</p>
<p style="margin: 5px 0 0 0;"><strong>✓ Successful:</strong> {{ successful_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>✗ Failed:</strong> {{ failed_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>Source Folder:</strong> {{ box_folder }}</p>
</div>
{% if successful_files %}
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Uploaded ({{ successful_count }}):</h3>
{% for asset in successful_files %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.clean_filename }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Original File:</span> {{ asset.filename }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Asset ID:</span> {{ asset.asset_id }}</p>
{% if asset.subfolder_path %}
<p style="margin: 5px 0;"><span style="font-weight: bold;">Folder Path:</span> {{ asset.subfolder_path }}</p>
{% endif %}
{% if asset.creativex_found %}
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {{ asset.creativex_score }}</p>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if failed_files %}
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Uploads ({{ failed_count }}):</h3>
{% for asset in failed_files %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.filename }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
{% if asset.tracking_id %}<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> {{ asset.tracking_id }}</p>{% endif %}
{% if asset.subfolder_path %}
<p style="margin: 5px 0;"><span style="font-weight: bold;">Folder Path:</span> {{ asset.subfolder_path }}</p>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Status:</strong> Batch processing complete.</p>
</div>
</div>
"""
},
'upload_failed': {
'subject': "❌ Upload Failed - {filename}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">❌ Upload Failed</h1>
</div>
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Filename:</strong> {{ filename }}</p>
<p style="margin: 5px 0 0 0;"><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
</div>
<div style="border: 1px solid #ddd; margin: 20px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>Error Details</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0; color: #d32f2f;"><strong>Error:</strong> {{ error }}</p>
</div>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 Action Required:</strong> Please investigate the error and retry the upload.</p>
</div>
</div>
"""
},
'a1_to_a2_partial': {
'subject': "⚠️ Partial Download - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ Campaign Partially Processed</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ total_assets }}</p>
<p style="margin: 5px 0 0 0;"><strong>Successful:</strong> {{ successful }} | <strong>Failed:</strong> {{ failed }}</p>
</div>
{% if successful > 0 %}
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Processed ({{ successful }}):</h3>
{% for asset in processed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if failed > 0 %}
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Assets ({{ failed }}):</h3>
{% for asset in failed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
</div>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
<p style="margin: 0; color: #d32f2f;"><strong>⚠️ Status NOT updated.</strong> Campaign remains at A1.</p>
<p style="margin: 5px 0 0 0;">The script will retry failed assets on the next run (every 5 minutes).</p>
</div>
</div>
"""
},
'a2_to_a3_file_uploaded': {
'subject': "✅ Asset Uploaded to DAM - {clean_filename}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ Asset Uploaded Successfully (A2→A3)</h1>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Original Filename (from Box):</strong> <code>{{ filename }}</code></p>
<p style="margin: 5px 0 0 0;"><strong>Clean Filename (in DAM):</strong> <code>{{ clean_filename }}</code></p>
<p style="margin: 5px 0 0 0;"><strong>DAM Asset ID:</strong> <code>{{ asset_id }}</code></p>
<p style="margin: 5px 0 0 0;"><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
</div>
<h3 style="margin-top: 30px; color: #333;">Processing Details:</h3>
<div style="padding: 15px; background-color: #f8f9fa; border-radius: 4px; margin: 10px 0;">
<p style="margin: 5px 0;"><strong>Master Asset ID:</strong> {{ master_asset_name }}</p>
<p style="margin: 5px 0;"><strong>Uploaded to DAM Folder:</strong> {{ upload_folder }}</p>
<p style="margin: 5px 0;"><strong>Downloaded from Box Folder:</strong> {{ box_folder }}</p>
</div>
<h3 style="margin-top: 30px; color: #333;">What Was Done:</h3>
<div style="padding: 15px; background-color: #f8f9fa; border-radius: 4px; margin: 10px 0;">
<ul style="margin: 10px 0; padding-left: 20px;">
<li>✅ Downloaded from Box processing folder (348526703108)</li>
<li>✅ Loaded master metadata from database ({{ tracking_id }})</li>
<li>✅ Built asset representation with 27 MVP fields</li>
<li>✅ Updated Description from filename</li>
<li>✅ Updated Language from filename</li>
<li>✅ Set State to "Local"</li>
{% if creativex_found %}
<li>✅ <strong>CreativeX Score Added:</strong> {{ creativex_score }} (from database)</li>
{% else %}
<li>⚠️ <strong>CreativeX Score:</strong> Not found - used default (0)</li>
{% endif %}
<li>✅ Stripped OMG Job Number and Tracking ID from filename</li>
<li>✅ Uploaded to DAM Final Assets folder</li>
<li>✅ <strong>Deleted file from Box</strong></li>
</ul>
</div>
{% if not creativex_found %}
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>⚠️ CreativeX Score Missing</strong></p>
<p style="margin: 5px 0 0 0;">No CreativeX score found in database for: <code>{{ clean_filename }}</code></p>
<p style="margin: 5px 0 0 0;"><strong>Default Values Used:</strong></p>
<ul style="margin: 5px 0 0 20px; padding: 0;">
<li>Score: 0</li>
<li>URL: None (no CreativeX URL sent)</li>
</ul>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
<em>To add CreativeX score: Upload PDF report to Box folder 350605024645 and run creativex_scoring_storing.py</em>
</p>
</div>
{% endif %}
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Status:</strong> Asset processing complete.</p>
<p style="margin: 5px 0 0 0;"><em>Note: Campaign status will be updated to A3 once all assets are uploaded.</em></p>
</div>
</div>
"""
},
'b1_to_b2_complete': {
'subject': "✅ Global Master Assets Downloaded - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #1976d2; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ Global Master Assets Downloaded Successfully</h1>
</div>
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters</p>
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ asset_count }}
{% if existing_asset_count and existing_asset_count > 0 %}
({{ existing_asset_count }} previously downloaded, <strong>{{ new_asset_count }} new this run</strong>)
{% endif %}
</p>
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> B1 → B2</p>
</div>
{% if new_assets is defined %}
{% if new_assets|length > 0 %}
<h3 style="margin-top: 30px; color: #1976d2;">🆕 New This Run ({{ new_assets|length }}):</h3>
{% for asset in new_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #1976d2; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if existing_assets is defined and existing_assets|length > 0 %}
<h3 style="margin-top: 30px; color: #666;">📁 Previously Downloaded ({{ existing_assets|length }}):</h3>
<div style="border: 1px solid #ddd; padding: 10px 15px; background-color: #f5f5f5; border-radius: 4px;">
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">These files were already in Box from an earlier run and were skipped.</p>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #555;">
{% for asset in existing_assets %}
<li style="margin: 3px 0;">{{ asset.asset_name }} <code style="color: #888; font-size: 11px;">({{ asset.tracking_id }})</code> &mdash; <span style="font-size: 12px;">CreativeX: {% if asset.creativex_score %}{{ asset.creativex_score }}{% else %}<span style="color: #999;">none</span>{% endif %}</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% else %}
<h3 style="margin-top: 30px; color: #333;">Processed Assets:</h3>
{% for asset in processed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #1976d2; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✓ Complete:</strong> All Global Master assets downloaded from DAM and uploaded to Box with tracking IDs.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from B1 to B2. Box Folder: 349261192115 (Global Masters)</p>
</div>
"""
},
'b1_to_b2_partial': {
'subject': "⚠️ Partial Download - Global Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ Global Campaign Partially Processed</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters (B1→B2)</p>
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ total_assets }}</p>
<p style="margin: 5px 0 0 0;"><strong>Successful:</strong> {{ successful }} | <strong>Failed:</strong> {{ failed }}</p>
</div>
{% if successful > 0 %}
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Processed ({{ successful }}):</h3>
{% for asset in processed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% if failed > 0 %}
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Assets ({{ failed }}):</h3>
{% for asset in failed_assets %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
</div>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
<p style="margin: 0; color: #d32f2f;"><strong>⚠️ Status NOT updated.</strong> Campaign remains at B1.</p>
<p style="margin: 5px 0 0 0;">The script will retry failed assets on the next run (every 5 minutes).</p>
</div>
</div>
"""
},
'a5_to_a6_rejections': {
'subject': "⚠️ NOT APPROVED Assets - Rework Required - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ NOT APPROVED ASSETS - REWORK REQUIRED</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>⚠️ Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>NOT APPROVED assets:</strong> {{ rejected_count }} | <strong>Approved/skipped:</strong> {{ skipped_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A5 → A6</p>
</div>
{% for asset in rejected_assets %}
<div style="border: 1px solid #ddd; margin: 20px 0; padding: 20px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #424242; color: white; padding: 12px 15px; margin: -20px -20px 20px -20px; border-radius: 4px 4px 0 0;">
<strong>{{ asset.asset_name }}</strong>
</div>
<div style="margin: 10px 0; padding: 10px; background-color: white; border-radius: 4px;">
<p><span style="font-weight: bold;">Status:</span> <span style="color: #d32f2f; font-weight: bold;">NOT APPROVED</span></p>
<p><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code>{% if asset.is_existing %} <span style="color: #007bff;">(Updated existing)</span>{% endif %}</p>
<p><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
{% if asset.folder_path %}<p><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
</div>
{# Rejection details commented out
{% if asset.rejection_details %}
{% set approver = asset.rejection_details.approver %}
{% set legal = asset.rejection_details.legal %}
{% set ia_cc = asset.rejection_details.ia_cc %}
{% if approver.comment %}
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">🔴 APPROVER REJECTION</div>
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ approver.comment }}</p>
{% if approver.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ approver.certifier_name }}</p>{% endif %}
{% if approver.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ approver.date }}</p>{% endif %}
</div>
{% endif %}
{% if legal.comment %}
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">⚖️ LEGAL REJECTION</div>
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ legal.comment }}</p>
{% if legal.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ legal.certifier_name }}</p>{% endif %}
{% if legal.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ legal.date }}</p>{% endif %}
</div>
{% endif %}
{% if ia_cc.comment %}
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">🔍 IA&CC REJECTION</div>
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ ia_cc.comment }}</p>
{% if ia_cc.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ ia_cc.certifier_name }}</p>{% endif %}
{% if ia_cc.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ ia_cc.date }}</p>{% endif %}
</div>
{% endif %}
{% if not approver.comment and not legal.comment and not ia_cc.comment %}
<div style="color: #999; font-style: italic; padding: 10px;">(No specific rejection comments available)</div>
{% endif %}
{% else %}
<div style="color: #999; font-style: italic; padding: 10px;">(No rejection details available)</div>
{% endif %}
#}
</div>
{% endfor %}
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<strong>📌 Next Steps:</strong> Please review and rework the above assets according to the rejection comments.
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">All rejected assets have been downloaded to Box Revisions folder (349441822875). Campaign status updated A5 → A6.</p>
</div>
"""
},
'a5_to_a6_partial': {
'subject': "⚠️ Partial Rework Download - Campaign {campaign_name}",
'html': """
<h2 style="color: orange;">Rework Campaign Partially Processed (A5→A6)</h2>
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
<p><strong>Total NOT APPROVED:</strong> {{ total_assets }}</p>
<p><strong>Successful:</strong> {{ successful }}</p>
<p><strong>Failed:</strong> {{ failed }}</p>
<hr>
{% if successful > 0 %}
<h3 style="color: green;">✅ Successfully Processed ({{ successful }}):</h3>
<ul>
{% for asset in rejected_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>{% if asset.is_existing %} <span style="color: #007bff;">(Updated existing)</span>{% endif %}
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if failed > 0 %}
<h3 style="color: red;">❌ Failed Assets ({{ failed }}):</h3>
<ul>
{% for asset in failed_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br><span style="color: red;">Error: {{ asset.error }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
<hr>
<p style="color: red;"><strong>Status NOT updated.</strong> Campaign remains at A5.</p>
<p>The script will retry failed assets on the next run (every 5 minutes).</p>
"""
},
'a5_to_a6_no_rejections': {
'subject': "✅ No Rework Required - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ No Rework Required</h1>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Total assets checked:</strong> {{ total_assets }}</p>
<p style="margin: 5px 0 0 0;"><strong>Approved/other status:</strong> {{ skipped_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>NOT APPROVED assets:</strong> 0</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #28a745; margin-top: 0;">Good News!</h3>
<p>All assets in the Final Assets folder are approved or have other status.</p>
<p>No assets with "NOT APPROVED" status were found, so no rework is required.</p>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 Note:</strong></p>
<ul style="margin: 10px 0;">
<li>No downloads were performed</li>
<li>Status NOT updated (remains A5)</li>
<li>Script will check again on next run</li>
</ul>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">A5→A6 script completed successfully with no rejected assets found.</p>
</div>
"""
},
'a1_to_a2_no_assets': {
'subject': "⚠️ No Assets Found - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ No Master Assets Found</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to A1 but No Assets Found</h3>
<p>The Master Assets folder was searched (including subfolders) but no assets were found.</p>
<p>This campaign is set to status A1 but appears to have no master assets ready for download.</p>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
<ul style="margin: 10px 0;">
<li>No downloads were performed</li>
<li>Status NOT updated (remains A1)</li>
<li>Script will check again on next run (every 5 minutes)</li>
<li>Please verify assets exist in Master Assets folder</li>
</ul>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script completed with no assets to process.</p>
</div>
"""
},
'a1_to_a2_no_assets_retry': {
'subject': "⚠️ No Assets Found (Attempt {retry_count}/3) - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ No Master Assets Found (Retry {{ retry_count }}/{{ max_retries }})</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
<p style="margin: 5px 0 0 0;"><strong>Retry Attempt:</strong> {{ retry_count }} of {{ max_retries }}</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to A1 but No Assets Found</h3>
<p>The Master Assets folder was searched (including subfolders) but no assets were found.</p>
<p>This campaign is set to status A1 but appears to have no master assets ready for download.</p>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
<ul style="margin: 10px 0;">
<li>This is attempt <strong>{{ retry_count }}</strong> of <strong>{{ max_retries }}</strong></li>
<li>System will retry automatically on next run (every 3 minutes)</li>
{% if retry_count < max_retries %}
<li><strong>{{ max_retries - retry_count }} attempt(s) remaining</strong> before marking as permanently failed</li>
{% else %}
<li style="color: #d32f2f;"><strong>WARNING: This is the final attempt!</strong> Next failure will mark campaign as permanently failed.</li>
{% endif %}
<li>Please verify assets exist in Master Assets folder</li>
</ul>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script will retry automatically. No action needed unless this persists.</p>
</div>
"""
},
'a1_to_a2_no_assets_warning': {
'subject': "⚠️ Campaign in A1 with no assets yet - {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ Campaign in A1 with No Assets Yet</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
<p style="margin: 5px 0 0 0;"><strong>Polls with empty folder:</strong> {{ poll_count }}</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #ff9800; margin-top: 0;">Master Assets Folder Has Been Empty for ~1 Hour</h3>
<p>This campaign has been at status A1 for roughly an hour with no master assets in the folder.</p>
<p>This is often expected — the folder may have been created before assets were uploaded — and the system will keep checking automatically.</p>
<p>This is a <strong>one-time warning</strong>; no further emails will be sent for this campaign.</p>
</div>
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 Action only needed if:</strong></p>
<ul style="margin: 10px 0;">
<li>You expected assets to be uploaded already</li>
<li>The campaign was set to A1 by mistake (change the status in DAM)</li>
</ul>
<p style="margin: 10px 0 0 0;">Otherwise no action needed — processing will start automatically as soon as assets appear in the Master Assets folder.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script will continue to check silently every 3 minutes.</p>
</div>
"""
},
'a1_to_a2_permanently_failed': {
'subject': "❌ PERMANENTLY FAILED - Campaign {campaign_name} (No Assets After 3 Attempts)",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">❌ CAMPAIGN PERMANENTLY FAILED</h1>
</div>
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
<p style="margin: 5px 0 0 0;"><strong>Failed Attempts:</strong> {{ retry_count }} / {{ max_retries }}</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #d32f2f; margin-top: 0;">Campaign Marked as Permanently Failed</h3>
<p>After {{ max_retries }} consecutive attempts, the system was unable to find any master assets in the Master Assets folder.</p>
<p><strong>This campaign will no longer be processed automatically.</strong></p>
</div>
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>🔧 Required Actions:</strong></p>
<ol style="margin: 10px 0;">
<li>Verify the campaign should actually be in A1 status</li>
<li>Check if Master Assets folder exists and contains files</li>
<li>If this is a mistake, change campaign status to something else</li>
<li>If assets need to be added, add them to Master Assets folder</li>
<li><strong>Once fixed, manually reset the retry counter</strong></li>
</ol>
</div>
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>💡 How to Reset This Campaign:</strong></p>
<p style="margin: 10px 0; padding: 15px; background-color: white; border-radius: 4px;">
To reset the status and retry this campaign, please contact support at: <br>
<strong><a href="mailto:optical@oliver.agency" style="color: #1976d2;">optical@oliver.agency</a></strong>
</p>
<p style="margin: 5px 0 0 0; font-size: 12px; color: #666;">Support will reset the retry counter and investigate the issue.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Automated processing stopped. Manual intervention required.</p>
</div>
"""
},
'b1_to_b2_no_assets': {
'subject': "⚠️ No Assets Found - Global Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ No Global Master Assets Found</h1>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> B1</p>
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters</p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to B1 but No Assets Found</h3>
<p>The Final Assets folder was searched (including subfolders) but no assets were found.</p>
<p>This Global Masters campaign is set to status B1 but appears to have no assets ready for download.</p>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
<ul style="margin: 10px 0;">
<li>No downloads were performed</li>
<li>Status NOT updated (remains B1)</li>
<li>Script will check again on next run (every 5 minutes)</li>
<li>Please verify assets exist in Final Assets folder</li>
</ul>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">B1→B2 script completed with no assets to process.</p>
</div>
"""
},
'a4_webhook_sent': {
'subject': "📢 A4 Campaign Webhook Sent - {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #607d8b; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">📢 A4 Campaign Webhook Sent</h1>
</div>
<div style="background-color: #eceff1; border-left: 4px solid #607d8b; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A4 (Not Going Live)</p>
<p style="margin: 5px 0 0 0;"><strong>Live Campaign:</strong> <span style="color: #d32f2f; font-weight: bold;">NO</span></p>
</div>
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #607d8b; margin-top: 0;">Webhook Notification Sent</h3>
<p>A webhook notification has been sent indicating this campaign is marked as A4 and will not go live.</p>
<p><strong>Webhook URL:</strong> <code style="font-size: 11px;">{{ webhook_url }}</code></p>
</div>
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Payload Sent:</strong></p>
<ul style="margin: 10px 0;">
<li>campaign_id: {{ campaign_id }}</li>
<li>campaign_number: {{ campaign_number }}</li>
<li>campaign_name: {{ campaign_name }}</li>
<li>status: A4</li>
<li><strong>live_campaign: NO</strong></li>
</ul>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">A4 webhook monitor completed successfully.</p>
</div>
"""
},
'daily_report': {
'subject': "📊 Ferrero Automation Daily Report - {report_date}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto;">
<div style="background-color: #1976d2; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">📊 Ferrero Automation Daily Report</h1>
<p style="margin: 10px 0 0 0; opacity: 0.9;">{{ report_date }} - Generated at {{ report_time }}</p>
</div>
<!-- Overall Summary -->
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
<h2 style="margin: 0 0 10px 0; color: #1976d2;">📈 Overall Summary (Last 24 Hours)</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
<div><strong>Campaigns Found:</strong> {{ total_stats.campaigns_found }}</div>
<div><strong>Campaigns Processed:</strong> {{ total_stats.campaigns_processed }}</div>
<div><strong>✅ Completed:</strong> <span style="color: #28a745;">{{ total_stats.campaigns_completed }}</span></div>
<div><strong>⚠️ Partial:</strong> <span style="color: #ff9800;">{{ total_stats.campaigns_partial }}</span></div>
<div><strong>No Assets:</strong> {{ total_stats.campaigns_no_assets }}</div>
<div><strong>Total Assets:</strong> {{ total_stats.total_assets }}</div>
<div><strong>✓ Successful:</strong> <span style="color: #28a745;">{{ total_stats.assets_successful }}</span></div>
<div><strong>✗ Failed:</strong> <span style="color: #d32f2f;">{{ total_stats.assets_failed }}</span></div>
{% if total_stats.assets_skipped > 0 %}<div><strong>Skipped (Approved):</strong> {{ total_stats.assets_skipped }}</div>{% endif %}
{% if total_stats.not_approved_count > 0 %}<div><strong>🔴 NOT APPROVED:</strong> {{ total_stats.not_approved_count }}</div>{% endif %}
</div>
{% if total_stats.total_assets > 0 %}
<div style="margin-top: 15px; padding: 10px; background-color: white; border-radius: 4px;">
<strong>Success Rate:</strong>
<span style="font-size: 24px; font-weight: bold; color: {% if total_stats.success_rate >= 95 %}#28a745{% elif total_stats.success_rate >= 80 %}#ff9800{% else %}#d32f2f{% endif %};">
{{ "%.1f"|format(total_stats.success_rate) }}%
</span>
</div>
{% endif %}
</div>
<!-- Workflow Breakdown -->
<h2 style="margin: 30px 0 20px 0; color: #333;">🔄 Workflow Breakdown</h2>
{% for workflow_name, stats in workflow_stats.items() %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 0; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #424242; color: white; padding: 12px 15px; border-radius: 4px 4px 0 0;">
<strong>{{ workflow_name }}</strong>
</div>
<div style="padding: 15px;">
{% if stats.campaigns_processed > 0 %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;">
<div><strong>Campaigns:</strong> {{ stats.campaigns_processed }}</div>
<div><strong>Assets:</strong> {{ stats.total_assets }}</div>
<div style="color: #28a745;"><strong>✓ Successful:</strong> {{ stats.assets_successful }}</div>
<div style="color: #d32f2f;"><strong>✗ Failed:</strong> {{ stats.assets_failed }}</div>
{% if stats.assets_skipped > 0 %}<div><strong>Skipped:</strong> {{ stats.assets_skipped }}</div>{% endif %}
{% if stats.not_approved_count > 0 %}<div style="color: #d32f2f;"><strong>NOT APPROVED:</strong> {{ stats.not_approved_count }}</div>{% endif %}
</div>
{% if stats.campaign_details %}
<details style="margin-top: 10px;">
<summary style="cursor: pointer; font-weight: bold; color: #1976d2;">View Campaign Details ({{ stats.campaign_details|length }})</summary>
<div style="margin-top: 10px;">
{% for campaign in stats.campaign_details %}
<div style="padding: 8px; margin: 5px 0; background-color: white; border-left: 3px solid {% if campaign.status == 'completed' %}#28a745{% elif campaign.status == 'partial' %}#ff9800{% else %}#999{% endif %}; border-radius: 2px;">
<strong>{{ campaign.name }}</strong> ({{ campaign.number }})
<br><small>Total: {{ campaign.total_assets }}, Success: {{ campaign.successful }}, Failed: {{ campaign.failed }}{% if campaign.get('skipped') %}, Skipped: {{ campaign.skipped }}{% endif %}</small>
</div>
{% endfor %}
</div>
</details>
{% endif %}
{% if stats.errors %}
<details style="margin-top: 10px;">
<summary style="cursor: pointer; font-weight: bold; color: #d32f2f;">⚠️ Errors ({{ stats.errors|length }})</summary>
<div style="margin-top: 10px;">
{% for error in stats.errors[:10] %}
<div style="padding: 5px; margin: 3px 0; background-color: #ffebee; border-radius: 2px; font-size: 12px;">
{{ error }}
</div>
{% endfor %}
{% if stats.errors|length > 10 %}
<div style="padding: 5px; margin: 3px 0; font-style: italic; color: #666;">
... and {{ stats.errors|length - 10 }} more errors
</div>
{% endif %}
</div>
</details>
{% endif %}
{% else %}
<p style="color: #999; font-style: italic; margin: 0;">No activity in the last 24 hours</p>
{% endif %}
</div>
</div>
{% endfor %}
<!-- Footer -->
<div style="background-color: #f8f9fa; padding: 15px; margin: 20px 0; border-radius: 4px; text-align: center;">
<p style="margin: 0; color: #666;">
<strong>📌 Automation Scripts:</strong> A1→A2, A2→A3, A5→A6, B1→B2
</p>
<p style="margin: 5px 0 0 0; color: #666; font-size: 12px;">
Scripts run every 5 minutes | Report generated daily at 7:00 PM
</p>
</div>
</div>
"""
},
'creativex_complete': {
'subject': "✅ CreativeX Scores Extracted - {file_count} files processed",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #9c27b0; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">✅ CreativeX Score Extraction Complete</h1>
</div>
<div style="background-color: #f3e5f5; border-left: 4px solid #9c27b0; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Files Processed:</strong> {{ file_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>Scores Extracted:</strong> {{ success_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
</div>
<h3 style="margin-top: 30px; color: #333;">Extracted Scores:</h3>
{% for score in processed_files %}
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
<div style="background-color: #9c27b0; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
<strong>{{ score.filename }}</strong>
{% if score.version_number > 1 %}<span style="float: right; background-color: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 3px; font-size: 12px;">Version {{ score.version_number }}{% if score.is_update %} (Updated){% endif %}</span>{% endif %}
</div>
<div style="padding: 10px; background-color: white; border-radius: 4px;">
<p style="margin: 5px 0;"><span style="font-weight: bold;">Quality Score:</span> <span style="font-size: 20px; color: #9c27b0;">{{ score.quality_score }}</span></p>
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX ID:</span> {{ score.creativex_id }}</p>
{% if score.creativex_url %}<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX URL:</span> <a href="{{ score.creativex_url }}">{{ score.creativex_url }}</a></p>{% endif %}
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ score.box_file_id }}</p>
{% if score.version_number > 1 %}<p style="margin: 5px 0; color: #ff9800;"><span style="font-weight: bold;">📝 Note:</span> This is version {{ score.version_number }} of this file (previous versions preserved in database)</p>{% endif %}
</div>
</div>
{% endfor %}
<div style="background-color: #f3e5f5; border-left: 4px solid #9c27b0; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✓ Complete:</strong> All scores extracted and stored in database.</p>
<p style="margin: 5px 0 0 0;"><strong>Files Removed:</strong> Processed PDFs deleted from Box folder.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">CreativeX scores stored with full JSON for future lookups.</p>
</div>
"""
},
'creativex_partial': {
'subject': "⚠️ CreativeX Extraction Partial - {success_count}/{file_count} successful",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;">⚠️ CreativeX Extraction Partially Complete</h1>
</div>
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Total Files:</strong> {{ file_count }}</p>
<p style="margin: 5px 0 0 0;"><strong>✓ Successful:</strong> <span style="color: #28a745;">{{ success_count }}</span></p>
<p style="margin: 5px 0 0 0;"><strong>✗ Failed:</strong> <span style="color: #d32f2f;">{{ failed_count }}</span></p>
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
</div>
{% if processed_files %}
<h3 style="margin-top: 30px; color: #28a745;">✅ Successful Extractions ({{ success_count }}):</h3>
{% for score in processed_files %}
<div style="border: 1px solid #c8e6c9; margin: 10px 0; padding: 12px; background-color: #f1f8e9; border-radius: 4px;">
<strong>{{ score.filename }}</strong> - Score: {{ score.quality_score }}
{% if score.version_number > 1 %}<span style="color: #ff9800; font-size: 12px;"> (Version {{ score.version_number }})</span>{% endif %}
</div>
{% endfor %}
{% endif %}
{% if failed_files %}
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Extractions ({{ failed_count }}):</h3>
{% for file in failed_files %}
<div style="border: 1px solid #ffcdd2; margin: 10px 0; padding: 12px; background-color: #ffebee; border-radius: 4px;">
<strong>{{ file.filename }}</strong>
<br><small style="color: #666;">Error: {{ file.error }}</small>
</div>
{% endfor %}
{% endif %}
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>⚠️ Action Required:</strong> Review failed extractions above.</p>
<p style="margin: 5px 0 0 0;">Failed files remain in Box folder for retry.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Successful scores stored in database. Failed files not deleted from Box.</p>
</div>
"""
},
'creativex_no_files': {
'subject': " CreativeX Extraction - No files found",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
<div style="background-color: #607d8b; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0;"> CreativeX Extraction - No Files</h1>
</div>
<div style="background-color: #eceff1; border-left: 4px solid #607d8b; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>Status:</strong> No PDF files found</p>
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
<p style="margin: 5px 0 0 0;"><strong>Run Time:</strong> {{ timestamp }}</p>
</div>
<div style="background-color: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong> Note:</strong> This is expected behavior when no new PDFs are ready for processing.</p>
<p style="margin: 5px 0 0 0;">Upload PDFs to Box folder 350605024645 to process CreativeX scores.</p>
</div>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Script completed successfully with no errors.</p>
</div>
"""
},
'a5_to_a6_no_rejections': {
'subject': "✅ No Rework Required - Campaign {campaign_name}",
'html': """
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
# ... (template content skipped for brevity)
</div>
"""
}
}
except Exception as e:
logger.error("Error creating templates: {}".format(str(e)))
return
try:
# 1. Prepare message content
template = templates.get(template_name)
if not template:
logger.error("Email template not found: {}".format(template_name))
return
jinja_template = Template(template['html'])
html_content = jinja_template.render(data)
subject = template['subject'].format(**data)
# 2. Send via Mailgun API or SMTP
recipient_list = recipients if isinstance(recipients, list) else [recipients]
if self.mailgun_api_key and self.mailgun_domain:
self._send_via_mailgun_api(recipient_list, subject, html_content, attachments)
else:
self._send_via_smtp(recipient_list, subject, html_content, attachments)
logger.info("Email sent to {} (Template: {})".format(recipients, template_name))
except Exception as e:
logger.error("Failed to send email: {}".format(str(e)))
def _send_via_mailgun_api(self, recipient_list, subject, html_content, attachments=None):
"""Send email via Mailgun REST API - sends one request per recipient for reliable delivery"""
import os
url = "https://api.mailgun.net/v3/{}/messages".format(self.mailgun_domain)
# Normalize: split any comma-separated strings into individual addresses
normalized = []
for r in recipient_list:
for addr in r.split(','):
addr = addr.strip()
if addr:
normalized.append(addr)
for recipient in normalized:
files = []
try:
if attachments:
for file_path in attachments:
if os.path.exists(file_path):
files.append(("attachment", (os.path.basename(file_path), open(file_path, "rb"))))
else:
logger.warning("Attachment not found: {}".format(file_path))
data = {
"from": self.mailgun_sender,
"to": [recipient],
"subject": subject,
"html": html_content,
}
response = requests.post(
url,
auth=("api", self.mailgun_api_key),
data=data,
files=files if files else None,
)
response.raise_for_status()
logger.info("Mailgun API sent to {}: {}".format(recipient, response.json()))
except Exception as e:
logger.error("Mailgun API failed for {}: {}".format(recipient, str(e)))
finally:
for _, file_tuple in files:
file_tuple[1].close()
def _send_via_smtp(self, recipient_list, subject, html_content, attachments=None):
"""Send email via SMTP"""
import os
from email.mime.base import MIMEBase
from email import encoders
if attachments:
message = MIMEMultipart()
message['From'] = self.sender_email
message['To'] = ", ".join(recipient_list)
message['Subject'] = subject
message.attach(MIMEText(html_content, "html"))
for file_path in attachments:
try:
if os.path.exists(file_path):
with open(file_path, "rb") as attachment:
part = MIMEBase("application", "octet-stream")
part.set_payload(attachment.read())
encoders.encode_base64(part)
filename = os.path.basename(file_path)
part.add_header(
"Content-Disposition",
"attachment; filename= {}".format(filename),
)
message.attach(part)
logger.info("Attached file: {}".format(filename))
else:
logger.warning("Attachment not found: {}".format(file_path))
except Exception as e:
logger.error("Failed to attach file {}: {}".format(file_path, str(e)))
else:
message = MIMEText(html_content, "html")
message['From'] = self.sender_email
message['To'] = ", ".join(recipient_list)
message['Subject'] = subject
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
server.starttls()
server.login(self.smtp_user, self.smtp_password)
server.send_message(message)
def send_webhook(self, url, payload):
"""
url: Webhook URL
payload: dict to send as JSON
Returns:
bool: Success status
"""
try:
# Get webhook config if exists
webhook_config = None
for name, config in self.webhook_config.items():
if config.get('url') == url:
webhook_config = config
break
if not webhook_config:
webhook_config = {'timeout_seconds': 10, 'auth': {}}
# Prepare headers
headers = {'Content-Type': 'application/json'}
# Add auth if configured
auth_config = webhook_config.get('auth', {})
if auth_config.get('type') == 'bearer' and auth_config.get('token'):
headers['Authorization'] = 'Bearer {}'.format(auth_config['token'])
elif auth_config.get('type') == 'basic':
# Could add basic auth here if needed
pass
# Send webhook
response = requests.post(
url,
json=payload,
headers=headers,
timeout=webhook_config.get('timeout_seconds', 10)
)
if response.status_code in [200, 201, 202]:
logger.info("Webhook sent successfully: {}".format(url))
return True
else:
logger.warning("Webhook failed: HTTP {} - {}".format(
response.status_code, response.text[:200]
))
return False
except Exception as e:
logger.error("Webhook error: {}".format(str(e)))
return False