Commit graph

100 commits

Author SHA1 Message Date
DJP
8ca44fcf1e Add metadata diagnostic tool for troubleshooting field issues 2025-11-26 14:52:39 -05:00
DJP
f9c11ef3f5 Fix misleading log message for A5 campaigns with no rejections 2025-11-26 14:45:48 -05:00
DJP
795e4e7d96 Improve A5 notification logic to handle status changes 2025-11-26 14:26:36 -05:00
DJP
0f1c3dd0ec Prevent duplicate 'no rejections' emails for A5 campaigns 2025-11-26 14:25:57 -05:00
DJP
936071d7ad Change script intervals from 5 to 3 minutes for faster processing 2025-11-26 14:18:45 -05:00
DJP
f15ae9a8d1 Stream full script output to console in real-time 2025-11-26 14:04:15 -05:00
DJP
d6b68af5d5 Fix A5→A6 to also use OAuth authentication 2025-11-26 14:03:07 -05:00
DJP
16527f6e43 Temporarily disable mTLS auth, use OAuth for troubleshooting 2025-11-26 13:59:48 -05:00
DJP
3518f7c909 Remove A1->A2 Download task from orchestrator and add run guide 2025-11-26 13:53:48 -05:00
DJP
7599fe7cd2 feat: Remove A1->A2 Download script from orchestrator configuration 2025-11-26 13:48:48 -05:00
DJP
99d8621266 Increase throughput: process 2 campaigns in A1→A2, all files in A2→A3 2025-11-26 13:43:06 -05:00
DJP
98fb7eaee2 Fix smoke test to use prod-auth endpoint instead of test-auth 2025-11-26 10:20:59 -05:00
DJP
9d207d0480 chore: Update DAM mTLS base and OAuth URLs in production environment to /token endpoint. 2025-11-26 10:18:56 -05:00
DJP
6064b0971e Fix smoke test to explicitly load .env-prod file 2025-11-26 10:14:26 -05:00
DJP
cabc1d5548 Add production smoke test script for mTLS V2 authentication 2025-11-26 10:08:04 -05:00
DJP
c1f338022c fix: Ensure type field is added when updating CreativeX URL
- Modified _set_field_value to include 'type': 'string' in all code paths
- Adds type field when updating existing CreativeX URL field
- Ensures consistent structure whether creating or updating field
2025-11-25 09:14:25 -05:00
DJP
80316cad32 fix: Add missing type field to CreativeX URL metadata
- Added 'type': 'string' to FERRERO.FIELD.CREATIVEX LINK value structure
- Fixes DAM validation error for CreativeX URL field
- Structure now matches DAM requirements
2025-11-25 09:11:41 -05:00
DJP
548c30344b feat: Support multiple CreativeX platforms in metadata
- Updated creativex_scoring_storing.py to map multiple placements to platforms
- Modified get_mapped_platform to get_mapped_platforms (returns list)
- Updated a2_to_a3_upload_polling.py to retrieve platforms list from DB
- Enhanced metadata_extractor_mvp.py to build multi-value CreativeX field
- Added DAM-CX mappings.csv for channel/placement to platform mapping
- Supports single channel with multiple placements generating multiple Platform^Score values
2025-11-24 14:44:11 -05:00
DJP
491fc8e938 feat: Add A1→A3 campaign advance script, introduce systemd service for orchestrator, and ref 2025-11-24 13:50:16 -05:00
DJP
0af15563bc feat: Implement new Python script locking, relocate PHP workflow, and update Python scripts and documentation. 2025-11-21 17:20:34 -05:00
DJP
22069ed66f refactor: Relocate test scripts to a dedicated tests/ directory and introduce orchestrator.py. 2025-11-21 17:10:04 -05:00
DJP
2cad2c2955 Fix AttributeError in DAMClient.test_connection 2025-11-21 16:57:43 -05:00
DJP
5aeab8d9a3 Update a1_to_a2_download.py to support Auth V2 2025-11-21 16:53:32 -05:00
DJP
6fe2ba234b Implement Auth V2 (Hybrid mTLS/OAuth) and update field mappings 2025-11-21 16:46:37 -05:00
DJP
b906434f67 Add A1->A2 and A4 Box CSV uploader scripts 2025-11-20 22:52:26 -05:00
DJP
20a187a61d feat: Add Python project dependencies. 2025-11-20 22:45:04 -05:00
DJP
0bf3c34cd0 Add asset type mapping from 3-letter codes to DAM codes
Maps frontend naming tool 3-letter codes (EHI, IMG, TVC) to DAM's
lowercase descriptive codes (heroimage, keyvisual, tvc) for uploads.

New File: config/asset_type_mappings.yaml
- 45 asset type mappings from DAM lookup domains
- Maps 3-letter codes to DAM codes
- E-Commerce types: EHI→heroimage, EBS→beautyshot, etc.
- Standard types: IMG→keyvisual, TVC→tvc, etc.
- Expandable as new asset types added

Field Mappings Update (field_mappings.yaml):
- Added FERRERO.FIELD.MKTG.ASSET TYPE to filename_updates
- Source: asset_type (from parsed filename)
- Required: true

Metadata Extractor Updates (metadata_extractor_mvp.py):
- Added _load_asset_type_mappings() method
- Added _map_asset_type() method
- Integrated mapping into _update_fields()
- Logs mappings: "Asset type mapping: EHI -> heroimage"
- Warns if no mapping found (may fail DAM validation)

Example Flow:
1. Filename: ROC_TEST-E2E2_EHI_1x1_DE_de.png
2. Parse: asset_type = "EHI"
3. Map: EHI → "heroimage"
4. Update field: FERRERO.FIELD.MKTG.ASSET TYPE = "heroimage"
5. DAM accepts "heroimage" (valid domain value)

Without Mapping (before):
- Field value: "EHI"
- DAM validation: FAILS (EHI not in domain)

With Mapping (after):
- Field value: "heroimage"
- DAM validation: PASSES 

Complete Mapping List (45 types):
E-Commerce: ECA, ECB, EBS, EBR, EEM, EHI, EIL, EOP, EUG, EWB
Standard: 3RT, APC, BBK, BRC, BSG, CKV, CID, DAT, FLA, FNT, GDT,
         GRG, IMG, FPO, LGL, LOG, MLF, OLV, PAW, PKI, POS, PDM,
         PRI, QRC, SND, SIP, SGL, TVC, VIE

DAM Answer: Uses descriptive lowercase codes, not 3-letter codes.
Frontend uses 3-letter for brevity, backend maps to DAM format.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 13:08:01 -05:00
DJP
d1ab8551e5 Add asset type filename update and make field updates configurable
Enables asset type to be updated from derivative filename and refactors
_update_fields() to use filename_updates configuration dynamically.

Field Mappings Configuration (field_mappings.yaml):

Added to filename_updates:
- FERRERO.FIELD.MKTG.ASSET TYPE:
    source: asset_type
    required: true

Now updates from derivative filename:
- ROC_TEST-E2E2_EHI_1x1_DE_de.png → Asset Type = "EHI"

Metadata Extractor Refactor (metadata_extractor_mvp.py):

Old _update_fields():
- Hardcoded field updates (ASSET NAME, DESCRIPTION, STATE)
- Not using filename_updates configuration
- Required code changes to add new fields

New _update_fields():
- Dynamically processes filename_updates from config
- Supports transform: uppercase/lowercase
- Supports any source field from parsed_filename
- Uses forced_values from config (was hardcoded before)
- Add new fields via config, no code changes needed

Configuration-Driven Updates:
- ARTESIA.FIELD.ASSET NAME ← clean_filename
- ARTESIA.FIELD.ASSET DESCRIPTION ← subject_title
- FERRERO.FIELD.MKTG.ASSET TYPE ← asset_type (NEW)
- MAIN_LANGUAGES ← language_code (uppercase)
- FERRERO.FIELD.STATE ← "Local" (forced value)

Benefits:
- Asset type now correctly populated from filename
- Configuration-driven (add fields without code changes)
- Cleaner code (uses config instead of hardcoded logic)
- Forced values also configurable
- Easier to maintain and extend

Example:
Filename: ROC_TEST-E2E2_EHI_1x1_DE_de.png
Parsed asset_type: "EHI"
Field FERRERO.FIELD.MKTG.ASSET TYPE updated to: "EHI"

Impact:
All A2→A3 uploads will now have correct Asset Type from derivative
filename instead of inheriting from master (which may be different).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 12:58:10 -05:00
DJP
e44f42dad5 Add master CreativeX score extraction and storage in A1→A2 workflow
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>
2025-11-13 13:56:22 -05:00
DJP
68180b23cf Update filename parser to V2.1 structure with new field positions
Complete rewrite of filename parser to support new field order where
Subject/Asset moved up and Country/Language moved down, plus new
Social Media field.

BREAKING CHANGE: V2.1 Structure (November 2025)

Old (V1):
[JOB]_[BRAND]_[COUNTRY]_[LANG]_[SUBJECT]_[ASSET]_[SPOT]_[DUR]_[RATIO]_[TRACKING]

New (V2.1):
[JOB]_[BRAND]_[SUBJECT]_[ASSET]_[DUR]_[RATIO]_[SPOT]_[COUNTRY]_[LANG]_[SOCIAL]_[TRACKING]

Field Position Changes:
- Subject Title: Position 5 → 3 (MOVED UP)
- Asset Type: Position 6 → 4 (MOVED UP)
- Duration: Position 8 → 5 (MOVED UP)
- Aspect Ratio: Position 9 → 6 (MOVED UP)
- Spot Version: 7 → 7 (SAME)
- Country Code: Position 3 → 8 (MOVED DOWN)
- Language Code: Position 4 → 9 (MOVED DOWN)
- Social Media: NEW → Position 10
- Tracking ID: Position 10 → 11

New Social Media Field:
- Field: social_media_version
- Position: 10 (after language, before tracking)
- Format: 3 uppercase letters
- Codes: FBP, FBR, IGF, IGR (expandable)
- Optional: Only present for social media assets

Parse Algorithm Changes:
- Positions 1-4 now: Job, Brand, Subject, Asset (fixed)
- Positions 5-11: Pattern-based detection (flexible)
- Duration detected by \d+S pattern
- Aspect ratio detected by \d+x\d+ or contains 'x'
- Spot detected by MST/REF
- Country detected by 2 upper alpha (after ratio)
- Language detected by 2-3 lower alpha (after country)
- Social detected by known codes (after language)
- Tracking ID detected by 6 alphanumeric + optional -N

strip_upload_components() Updated:
Now outputs: [BRAND]_[SUBJECT]_[ASSET]_[DUR]_[RATIO]_[SPOT]_[COUNTRY]_[LANG]_[SOCIAL]
- Includes social media version if present
- Still strips job number and tracking ID

Testing:
All 7 test cases from specification passed:
 All fields present
 Minimal (no duration/social/tracking)
 No duration
 No spot version
 With -N tracking (folder-only mode)
 No social media (most common)
 No tracking ID

Example:
Input:  1234567_RAF_TEST_OLV_6S_1x1_REF_GL_it_IGF_abc123.mp4
Parsed: brand=RAF, subject=TEST, country=GL, lang=it, social=IGF
Clean:  RAF_TEST_OLV_6S_1x1_REF_GL_it_IGF.mp4

Backward Compatibility:
None - system not live yet, clean cutover to V2.1 format only.

Backup: filename_parser_v1_backup.py contains old version for reference.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 13:41:19 -05:00
DJP
15eb47fc43 Add CreativeX fields to asset representation if missing from master metadata
Fixes issue where CreativeX score field was not appearing in final upload
because it didn't exist in the master metadata from DAM.

Problem:
- Master metadata from A1→A2 doesn't include CREATIVEX fields (new fields)
- _update_creativex_fields() only UPDATED existing fields
- If field not present, it logged error but didn't add the field
- Result: CREATIVEX score missing from upload, only URL appeared

Solution:
- Check if CREATIVEX Score field exists in mvp_fields
- If NOT found: Create and append field with proper structure
- If found: Update value as before
- Same logic for CREATIVEX URL field

Field Structures Created:

CREATIVEX Score (FERRERO.TAB.FIELD.CREATIVEX):
- Type: MetadataTableField (tabular field)
- Parent: FERRERO.TABULAR.FIELD.PLATFORMRATING
- Data type: INTEGER
- Value structure: {'value': {'value': score}}

CREATIVEX URL (FERRERO.FIELD.CREATIVEX LINK):
- Type: MetadataField (regular field)
- Data type: CHAR
- Value structure: {'value': {'value': url}}

Logging:
- Changed from ERROR to WARNING when field not found
- Logs "adding it now" instead of just error
- Confirms field added with value

Impact:
Both CreativeX fields will now appear in uploads even if master
metadata doesn't have them (common for older campaigns downloaded
before CreativeX integration).

Testing:
Run with --dryrun to verify both CREATIVEX fields in JSON output.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 12:44:17 -05:00
DJP
39a41df21d Fix CreativeX lookup to use original Box filename not stripped version
Changes database lookup strategy to match on full filename as it appears
in Box and in the CreativeX PDF report filename field.

Critical Design Change:

Old (incorrect):
- Strip job number and tracking ID from Box filename
- Lookup: NUT_PL_pl_TEST-E2E_EHI_1x1.png
- Database has: 6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png
- RESULT: No match found, uses defaults

New (correct):
- Use original Box filename for lookup
- Lookup: 6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png
- Database has: 6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png
- RESULT: Match found, uses actual score

Rationale:
The CreativeX PDF report contains a "filename" field that stores the
actual asset filename including job number and tracking ID. This is
the name that gets extracted by LlamaExtract and stored in database.

The A2→A3 workflow receives files from Box with the SAME filename
structure (job_brand_country_lang_subject_trackingID.ext).

Therefore, we match on the complete original filename, not the stripped
version.

Database Storage Pattern:
- CreativeX PDF named: anything.pdf (name doesn't matter)
- PDF contains field: filename = "6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png"
- Database stores: filename = "6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png"
- A2→A3 receives: 6487512_NUT_PL_pl_TEST-E2E_EHI_1x1_7xXgKp.png from Box
- Lookup matches exactly

Clean filename still used for DAM upload, only the lookup is on original.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 12:29:55 -05:00
DJP
2fef8878cd Add verbose debugging to _set_field_value for CreativeX troubleshooting
Adds detailed logging to trace exactly how field values are being set
and diagnose why CreativeX score/URL aren't appearing in final JSON.

Changes to _set_field_value():
- Logs field ID being updated
- Logs current field['value'] structure BEFORE setting
- Logs which code path is taken (nested vs created)
- Logs field['value'] structure AFTER setting
- Shows full JSON structure at each step

Output Example:
_set_field_value called for: FERRERO.TAB.FIELD.CREATIVEX with value: 85
Current field['value']: {
  "is_locked": false,
  "domain_value": false,
  ...
}
Created field['value'] = {'value': {'value': 85}}
After setting, field['value']: {
  "value": {
    "value": 85
  }
}

Purpose:
Diagnose why CreativeX fields show empty value dicts in asset
representation even though logs say "Set CREATIVEX Score to: 0".

This verbose logging will show:
1. What the field structure looks like before we set it
2. Which code path is executed
3. What the field structure looks like after we set it
4. Whether the value is actually being placed in the right location

Run with --dryrun to see full debug output without uploading.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 12:16:53 -05:00
DJP
d3722d1bb2 Fix CreativeX URL logging to say 'from database' not 'from Box'
Corrects misleading log messages and adds debugging for URL field.

Changes:
- Changed: "Updating CreativeX URL from Box"
- To: "Updating CreativeX URL from database"
- Added url_field_found flag
- Added URL field structure logging
- Added error handling with traceback
- Logs error if URL field not found in mvp_fields

Now both CreativeX fields log correctly:
- "Updating CreativeX Score from database: 0"
- "Updating CreativeX URL from database: https://..."

Accurate logging shows data source is PostgreSQL creativex_scores
table, not Box metadata templates (which are no longer used).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 12:11:58 -05:00
DJP
899d15322b Improve CreativeX field value setting with better error handling
Enhances _set_field_value() to handle empty value structures and
adds detailed logging for debugging CreativeX field issues.

Changes to metadata_extractor_mvp.py:

_set_field_value() Enhancement:
- Handles empty nested dicts by creating value structure
- If field['value']['value'] is empty dict, creates {'value': value}
- If field['value'] is empty dict, creates {'value': {'value': value}}
- Preserves existing behavior for populated structures

CreativeX Score Field Debugging:
- Added score_field_found flag to detect if field exists in mvp_fields
- Logs field structure before attempting to set value
- Shows dict keys to understand nesting
- Catches and logs full traceback on errors
- Errors if CREATIVEX Score field not found in mvp_fields
- Changed log: "from Box" → "from database" (accurate)

CreativeX URL Field:
- Existing logic preserved
- Uses same enhanced _set_field_value()

Purpose:
Diagnose why CreativeX score not appearing in asset representation
even though logs show "Set CREATIVEX Score to: 0"

Next Steps:
Run with --dryrun to see field structure logging and verify values
are being set correctly in the JSON output.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 12:11:22 -05:00
DJP
cdb839e3a8 Add --dryrun mode to A2→A3 and fix Agency name to Oliver
Adds debugging mode to A2→A3 workflow that builds full asset metadata
but doesn't upload to DAM, displaying complete JSON for field validation.

Changes to A2→A3 Script (a2_to_a3_upload_polling.py):

--dryrun Flag:
- New argument: --dryrun (build metadata but don't upload)
- Displays full asset representation as formatted JSON
- Shows field count
- Shows CreativeX lookup status
- Keeps file in Box (no deletion)
- Logs "DRYRUN MODE" clearly
- Returns success with 'DRYRUN_NO_UPLOAD' as asset_id

Dryrun Output Includes:
- Complete asset_representation JSON (all MVP fields)
- Field count (should be 27 fields)
- CreativeX status (found/missing)
- CreativeX score and URL values
- Clean separation with === lines

Usage:
python scripts/a2_to_a3_upload_polling.py --dryrun

Benefits:
- Debug metadata issues without DAM uploads
- Verify all fields present before going live
- Check CreativeX integration working
- Validate field values and formatting
- Safe testing with production data

Changes to Field Mappings (config/field_mappings.yaml):

Agency Name Fixed:
- Changed: FERRERO.MARKETING.FIELD.AGENCY NAME: "-"
- To: FERRERO.MARKETING.FIELD.AGENCY NAME: "Oliver"
- Exact case as required by DAM
- Comment updated to reflect this is final value

Impact:
- All A2→A3 uploads now have Agency Name = "Oliver"
- Not "Oliver Agency" (wrong)
- Not "-" placeholder (old)
- Exact case: "Oliver" (capital O, lowercase liver)

Use Case:
Run with --dryrun to see full JSON metadata, verify Agency name is
"Oliver", check all 27 MVP fields are present, then remove --dryrun
flag to perform actual uploads.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 11:59:55 -05:00
DJP
ca43aaf1bc Remove email notification when no CreativeX files found
Changes behavior to only log when Box folder is empty, not send emails.

Rationale:
- Empty folder is normal operation (not an error condition)
- Reduces email noise when script runs on cron
- Still logs the event for monitoring
- Similar to other workflows (a1_to_a2, etc.) that don't email when no work to do

Changes:
- Removed notifier.send_email() call for 'creativex_no_files' template
- Enhanced log message: "No PDF files found - this is normal when folder is empty"
- Added: "Script completed successfully with no files to process"
- Still returns success=True (not an error)

Template 'creativex_no_files' retained for potential future use but not called.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:58:09 -05:00
DJP
946c580024 Integrate CreativeX database lookup into A2→A3 upload workflow
Replaces Box metadata template with database lookup for CreativeX scores,
adding automatic fallback to default values when scores are missing.

Changes to A2→A3 Script (a2_to_a3_upload_polling.py):
- Removed: box.get_file_metadata() for CreativeX data
- Added: db.get_creativex_score_by_filename() database lookup
- Uses clean filename (stripped of OMG Job + Tracking ID) for lookup
- Implements fallback when score not found:
  - Default score: 0
  - Default URL: https://app.creativex.com/preflight/pretests
- Tracks creativex_found flag for email notifications
- Logs warnings when defaults are used

Email Template Updates (notifier.py):
- Shows " CreativeX Score Added: 85 (from database)" when found
- Shows "⚠️ CreativeX Score: Not found - used default (0)" when missing
- Adds orange warning box when defaults used:
  - Lists default values (Score: 0, placeholder URL)
  - Provides instructions to add score
  - References Box folder 350605024645 and scoring script

Benefits:
- Automatic CreativeX lookup (no manual Box metadata entry)
- Graceful degradation (uploads succeed even without scores)
- Clear notification when scores are missing
- Preserves history (uses latest active version)
- No breaking changes (existing workflow continues to work)

Default Value Strategy:
- Score 0 indicates "not scored" but doesn't block upload
- Placeholder URL is valid CreativeX domain
- Email clearly shows when defaults are used
- Provides actionable instructions for adding scores

Workflow Integration:
1. CreativeX PDFs uploaded to folder 350605024645
2. creativex_scoring_storing.py extracts and stores scores
3. A2→A3 automatically looks up scores by filename
4. Uploads proceed with actual scores OR defaults
5. Email indicates which path was taken

Documentation: A2_A3_CREATIVEX_INTEGRATION.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 17:02:25 -05:00
DJP
6fee0cc725 Add version tracking and remove .0 decimals from CreativeX scores
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>
2025-11-11 16:55:07 -05:00
DJP
1264eaf9bc Implement soft delete for CreativeX scores with history preservation
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>
2025-11-11 16:40:46 -05:00
DJP
b6b9d7337a Add CreativeX score extraction and storage system
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>
2025-11-11 16:15:45 -05:00
DJP
60a2f09e42 Add CreativeX fields to MVP and fix score update logic
Added CreativeX fields to MVP fields list:
- FERRERO.TAB.FIELD.CREATIVEX (CreativeX Score from Box)
- FERRERO.FIELD.CREATIVEX LINK (CreativeX URL from Box)

Fixed _update_creativex_fields method:
- Now actually SETS CreativeX Score value (was only logging before)
- Uses _set_field_value() to update the field
- Added try/except for tabular field structure handling
- Both score and URL now properly set from Box metadata

Flow:
1. Box metadata retrieved from Ferrero-DAM-Metadata template
2. creativexScore and creativexUrl extracted
3. Passed to metadata extractor
4. Applied to MVP fields during upload
5. CreativeX data preserved from Box → DAM

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:54:39 -05:00
DJP
0c2edc560f Add targeted campaign status update script
Created update_campaign_status.py - targeted campaign status control:
- Update specific campaign by number (--camp C000000078)
- Or search by partial campaign name (--camp "CONTENT SCALING")
- Set to any status (--status A1, A2, A3, etc.)
- Searches all statuses to find the campaign
- If multiple matches, lets user choose
- Interactive yes/no confirmation
- Shows current and target status
- Supports --auth-pfx for mTLS authentication

Usage examples:
  python scripts/update_campaign_status.py --camp C000000078 --status A2
  python scripts/update_campaign_status.py --camp "KINDER" --status A4
  python scripts/update_campaign_status.py --camp C000000551 --status A1 --auth-pfx

Features:
- Required flags: --camp and --status
- Searches across all statuses (A1-A6, B1-B2)
- Partial name matching (case insensitive)
- Multiple match handling with user selection
- Clear success/failure feedback

Updated README with complete documentation and examples

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 12:49:46 -05:00
DJP
a2e1058d20 Add interactive campaign status management scripts
Created 2 new interactive scripts for manual campaign status control:

1. reset_campaign_to_a1.py
   - Resets any campaign from any A# status back to A1
   - Allows filtering by specific status (--status A2, A3, A4, etc.)
   - Interactive yes/no confirmation for each campaign
   - Shows campaign name, number, and current status
   - Processes one at a time, waits for user input
   - Summary statistics at the end

2. advance_a1_to_a3.py
   - Advances A1 campaigns directly to A3 (skips A2)
   - Useful for testing A2→A3 upload workflow
   - Interactive yes/no confirmation for each campaign
   - Shows campaign details before advancing
   - Summary statistics at the end

Both scripts:
- Support --auth-pfx flag for mTLS authentication
- Process campaigns one at a time (not batch)
- Wait for user input before proceeding
- Provide clear feedback on success/failure
- Useful for testing and manual workflow control

Updated README.md with complete documentation and examples

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 15:23:26 -05:00
DJP
15aa68da92 Add --auth-pfx flag to A2→A3 upload script for mTLS support
- Added --auth-pfx argument to support mTLS certificate authentication
- DAMClient now initialized with use_mtls parameter
- Logs authentication method on startup
- Matches authentication options in other scripts (A1→A2, A4, A5→A6, B1→B2)

Usage:
  python scripts/a2_to_a3_upload_polling.py              # OAuth2 (default)
  python scripts/a2_to_a3_upload_polling.py --auth-pfx   # mTLS certificate

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:44:06 -05:00
DJP
d5c71b4c45 Add country code mapping system (ISO -> DAM codes)
- Created config/country_code_mappings.yaml with 165 country mappings
- ISO codes (used in filenames) now map to DAM-specific codes
- Codes under review set to XX as placeholder
- Added load_country_code_mappings() to config_loader.py
- Updated MetadataExtractorMVP to load and apply country mappings
- Added _map_country_code() and _get_field_value() helper methods
- Country mapping applies in both full and folder-only modes

Key mappings:
- BD (Bangladesh) -> BG (DAM code, though appears incorrect)
- DE (Germany) -> DE (same)
- IT (Italy) -> IT (same)
- Most codes under review -> XX (placeholder)

Mapping file can be edited without code changes - updates apply automatically

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:15:26 -05:00
DJP
914a178dc5 Implement V2 naming convention updates and folder structure support
Major changes:
1. Updated filename_parser.py for new V2 naming convention:
   - Spot version now accepts only MST or REF (optional)
   - Duration field is now optional
   - Tracking ID supports -N suffix for folder-only mode
   - Reduced minimum required parts from 9 to 7
   - Improved asset type detection logic

2. Added recursive folder scanning to box_client.py:
   - New list_folder_files_recursive() method
   - Skips first-level job/batch folders
   - Preserves folder structure from 2nd level onwards
   - Skips hidden folders (starting with . or _)

3. Updated A2→A3 upload workflow:
   - Uses recursive folder scanning
   - Extracts and logs tracking mode (full vs folder_only)
   - Handles subfolder paths for DAM uploads
   - Shows folder distribution in logs

4. Added folder-only mode to metadata_extractor_mvp.py:
   - New tracking_mode parameter (full/folder_only)
   - folder_only mode builds metadata entirely from filename
   - New _build_fields_from_filename() method

5. Added DAM subfolder creation to dam_client.py:
   - New get_or_create_subfolder_path() method
   - Creates matching folder structure in DAM
   - Helper methods _find_subfolder_by_name() and _create_folder()

Folder structure behavior:
- Box: DAM-UPLOAD/1234567/Europe/Germany/file.mp4
- DAM: 01. Final Assets/Europe/Germany/file.mp4
- Job folder (1234567) is skipped, structure preserved from 2nd level

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 18:33:35 -05:00
DJP
0d69c2b37f Update A4 webhook monitor to process all campaigns at once
- Changed from single-campaign mode to process all A4 campaigns in one run
- Added summary statistics (total found, webhooks sent, already processed, failed)
- Since webhooks are lightweight operations, processing all at once is efficient
- Duplicate prevention still works via database tracking

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:34:24 -05:00
DJP
89d36fc6b6 Fix campaign_number NULL constraint violation in A4 webhook
- Changed campaign.get('campaign_id', 'N/A') to use 'or' operator
- Now returns 'UNKNOWN' instead of None when campaign_id is missing
- Prevents NULL constraint violation in campaign_status table

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 16:40:21 -05:00
DJP
5f6d24c550 Fix timestamp bug in campaign status recording
- 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>
2025-11-05 16:34:46 -05:00