#!/usr/bin/env python3 """ A4 Box Uploader Monitors campaigns with status A4 (Not Going Live) Updates status in DB to live_campaign='NO' Generates and uploads updated CSV of live campaigns to Box """ import sys import os import time import logging import argparse import csv from datetime import datetime, timezone # Add shared library to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from shared.config_loader import load_config from shared.dam_client import DAMClient from shared.box_client import BoxClient from shared.database import Database from shared.notifier import Notifier # Setup logging with rotation from logging.handlers import RotatingFileHandler # Create logs directory if it doesn't exist os.makedirs('logs', exist_ok=True) os.makedirs('logs/backup', exist_ok=True) # Configure logging with rotation log_handler = RotatingFileHandler( 'logs/a4_box.log', maxBytes=10*1024*1024, # 10MB per file backupCount=28 ) log_handler.setLevel(logging.INFO) log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logging.basicConfig( level=logging.INFO, handlers=[log_handler, console_handler] ) logger = logging.getLogger('A4Box') def generate_and_upload_csv(db, box, config): """ Generate the combined live-campaigns CSV (A-series + B-series) and upload to Box. OMG's automation treats each new file as a full replacement of its live list, so we always emit the complete list under one filename. """ try: logger.info("Generating live campaigns CSV...") campaigns = db.get_all_live_campaigns() if not campaigns: logger.warning("No live campaigns found to report") logger.info("Found {} live campaigns".format(len(campaigns))) timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S_UTC') csv_filename = 'live_campaigns_{}.csv'.format(timestamp) csv_path = os.path.join('temp', csv_filename) os.makedirs('temp', exist_ok=True) with open(csv_path, 'w', newline='') as csvfile: fieldnames = ['code', 'description'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for camp in campaigns: writer.writerow({ 'code': "{}-{}".format(camp['campaign_number'], camp['campaign_name']), 'description': camp['campaign_name'] }) logger.info("Generated CSV: {}".format(csv_path)) folder_id = config['box'].get('live_campaigns_folder_id') if not folder_id: logger.error("Box live_campaigns_folder_id not configured") return False upload_result = box.upload_file( file_path=csv_path, folder_id=folder_id, target_filename=csv_filename ) logger.info("Uploaded CSV to Box: {} (File ID: {})".format( csv_filename, upload_result['file_id'] )) os.remove(csv_path) return True except Exception as e: logger.error("Failed to generate/upload CSV: {}".format(str(e))) return False def process_campaign(campaign, dam, box, db, notifier, config): """ Process A4 campaign - update status and upload CSV """ campaign_id = campaign['asset_id'] campaign_name = campaign['campaign_name'] campaign_number = campaign.get('campaign_id') or 'UNKNOWN' logger.info("=" * 60) logger.info("Processing A4 campaign: {} ({})".format(campaign_name, campaign_number)) logger.info("=" * 60) try: # Check if campaign already processed campaign_check = db.check_campaign_processed(campaign_id) if campaign_check['exists'] and campaign_check['webhook_sent']: # Note: We reuse 'webhook_sent' column to track if we've processed it, # even though we aren't sending a webhook anymore. logger.info("Campaign already processed") logger.info(" Processed at: {}".format(campaign_check['webhook_sent_at'])) logger.info(" Status: {}".format(campaign_check['status'])) logger.info(" Live Campaign: {}".format(campaign_check['live_campaign'])) logger.info("Skipping to avoid duplicate processing") return {'success': True, 'processed': False, 'already_processed': True} # Record campaign status in database # This marks it as NOT LIVE logger.info("Recording campaign status in database (Live: NO)...") db.record_campaign_status( campaign_id=campaign_id, campaign_number=campaign_number, campaign_name=campaign_name, live_campaign='NO', # A4 campaigns are NOT going live status='A4', webhook_sent=True # Mark as processed ) logger.info("Generating and uploading updated live campaigns CSV...") csv_success = generate_and_upload_csv(db, box, config) if csv_success: logger.info("✓ CSV report uploaded successfully") else: logger.error("✗ CSV report generation/upload failed") # Send email notification (internal) notifier.send_email( template_name='a4_webhook_sent', # Reuse template or create new one? Reusing for now as it conveys "A4 processed" recipients=config['notifications']['recipients']['success'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'webhook_url': 'CSV Uploaded to Box' # Placeholder } ) return {'success': True, 'processed': True} except Exception as e: logger.error("Campaign processing failed: {}".format(str(e))) return {'success': False, 'processed': False} def main(): """Main polling loop""" parser = argparse.ArgumentParser(description='Ferrero A4 Box Uploader') parser.add_argument('--auth-pfx', action='store_true', help='Use mTLS certificate authentication (Legacy APIM)') parser.add_argument('--auth-pfx-v2', action='store_true', help='Use mTLS V2 (Hybrid) authentication') args = parser.parse_args() logger.info("=" * 60) logger.info("Ferrero A4 Box Uploader Starting") # Determine auth mode auth_mode = 'oauth' if args.auth_pfx_v2: auth_mode = 'mtls_v2' logger.info("Authentication: mTLS V2 (Hybrid)") elif args.auth_pfx: auth_mode = 'mtls' logger.info("Authentication: mTLS Certificate (Legacy)") else: logger.info("Authentication: OAuth2 (default)") logger.info("=" * 60) # Load configuration config = load_config('config/config.yaml') # Initialize clients dam = DAMClient(config, auth_mode=auth_mode) box = BoxClient(config) # Need Box client now db = Database(config) notifier = Notifier(config) # Test connections logger.info("Testing connections...") if not dam.test_connection(): logger.error("DAM connection failed - exiting") sys.exit(1) if not box.test_connection(): logger.error("Box connection failed - exiting") sys.exit(1) if not db.test_connection(): logger.error("Database connection failed - exiting") sys.exit(1) logger.info("All connections OK") logger.info("") try: logger.info("Searching for A4 campaigns...") campaigns = dam.search_campaigns(status='A4') if not campaigns: logger.info("No A4 campaigns found - exiting") db.close() sys.exit(0) logger.info("Found {} A4 campaign(s) - processing all".format(len(campaigns))) logger.info("") processed_count = 0 failed_count = 0 already_processed_count = 0 for campaign in campaigns: result = process_campaign(campaign, dam, box, db, notifier, config) if result['success']: if result.get('processed'): processed_count += 1 if result.get('already_processed'): already_processed_count += 1 else: failed_count += 1 logger.info("") logger.info("=" * 60) logger.info("A4 Box Uploader Summary") logger.info("=" * 60) logger.info("Total campaigns found: {}".format(len(campaigns))) logger.info("Processed (CSV updated): {}".format(processed_count)) logger.info("Already processed: {}".format(already_processed_count)) logger.info("Failed: {}".format(failed_count)) logger.info("=" * 60) db.close() if failed_count == 0: sys.exit(0) elif processed_count > 0: sys.exit(0) else: sys.exit(1) except Exception as e: logger.critical("Script error: {}".format(str(e))) notifier.send_email( template_name='upload_failed', recipients=config['notifications']['recipients']['critical'], data={ 'filename': 'A4 Box Uploader', 'tracking_id': 'N/A', 'error': str(e) } ) db.close() sys.exit(1) if __name__ == '__main__': main()