OMG's Box automation treats each new live_campaigns_*.csv as a full-list
replacement, so the per-series global CSV introduced 2026-04-30 stomped
the local list whenever a B1→B2 ran. Collapse to one combined CSV
(A-series + B-series) emitted by every handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires B-series (global) campaigns into OMG using the same Box
automation as A-series. Mirrors the A1/A4 lifecycle for B1/B4.
- b1_to_b2_download: after B2 status update, mark live=YES status=B2
and upload live_campaigns_global_<ts>.csv to the existing Box folder
(BOX_LIVE_CAMPAIGNS_FOLDER_ID, 352181382858 in PROD). Filename keeps
the live_campaigns_ prefix so the existing OMG automation rule picks
it up.
- b4_box_uploader (new): polls DAM for status B4, marks live=NO, regens
the global CSV. Mirrors a4_box_uploader.
- a4_box_uploader: reads prior status before overwriting; if it was
B-series, regenerate the global CSV instead. b4_box_uploader does the
symmetric A-series fallback. Defensive in case DAM doesn't enforce
type-specific status transitions.
- database: add get_all_live_global_campaigns() (status LIKE 'B%').
Tighten get_all_live_campaigns() to status LIKE 'A%' so any cross-type
rows can't leak into the wrong CSV.
- orchestrator + orchestrator-prod: register B4 Box Uploader at 10min.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DAM stores the CreativeX tabular cell as '<platform>^<score>', e.g.
'DV360^100'. Add format_cx_score_for_display() and apply at the point
where the email asset dict is built — both new-download and skipped
paths. Raw value stays in creativex_scores.quality_score so all platform
info is preserved for queries; only the email display is reshaped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic confirmed FERRERO.TAB.FIELD.CREATIVEX (score) lives at depth 2
in B1 master metadata — nested under FERRERO.TABULAR.FIELD.CREATIVEX
inside a category — and FERRERO.FIELD.CREATIVEX LINK lives at depth 1.
The flat top-level walk used previously never reached them, so live B1
runs and the backfill both reported zero CX scores. Updated extractor
in b1_to_b2_download.py and the inline copy in
backfill_b1_creativex_scores.py to descend recursively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
When a campaign is re-opened (status reset to A1/B1 after new files are
added), the tool correctly skips already-downloaded assets but the email
report and CSV previously listed the whole folder as "processed", which
was misleading. Reports now show "Total: 14 (12 previously downloaded,
2 new this run)" with new assets in full detail and previously-downloaded
assets in a compact list. B1→B2 CSV gains a Status column matching A1→A2.
DAM subfolder "WND_PCS 2026 2.0" was being treated as a downloadable
asset because ".0" passed the existing extension check. Added safeguard
to skip items with numeric-only extensions (e.g. .0, .1) which are
version numbers in folder names, not real files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the script re-downloaded and re-uploaded all assets on every
retry, even those already successfully stored in DB and Box. For large
campaigns (1300+ assets) this caused unnecessary load and duplicate uploads.
Now checks DB via find_global_master_by_opentext_id() before downloading.
Assets already in DB with a valid Box URL are skipped and counted toward
the processed total, so only genuinely failed assets are retried.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ensures all workflows send email notifications even when no assets are found.
NEW EMAIL TEMPLATES:
1. a1_to_a2_no_assets - Warns when A1 campaign has no master assets
2. b1_to_b2_no_assets - Warns when B1 campaign has no global master assets
Both templates use orange warning theme and explain:
- Campaign is set to A1/B1 but no assets found
- Status NOT updated (remains at current status)
- Script will retry on next run
- Suggests verifying assets exist in folder
Changes:
- a1_to_a2_download.py: Send email when total_assets == 0
- b1_to_b2_download.py: Send email when total_assets == 0
- notifier.py: Add 2 new warning templates
This completes email coverage for all scenarios:
✓ Success (all assets processed)
✓ Partial (some failed)
✓ No assets found (campaign empty)
✓ No rejections (A5→A6 specific)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Issue: Folder named 'MASTERS_NUTELLA PLANT-BASED LAUNCH-'
Missing: Campaign number (C000000068)
Fix: Include campaign number in folder name
- campaign_id: MASTERS_C000000068
- campaign_name: NUTELLA_PLANT_BASED_LAUNCH
- Result: MASTERS_C000000068-NUTELLA_PLANT_BASED_LAUNCH
Format:
MASTERS_[CampaignNumber]-[CampaignName]
Examples:
- MASTERS_C000000068-NUTELLA_PLANT_BASED_LAUNCH
- MASTERS_C000000069-KINDER_SURPRISE_GLOBAL
Spaces and dashes in campaign name converted to underscores.
🤖 Generated with Claude Code
Issue: Folder named 'MASTERS_NUTELLA PLANT-BASED LAUNCH-' (with trailing dash)
Cause: Passing entire folder name as campaign_id, empty campaign_name
Fix: Use BoxClient's built-in naming logic
- campaign_id: 'MASTERS' (prefix)
- campaign_name: 'NUTELLA_PLANT_BASED_LAUNCH' (cleaned)
- Result: MASTERS-NUTELLA_PLANT_BASED_LAUNCH
Changes:
- Replace spaces with underscores
- Replace dashes with underscores
- BoxClient adds dash between ID and name
- Final format: MASTERS-Campaign_Name
Example Results:
Before: MASTERS_NUTELLA PLANT-BASED LAUNCH-
After: MASTERS-NUTELLA_PLANT_BASED_LAUNCH
Clean, consistent folder naming for Global Masters!
🤖 Generated with Claude Code
Email Templates Added:
1. b1_to_b2_complete
- Subject: Global Master Assets Downloaded
- Shows campaign name, ID, asset count
- Lists all processed assets with tracking IDs and Box URLs
- Notes Box folder: 349261192115
- Status updated: B1 → B2
2. b1_to_b2_partial
- Subject: Partial Download - Global Campaign
- Shows successful and failed assets separately
- Each asset listed with name, tracking ID, error
- Notes status NOT updated (remains B1)
- Mentions automatic retry
Webhook Removed from B1→B2:
- B1→B2 workflow now only sends email (no webhook)
- Webhook only for A1→A2 workflow
- Simplified B1→B2 notifications
Email will now render properly with formatted HTML instead of raw dict.
Next B1→B2 run will send properly formatted email!
🤖 Generated with Claude Code
Changed: 'All assets processed - Updating status A1 → A2'
To: 'All assets processed - Updating status B1 → B2'
All log messages now correctly reference B1→B2 workflow.
Test output should now show:
- Searching for B1 Global campaigns
- All assets processed - Updating status B1 → B2
- ✓ Status updated: B1 → B2
Complete and correct! 🎊🤖 Generated with Claude Code
Fixes:
1. PHP: Fixed function name
- Changed findFinalAssetsFolder() → findUploadFolder()
- This function already looks for Final Assets folder
- Now PHP interface works without fatal error
2. Python: Search for Global comm campaigns
- Added campaign_type parameter to search_campaigns()
- B1→B2 uses: campaign_type='Global comm'
- A1→A2 uses: campaign_type='Local Adaptation' (default)
3. Python: Fixed log messages
- 'Searching for B1 Global campaigns' (not A1)
- 'No B1 campaigns found' (not A1)
4. Box Folder Configuration
- B1→B2 uses folder: 349261192115
- Folder naming: MASTERS_Campaign_Name
B1→B2 Now:
✅ Searches Global comm campaigns
✅ Filters for B1 status
✅ Uses Final Assets folder (05. not 01.)
✅ Uploads to correct Box folder (349261192115)
✅ Names folders: MASTERS_NUTELLA_PLANT-BASED_LAUNCH
Test:
1. Refresh PHP app - should load now
2. B1→B2 tab should work
3. Python script should find B1 campaigns
🤖 Generated with Claude Code
Created complete B1→B2 automation script:
✅ Based on tested a1_to_a2_download.py structure
✅ Searches for campaigns with status B1
✅ Searches Global comm campaigns (not Local Adaptation)
✅ Downloads Global Master assets from DAM
✅ Uploads to Box with tracking IDs (folder: 348304357505)
✅ Stores in PostgreSQL with full metadata
✅ Updates status B1 → B2 when all successful
✅ Sends webhook with B1→B2 status change
✅ Sends email notifications (b1_to_b2_complete, b1_to_b2_partial)
✅ Log rotation (28 files, 10MB each)
✅ Single-run mode (process one campaign and exit)
Usage:
cd Python-Version
source venv/bin/activate
python scripts/b1_to_b2_download.py
Cron Setup:
*/5 * * * * python scripts/b1_to_b2_download.py
Test Campaign Available:
- NUTELLA PLANT-BASED LAUNCH
- Folder ID: 676f2bcde4c7bcf7ef783e97f7495069bf50b6bc
- Status: B1
Complete B1→B2 automation ready for testing!
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>