ferrero-opentext/Python-Version/scripts/shared/notifier.py
DJP 055fc9ad16 Add recursive folder search, NOT APPROVED filtering, and rejection details for A5→A6
Major enhancements to all workflow scripts with recursive search and detailed rejection tracking.

NEW FEATURES:
1. Recursive Folder Search (ALL workflows: A1→A2, A5→A6, B1→B2)
   - Searches subfolders within Master/Final Assets folders
   - Preserves folder structure in Box
   - Adds 'folder_path' attribute to each asset

2. NOT APPROVED Filtering (A5→A6 ONLY)
   - Only downloads assets with ECOMMERCE STATUS = "NOT APPROVED"
   - Skips approved/other status assets
   - Logs rejected vs skipped counts

3. Rejection Details Extraction (A5→A6)
   - Extracts comments from 3 reviewers: Approver, Legal, IA&CC
   - Includes certifier names and dates
   - Displays in detailed email notifications

CHANGES BY FILE:

dam_client.py:
- NEW: _get_assets_recursive() - Recursively searches folders
- UPDATED: get_master_assets() - Now uses recursive search, adds folder_path to assets
- NEW: is_asset_not_approved() - Checks FERRERO.FIELD.ECOMMERCE STATUS
- NEW: extract_rejection_details() - Extracts all rejection comments from 10 fields

box_client.py:
- UPDATED: upload_with_tracking_id() - Added subfolder_path parameter
- NEW: _get_or_create_subfolder_path() - Creates/navigates Box subfolders
- Preserves DAM folder structure in Box uploads

a1_to_a2_download.py:
- Added folder_path extraction from assets
- Pass subfolder_path to Box upload
- Logs subfolder info during processing

b1_to_b2_download.py:
- Added folder_path extraction from assets
- Pass subfolder_path to Box upload
- Logs subfolder info during processing

a5_to_a6_download.py:
- Filter assets for NOT APPROVED status ONLY
- Extract rejection details for each asset
- Pass subfolder_path to Box upload
- Updated email data with rejection_details
- Handle "no rejections" scenario with email
- Updated logging to show rejected vs skipped counts

notifier.py:
- REPLACED: a5_to_a6_complete → a5_to_a6_rejections
- Detailed HTML template with rejection sections
- Shows Approver, Legal, and IA&CC rejections
- Styled with red warnings and bordered sections
- NEW: a5_to_a6_no_rejections template
- Green success message when no rejected assets found
- UPDATED: a5_to_a6_partial - Now uses rejected_assets

FIELD IDs EXTRACTED (A5→A6):
- FERRERO.FIELD.ECOMMERCE STATUS (primary check)
- FERRERO.MARKETING.FIELD.CERTIFIER COMMENT
- FERRERO.FIELD.ECOMMERCE CERTIFIER
- FERRERO.MARKETING.FIELD.APPROVAL DATE
- FERRERO.MARKETING.FIELD.LEGAL COMMENT
- FERRERO.FIELD.LEGAL CERTIFER (typo in field ID)
- FERRERO.MARKETING.FIELD.LEGAL APPROVAL DATE
- FERRERO.MARKETING.FIELD.IA CC COMMENT
- FERRERO.MARKETING.FIELD.IA CERTIFIER
- FERRERO.MARKETING.FIELD.IA CC APPROVAL DATE

TESTING:
✓ All connections working (DAM, Box, Database)
✓ A5→A6 script executes correctly
✓ Recursive search working
✓ NOT APPROVED filtering working
✓ "No rejections" email sent successfully
✓ Folder structure preserved in logs

WORKFLOW IMPACTS:
- A1→A2: Now searches recursively, preserves folder structure
- A5→A6: Filters for NOT APPROVED only, shows rejection details
- B1→B2: Now searches recursively, preserves folder structure

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 14:49:19 -05:00

482 lines
26 KiB
Python

"""
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 (preferred method)
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')
self.recipients = config['notifications']['recipients']
self.webhook_config = config.get('webhooks', {})
def send_email(self, template_name, recipients, data):
"""
Send email via SMTP (Mailgun)
Args:
template_name: Name of email template
recipients: List of email addresses or single email
data: Template data dict
"""
if not self.enabled:
logger.info("Notifications disabled, skipping email")
return
if not self.smtp_server or not self.smtp_user:
logger.warning("SMTP not 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': """
<h2>Master Assets Downloaded Successfully</h2>
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
<p><strong>Assets Downloaded:</strong> {{ asset_count }}</p>
<p><strong>Status Updated:</strong> A1 → A2</p>
<hr>
<h3>Processed Assets:</h3>
<ul>
{% for asset in processed_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
<br>Box File ID: {{ asset.box_file_id }}
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
</li>
{% endfor %}
</ul>
<hr>
<p>All assets have been downloaded from DAM and uploaded to Box with tracking IDs.</p>
<p>Campaign status has been updated from A1 to A2.</p>
"""
},
'a2_to_a3_complete': {
'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}",
'html': """
<h2>Localized Assets Uploaded Successfully</h2>
<p><strong>Campaign:</strong> {{ campaign_name }}</p>
<p><strong>Campaign ID:</strong> {{ campaign_id }}</p>
<p><strong>Assets Uploaded:</strong> {{ asset_count }}</p>
<p><strong>Status Updated:</strong> A2 → A3</p>
<hr>
<p>All localized assets have been uploaded to DAM.</p>
"""
},
'upload_failed': {
'subject': "❌ Upload Failed - {filename}",
'html': """
<h2 style="color: red;">Upload Failed</h2>
<p><strong>Filename:</strong> {{ filename }}</p>
<p><strong>Tracking ID:</strong> {{ tracking_id }}</p>
<p><strong>Error:</strong> {{ error }}</p>
<hr>
<p>Please investigate the error.</p>
"""
},
'a1_to_a2_partial': {
'subject': "⚠️ Partial Download - Campaign {campaign_name}",
'html': """
<h2 style="color: orange;">Campaign Partially Processed</h2>
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
<p><strong>Total Assets:</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 processed_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
<br>Box File ID: {{ asset.box_file_id }}
<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 A1.</p>
<p>The script will retry failed assets on the next run (every 5 minutes).</p>
"""
},
'a2_to_a3_file_uploaded': {
'subject': "✅ Asset Uploaded to DAM - {clean_filename}",
'html': """
<h2>Asset Uploaded Successfully (A2→A3)</h2>
<h3>File Details:</h3>
<p><strong>Original Filename (from Box):</strong><br>
<code>{{ filename }}</code></p>
<p><strong>Clean Filename (in DAM):</strong><br>
<code>{{ clean_filename }}</code></p>
<p><strong>DAM Asset ID:</strong> <code>{{ asset_id }}</code></p>
<p><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
<hr>
<h3>Processing Details:</h3>
<p><strong>Master Asset ID:</strong> {{ master_asset_name }}</p>
<p><strong>Uploaded to DAM Folder:</strong> {{ upload_folder }}</p>
<p><strong>Downloaded from Box Folder:</strong> {{ box_folder }}</p>
<hr>
<h3>What Was Done:</h3>
<ul>
<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>
<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>
<hr>
<p><strong>Status:</strong> Asset processing complete.</p>
<p><em>Note: Campaign status will be updated to A3 once all assets are uploaded.</em></p>
"""
},
'b1_to_b2_complete': {
'subject': "✅ Global Master Assets Downloaded - Campaign {campaign_name}",
'html': """
<h2>Global Master Assets Downloaded Successfully (B1→B2)</h2>
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
<p><strong>Assets Downloaded:</strong> {{ asset_count }}</p>
<p><strong>Status Updated:</strong> B1 → B2</p>
<p><strong>Campaign Type:</strong> Global Masters</p>
<hr>
<h3>Processed Assets:</h3>
<ul>
{% for asset in processed_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
<br>Box File ID: {{ asset.box_file_id }}
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
</li>
{% endfor %}
</ul>
<hr>
<p>All Global Master assets have been downloaded from DAM and uploaded to Box with tracking IDs.</p>
<p><strong>Box Folder:</strong> 349261192115 (Global Masters)</p>
<p>Campaign status has been updated from B1 to B2.</p>
"""
},
'b1_to_b2_partial': {
'subject': "⚠️ Partial Download - Global Campaign {campaign_name}",
'html': """
<h2 style="color: orange;">Global Campaign Partially Processed (B1→B2)</h2>
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
<p><strong>Total Assets:</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 processed_assets %}
<li><strong>{{ asset.asset_name }}</strong>
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>
<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 B1.</p>
<p>The script will retry failed assets on the next run (every 5 minutes).</p>
"""
},
'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>
{% 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>
"""
}
}
template_config = templates.get(template_name, {
'subject': 'Ferrero Automation Notification',
'html': '<p>{}</p>'.format(data)
})
# Render subject and body
subject = template_config['subject'].format(**data)
html_template = Template(template_config['html'])
html_body = html_template.render(**data)
# Prepare email
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = self.sender_email
msg['To'] = ', '.join(recipients) if isinstance(recipients, list) else recipients
# Attach HTML body
html_part = MIMEText(html_body, 'html')
msg.attach(html_part)
# Send via SMTP
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
server.starttls()
server.login(self.smtp_user, self.smtp_password)
server.send_message(msg)
logger.info("Email sent via SMTP: {} to {}".format(template_name, recipients))
except Exception as e:
logger.error("Email error: {}".format(str(e)))
def send_webhook(self, url, payload):
"""
Send outgoing webhook notification
Args:
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