Critical Fix:
- extract_global_campaign_reference() now accepts campaign_id parameter
- Always sets local_campaign_id to current campaign as fallback
- Prevents NULL local_campaign_id when no Global Campaign Reference exists
Root Cause:
- Assets without Global Campaign Reference had NULL local_campaign_id
- Caused derivatives to be linked to wrong campaigns
- Same asset in multiple campaigns would share tracking IDs incorrectly
Impact:
- Every asset now has proper local_campaign_id
- Derivatives correctly linked to their source campaign
- Fixes issue where C000001177 assets were showing as C000002098
Changes:
- database.py: Added campaign_id parameter with fallback logic
- a1_to_a2_box_uploader.py: Pass campaign_number to function
- a5_to_a6_download.py: Pass campaign_number to function
Reverted master_asset_id changes per user feedback:
- tracking_id already links derivatives to masters
- No need for additional master_asset_id foreign key
- Only storing dam_asset_id for DAM asset tracking
Changes:
- Reverted get_master_asset() to not return database 'id'
- Updated store_derivative_asset() to only INSERT dam_asset_id
- Updated a2_to_a3_upload_polling.py to pass None for master_asset_id
- Removed master_asset_id from INSERT statement
Note: Migration script still needed for dam_asset_id column only
Email Template Fix:
- Fixed subject line syntax error in a2_to_a3_batch_complete template
- Removed Jinja2 control flow ({% if %}) from subject line
- Changed to simple expression-only format
- Fixes 'Failed to send email' error
Database Logging Fix:
- Updated get_master_asset() to return database primary key 'id'
- Updated store_derivative_asset() to actually store master_asset_id and dam_asset_id
- Updated a2_to_a3_upload_polling.py to pass master_asset['id'] instead of None
- Added migration script to add dam_asset_id column to derivative_assets table
- Fixes issue where derivatives weren't being linked to masters in database
- Enables proper lookups and tracking of uploaded derivatives
Impact:
- Email notifications will now send successfully
- Derivatives will be properly logged and linked to master assets
- Other tools can now find uploaded derivatives in database
Stores master asset CreativeX scores from DAM metadata during A1→A2
download for reference/reporting purposes (not used in uploads).
Database Changes:
creativex_scores table:
- Added: tracking_id VARCHAR(6) column
- Added: idx_creativex_tracking_id index
- Updated comment: status can be 'active', 'superseded', or 'master-cx-score'
Status Values:
- 'active' - Current derivative score (from PDF extraction)
- 'superseded' - Old derivative score (version history)
- 'master-cx-score' - Master asset score (from A1→A2 DAM metadata) ← NEW
Migration SQL (for existing databases):
ALTER TABLE creativex_scores ADD COLUMN tracking_id VARCHAR(6);
CREATE INDEX idx_creativex_tracking_id ON creativex_scores(tracking_id);
Database Method Updates (database.py):
store_creativex_score() signature:
- Added: tracking_id parameter (optional, default None)
- Added: status parameter (optional, default 'active')
Logic:
- If status='master-cx-score': Simple insert, no versioning
- If status='active': Soft delete versioning as before
- Always stores tracking_id if provided
A1→A2 Script Updates (a1_to_a2_download.py):
New Function: extract_creativex_from_dam_metadata()
- Searches metadata_element_list for CREATIVEX fields
- Extracts FERRERO.TAB.FIELD.CREATIVEX (score)
- Extracts FERRERO.FIELD.CREATIVEX LINK (url)
- Returns dict with score/url or None if not found
- Handles tabular field structure for score
- Handles nested value structure for URL
Integration:
- After successful master asset storage
- Extracts CreativeX from asset metadata
- If found: Stores in creativex_scores with status='master-cx-score'
- Links to master via tracking_id
- Logs when score found/stored
- Logs "normal" when not found (not all masters are scored)
Use Cases:
A2→A3 Upload:
- Still uses filename-based lookup ONLY ✅
- No changes to A2→A3 logic ✅
- Master scores not used for uploads ✅
Reporting/Analytics Tools:
- Can query master score by tracking_id
- Compare master vs derivative scores
- Track score improvements
- Audit trail
Query Examples:
-- Get master score for tracking ID
SELECT * FROM creativex_scores
WHERE tracking_id = '7xXgKp' AND status = 'master-cx-score';
-- Get derivative score for filename
SELECT * FROM creativex_scores
WHERE filename = 'file.mp4' AND status = 'active';
Test Record Created:
- Filename: nutella_pbased.jpg
- Tracking ID: 7xXgKp
- Score: 85
- Status: master-cx-score
Benefits:
- Historical reference of master scores
- Enables score comparison analytics
- No impact on A2→A3 upload logic
- Automatic extraction during A1→A2
- Optional (works even if masters don't have scores)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements version counter for re-scored files and cleans up numeric formatting.
Decimal Removal:
- Strip .0 suffix from creativex_id (6864255.0 → 6864255)
- Strip .0 suffix from quality_score (80.0 → 80)
- Converts float → int → string before storing
- Cleaner data for display and DAM integration
Version Tracking:
- Counts total versions per filename (active + superseded)
- Returns version_number in database result
- Logs show version: "Score 80 extracted (Version 3)"
- Email templates display version badges for updates
Email Template Updates:
- Complete template: Shows "Version 3 (Updated)" badge in header
- Includes note: "This is version 3 of this file"
- Partial template: Shows "(Version 3)" inline
- Only displays version info if > 1
Database Changes:
- Query counts ALL versions before insert
- Returns version_number in result dict
- Logs include version in success/update messages
Benefits:
- Clean numeric values without unnecessary decimals
- Users can see if file was re-scored
- Version history visible in emails
- Still preserves all history in database
- A2→A3 integration unaffected (always gets latest active)
Example progression:
Upload 1: Score 80 (no version shown - it's the first)
Upload 2: Score 85 (Version 2 badge shown)
Upload 3: Score 90 (Version 3 badge shown)
Documentation: CREATIVEX_VERSION_UPDATES.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Adds upsert logic that marks old records as 'superseded' while creating
new 'active' records, preserving full history for audit/analysis.
Changes:
- Updated store_creativex_score() to check for existing filename
- Old records marked status='superseded' before inserting new 'active' record
- Returns is_update flag to indicate if this was an update vs new insert
- Logs score changes (e.g., "Score: 80.0 -> 85.0")
Documentation updates:
- Added "Understanding Status Field" section with soft delete explanation
- Separated queries into "Latest Scores" vs "History/Audit" sections
- Added A2→A3 integration guide with example code
- Documented query logic and behavior table for future integration
- Added migration notes for existing data
Query patterns for A2→A3:
- status='active' → Latest/current score (use this in workflows)
- status='superseded' → Previous scores (history/audit trail)
- get_creativex_score_by_filename() automatically filters for active
Benefits:
- Easy lookup of latest scores (just filter status='active')
- Full history preserved for tracking score changes over time
- No data loss when files are re-scored
- Clear audit trail of when scores changed
Tested and verified:
- Existing record (80.0) marked as superseded
- New record (85.0) created as active
- Queries correctly return only active record
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements new workflow to extract CreativeX quality scores from PDFs
using LlamaExtract AI and store results in PostgreSQL database.
Components added:
- creativex_scoring_storing.py: Main script to process PDFs from Box
- creativex_scores table: Database table with JSONB for full JSON storage
- Database methods: store_creativex_score() and get_creativex_score_by_filename()
- Email templates: creativex_complete, creativex_partial, creativex_no_files
- Configuration: creativex section in config.yaml
- CREATIVEX_DEPLOYMENT.md: Complete deployment and usage guide
Features:
- Monitors Box folder 350605024645 for PDFs
- Extracts scores using LlamaExtract agent "Creativex-Extract"
- Stores 4 key fields (filename, ID, URL, score) + full JSON
- Deletes processed PDFs from Box after successful extraction
- Sends email notifications for success/partial/no-files scenarios
- Manual execution (python scripts/creativex_scoring_storing.py)
Database schema:
- Table: creativex_scores with 10 columns
- Indexes on filename, box_file_id, status for fast lookups
- JSONB column stores complete extraction for future flexibility
Future integration ready:
db.get_creativex_score_by_filename() available for DAM upload workflows
to attach CreativeX metadata during asset processing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed database.py line 479: Changed 'CURRENT_TIMESTAMP' string to actual datetime
- Added datetime import for proper UTC timestamp generation
- This fixes the PostgreSQL error: invalid input syntax for type timestamp
- Added migration file for campaign_status table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Database Schema:
✅ Added local_campaign_id VARCHAR(50) column
✅ Stores the immediate campaign the asset belongs to (C000000551)
Enhanced Extraction:
✅ extract_global_campaign_reference() now returns 3 values:
- global_master_campaign_id (C000000068)
- global_master_folder_id (676f2bcde4c7...)
- local_campaign_id (C000000551)
✅ Extracts FERRERO.FIELD.CAMPAIGN ID from same collection
✅ Only sets local_campaign_id if that collection has global reference
✅ Logs all three IDs when found
A1→A2 Script:
✅ Passes local_campaign_id to store_master_asset()
✅ Stores complete campaign relationship
Database Now Stores:
- tracking_id: ABC123 (unique 6-char)
- opentext_id: 0008a50... (DAM asset ID)
- local_campaign_id: C000000551 (immediate campaign)
- global_master_campaign_id: C000000068 (global master)
- global_master_folder_id: 676f2bcde4c7... (global folder)
Example Relationship:
- Asset downloaded from Local Campaign C000000551
- That campaign references Global Master C000000068
- All three IDs stored for complete traceability
Database schema now complete with full campaign relationship tracking!
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Database Schema:
✅ Added global_master_campaign_id VARCHAR(50) column
✅ Added global_master_folder_id VARCHAR(255) column
✅ Stores relationship between local campaigns and their global masters
Database Module:
✅ Added extract_global_campaign_reference() method
✅ Searches inherited_metadata_collections for L7+ - CAMPAIGN containers
✅ Extracts FERRERO.FIELD.GLOBAL CAMPAIGN REFERENCE field
✅ Extracts container_id as global_master_folder_id
✅ Returns dict with both IDs
A1→A2 Script:
✅ Calls db.extract_global_campaign_reference() for each asset
✅ Passes global_master_campaign_id to store_master_asset()
✅ Passes global_master_folder_id to store_master_asset()
✅ Logs when Global Campaign Reference found
Example Data Stored:
- Local Campaign C000000551 asset
- global_master_campaign_id: C000000068
- global_master_folder_id: 676f2bcde4c7bcf7ef783e97f7495069bf50b6bc
Usage:
This data enables tracking which Global Master a local asset came from.
Can query all local assets for a specific Global Master campaign.
Foundation for future cross-campaign features.
Based on EXTRACTION_GUIDE.md implementation pattern.
Note: B1→B2 workflow NOT updated (those ARE the global masters)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Created a2_to_a3_upload_polling.py:
- Polls Box folder (348526703108) instead of webhook
- Works locally (no need for public URL)
- Single-run mode (process one file and exit)
- Can be run via cron every 5 minutes
Why Polling Instead of Webhook:
- Webhooks require public URL (doesn't work on localhost)
- Polling works everywhere (local and server)
- Same functionality, different trigger mechanism
Database Fix:
- Don't create new columns (dam_asset_id, upload_status)
- Use existing schema: tracking_id, derivative_filename, file_extension, status
- Simplified store_derivative_asset() to use existing columns only
- Database now compatible with existing schema
Test Results - A2→A3 Polling:
✅ Polls Box folder 348526703108
✅ Finds V2 files with tracking IDs
✅ Downloads from Box
✅ Loads master metadata from PostgreSQL
✅ Builds 27 MVP fields
✅ Updates Description, State, Language from filename
✅ Uploads to DAM successfully (Asset ID: 214924)
✅ Stores derivative record
✅ Processes one file and exits
Both Scripts Working:
✅ A1→A2: Downloads from DAM → Box (folder 348304357505)
✅ A2→A3: Uploads from Box → DAM (folder 348526703108)
Cron Setup:
*/5 * * * * python scripts/a1_to_a2_download.py
*/5 * * * * python scripts/a2_to_a3_upload_polling.py
Complete automation ready for production!
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>