Add Upload from Box workflow - Phase 1 Complete
Core Components Implemented:
- FilenameParser: V2 naming convention parser with strict validation
- MetadataMerger: Merge master + filename metadata (filename priority)
- BoxFileRetriever: List/download files from Box folders
- DAM Lookup Domains: Complete documentation (182 domains)
Features:
- Parse V2 filenames: OMG_JOB_BRAND_COUNTRY_LANG_TITLE_TYPE_VER_SEC_RATIO_TRACKING
- Strip upload components (Job Number & Tracking ID)
- Extract tracking IDs and load master metadata from PostgreSQL
- Merge metadata with filename always winning
- Identify editable vs locked fields
- Build proper asset representation for DAM upload
Files Added:
- src/FilenameParser.php (tested - 8/8 passing)
- src/MetadataMerger.php
- src/BoxFileRetriever.php
- ECOMMERCE_ALLOWED_FIELDS.md (182 lookup domains)
- DAM_LOOKUPDOMAINS_RAW.json (15MB raw data)
- test_filename_parser.php
- fetch_lookupdomains.php
- UPLOAD_FROM_BOX_STATUS.md (complete documentation)
Next Phase: UI integration - Add "Upload from Box" tab to workflow_v3.php
🤖 Generated with Claude Code
This commit is contained in:
parent
8d588f0cac
commit
3a95076726
8 changed files with 2340 additions and 0 deletions
1
DAM_LOOKUPDOMAINS_RAW.json
Normal file
1
DAM_LOOKUPDOMAINS_RAW.json
Normal file
File diff suppressed because one or more lines are too long
222
ECOMMERCE_ALLOWED_FIELDS.md
Normal file
222
ECOMMERCE_ALLOWED_FIELDS.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# DAM Lookup Domains - All Available Fields
|
||||
|
||||
**Generated:** 2025-10-29 19:41:32
|
||||
**Endpoint:** `GET /v6/lookupdomains`
|
||||
**Base URL:** https://ppr.dam.ferrero.com/otmmapi
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Total lookup domains: 182
|
||||
|
||||
---
|
||||
|
||||
## Lookup Domains Summary
|
||||
|
||||
| Domain ID | Datatype | Values Count | Example Value |
|
||||
|-----------|----------|--------------|---------------|
|
||||
| `FERRERO.MARKETUNIT` | CHAR | 51 | `E**` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.CAMPID` | CHAR | 585 | ` S006` |
|
||||
| `ARTESIA.DOMAIN.MEDIA_ANALYSIS.SOURCE.LANGUAGE` | CHAR | 805 | `rw_RW` |
|
||||
| `ARTESIA.DOMAIN.LC_APPROVAL_STATE` | CHAR | 3 | `APPROVED` |
|
||||
| `FERRERO.DOMAIN.CAMPAIGN STAGES` | CHAR | 4 | `1` |
|
||||
| `FERRERO.DOMAIN.APPROVAL TAGS` | CHAR | 4 | `External Use` |
|
||||
| `ARTESIA.DOMAIN.RJ_SIMPLE_ROLE` | INTEGER | 3 | `60` |
|
||||
| `ARTESIA_EMBED.DOMAIN.FLASH_MODE_LU` | INTEGER | 4 | `0` |
|
||||
| `ARTESIA.DOMAIN.SECURITY_POLICIES` | INTEGER | 1810 | `8` |
|
||||
| `FERRERO.DOMAIN.ARCHIVE.RELEVANT` | CHAR | 2 | `Explore` |
|
||||
| `ARTESIA.DOMAIN.RIGHT_HOLDERS` | NUMBER | 1 | `1` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.CAMPNAME` | CHAR | 605 | `01_TIMEOUT_TE_DH_GL_BIS_FY2...` |
|
||||
| `FERRERO.DOMAIN.CREATIVEX.CHANNEL` | CHAR | 8 | `Google Ads` |
|
||||
| `FERRERO.DOMAIN.PACK` | CHAR | 1856 | `001` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.OCCASION` | CHAR | 39 | `E01` |
|
||||
| `FERRERO.DOMAIN.TAGS` | CHAR | 21 | `Book` |
|
||||
| `FERRERO_MARKETING_FISCAL_YEAR` | CHAR | 13 | `2017/2018` |
|
||||
| `ARTESIA_EMBED.DOMAIN.DESCRIPTION_CR_LU` | CHAR | 3 | `CS-PRO` |
|
||||
| `FERRERO.DOMAIN.MAIN CATEGORY` | CHAR | 14 | `00` |
|
||||
| `FERRERO.DOMAIN.CATEGORY` | CHAR | 18 | `BISCUITS` |
|
||||
| `ARTESIA.DOMAIN.RJ_APPROVAL_STATE` | INTEGER | 3 | `10` |
|
||||
| `ARTESIA.DOMAIN.MODELS` | INTEGER | 39 | `1` |
|
||||
| `FERRERO.DOMAIN.CAMPAIGN.STAGE` | CHAR | 4 | `1 - Brief` |
|
||||
| `OTMM.DOMAIN.OTMM_COUNTRY_LU` | CHAR | 197 | `1` |
|
||||
| `FERRERO.MARKET.TECH_VALID` | CHAR | 2 | `No` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.RETIREDATE` | CHAR | 27 | `-` |
|
||||
| `OTMM.DOMAIN.OTMM_PRODUCTMODEL_LU` | CHAR | 0 | `N/A` |
|
||||
| `FERRERO.DOMAIN.CAMPAIGN.DEPARTMENT` | CHAR | 5 | `Brand Manager team` |
|
||||
| `FERRERO.CONTENTSCALING.STATUS` | CHAR | 9 | `A6` |
|
||||
| `FERRERO.DOMAIN.DESTINATIONS` | CHAR | 0 | `N/A` |
|
||||
| `REGION` | CHAR | 28 | `E001` |
|
||||
| `FERRERO.DOMAIN.CONTENT_STATUS` | CHAR | 2 | `COMPLETE_ASSET` |
|
||||
| `FERRERO.DOMAIN.PREDECESSOR` | CHAR | 448 | `01` |
|
||||
| `ARTESIA.DOMAIN.PARTICIPANT_ROLE` | CHAR | 53 | `Account Executive` |
|
||||
| `ARTESIA.DOMAIN.BITMAP_ORIENTATIONS` | CHAR | 10 | `BottomLeft` |
|
||||
| `ARTESIA_EMBED.DOMAIN.RATING_LU` | INTEGER | 6 | `0` |
|
||||
| `ARTESIA.DOMAIN.METADATA_STATES` | CHAR | 4 | `LOCKED` |
|
||||
| `FERRERO.DOMAIN.MIGRATION.INDEX` | CHAR | 41607 | `B01_00001` |
|
||||
| `ARTESIA.DOMAIN.CR_USER_STATUS` | INTEGER | 4 | `1003` |
|
||||
| `ARTESIA_EMBED.DOMAIN.RESOLUTION_UNIT_LU` | INTEGER | 5 | `2` |
|
||||
| `FERRERO_DOMAIN_FISCAL_YEAR` | CHAR | 11 | `2019/2020` |
|
||||
| `FERRERO.DOMAIN.MEDIA_TYPE` | CHAR | 8 | `BILLBOARD` |
|
||||
| `FERRERO_CONTENTSCALING_AN_LU` | CHAR | 2 | `HWW` |
|
||||
| `ARTESIA.DOMAIN.REVIEWJOB_USERS` | CHAR | 0 | `N/A` |
|
||||
| `FERRERO.DOMAIN.GLOBAL.LOCAL` | CHAR | 2 | `GLOBAL` |
|
||||
| `ARTESIA.DOMAIN.PO_LOCATIONS` | INTEGER | 4 | `3` |
|
||||
| `FERRERO.DOMAIN.NUMBER.CHAR` | CHAR | 26 | `0` |
|
||||
| `ARTESIA.DOMAIN.CR_PRIORITY` | INTEGER | 3 | `3` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.STATUS` | CHAR | 2 | `PUBLISHED` |
|
||||
| `ARTESIA.DOMAIN.PO_COLORS` | INTEGER | 3 | `3` |
|
||||
| `FERRERO.DOMAIN.SUBBRAND` | CHAR | 1610 | `EPF0100` |
|
||||
| `ARTESIA.DOMAIN.RJ_SIMPLE_TASK_TYPE` | INTEGER | 2 | `6` |
|
||||
| `FERRERO.DOMAIN.MARKETING.SIZE` | CHAR | 2 | `Mini` |
|
||||
| `CATEGORY` | CHAR | 23 | `AA` |
|
||||
| `FERRERO.DOMAIN.HISTORICAL.CONTENT TYPE` | CHAR | 1 | `Historical Archive` |
|
||||
| `FERRERO.MARKETING.SPOT_TYPE` | CHAR | 5 | `-` |
|
||||
| `FERRERO.DOMAIN.SCOPE` | CHAR | 2 | `GLOBAL` |
|
||||
| `FERRERO.DOMAIN.PRODUCT VIEW` | CHAR | 7 | `BACK` |
|
||||
| `FERRERO.DOMAIN.ASSETCOMPLIANCE` | CHAR | 2 | `-` |
|
||||
| `FERRERO.MARKET.TYPE_VID_STAT` | CHAR | 4 | `Actor` |
|
||||
| `MIGRATION_DOMAIN_TABLE` | CHAR | 33 | `additionalLanguage` |
|
||||
| `ARTESIA.DOMAIN.BUSINESS_UNITS` | CHAR | 0 | `N/A` |
|
||||
| `FERRERO.IPCOVERAGE` | CHAR | 2 | `Global` |
|
||||
| `ARTESIA.DOMAIN.RJ_TASK_STATUS` | INTEGER | 3 | `1003` |
|
||||
| `ARTESIA.DOMAIN.RJ_TASK_TYPE` | INTEGER | 4 | `4` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.CAMPCONCEPT` | CHAR | 586 | `-` |
|
||||
| `ARTESIA_EMBED.DOMAIN.URGENCY_LU` | INTEGER | 8 | `1` |
|
||||
| `FERRERO.DOMAIN.REGION` | CHAR | 24 | `E001` |
|
||||
| `FERRERO.DOMAIN.PROJECT TIMING` | CHAR | 39 | `E01` |
|
||||
| `ARTESIA_EMBED.DOMAIN.FLASH_RETURN_LU` | INTEGER | 3 | `0` |
|
||||
| `ARTESIA_EMBED.DOMAIN.AUDIO_LOOP_LU` | INTEGER | 10 | `0` |
|
||||
| `FERRERO.DOMAIN.MARKETING.LICENSE.COUNTRY` | CHAR | 224 | `-` |
|
||||
| `FERRERO.DOMAIN.TYPE` | CHAR | 5 | `ACTOR` |
|
||||
| `ARTESIA.DOMAIN.CR_ROLE` | INTEGER | 5 | `40` |
|
||||
| `OTMM.DOMAIN.OTMM_BRAND_LU` | CHAR | 0 | `N/A` |
|
||||
| `FERRERO.DOMAIN.MARKETING.PRODUCT` | CHAR | 1795 | `-` |
|
||||
| `FERRERO.DOMAIN.RELATED.BRAND` | CHAR | 448 | `01` |
|
||||
| `FERRERO.MARKETING.DOCUMENT TYPE` | CHAR | 10 | `Area Annual Communication I...` |
|
||||
| `FERRERO.DOMAIN.MARKETING.FLAVOUR` | CHAR | 75 | `Apricot` |
|
||||
| `ARTESIA.DOMAIN.BITMAP_COLOR_MAP` | CHAR | 36 | `BAW` |
|
||||
| `FERRERO.DOMAIN.ASSETSOURCELANGUAGE` | CHAR | 26 | `Arabic` |
|
||||
| `ARTESIA.DOMAIN.ROLES` | NUMBER | 46 | `10000` |
|
||||
| `FERRERO.DMAIN.RETAILER` | CHAR | 222 | `-` |
|
||||
| `FERRERO.DOMAIN.STATE` | CHAR | 2 | `Global` |
|
||||
| `FERRERO.MARKETING.AGENCY_NAME` | CHAR | 1938 | `-` |
|
||||
| `ARTESIA.DOMAIN.CONTAINER TYPE NAMES` | CHAR | 36 | `ARTESIA.BASIC.FOLDER` |
|
||||
| `FERRERO.MARKETING.TOTAL_BUYOUT` | CHAR | 3 | `Digital` |
|
||||
| `ARTESIA.DOMAIN.CR_STATUS` | INTEGER | 4 | `3` |
|
||||
| `FERRERO.DOMAIN.CONTENT TYPE` | CHAR | 1 | `Corporate` |
|
||||
| `FERRERO.DOMAIN.AREA` | CHAR | 6 | `E0` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.FISCALYR` | CHAR | 11 | `2016/2017` |
|
||||
| `FERRERO.MARKETING.ASPECT_RATIO` | CHAR | 6 | ` 2:3` |
|
||||
| `ARTESIA.DOMAIN.BITMAP_COLOR_DEPTHS` | CHAR | 15 | `0` |
|
||||
| `ARTESIA.DOMAIN.CR_APPROVAL_STATE` | INTEGER | 3 | `10` |
|
||||
| `ARTESIA.DOMAIN.RJ_TYPE` | INTEGER | 2 | `2` |
|
||||
| `AREA` | CHAR | 16 | `E0` |
|
||||
| `ARTESIA.DOMAIN.MEDIA_ANALYSIS.AGE GROUP` | NUMBER | 5 | `4` |
|
||||
| `FERRERO.DOMAIN.CAMPAIGN.PHASE` | CHAR | 6 | `1.1 - Brief approval` |
|
||||
| `FERRERO.DOMAIN.SUCCESSOR` | CHAR | 448 | `01` |
|
||||
| `FERRERO_MARKETING_ADVANCE_PRODUCT_LINK` | CHAR | 2 | `Brand` |
|
||||
| `ARTESIA.DOMAIN.USAGE APPLICATION` | INTEGER | 3 | `1` |
|
||||
| `FERRERO.DOMAIN.RETAILER` | CHAR | 2 | `01` |
|
||||
| `FERRERO.DOMAIN.ECOMMERCE.CATEGORY` | CHAR | 23 | `AA` |
|
||||
| `OTMM.DOMAIN.OTMM_PERMITTEDUSE_LU` | CHAR | 6 | `1` |
|
||||
| `ARTESIA.DOMAIN.RJ_REVIEW_MODE` | INTEGER | 2 | `2` |
|
||||
| `ARTESIA.DOMAIN.DOCUMENT_TYPE` | CHAR | 5 | `DOC` |
|
||||
| `FERRERO.DOMAIN.HARMONIZEDBRAND` | CHAR | 212 | `01` |
|
||||
| `ARTESIA.DOMAIN.CONTENT_STATES` | CHAR | 4 | `DELETED` |
|
||||
| `FERRERO.DOMAIN.MARKETING.LICENSE.CAMPAIGN` | CHAR | 12 | `2020/2021` |
|
||||
| `FERRERO.MARKETING.IPRIGHTS` | CHAR | 2 | `No` |
|
||||
| `OTMM.DOMAIN.OTMM_PRODUCTSKU_LU` | CHAR | 0 | `N/A` |
|
||||
| `ARTESIA.DOMAIN.GENERATIVE_AI_PROVIDERS` | CHAR | 1 | `Google Vertex AI` |
|
||||
| `DOMAIN.CHROMATISM` | CHAR | 2 | `BLACK&WHITE` |
|
||||
| `ARTESIA.DOMAIN.PARTICIPANT_DEPARTMENTS` | CHAR | 32 | `Account Planning` |
|
||||
| `ARTESIA.DOMAIN.TEAMS_USER_IDS` | CHAR | 1893 | `2837` |
|
||||
| `ARTESIA.DOMAIN.TAGS` | CHAR | 2408 | `10 sec` |
|
||||
| `FERRERO.MARKETING.FERRERO_PROPERTY` | CHAR | 2 | `No` |
|
||||
| `FERRERO.DOMAIN.CONTENTSCALING.AN` | CHAR | 2 | `HWW` |
|
||||
| `BRAND` | CHAR | 469 | `-` |
|
||||
| `FERRERO.DOMAIN.MARKETING.ASSETTYPE` | CHAR | 45 | `aplus` |
|
||||
| `FERRERO.ECOM.DOMAIN.RTAG` | CHAR | 1 | `Retailer Specific` |
|
||||
| `ARTESIA.DOMAIN.CR_TYPE` | INTEGER | 2 | `2` |
|
||||
| `FERRERO.DOMAIN.ECOMMERCE.BRAND` | CHAR | 468 | `EPFBI` |
|
||||
| `FERRERO.DOMAIN.SUPPORTVALUELIST` | CHAR | 13 | `16mm` |
|
||||
| `ARTESIA.DOMAIN.RIGHT_HOLDER_TYPES` | NUMBER | 3 | `10001` |
|
||||
| `ARTESIA.DOMAIN.CONTENT_TYPES` | CHAR | 18 | `3D` |
|
||||
| `FERRERO.MARKETING.ASSETNAMINGCONVENTION` | CHAR | 2 | `NO` |
|
||||
| `ARTESIA.DOMAIN.RJ_STATUS` | INTEGER | 4 | `3` |
|
||||
| `FERRERO.MARKET.DOMAIN.TAG` | CHAR | 563 | ` bite` |
|
||||
| `FERRERO.DOMAIN.HA.TAGS` | CHAR | 111 | `ALMOND` |
|
||||
| `ARTESIA.DOMAIN.RJ_PRIORITY` | INTEGER | 3 | `3` |
|
||||
| `OTMM.DOMAIN.OTMM_LANGUAGE_LU` | CHAR | 83 | `1` |
|
||||
| `FERRERO.DOMAIN.AGENCYCOLLABORATION` | CHAR | 2 | `HWW Collaboration` |
|
||||
| `FERRERO.ECOM.DOMAIN.APPROVAL` | CHAR | 2 | `CC Approved` |
|
||||
| `FERRERO_AGENCYSHARING_LU` | CHAR | 2 | `HWW Sharing` |
|
||||
| `MARKET_UNIT` | CHAR | 51 | `E**` |
|
||||
| `EAN.CODE` | CHAR | 18664 | `10052745652614` |
|
||||
| `ARTESIA.DOMAIN.PO_TYPES` | INTEGER | 10 | `9` |
|
||||
| `ARTESIA_EMBED.DOMAIN.FLASH_IS_FIRED_LU` | CHAR | 2 | `No` |
|
||||
| `DOMAIN.GLOBAL.LOCAL` | CHAR | 2 | `GLOBAL` |
|
||||
| `FERRERO.DOMAIN.MAIN LAGUAGES_LU` | CHAR | 28 | `AR` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.CAMPTYPE` | CHAR | 8 | ` LOCAL COMM ON GLOBAL BRANDS ` |
|
||||
| `FERRERO.DOMAIN.MATERIAL TYPE` | CHAR | 16 | `Albums and stickers` |
|
||||
| `ARTESIA.DOMAIN.MIME_TYPES` | NUMBER | 1978 | `772` |
|
||||
| `FERRERO.DOMAIN.CONFIDENTIALITY` | CHAR | 3 | `CONFIDENTIAL` |
|
||||
| `ARTESIA.DOMAIN.MIME_TYPE_NAMES` | CHAR | 1978 | `application/1d-interleaved-...` |
|
||||
| `FERRERO.DOMAIN.CAMPAIGN.STEP` | CHAR | 35 | `1.1.0 - Brief to be uploaded` |
|
||||
| `FERRERO.DOMAIN.PRODUCT.ANGLE` | CHAR | 5 | `CENTRAL` |
|
||||
| `FERRERO_AGENCYCOLLABORATION_LU` | CHAR | 2 | `HWW Collaboration` |
|
||||
| `SUBCATEGORY` | CHAR | 34 | `Better For You` |
|
||||
| `FERRERO.MARKETING.CREATOR` | CHAR | 1893 | ` Chandra123` |
|
||||
| `ARTESIA_EMBED.DOMAIN.COLOR_SPACE_LU` | INTEGER | 4 | `-1` |
|
||||
| `ARTESIA.DOMAIN.CONTENT.STORAGE_PROVIDER` | CHAR | 2 | `ARTESIA.STORAGE.PROVIDER.CLOUD` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.MAINCATE` | CHAR | 23 | `AA` |
|
||||
| `ARTESIA.DOMAIN.RJ_ROLE` | INTEGER | 4 | `30` |
|
||||
| `FERRERO.DOMAIN.GLOBAL` | CHAR | 2 | `Global` |
|
||||
| `OTMM.DOMAIN.OTMM_ASSETTYPE_LU` | CHAR | 12 | `angledview` |
|
||||
| `FERRERO.DOMAIN.AGENCYSHARING` | CHAR | 2 | `HWW Sharing` |
|
||||
| `FERRERO.DOMAIN.PRODUCT STATE` | CHAR | 5 | `CASE` |
|
||||
| `FERRERO.DOMAIN.ONTHEMARKET` | CHAR | 2 | `01` |
|
||||
| `ARTESIA.DOMAIN.DO_TYPES` | INTEGER | 6 | `4` |
|
||||
| `FERRERO.DOMAIN.MARKET` | CHAR | 55 | `AREA IBERICA` |
|
||||
| `ARTESIA.DOMAIN.EXT.COLLAB.SRVS` | NUMBER | 1 | `1` |
|
||||
| `FERRERO.DOMAIN.BRAND` | CHAR | 77 | `N8` |
|
||||
| `ARTESIA.DOMAIN.MEDIA_ANALYSIS.GENDER` | CHAR | 2 | `ARTESIA.MA.GENDER.TYPE.FEMALE` |
|
||||
| `FERRERO.DOMAIN.STATUS` | CHAR | 2 | `HIDE` |
|
||||
| `ARTESIA.DOMAIN.USERS` | CHAR | 1893 | `2837` |
|
||||
| `FERRERO.DOMAIN.PROJECT_TYPE` | CHAR | 4 | `E01` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.GLOBALREF` | CHAR | 32 | `-` |
|
||||
| `FERRERO.MARKETING.SPOT_VERSION` | CHAR | 2 | `Master` |
|
||||
| `ARTESIA.DOMAIN.CONTENT.STORAGE_TYPE` | CHAR | 4 | `ARTESIA.STORAGE.TYPE.DEFAULT` |
|
||||
| `FERRERO.DOMAIN.MARKETING.PROPERTY.LICENSE` | CHAR | 79 | ` MY LITTLE PONY EQUESTRIA G...` |
|
||||
| `ARTESIA.DOMAIN.METADATA_PROFILES` | CHAR | 29 | `b803833bdb5e71200fc0e1dc2f0...` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.COUNTRY` | CHAR | 85 | ` ` |
|
||||
| `ARTESIA.DOMAIN.CREATIVEREVIEW USERS` | CHAR | 0 | `N/A` |
|
||||
| `ARTESIA.DOMAIN.USAGE CATEGORY` | INTEGER | 2 | `2` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.RELEASEDATE` | CHAR | 135 | `01/07/2025` |
|
||||
| `FERRERO.DOMAIN.MARKETING.LICENSOR` | CHAR | 43 | `ACAMAR` |
|
||||
| `ARTESIA.DOMAIN.YES_NO` | CHAR | 2 | `N` |
|
||||
| `ARTESIA.DOMAIN.CR_REVIEW_MODE` | INTEGER | 2 | `2` |
|
||||
| `ARTESIA_EMBED.DOMAIN.LIGHT_SOURCE_LU` | INTEGER | 21 | `0` |
|
||||
| `FERRERO.CAMPAIGNFOLDER.BRAND` | CHAR | 469 | `-` |
|
||||
|
||||
> **Note:** Full details and all allowed values are available in `DAM_LOOKUPDOMAINS_RAW.json`
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Usage in Asset Upload
|
||||
|
||||
These lookup domains define the allowed values for metadata fields in the ECOMMERCE model.
|
||||
|
||||
When uploading an asset, you must use field IDs and values from these domains.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "FERRERO.FIELD.MKTG.ASSET TYPE",\n "value": {\n "domain_value": true,\n "value": {\n "type": "string",\n "value": "heroimage" // Must be from allowed values\n }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
715
UPLOAD_FROM_BOX_STATUS.md
Normal file
715
UPLOAD_FROM_BOX_STATUS.md
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
# Upload from Box Workflow - Implementation Status
|
||||
|
||||
**Date:** October 29, 2025
|
||||
**Session:** Asset Upload (A2→A3) - Phase 1 Complete
|
||||
**Status:** Core Components Built ✅ | UI Integration Pending ⏳
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Successfully implemented the core backend components for the "Upload from Box" workflow. This feature allows users to:
|
||||
1. Load files from a Box folder (by Folder ID)
|
||||
2. Parse V2 naming convention filenames
|
||||
3. Extract tracking IDs and load master metadata from PostgreSQL
|
||||
4. Merge master metadata with filename-derived data (filename wins)
|
||||
5. Prepare assets for upload to DAM
|
||||
|
||||
**Phase 1 (Core Components):** ✅ COMPLETE
|
||||
**Phase 2 (UI Integration):** ⏳ READY TO START
|
||||
|
||||
---
|
||||
|
||||
## What Was Built
|
||||
|
||||
### 1. DAM Lookup Domains Documentation ✅
|
||||
|
||||
**File:** `ECOMMERCE_ALLOWED_FIELDS.md`
|
||||
**Purpose:** Complete reference of all 182 DAM lookup domains and their allowed values
|
||||
|
||||
**Key Information:**
|
||||
- All available field IDs
|
||||
- Field datatypes
|
||||
- Allowed values count
|
||||
- Example values
|
||||
|
||||
**Usage:** Reference this when building metadata or validating field values
|
||||
|
||||
---
|
||||
|
||||
### 2. FilenameParser Class ✅
|
||||
|
||||
**File:** `src/FilenameParser.php`
|
||||
**Purpose:** Parse and validate V2 naming convention filenames
|
||||
|
||||
**V2 Naming Convention:**
|
||||
```
|
||||
[OMG_JOB_NUMBER]_[BRAND_CODE]_[COUNTRY_CODE]_[LANGUAGE_CODE]_[SUBJECT_TITLE]_[ASSET_TYPE]_[SPOT_VERSION]_[SECONDS]S_[ASPECT_RATIO]_[TRACKING_ID]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Input: 1234567_RAF_CH_de_TEST_FILE_OLV_001_15S_16x9_a7K9mP.mp4
|
||||
Output: RAF_CH_de_TEST_FILE_OLV_001_15S_16x9.mp4 (OMG Job & Tracking ID stripped)
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `parseFilename($filename)` - Parse into components
|
||||
- `validateStructure($filename)` - Strict validation
|
||||
- `stripUploadComponents($filename)` - Remove job number & tracking ID
|
||||
- `getCleanFilename($filename)` - Get upload-ready filename
|
||||
- `extractTrackingId($filename)` - Get tracking ID only
|
||||
|
||||
**Features:**
|
||||
- Strict validation (rejects non-compliant filenames)
|
||||
- Detailed error messages for each component
|
||||
- Warnings for non-critical issues
|
||||
- Handles multi-word subject titles
|
||||
- MST (Master) file detection
|
||||
- Comprehensive test suite (8/8 tests passing)
|
||||
|
||||
**Test File:** `test_filename_parser.php` - Run to verify functionality
|
||||
|
||||
---
|
||||
|
||||
### 3. MetadataMerger Class ✅
|
||||
|
||||
**File:** `src/MetadataMerger.php`
|
||||
**Purpose:** Merge master metadata with filename-derived data
|
||||
|
||||
**Merge Strategy:**
|
||||
- **Priority:** Filename always wins (as per requirements)
|
||||
- **Master Fields:** Locked (read-only in UI)
|
||||
- **Derived Fields:** Editable (from filename parsing)
|
||||
|
||||
**Derived (Editable) Fields:**
|
||||
- `FERRERO.FIELD.MKTG.ASSET TYPE` - From filename asset_type
|
||||
- `MAIN_LANGUAGES` - From filename language_code
|
||||
- `ARTESIA.FIELD.ASSET NAME` - From filename
|
||||
- `FERRERO.FIELD.SUB BRAND` - From filename brand_code
|
||||
- `FERRERO.FIELD.STATE` - Default: "Local"
|
||||
- `FERRERO.FIELD.FISCAL YEAR` - Default: "2025/2026"
|
||||
|
||||
**Key Methods:**
|
||||
- `mergeMetadata($masterMetadata, $parsedFilename)` - Merge data sources
|
||||
- `buildAssetRepresentation($mergedMetadata)` - Create API upload JSON
|
||||
- `identifyEditableFields($mergedMetadata)` - List editable field IDs
|
||||
- `getConflicts($mergedMetadata)` - Track filename vs master conflicts
|
||||
- `formatForDisplay($mergedMetadata)` - Human-readable output
|
||||
|
||||
**Conflict Tracking:**
|
||||
- Logs all cases where filename data overrides master data
|
||||
- Useful for debugging and validation
|
||||
|
||||
---
|
||||
|
||||
### 4. BoxFileRetriever Class ✅
|
||||
|
||||
**File:** `src/BoxFileRetriever.php`
|
||||
**Purpose:** List and download files from Box folders
|
||||
|
||||
**Key Methods:**
|
||||
- `listFilesInFolder($folderId)` - Get all files in Box folder
|
||||
- `listFilesWithTrackingIDs($folderId)` - Include tracking ID parsing
|
||||
- `getFileMetadata($fileId)` - Get specific file details
|
||||
- `downloadFile($fileId, $filename)` - Download to temp directory
|
||||
- `extractTrackingId($filename)` - Parse tracking ID from filename
|
||||
- `testConnection()` - Verify Box access
|
||||
|
||||
**Features:**
|
||||
- JWT authentication (production-ready, no expiring tokens)
|
||||
- Automatic temp directory management
|
||||
- File filtering (only files, not folders)
|
||||
- Tracking ID extraction
|
||||
- Box URL generation
|
||||
|
||||
**Temp Directory:** `/tmp/ferrero_box_downloads/`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Ferrero-Opentext/
|
||||
├── src/
|
||||
│ ├── FilenameParser.php ✅ NEW - V2 naming parser
|
||||
│ ├── MetadataMerger.php ✅ NEW - Merge master + filename
|
||||
│ ├── BoxFileRetriever.php ✅ NEW - Box folder file listing
|
||||
│ ├── BoxClient.php ✅ EXISTING - Box JWT auth
|
||||
│ ├── DatabaseClient.php ✅ EXISTING - PostgreSQL access
|
||||
│ ├── AssetUploaderSimple.php ✅ EXISTING - Upload to DAM
|
||||
│ └── [other existing files]
|
||||
│
|
||||
├── ECOMMERCE_ALLOWED_FIELDS.md ✅ NEW - Field documentation
|
||||
├── DAM_LOOKUPDOMAINS_RAW.json ✅ NEW - Raw API response (15MB)
|
||||
├── test_filename_parser.php ✅ NEW - Parser test suite
|
||||
├── fetch_lookupdomains.php ✅ NEW - Lookup fetch script
|
||||
│
|
||||
├── workflow_v3.php ⏳ NEEDS UPDATE - Add "Upload from Box" tab
|
||||
└── UPLOAD_FROM_BOX_STATUS.md ✅ THIS FILE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works - Complete Flow
|
||||
|
||||
### Step 1: User Provides Box Folder ID
|
||||
**Input:** Box Folder ID (e.g., 348304357505)
|
||||
**Action:** Paste into interface
|
||||
|
||||
### Step 2: List Files from Box
|
||||
```php
|
||||
$retriever = new BoxFileRetriever();
|
||||
$result = $retriever->listFilesWithTrackingIDs($folderId);
|
||||
|
||||
// Returns:
|
||||
[
|
||||
'success' => true,
|
||||
'file_count' => 5,
|
||||
'files' => [
|
||||
[
|
||||
'id' => '2029961099212',
|
||||
'name' => '1234567_RAF_CH_de_TEST_OLV_001_15S_16x9_a7K9mP.mp4',
|
||||
'tracking_id' => 'a7K9mP',
|
||||
'has_tracking_id' => true,
|
||||
'size' => 5242880,
|
||||
'box_url' => 'https://app.box.com/file/2029961099212'
|
||||
],
|
||||
// ... more files
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### Step 3: Parse Each Filename
|
||||
```php
|
||||
$parser = new FilenameParser();
|
||||
$parsed = $parser->parseFilename($filename);
|
||||
|
||||
// Returns:
|
||||
[
|
||||
'omg_job_number' => '1234567',
|
||||
'brand_code' => 'RAF',
|
||||
'country_code' => 'CH',
|
||||
'language_code' => 'de',
|
||||
'subject_title' => 'TEST',
|
||||
'asset_type' => 'OLV',
|
||||
'spot_version' => '001',
|
||||
'seconds' => '15',
|
||||
'aspect_ratio' => '16x9',
|
||||
'tracking_id' => 'a7K9mP',
|
||||
'is_valid' => true,
|
||||
'validation_errors' => []
|
||||
]
|
||||
```
|
||||
|
||||
### Step 4: Load Master Metadata from Database
|
||||
```php
|
||||
$db = DatabaseClient::getInstance();
|
||||
$masterAsset = $db->lookupByTrackingId('a7K9mP');
|
||||
|
||||
// Returns master asset metadata from PostgreSQL:
|
||||
[
|
||||
'tracking_id' => 'a7K9mP',
|
||||
'opentext_id' => '0008a50461e6a554...',
|
||||
'upload_directory' => 'ea0dbf86e13e3634...',
|
||||
'metadata' => { /* Full DAM metadata JSON */ }
|
||||
]
|
||||
```
|
||||
|
||||
### Step 5: Merge Metadata
|
||||
```php
|
||||
$merger = new MetadataMerger();
|
||||
$merged = $merger->mergeMetadata($masterAsset, $parsed);
|
||||
|
||||
// Filename data overrides master data
|
||||
// Tracks source of each field (master/filename/default)
|
||||
// Identifies editable vs locked fields
|
||||
```
|
||||
|
||||
### Step 6: Build Asset Representation
|
||||
```php
|
||||
$assetRepresentation = $merger->buildAssetRepresentation($merged);
|
||||
|
||||
// Creates proper structure for DAM API:
|
||||
[
|
||||
'asset_resource' => [
|
||||
'asset' => [
|
||||
'metadata' => { /* Merged metadata */ },
|
||||
'metadata_model_id' => 'ECOMMERCE',
|
||||
'security_policy_list' => [['id' => 1594]]
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### Step 7: Strip Filename Components
|
||||
```php
|
||||
$cleanFilename = $parser->stripUploadComponents($filename);
|
||||
|
||||
// Input: 1234567_RAF_CH_de_TEST_OLV_001_15S_16x9_a7K9mP.mp4
|
||||
// Output: RAF_CH_de_TEST_OLV_001_15S_16x9.mp4
|
||||
```
|
||||
|
||||
### Step 8: Upload to DAM
|
||||
```php
|
||||
$uploader = new AssetUploaderSimple();
|
||||
$uploadResult = $uploader->uploadAsset(
|
||||
$cleanFilename,
|
||||
$localFilePath,
|
||||
$assetRepresentation,
|
||||
$masterAsset['upload_directory']
|
||||
);
|
||||
|
||||
// Uploads to correct folder with proper metadata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps - Phase 2: UI Integration
|
||||
|
||||
### Task 5: Add "Upload from Box" Tab ⏳
|
||||
|
||||
**File to Edit:** `workflow_v3.php`
|
||||
|
||||
**Location:** After the "Upload" tab (around line 800-900)
|
||||
|
||||
**UI Components Needed:**
|
||||
|
||||
#### A. Box Folder Input Section
|
||||
```html
|
||||
<div class="form-group">
|
||||
<label>Box Folder ID</label>
|
||||
<input type="text" id="box-folder-id" placeholder="e.g., 348304357505">
|
||||
<button id="load-box-files" class="btn btn-primary">Load Files</button>
|
||||
</div>
|
||||
<div id="box-status"></div>
|
||||
```
|
||||
|
||||
#### B. File List Table
|
||||
```html
|
||||
<table id="box-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>Filename</th>
|
||||
<th>Tracking ID</th>
|
||||
<th>Master Asset</th>
|
||||
<th>Valid</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="box-files-list">
|
||||
<!-- Populated by AJAX -->
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
#### C. Metadata Preview Section
|
||||
```html
|
||||
<div id="metadata-preview">
|
||||
<h4>Metadata for Selected File</h4>
|
||||
|
||||
<div class="metadata-section">
|
||||
<h5>🔒 Master Fields (Locked)</h5>
|
||||
<div id="locked-fields">
|
||||
<!-- Grayed out, read-only fields -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metadata-section">
|
||||
<h5>✏️ Derived Fields (Editable)</h5>
|
||||
<div id="editable-fields">
|
||||
<!-- Editable input fields -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="validation-warnings"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### D. Upload Controls
|
||||
```html
|
||||
<div id="upload-controls">
|
||||
<button id="upload-selected" class="btn btn-success">Upload Selected Assets</button>
|
||||
<div id="upload-progress"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Implement AJAX Endpoints ⏳
|
||||
|
||||
**File to Edit:** `workflow_v3.php` (around line 100-200, where other AJAX handlers are)
|
||||
|
||||
**Endpoints to Add:**
|
||||
|
||||
#### 1. Load Box Files
|
||||
```php
|
||||
case 'box_list_files':
|
||||
$folderId = $_POST['folder_id'] ?? '';
|
||||
$retriever = new BoxFileRetriever();
|
||||
$result = $retriever->listFilesWithTrackingIDs($folderId);
|
||||
echo json_encode($result);
|
||||
break;
|
||||
```
|
||||
|
||||
#### 2. Parse Filename
|
||||
```php
|
||||
case 'parse_filename':
|
||||
$filename = $_POST['filename'] ?? '';
|
||||
$parser = new FilenameParser();
|
||||
$parsed = $parser->parseFilename($filename);
|
||||
echo json_encode($parsed);
|
||||
break;
|
||||
```
|
||||
|
||||
#### 3. Load Master Metadata
|
||||
```php
|
||||
case 'load_master_metadata':
|
||||
$trackingId = $_POST['tracking_id'] ?? '';
|
||||
$db = DatabaseClient::getInstance();
|
||||
$masterAsset = $db->lookupByTrackingId($trackingId);
|
||||
echo json_encode($masterAsset);
|
||||
break;
|
||||
```
|
||||
|
||||
#### 4. Merge Metadata
|
||||
```php
|
||||
case 'merge_metadata':
|
||||
$masterMetadata = json_decode($_POST['master_metadata'], true);
|
||||
$parsedFilename = json_decode($_POST['parsed_filename'], true);
|
||||
|
||||
$merger = new MetadataMerger();
|
||||
$merged = $merger->mergeMetadata($masterMetadata, $parsedFilename);
|
||||
$editableFields = $merger->identifyEditableFields($merged);
|
||||
|
||||
echo json_encode([
|
||||
'merged' => $merged,
|
||||
'editable_fields' => $editableFields
|
||||
]);
|
||||
break;
|
||||
```
|
||||
|
||||
#### 5. Upload from Box
|
||||
```php
|
||||
case 'upload_from_box':
|
||||
// 1. Get Box file
|
||||
// 2. Download to temp
|
||||
// 3. Parse filename
|
||||
// 4. Load master metadata
|
||||
// 5. Merge metadata
|
||||
// 6. Build asset representation
|
||||
// 7. Upload to DAM
|
||||
// 8. Clean up temp file
|
||||
// 9. Return result
|
||||
break;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Build Metadata Edit UI ⏳
|
||||
|
||||
**JavaScript Functions Needed:**
|
||||
|
||||
```javascript
|
||||
// Load files from Box
|
||||
function loadBoxFiles(folderId) {
|
||||
$.ajax({
|
||||
url: 'workflow_v3.php',
|
||||
method: 'POST',
|
||||
data: { action: 'box_list_files', folder_id: folderId },
|
||||
success: function(response) {
|
||||
displayBoxFiles(response.files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Display files in table
|
||||
function displayBoxFiles(files) {
|
||||
files.forEach(file => {
|
||||
// Parse filename
|
||||
// Validate
|
||||
// Show in table with checkbox
|
||||
});
|
||||
}
|
||||
|
||||
// Load metadata for selected file
|
||||
function loadFileMetadata(filename, trackingId) {
|
||||
// Parse filename
|
||||
// Load master metadata
|
||||
// Merge
|
||||
// Display in preview
|
||||
}
|
||||
|
||||
// Display metadata with locked/editable sections
|
||||
function displayMetadata(merged, editableFields) {
|
||||
// Clear containers
|
||||
// Populate locked fields (read-only)
|
||||
// Populate editable fields (input elements)
|
||||
}
|
||||
|
||||
// Upload selected files
|
||||
function uploadSelectedFiles() {
|
||||
var selectedFiles = getSelectedFiles();
|
||||
selectedFiles.forEach(file => {
|
||||
uploadSingleFile(file);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Implement Upload Processing ⏳
|
||||
|
||||
**Steps:**
|
||||
1. Download file from Box to temp
|
||||
2. Parse filename and validate
|
||||
3. Load master metadata from DB
|
||||
4. Merge metadata with filename data
|
||||
5. Get user edits from UI
|
||||
6. Build final asset representation
|
||||
7. Strip filename components
|
||||
8. Upload to DAM using AssetUploaderSimple
|
||||
9. Update status (if all files done)
|
||||
10. Clean up temp files
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests
|
||||
- [x] FilenameParser - All 8 tests passing
|
||||
- [ ] MetadataMerger - Need test cases
|
||||
- [ ] BoxFileRetriever - Need test cases
|
||||
|
||||
### Integration Tests
|
||||
- [ ] Load files from Box folder
|
||||
- [ ] Parse V2 filenames correctly
|
||||
- [ ] Extract tracking IDs
|
||||
- [ ] Load master metadata from DB
|
||||
- [ ] Merge metadata (filename wins)
|
||||
- [ ] Build asset representation
|
||||
- [ ] Upload to DAM successfully
|
||||
- [ ] Update campaign status to A3
|
||||
|
||||
### UI Tests
|
||||
- [ ] Box Folder ID input validation
|
||||
- [ ] File list display
|
||||
- [ ] Filename validation display
|
||||
- [ ] Metadata preview (locked vs editable)
|
||||
- [ ] Field editing
|
||||
- [ ] Upload progress display
|
||||
- [ ] Error handling
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations & Considerations
|
||||
|
||||
### 1. Box API Rate Limits
|
||||
- JWT tokens are valid for 60 minutes
|
||||
- File downloads have no explicit timeout
|
||||
- Consider batch processing for large folders
|
||||
|
||||
### 2. Filename Validation
|
||||
- Strict validation will reject non-compliant filenames
|
||||
- User must fix filenames before upload
|
||||
- Consider adding "force upload" option for special cases
|
||||
|
||||
### 3. Metadata Conflicts
|
||||
- Filename always wins (as designed)
|
||||
- Conflicts are logged but not shown to user
|
||||
- Consider adding conflict warning UI
|
||||
|
||||
### 4. Database Lookups
|
||||
- Tracking IDs must exist in PostgreSQL
|
||||
- No fallback if tracking ID not found
|
||||
- Consider adding "create new master" option
|
||||
|
||||
### 5. Upload Folder Extraction
|
||||
- Relies on tracking ID → master asset → upload_directory
|
||||
- If upload_directory is NULL, upload will fail
|
||||
- Need error handling for this case
|
||||
|
||||
---
|
||||
|
||||
## Configuration Requirements
|
||||
|
||||
### PostgreSQL Database
|
||||
```
|
||||
Host: localhost
|
||||
Port: 5433
|
||||
Database: ferrero_tracking
|
||||
Table: master_assets
|
||||
|
||||
Required Fields:
|
||||
- tracking_id (primary key)
|
||||
- opentext_id (DAM asset ID)
|
||||
- upload_directory (target folder ID)
|
||||
- metadata (JSON - full DAM metadata)
|
||||
```
|
||||
|
||||
### Box Configuration
|
||||
```
|
||||
File: Box-config.json or 43984435_n1izyn3l_config.json
|
||||
|
||||
Auth Method: JWT (RSA-256)
|
||||
Root Folder: 348304357505
|
||||
```
|
||||
|
||||
### DAM Configuration
|
||||
```
|
||||
Environment: Production
|
||||
Base URL: https://ppr.dam.ferrero.com/otmmapi
|
||||
Metadata Model: ECOMMERCE
|
||||
Security Policy: 1594
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Notes
|
||||
|
||||
### Well Implemented ✅
|
||||
- Strict filename validation with detailed errors
|
||||
- Filename always wins (requirement met)
|
||||
- Editable field identification
|
||||
- Conflict tracking
|
||||
- Comprehensive parsing
|
||||
- JWT authentication (production-ready)
|
||||
- Test suite for parser
|
||||
|
||||
### Could Enhance
|
||||
- Add test suites for Merger and Retriever
|
||||
- Batch upload optimization
|
||||
- Progress tracking for large uploads
|
||||
- Error recovery strategies
|
||||
- Conflict resolution UI
|
||||
- Manual metadata override option
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Expected Performance
|
||||
- Box file listing: 1-3 seconds
|
||||
- Filename parsing: <1ms per file
|
||||
- DB metadata lookup: <100ms per tracking ID
|
||||
- Metadata merge: <10ms per file
|
||||
- File download from Box: 5-30 seconds (depending on size)
|
||||
- DAM upload: 3-10 seconds per file
|
||||
|
||||
### Optimization Opportunities
|
||||
- Cache parsed filenames
|
||||
- Batch DB lookups
|
||||
- Parallel Box downloads
|
||||
- Streaming uploads (for large files)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
### Validation Errors
|
||||
- Invalid filename structure → Show errors, block upload
|
||||
- Missing tracking ID → Show error, allow manual entry
|
||||
- Tracking ID not in DB → Error, cannot proceed
|
||||
|
||||
### Runtime Errors
|
||||
- Box connection failure → Retry with exponential backoff
|
||||
- Download failure → Log, skip file, continue with others
|
||||
- Upload failure → Log, mark file for retry, don't update status
|
||||
- DB connection failure → Fatal error, stop process
|
||||
|
||||
### User Experience
|
||||
- Clear error messages
|
||||
- Validation feedback in real-time
|
||||
- Progress indicators
|
||||
- Retry options
|
||||
- Detailed logs for debugging
|
||||
|
||||
---
|
||||
|
||||
## Documentation Status
|
||||
|
||||
### Completed ✅
|
||||
- [x] Lookup domains documentation
|
||||
- [x] V2 naming convention reference
|
||||
- [x] FilenameParser API documentation
|
||||
- [x] MetadataMerger API documentation
|
||||
- [x] BoxFileRetriever API documentation
|
||||
- [x] Complete workflow documentation
|
||||
- [x] This status document
|
||||
|
||||
### Pending ⏳
|
||||
- [ ] UI component documentation
|
||||
- [ ] AJAX endpoint documentation
|
||||
- [ ] Testing procedures
|
||||
- [ ] Deployment guide
|
||||
- [ ] User manual
|
||||
|
||||
---
|
||||
|
||||
## Next Session Quick Start
|
||||
|
||||
### To Continue Implementation:
|
||||
|
||||
1. **Read this file** (UPLOAD_FROM_BOX_STATUS.md) for full context
|
||||
|
||||
2. **Test existing components:**
|
||||
```bash
|
||||
cd /Users/daveporter/Desktop/CODING-2024/Ferrero-Opentext
|
||||
php test_filename_parser.php
|
||||
```
|
||||
|
||||
3. **Start UI integration:**
|
||||
- Edit `workflow_v3.php`
|
||||
- Add "Upload from Box" tab (see Task 5 above)
|
||||
- Implement AJAX endpoints (see Task 6 above)
|
||||
|
||||
4. **Reference files:**
|
||||
- `ECOMMERCE_ALLOWED_FIELDS.md` - Field reference
|
||||
- `downloads/asset_representation MVP.json` - Metadata structure
|
||||
- `PROJECT_STATUS_2025-10-29.md` - Overall project status
|
||||
|
||||
5. **Key Classes to Use:**
|
||||
```php
|
||||
require_once 'src/FilenameParser.php';
|
||||
require_once 'src/MetadataMerger.php';
|
||||
require_once 'src/BoxFileRetriever.php';
|
||||
require_once 'src/DatabaseClient.php';
|
||||
require_once 'src/AssetUploaderSimple.php';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Phase 1 (Core Components) ✅ COMPLETE
|
||||
- [x] Lookup domains documentation exported
|
||||
- [x] FilenameParser implemented and tested
|
||||
- [x] MetadataMerger implemented
|
||||
- [x] BoxFileRetriever implemented
|
||||
- [x] All integration points identified
|
||||
|
||||
### Phase 2 (UI Integration) ⏳ READY TO START
|
||||
- [ ] "Upload from Box" tab added to workflow
|
||||
- [ ] All AJAX endpoints implemented
|
||||
- [ ] File list display working
|
||||
- [ ] Metadata preview/edit UI working
|
||||
- [ ] Upload processing complete
|
||||
- [ ] Status update to A3 working
|
||||
|
||||
### Phase 3 (Testing & Polish) ⏳ PENDING
|
||||
- [ ] Integration tests passing
|
||||
- [ ] Error handling verified
|
||||
- [ ] Performance acceptable
|
||||
- [ ] User experience polished
|
||||
- [ ] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
**Status:** Ready for Phase 2 Implementation
|
||||
**Estimated Time to Complete Phase 2:** 4-6 hours
|
||||
**Overall Progress:** 40% Complete (Core backend done, UI pending)
|
||||
|
||||
---
|
||||
|
||||
**End of Status Report**
|
||||
165
fetch_lookupdomains.php
Normal file
165
fetch_lookupdomains.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
/**
|
||||
* Fetch Lookupdomains Data for ECOMMERCE Model
|
||||
* This script retrieves all allowed fields for the ECOMMERCE metadata model
|
||||
*/
|
||||
|
||||
// Increase memory limit for large responses
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
require_once 'src/TestRunner.php';
|
||||
require_once 'config_v3.php';
|
||||
|
||||
echo "=== Fetching DAM Lookup Domains ===\n\n";
|
||||
|
||||
// Setup configuration
|
||||
$config = new ConfigV3();
|
||||
$baseUrl = $config->getBaseUrl();
|
||||
$collectionPath = $config->get('postman_collection');
|
||||
|
||||
echo "Base URL: $baseUrl\n";
|
||||
echo "Collection: $collectionPath\n\n";
|
||||
|
||||
// Create TestRunner to handle authentication
|
||||
$testRunner = new TestRunner($collectionPath, [
|
||||
'baseUrl' => $baseUrl,
|
||||
'timeout' => 60
|
||||
]);
|
||||
|
||||
// Manual API call to lookupdomains endpoint
|
||||
echo "Step 1: Getting OAuth token...\n";
|
||||
|
||||
// Access the OAuth handler through TestRunner
|
||||
$reflection = new ReflectionClass($testRunner);
|
||||
$property = $reflection->getProperty('oauth2Handler');
|
||||
$property->setAccessible(true);
|
||||
$oauth2Handler = $property->getValue($testRunner);
|
||||
|
||||
if (!$oauth2Handler) {
|
||||
die("ERROR: Could not get OAuth handler\n");
|
||||
}
|
||||
|
||||
try {
|
||||
$accessToken = $oauth2Handler->getAccessToken();
|
||||
echo "✓ Token obtained\n\n";
|
||||
} catch (Exception $e) {
|
||||
die("ERROR: Failed to get access token: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
// First, list all available lookup domains
|
||||
echo "Step 2: Listing all available lookup domains...\n";
|
||||
|
||||
$url = $baseUrl . '/v6/lookupdomains';
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json'
|
||||
],
|
||||
CURLOPT_TIMEOUT => 60
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
die("ERROR: cURL error: $error\n");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
die("ERROR: HTTP $httpCode - Response: " . substr($response, 0, 500) . "\n");
|
||||
}
|
||||
|
||||
echo "✓ Response received (HTTP $httpCode)\n\n";
|
||||
|
||||
// Save raw response first (without parsing to save memory)
|
||||
$jsonFile = 'DAM_LOOKUPDOMAINS_RAW.json';
|
||||
file_put_contents($jsonFile, $response);
|
||||
echo "✓ Raw JSON saved to: $jsonFile (" . strlen($response) . " bytes)\n\n";
|
||||
|
||||
// Parse response
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!$data) {
|
||||
die("ERROR: Failed to parse JSON response\n");
|
||||
}
|
||||
|
||||
// Generate Markdown documentation
|
||||
echo "Step 3: Generating Markdown documentation...\n";
|
||||
|
||||
$md = "# DAM Lookup Domains - All Available Fields\n\n";
|
||||
$md .= "**Generated:** " . date('Y-m-d H:i:s') . "\n";
|
||||
$md .= "**Endpoint:** `GET /v6/lookupdomains`\n";
|
||||
$md .= "**Base URL:** $baseUrl\n\n";
|
||||
$md .= "---\n\n";
|
||||
|
||||
// Check structure and create summary only
|
||||
if (isset($data['lookup_domains_resource']['lookup_domains'])) {
|
||||
$lookups = $data['lookup_domains_resource']['lookup_domains'];
|
||||
$md .= "## Overview\n\n";
|
||||
$md .= "Total lookup domains: " . count($lookups) . "\n\n";
|
||||
$md .= "---\n\n";
|
||||
$md .= "## Lookup Domains Summary\n\n";
|
||||
$md .= "| Domain ID | Datatype | Values Count | Example Value |\n";
|
||||
$md .= "|-----------|----------|--------------|---------------|\n";
|
||||
|
||||
foreach ($lookups as $lookup) {
|
||||
$domainId = $lookup['domainId'] ?? 'N/A';
|
||||
$datatype = $lookup['datatype'] ?? 'N/A';
|
||||
$domainValues = $lookup['domainValues'] ?? [];
|
||||
$valueCount = count($domainValues);
|
||||
|
||||
// Get first value as example
|
||||
$exampleValue = 'N/A';
|
||||
if ($valueCount > 0 && isset($domainValues[0]['field_value']['value'])) {
|
||||
$exampleValue = $domainValues[0]['field_value']['value'];
|
||||
if (strlen($exampleValue) > 30) {
|
||||
$exampleValue = substr($exampleValue, 0, 27) . '...';
|
||||
}
|
||||
}
|
||||
|
||||
$md .= "| `$domainId` | $datatype | $valueCount | `$exampleValue` |\n";
|
||||
}
|
||||
|
||||
$md .= "\n";
|
||||
$md .= "> **Note:** Full details and all allowed values are available in `DAM_LOOKUPDOMAINS_RAW.json`\n\n";
|
||||
$md .= "---\n\n";
|
||||
} else {
|
||||
$md .= "## No lookup domains found\n\n";
|
||||
$md .= "See `DAM_LOOKUPDOMAINS_RAW.json` for raw response.\n\n";
|
||||
}
|
||||
|
||||
$md .= "---\n\n";
|
||||
$md .= "## Usage in Asset Upload\n\n";
|
||||
$md .= "These lookup domains define the allowed values for metadata fields in the ECOMMERCE model.\n\n";
|
||||
$md .= "When uploading an asset, you must use field IDs and values from these domains.\n\n";
|
||||
$md .= "### Example Usage\n\n";
|
||||
$md .= "```json\n";
|
||||
$md .= "{\n";
|
||||
$md .= ' "id": "FERRERO.FIELD.MKTG.ASSET TYPE",\n';
|
||||
$md .= ' "value": {\n';
|
||||
$md .= ' "domain_value": true,\n';
|
||||
$md .= ' "value": {\n';
|
||||
$md .= ' "type": "string",\n';
|
||||
$md .= ' "value": "heroimage" // Must be from allowed values\n';
|
||||
$md .= " }\n";
|
||||
$md .= " }\n";
|
||||
$md .= "}\n";
|
||||
$md .= "```\n\n";
|
||||
|
||||
$mdFile = 'ECOMMERCE_ALLOWED_FIELDS.md';
|
||||
file_put_contents($mdFile, $md);
|
||||
echo "✓ Markdown documentation saved to: $mdFile\n\n";
|
||||
|
||||
// Summary
|
||||
echo "=== SUMMARY ===\n";
|
||||
echo "✓ Successfully fetched ECOMMERCE lookupdomains\n";
|
||||
echo "✓ Files created:\n";
|
||||
echo " - $jsonFile (raw JSON data)\n";
|
||||
echo " - $mdFile (formatted documentation)\n";
|
||||
echo "\nDone!\n";
|
||||
308
src/BoxFileRetriever.php
Normal file
308
src/BoxFileRetriever.php
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
require_once 'BoxClient.php';
|
||||
|
||||
/**
|
||||
* BoxFileRetriever - List and Download Files from Box Folders
|
||||
*
|
||||
* This class handles:
|
||||
* 1. Listing files in a Box folder
|
||||
* 2. Getting file metadata
|
||||
* 3. Downloading files to temp location
|
||||
* 4. Extracting tracking IDs from filenames
|
||||
*/
|
||||
class BoxFileRetriever
|
||||
{
|
||||
private $boxClient;
|
||||
private $tempDir;
|
||||
|
||||
public function __construct($boxConfigFile = 'Box-config.json')
|
||||
{
|
||||
$this->boxClient = new BoxClient($boxConfigFile);
|
||||
$this->tempDir = sys_get_temp_dir() . '/ferrero_box_downloads';
|
||||
|
||||
// Create temp directory if it doesn't exist
|
||||
if (!is_dir($this->tempDir)) {
|
||||
mkdir($this->tempDir, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all files in a Box folder
|
||||
*
|
||||
* @param string $folderId Box folder ID
|
||||
* @return array List of files with metadata
|
||||
*/
|
||||
public function listFilesInFolder($folderId)
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->boxClient->getAccessToken();
|
||||
|
||||
// Call Box API to list folder items
|
||||
$url = "https://api.box.com/2.0/folders/$folderId/items?fields=id,name,type,size,modified_at,description";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json'
|
||||
],
|
||||
CURLOPT_TIMEOUT => 60
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("Box API error: $error");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Box API returned HTTP $httpCode: $response");
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!isset($data['entries'])) {
|
||||
throw new Exception("Invalid response from Box API");
|
||||
}
|
||||
|
||||
// Filter to only files (not folders)
|
||||
$files = [];
|
||||
foreach ($data['entries'] as $item) {
|
||||
if ($item['type'] === 'file') {
|
||||
$files[] = [
|
||||
'id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'size' => $item['size'] ?? 0,
|
||||
'modified_at' => $item['modified_at'] ?? null,
|
||||
'description' => $item['description'] ?? '',
|
||||
'box_url' => "https://app.box.com/file/{$item['id']}"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'folder_id' => $folderId,
|
||||
'file_count' => count($files),
|
||||
'files' => $files
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'folder_id' => $folderId
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for a specific file
|
||||
*
|
||||
* @param string $fileId Box file ID
|
||||
* @return array File metadata
|
||||
*/
|
||||
public function getFileMetadata($fileId)
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->boxClient->getAccessToken();
|
||||
|
||||
$url = "https://api.box.com/2.0/files/$fileId?fields=id,name,type,size,modified_at,description,created_at,shared_link";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json'
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Box API returned HTTP $httpCode");
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'metadata' => $data
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from Box
|
||||
*
|
||||
* @param string $fileId Box file ID
|
||||
* @param string $filename Filename for local save
|
||||
* @return array Result with local path
|
||||
*/
|
||||
public function downloadFile($fileId, $filename)
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->boxClient->getAccessToken();
|
||||
|
||||
// Download URL
|
||||
$url = "https://api.box.com/2.0/files/$fileId/content";
|
||||
|
||||
$localPath = $this->tempDir . '/' . $filename;
|
||||
|
||||
// Download file
|
||||
$ch = curl_init();
|
||||
$fp = fopen($localPath, 'w');
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken
|
||||
],
|
||||
CURLOPT_FILE => $fp,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 300
|
||||
]);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if (!$result || $httpCode !== 200) {
|
||||
unlink($localPath);
|
||||
throw new Exception("Download failed with HTTP $httpCode");
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'local_path' => $localPath,
|
||||
'size' => filesize($localPath)
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tracking ID from filename
|
||||
*
|
||||
* @param string $filename Full filename
|
||||
* @return string|null Tracking ID or null if not found
|
||||
*/
|
||||
public function extractTrackingId($filename)
|
||||
{
|
||||
// Remove extension
|
||||
$pathInfo = pathinfo($filename);
|
||||
$filenameWithoutExt = $pathInfo['filename'];
|
||||
|
||||
// Split by underscore
|
||||
$parts = explode('_', $filenameWithoutExt);
|
||||
|
||||
// Last part should be tracking ID (6 alphanumeric characters)
|
||||
if (count($parts) > 0) {
|
||||
$lastPart = end($parts);
|
||||
if (strlen($lastPart) === 6 && ctype_alnum($lastPart)) {
|
||||
return $lastPart;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List files with parsed tracking IDs
|
||||
*
|
||||
* @param string $folderId Box folder ID
|
||||
* @return array Files with tracking ID info
|
||||
*/
|
||||
public function listFilesWithTracking IDs($folderId)
|
||||
{
|
||||
$result = $this->listFilesInFolder($folderId);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Add tracking ID to each file
|
||||
foreach ($result['files'] as &$file) {
|
||||
$file['tracking_id'] = $this->extractTrackingId($file['name']);
|
||||
$file['has_tracking_id'] = !empty($file['tracking_id']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get temp directory path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTempDir()
|
||||
{
|
||||
return $this->tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temp directory
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function cleanTempDir()
|
||||
{
|
||||
if (!is_dir($this->tempDir)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$files = glob($this->tempDir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Box connection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function testConnection()
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->boxClient->getAccessToken();
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Box connection successful',
|
||||
'has_token' => !empty($accessToken)
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
384
src/FilenameParser.php
Normal file
384
src/FilenameParser.php
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* FilenameParser - V2 Naming Convention Parser
|
||||
*
|
||||
* Parses filenames according to Ferrero V2 naming convention:
|
||||
* [OMG_JOB_NUMBER]_[BRAND_CODE]_[COUNTRY_CODE]_[LANGUAGE_CODE]_[SUBJECT_TITLE]_[ASSET_TYPE]_[SPOT_VERSION]_[SECONDS]S_[ASPECT_RATIO]_[TRACKING_ID]
|
||||
*
|
||||
* Example: 1234567_RAF_CH_de_TEST_FILE_OLV_001_15S_16x9_a7K9mP.mp4
|
||||
*
|
||||
* On upload to DAM, the OMG Job Number and Tracking ID are stripped:
|
||||
* Final: RAF_CH_de_TEST_FILE_OLV_001_15S_16x9.mp4
|
||||
*/
|
||||
class FilenameParser
|
||||
{
|
||||
private $validationErrors = [];
|
||||
private $warnings = [];
|
||||
|
||||
/**
|
||||
* Parse a filename according to V2 naming convention
|
||||
*
|
||||
* @param string $filename The filename to parse (with or without extension)
|
||||
* @return array Parsed components or null if invalid
|
||||
*/
|
||||
public function parseFilename($filename)
|
||||
{
|
||||
$this->validationErrors = [];
|
||||
$this->warnings = [];
|
||||
|
||||
// Remove extension
|
||||
$pathInfo = pathinfo($filename);
|
||||
$filenameWithoutExt = $pathInfo['filename'];
|
||||
$extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
|
||||
|
||||
// Split by underscore
|
||||
$parts = explode('_', $filenameWithoutExt);
|
||||
|
||||
// V2 naming convention has minimum 10 parts (with tracking ID)
|
||||
// Without tracking ID: 9 parts minimum
|
||||
// Allow fewer parts for better error messages, but still validate
|
||||
if (count($parts) < 8) {
|
||||
$this->validationErrors[] = "Invalid filename structure. Expected minimum 9 parts, got " . count($parts);
|
||||
// Continue parsing to provide detailed error messages
|
||||
}
|
||||
|
||||
// Parse components
|
||||
$parsed = [
|
||||
'original_filename' => $filename,
|
||||
'filename_without_ext' => $filenameWithoutExt,
|
||||
'extension' => $extension,
|
||||
'omg_job_number' => null,
|
||||
'brand_code' => null,
|
||||
'country_code' => null,
|
||||
'language_code' => null,
|
||||
'subject_title' => null,
|
||||
'asset_type' => null,
|
||||
'spot_version' => null,
|
||||
'has_master' => false,
|
||||
'seconds' => null,
|
||||
'aspect_ratio' => null,
|
||||
'tracking_id' => null,
|
||||
'validation_errors' => [],
|
||||
'warnings' => []
|
||||
];
|
||||
|
||||
$index = 0;
|
||||
|
||||
// 1. OMG Job Number (must be all digits, max 10 digits)
|
||||
if (isset($parts[$index]) && ctype_digit($parts[$index])) {
|
||||
$omgJobNumber = $parts[$index];
|
||||
if (strlen($omgJobNumber) > 10) {
|
||||
$this->validationErrors[] = "OMG Job Number too long: $omgJobNumber (max 10 digits)";
|
||||
} else {
|
||||
$parsed['omg_job_number'] = $omgJobNumber;
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
// Try to proceed without OMG Job Number (allow parsing but mark as invalid)
|
||||
if (isset($parts[$index]) && !ctype_digit($parts[$index])) {
|
||||
$this->validationErrors[] = "OMG Job Number missing or invalid (must be numbers only). Found: {$parts[$index]}";
|
||||
// Don't increment index - treat current part as brand code
|
||||
} else {
|
||||
$this->validationErrors[] = "OMG Job Number missing";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Brand Code (2-5 characters, uppercase)
|
||||
if (isset($parts[$index])) {
|
||||
$brandCode = $parts[$index];
|
||||
if (strlen($brandCode) >= 2 && strlen($brandCode) <= 5) {
|
||||
$parsed['brand_code'] = strtoupper($brandCode);
|
||||
} else {
|
||||
$this->validationErrors[] = "Brand Code invalid: $brandCode (must be 2-5 characters)";
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Brand Code missing";
|
||||
}
|
||||
|
||||
// 3. Country Code (2 characters, uppercase)
|
||||
if (isset($parts[$index])) {
|
||||
$countryCode = $parts[$index];
|
||||
if (strlen($countryCode) === 2) {
|
||||
$parsed['country_code'] = strtoupper($countryCode);
|
||||
} else {
|
||||
$this->validationErrors[] = "Country Code invalid: $countryCode (must be 2 characters)";
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Country Code missing";
|
||||
}
|
||||
|
||||
// 4. Language Code (2-3 characters, lowercase)
|
||||
if (isset($parts[$index])) {
|
||||
$languageCode = $parts[$index];
|
||||
if (strlen($languageCode) >= 2 && strlen($languageCode) <= 3) {
|
||||
$parsed['language_code'] = strtolower($languageCode);
|
||||
} else {
|
||||
$this->validationErrors[] = "Language Code invalid: $languageCode (must be 2-3 characters)";
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Language Code missing";
|
||||
}
|
||||
|
||||
// 5. Subject Title (can be multiple parts until we hit a 3-char asset type)
|
||||
// Asset type is always 3 characters, so we need to find it
|
||||
$subjectTitleParts = [];
|
||||
$foundAssetType = false;
|
||||
$assetTypeIndex = $index;
|
||||
|
||||
// Look ahead to find asset type (3 characters uppercase)
|
||||
for ($i = $index; $i < count($parts); $i++) {
|
||||
if (strlen($parts[$i]) === 3 && ctype_alpha($parts[$i]) && ctype_upper($parts[$i])) {
|
||||
// Potential asset type - check if it's followed by spot version pattern
|
||||
if (isset($parts[$i + 1]) && (strlen($parts[$i + 1]) === 3 || $parts[$i + 1] === 'MST')) {
|
||||
// This is likely the asset type
|
||||
$assetTypeIndex = $i;
|
||||
$foundAssetType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundAssetType) {
|
||||
// Everything between language code and asset type is subject title
|
||||
for ($i = $index; $i < $assetTypeIndex; $i++) {
|
||||
$subjectTitleParts[] = $parts[$i];
|
||||
}
|
||||
$parsed['subject_title'] = implode('_', $subjectTitleParts);
|
||||
|
||||
if (strlen($parsed['subject_title']) > 15) {
|
||||
$this->warnings[] = "Subject Title exceeds 15 characters: {$parsed['subject_title']}";
|
||||
}
|
||||
|
||||
// Move index to asset type
|
||||
$index = $assetTypeIndex;
|
||||
} else {
|
||||
$this->validationErrors[] = "Could not locate Asset Type (must be 3 uppercase letters)";
|
||||
// Assume next part is subject title
|
||||
if (isset($parts[$index])) {
|
||||
$parsed['subject_title'] = $parts[$index];
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Asset Type (3 characters, uppercase)
|
||||
if (isset($parts[$index]) && strlen($parts[$index]) === 3) {
|
||||
$parsed['asset_type'] = strtoupper($parts[$index]);
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Asset Type missing or invalid (must be 3 characters)";
|
||||
}
|
||||
|
||||
// 7. Spot Version (3 characters or 'MST' for master)
|
||||
if (isset($parts[$index])) {
|
||||
$spotVersion = $parts[$index];
|
||||
if ($spotVersion === 'MST' || strtoupper($spotVersion) === 'MST') {
|
||||
$parsed['has_master'] = true;
|
||||
$parsed['spot_version'] = 'MST';
|
||||
} else if (strlen($spotVersion) === 3) {
|
||||
$parsed['spot_version'] = strtoupper($spotVersion);
|
||||
// Check if it contains MST
|
||||
if (strpos(strtoupper($spotVersion), 'MST') !== false) {
|
||||
$parsed['has_master'] = true;
|
||||
}
|
||||
} else {
|
||||
$this->validationErrors[] = "Spot Version invalid: $spotVersion (must be 3 characters)";
|
||||
$parsed['spot_version'] = $spotVersion;
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Spot Version missing";
|
||||
}
|
||||
|
||||
// 8. Duration (Seconds) - format: 15S or 6S
|
||||
if (isset($parts[$index])) {
|
||||
$durationPart = $parts[$index];
|
||||
if (preg_match('/^(\d+)S$/i', $durationPart, $matches)) {
|
||||
$parsed['seconds'] = $matches[1];
|
||||
} else {
|
||||
$this->validationErrors[] = "Duration invalid: $durationPart (must be format: 15S)";
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Duration missing";
|
||||
}
|
||||
|
||||
// 9. Aspect Ratio (3-4 characters) - format: 16x9, 4x3, 1x1
|
||||
if (isset($parts[$index])) {
|
||||
$aspectRatio = $parts[$index];
|
||||
if (preg_match('/^\d+x\d+$/i', $aspectRatio)) {
|
||||
$parsed['aspect_ratio'] = $aspectRatio;
|
||||
} else {
|
||||
$this->validationErrors[] = "Aspect Ratio invalid: $aspectRatio (must be format: 16x9)";
|
||||
$parsed['aspect_ratio'] = $aspectRatio;
|
||||
}
|
||||
$index++;
|
||||
} else {
|
||||
$this->validationErrors[] = "Aspect Ratio missing";
|
||||
}
|
||||
|
||||
// 10. Tracking ID (optional, 6 alphanumeric characters)
|
||||
if (isset($parts[$index])) {
|
||||
$trackingId = $parts[$index];
|
||||
if (strlen($trackingId) === 6 && ctype_alnum($trackingId)) {
|
||||
$parsed['tracking_id'] = $trackingId;
|
||||
} else {
|
||||
$this->warnings[] = "Tracking ID format invalid: $trackingId (should be 6 alphanumeric characters)";
|
||||
$parsed['tracking_id'] = $trackingId;
|
||||
}
|
||||
$index++;
|
||||
}
|
||||
|
||||
// Check for extra parts
|
||||
if ($index < count($parts)) {
|
||||
$extraParts = array_slice($parts, $index);
|
||||
$this->warnings[] = "Extra parts in filename: " . implode('_', $extraParts);
|
||||
}
|
||||
|
||||
// Add validation results
|
||||
$parsed['validation_errors'] = $this->validationErrors;
|
||||
$parsed['warnings'] = $this->warnings;
|
||||
$parsed['is_valid'] = empty($this->validationErrors);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate filename structure strictly
|
||||
*
|
||||
* @param string $filename
|
||||
* @return bool
|
||||
*/
|
||||
public function validateStructure($filename)
|
||||
{
|
||||
$parsed = $this->parseFilename($filename);
|
||||
return $parsed && $parsed['is_valid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip upload components (OMG Job Number and Tracking ID)
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string|null Clean filename for upload, or null if parsing failed
|
||||
*/
|
||||
public function stripUploadComponents($filename)
|
||||
{
|
||||
$parsed = $this->parseFilename($filename);
|
||||
|
||||
if (!$parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build clean filename without OMG Job Number and Tracking ID
|
||||
$cleanParts = [];
|
||||
|
||||
if ($parsed['brand_code']) $cleanParts[] = $parsed['brand_code'];
|
||||
if ($parsed['country_code']) $cleanParts[] = $parsed['country_code'];
|
||||
if ($parsed['language_code']) $cleanParts[] = $parsed['language_code'];
|
||||
if ($parsed['subject_title']) $cleanParts[] = $parsed['subject_title'];
|
||||
if ($parsed['asset_type']) $cleanParts[] = $parsed['asset_type'];
|
||||
if ($parsed['spot_version']) $cleanParts[] = $parsed['spot_version'];
|
||||
if ($parsed['seconds']) $cleanParts[] = $parsed['seconds'] . 'S';
|
||||
if ($parsed['aspect_ratio']) $cleanParts[] = $parsed['aspect_ratio'];
|
||||
|
||||
$cleanFilename = implode('_', $cleanParts);
|
||||
|
||||
// Add extension back
|
||||
if ($parsed['extension']) {
|
||||
$cleanFilename .= $parsed['extension'];
|
||||
}
|
||||
|
||||
return $cleanFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clean filename for upload (same as stripUploadComponents)
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCleanFilename($filename)
|
||||
{
|
||||
return $this->stripUploadComponents($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tracking ID from filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string|null Tracking ID or null if not found
|
||||
*/
|
||||
public function extractTrackingId($filename)
|
||||
{
|
||||
$parsed = $this->parseFilename($filename);
|
||||
return $parsed ? $parsed['tracking_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation errors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValidationErrors()
|
||||
{
|
||||
return $this->validationErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get warnings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWarnings()
|
||||
{
|
||||
return $this->warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format parsed data for display
|
||||
*
|
||||
* @param array $parsed
|
||||
* @return string
|
||||
*/
|
||||
public function formatForDisplay($parsed)
|
||||
{
|
||||
if (!$parsed) {
|
||||
return "Invalid filename";
|
||||
}
|
||||
|
||||
$output = "Filename: {$parsed['original_filename']}\n";
|
||||
$output .= "Valid: " . ($parsed['is_valid'] ? 'YES' : 'NO') . "\n\n";
|
||||
|
||||
$output .= "Components:\n";
|
||||
$output .= " OMG Job Number: " . ($parsed['omg_job_number'] ?? 'N/A') . "\n";
|
||||
$output .= " Brand Code: " . ($parsed['brand_code'] ?? 'N/A') . "\n";
|
||||
$output .= " Country Code: " . ($parsed['country_code'] ?? 'N/A') . "\n";
|
||||
$output .= " Language Code: " . ($parsed['language_code'] ?? 'N/A') . "\n";
|
||||
$output .= " Subject Title: " . ($parsed['subject_title'] ?? 'N/A') . "\n";
|
||||
$output .= " Asset Type: " . ($parsed['asset_type'] ?? 'N/A') . "\n";
|
||||
$output .= " Spot Version: " . ($parsed['spot_version'] ?? 'N/A') . "\n";
|
||||
$output .= " Has Master: " . ($parsed['has_master'] ? 'YES' : 'NO') . "\n";
|
||||
$output .= " Duration: " . ($parsed['seconds'] ?? 'N/A') . " seconds\n";
|
||||
$output .= " Aspect Ratio: " . ($parsed['aspect_ratio'] ?? 'N/A') . "\n";
|
||||
$output .= " Tracking ID: " . ($parsed['tracking_id'] ?? 'N/A') . "\n";
|
||||
|
||||
if (!empty($parsed['validation_errors'])) {
|
||||
$output .= "\nValidation Errors:\n";
|
||||
foreach ($parsed['validation_errors'] as $error) {
|
||||
$output .= " - $error\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parsed['warnings'])) {
|
||||
$output .= "\nWarnings:\n";
|
||||
foreach ($parsed['warnings'] as $warning) {
|
||||
$output .= " - $warning\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
408
src/MetadataMerger.php
Normal file
408
src/MetadataMerger.php
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* MetadataMerger - Merge Master Metadata with Filename-Derived Data
|
||||
*
|
||||
* This class combines:
|
||||
* 1. Master asset metadata from PostgreSQL database
|
||||
* 2. Metadata extracted from V2 filename parsing
|
||||
*
|
||||
* Priority: Filename data always wins (as per requirements)
|
||||
*
|
||||
* Editable fields (derived from filename):
|
||||
* - Country Code
|
||||
* - Language Code
|
||||
* - Asset Type
|
||||
* - Aspect Ratio
|
||||
* - Duration (Seconds)
|
||||
*
|
||||
* Locked fields (from master metadata):
|
||||
* - All other fields from master asset
|
||||
*/
|
||||
class MetadataMerger
|
||||
{
|
||||
/**
|
||||
* Field mapping from V2 filename components to DAM field IDs
|
||||
*/
|
||||
private $fieldMapping = [
|
||||
'brand_code' => 'FERRERO.FIELD.SUB BRAND',
|
||||
'language_code' => 'MAIN_LANGUAGES', // Tabular field
|
||||
'asset_type' => 'FERRERO.FIELD.MKTG.ASSET TYPE',
|
||||
'fiscal_year' => 'FERRERO.FIELD.FISCAL YEAR',
|
||||
'state' => 'FERRERO.FIELD.STATE',
|
||||
'asset_name' => 'ARTESIA.FIELD.ASSET NAME',
|
||||
'asset_description' => 'ARTESIA.FIELD.ASSET DESCRIPTION'
|
||||
];
|
||||
|
||||
/**
|
||||
* Fields that are derived from filename (editable)
|
||||
*/
|
||||
private $derivedFields = [
|
||||
'MAIN_LANGUAGES',
|
||||
'FERRERO.FIELD.MKTG.ASSET TYPE',
|
||||
'ARTESIA.FIELD.ASSET NAME'
|
||||
];
|
||||
|
||||
/**
|
||||
* Merge master metadata with filename-parsed data
|
||||
*
|
||||
* @param array $masterMetadata Master asset metadata from database
|
||||
* @param array $parsedFilename Parsed filename data from FilenameParser
|
||||
* @return array Merged metadata with source tracking
|
||||
*/
|
||||
public function mergeMetadata($masterMetadata, $parsedFilename)
|
||||
{
|
||||
$merged = [
|
||||
'fields' => [],
|
||||
'sources' => [], // Track which field came from where
|
||||
'conflicts' => [] // Track any conflicts (for logging)
|
||||
];
|
||||
|
||||
// Start with master metadata as base
|
||||
if (isset($masterMetadata['metadata'])) {
|
||||
$masterMeta = is_string($masterMetadata['metadata'])
|
||||
? json_decode($masterMetadata['metadata'], true)
|
||||
: $masterMetadata['metadata'];
|
||||
|
||||
if ($masterMeta && is_array($masterMeta)) {
|
||||
$merged['fields'] = $masterMeta;
|
||||
|
||||
// Mark all initial fields as from master
|
||||
if (isset($masterMeta['metadata_element_list'])) {
|
||||
foreach ($masterMeta['metadata_element_list'] as $field) {
|
||||
if (isset($field['id'])) {
|
||||
$merged['sources'][$field['id']] = 'master';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with filename-derived fields (filename always wins)
|
||||
$this->applyFilenameData($merged, $parsedFilename);
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filename-derived data to merged metadata
|
||||
*
|
||||
* @param array &$merged Merged metadata array (modified in place)
|
||||
* @param array $parsedFilename Parsed filename data
|
||||
*/
|
||||
private function applyFilenameData(&$merged, $parsedFilename)
|
||||
{
|
||||
if (!isset($merged['fields']['metadata_element_list'])) {
|
||||
$merged['fields']['metadata_element_list'] = [];
|
||||
}
|
||||
|
||||
// 1. Asset Type
|
||||
if (!empty($parsedFilename['asset_type'])) {
|
||||
$this->updateOrAddField(
|
||||
$merged,
|
||||
'FERRERO.FIELD.MKTG.ASSET TYPE',
|
||||
$parsedFilename['asset_type'],
|
||||
'filename',
|
||||
'com.artesia.metadata.MetadataField',
|
||||
true // domain_value
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Language Code (MAIN_LANGUAGES - tabular field)
|
||||
if (!empty($parsedFilename['language_code'])) {
|
||||
$this->updateOrAddTabularField(
|
||||
$merged,
|
||||
'MAIN_LANGUAGES',
|
||||
'FERRERO.TABULAR.FIELD.MAIN LANGUAGES',
|
||||
strtoupper($parsedFilename['language_code']),
|
||||
'filename'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Asset Name (use clean filename without extension)
|
||||
$assetName = $parsedFilename['original_filename'];
|
||||
if (!empty($parsedFilename['extension'])) {
|
||||
$assetName = str_replace($parsedFilename['extension'], '', $assetName);
|
||||
}
|
||||
$this->updateOrAddField(
|
||||
$merged,
|
||||
'ARTESIA.FIELD.ASSET NAME',
|
||||
$assetName,
|
||||
'filename',
|
||||
'com.artesia.metadata.MetadataField',
|
||||
false // not domain_value
|
||||
);
|
||||
|
||||
// 4. Brand Code (if available)
|
||||
if (!empty($parsedFilename['brand_code'])) {
|
||||
$this->updateOrAddField(
|
||||
$merged,
|
||||
'FERRERO.FIELD.SUB BRAND',
|
||||
$parsedFilename['brand_code'],
|
||||
'filename',
|
||||
'com.artesia.metadata.MetadataField',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// 5. State (default to 'Local')
|
||||
$this->updateOrAddField(
|
||||
$merged,
|
||||
'FERRERO.FIELD.STATE',
|
||||
'Local',
|
||||
'default',
|
||||
'com.artesia.metadata.MetadataField',
|
||||
true // domain_value
|
||||
);
|
||||
|
||||
// 6. Fiscal Year (use from master or default)
|
||||
if (!$this->fieldExists($merged, 'FERRERO.FIELD.FISCAL YEAR')) {
|
||||
$this->updateOrAddField(
|
||||
$merged,
|
||||
'FERRERO.FIELD.FISCAL YEAR',
|
||||
'2025/2026',
|
||||
'default',
|
||||
'com.artesia.metadata.MetadataField',
|
||||
true // domain_value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or add a metadata field
|
||||
*
|
||||
* @param array &$merged Merged metadata array
|
||||
* @param string $fieldId Field ID
|
||||
* @param mixed $value Field value
|
||||
* @param string $source Source (master/filename/default)
|
||||
* @param string $type Field type
|
||||
* @param bool $isDomainValue Whether this is a domain value
|
||||
*/
|
||||
private function updateOrAddField(&$merged, $fieldId, $value, $source, $type, $isDomainValue = false)
|
||||
{
|
||||
$fieldIndex = $this->findFieldIndex($merged, $fieldId);
|
||||
|
||||
$fieldStructure = [
|
||||
'id' => $fieldId,
|
||||
'type' => $type,
|
||||
'value' => [
|
||||
'cascading_domain_value' => false,
|
||||
'domain_value' => $isDomainValue,
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
'value' => $value
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ($fieldIndex !== false) {
|
||||
// Track conflict if overriding
|
||||
if ($merged['sources'][$fieldId] !== $source) {
|
||||
$merged['conflicts'][] = [
|
||||
'field' => $fieldId,
|
||||
'old_source' => $merged['sources'][$fieldId],
|
||||
'new_source' => $source,
|
||||
'old_value' => $merged['fields']['metadata_element_list'][$fieldIndex]['value']['value']['value'] ?? null,
|
||||
'new_value' => $value
|
||||
];
|
||||
}
|
||||
|
||||
// Update existing field
|
||||
$merged['fields']['metadata_element_list'][$fieldIndex] = $fieldStructure;
|
||||
} else {
|
||||
// Add new field
|
||||
$merged['fields']['metadata_element_list'][] = $fieldStructure;
|
||||
}
|
||||
|
||||
$merged['sources'][$fieldId] = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or add a tabular field
|
||||
*
|
||||
* @param array &$merged Merged metadata array
|
||||
* @param string $fieldId Field ID
|
||||
* @param string $parentTableId Parent table ID
|
||||
* @param mixed $value Field value
|
||||
* @param string $source Source (master/filename/default)
|
||||
*/
|
||||
private function updateOrAddTabularField(&$merged, $fieldId, $parentTableId, $value, $source)
|
||||
{
|
||||
$fieldIndex = $this->findFieldIndex($merged, $fieldId);
|
||||
|
||||
$fieldStructure = [
|
||||
'id' => $fieldId,
|
||||
'parent_table_id' => $parentTableId,
|
||||
'type' => 'com.artesia.metadata.MetadataTableField',
|
||||
'values' => [
|
||||
[
|
||||
'cascading_domain_value' => false,
|
||||
'domain_value' => true,
|
||||
'value' => [
|
||||
'field_value' => [
|
||||
'type' => 'string',
|
||||
'value' => $value
|
||||
],
|
||||
'type' => 'com.artesia.metadata.DomainValue'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ($fieldIndex !== false) {
|
||||
// Track conflict
|
||||
if ($merged['sources'][$fieldId] !== $source) {
|
||||
$merged['conflicts'][] = [
|
||||
'field' => $fieldId,
|
||||
'old_source' => $merged['sources'][$fieldId],
|
||||
'new_source' => $source
|
||||
];
|
||||
}
|
||||
|
||||
// Update existing field
|
||||
$merged['fields']['metadata_element_list'][$fieldIndex] = $fieldStructure;
|
||||
} else {
|
||||
// Add new field
|
||||
$merged['fields']['metadata_element_list'][] = $fieldStructure;
|
||||
}
|
||||
|
||||
$merged['sources'][$fieldId] = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find index of a field in metadata_element_list
|
||||
*
|
||||
* @param array $merged Merged metadata array
|
||||
* @param string $fieldId Field ID to find
|
||||
* @return int|false Field index or false if not found
|
||||
*/
|
||||
private function findFieldIndex($merged, $fieldId)
|
||||
{
|
||||
if (!isset($merged['fields']['metadata_element_list'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($merged['fields']['metadata_element_list'] as $index => $field) {
|
||||
if (isset($field['id']) && $field['id'] === $fieldId) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field exists
|
||||
*
|
||||
* @param array $merged Merged metadata array
|
||||
* @param string $fieldId Field ID to check
|
||||
* @return bool
|
||||
*/
|
||||
private function fieldExists($merged, $fieldId)
|
||||
{
|
||||
return $this->findFieldIndex($merged, $fieldId) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build asset representation for upload
|
||||
*
|
||||
* @param array $mergedMetadata Merged metadata from mergeMetadata()
|
||||
* @return array Asset representation ready for API upload
|
||||
*/
|
||||
public function buildAssetRepresentation($mergedMetadata)
|
||||
{
|
||||
return [
|
||||
'asset_resource' => [
|
||||
'asset' => [
|
||||
'metadata' => $mergedMetadata['fields'],
|
||||
'metadata_model_id' => 'ECOMMERCE',
|
||||
'security_policy_list' => [
|
||||
['id' => 1594]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify which fields are editable (derived from filename)
|
||||
*
|
||||
* @param array $mergedMetadata Merged metadata from mergeMetadata()
|
||||
* @return array List of field IDs that are editable
|
||||
*/
|
||||
public function identifyEditableFields($mergedMetadata)
|
||||
{
|
||||
$editableFields = [];
|
||||
|
||||
foreach ($mergedMetadata['sources'] as $fieldId => $source) {
|
||||
if ($source === 'filename' || $source === 'default') {
|
||||
$editableFields[] = $fieldId;
|
||||
}
|
||||
}
|
||||
|
||||
return $editableFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field mapping
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldMapping()
|
||||
{
|
||||
return $this->fieldMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conflicts from merge
|
||||
*
|
||||
* @param array $mergedMetadata
|
||||
* @return array
|
||||
*/
|
||||
public function getConflicts($mergedMetadata)
|
||||
{
|
||||
return $mergedMetadata['conflicts'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format merged metadata for display
|
||||
*
|
||||
* @param array $mergedMetadata Merged metadata from mergeMetadata()
|
||||
* @return string
|
||||
*/
|
||||
public function formatForDisplay($mergedMetadata)
|
||||
{
|
||||
$output = "=== Merged Metadata ===\n\n";
|
||||
|
||||
if (!empty($mergedMetadata['fields']['metadata_element_list'])) {
|
||||
$output .= "Fields:\n";
|
||||
foreach ($mergedMetadata['fields']['metadata_element_list'] as $field) {
|
||||
$fieldId = $field['id'] ?? 'Unknown';
|
||||
$source = $mergedMetadata['sources'][$fieldId] ?? 'unknown';
|
||||
$editable = in_array($source, ['filename', 'default']) ? 'Editable' : 'Locked';
|
||||
|
||||
$output .= " - $fieldId [$source] [$editable]\n";
|
||||
|
||||
// Extract value
|
||||
if (isset($field['value']['value']['value'])) {
|
||||
$value = $field['value']['value']['value'];
|
||||
$output .= " Value: $value\n";
|
||||
} elseif (isset($field['values'][0]['value']['field_value']['value'])) {
|
||||
$value = $field['values'][0]['value']['field_value']['value'];
|
||||
$output .= " Value: $value\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($mergedMetadata['conflicts'])) {
|
||||
$output .= "\nConflicts (Filename Wins):\n";
|
||||
foreach ($mergedMetadata['conflicts'] as $conflict) {
|
||||
$output .= " - {$conflict['field']}:\n";
|
||||
$output .= " Old ({$conflict['old_source']}): {$conflict['old_value']}\n";
|
||||
$output .= " New ({$conflict['new_source']}): {$conflict['new_value']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
137
test_filename_parser.php
Normal file
137
test_filename_parser.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
/**
|
||||
* Test FilenameParser with various V2 naming convention examples
|
||||
*/
|
||||
|
||||
require_once 'src/FilenameParser.php';
|
||||
|
||||
echo "=== FilenameParser Test Suite ===\n\n";
|
||||
|
||||
$parser = new FilenameParser();
|
||||
|
||||
// Test cases
|
||||
$testCases = [
|
||||
// Valid filenames
|
||||
[
|
||||
'filename' => '1234567_RAF_CH_de_TEST_FILE_OLV_001_15S_16x9_a7K9mP.mp4',
|
||||
'description' => 'Complete V2 filename with tracking ID',
|
||||
'expected_valid' => true
|
||||
],
|
||||
[
|
||||
'filename' => '9876543_KIN_IT_it_CHRISTMAS_TVC_MST_30S_16x9_xYz123.mov',
|
||||
'description' => 'Master file (MST) with tracking ID',
|
||||
'expected_valid' => true
|
||||
],
|
||||
[
|
||||
'filename' => '555_RAF_DE_de_SAMPLE_ECB_002_6S_4x3.jpg',
|
||||
'description' => 'Without tracking ID (optional)',
|
||||
'expected_valid' => true
|
||||
],
|
||||
[
|
||||
'filename' => '12345_FERR_US_en_MULTI_WORD_TITLE_OLV_001_15S_16x9_abc123.mp4',
|
||||
'description' => 'Multi-word subject title',
|
||||
'expected_valid' => true
|
||||
],
|
||||
|
||||
// Invalid filenames
|
||||
[
|
||||
'filename' => 'RAF_CH_de_TEST_OLV_001_15S_16x9.mp4',
|
||||
'description' => 'Missing OMG Job Number',
|
||||
'expected_valid' => false
|
||||
],
|
||||
[
|
||||
'filename' => '1234567_R_CH_de_TEST_OLV_001_15S_16x9.mp4',
|
||||
'description' => 'Brand code too short',
|
||||
'expected_valid' => false
|
||||
],
|
||||
[
|
||||
'filename' => '1234567_RAF_CHE_de_TEST_OLV_001_15S_16x9.mp4',
|
||||
'description' => 'Country code wrong length',
|
||||
'expected_valid' => false
|
||||
],
|
||||
[
|
||||
'filename' => '1234567_RAF_CH_d_TEST_OLV_001_15S_16x9.mp4',
|
||||
'description' => 'Language code too short',
|
||||
'expected_valid' => false
|
||||
]
|
||||
];
|
||||
|
||||
// Run tests
|
||||
$passed = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($testCases as $index => $test) {
|
||||
echo "Test " . ($index + 1) . ": {$test['description']}\n";
|
||||
echo "Filename: {$test['filename']}\n";
|
||||
|
||||
$parsed = $parser->parseFilename($test['filename']);
|
||||
|
||||
if ($parsed) {
|
||||
$isValid = $parsed['is_valid'];
|
||||
$expectedValid = $test['expected_valid'];
|
||||
|
||||
if ($isValid === $expectedValid) {
|
||||
echo "✓ PASSED\n";
|
||||
$passed++;
|
||||
|
||||
// Show parsed components for valid files
|
||||
if ($isValid) {
|
||||
echo " Components:\n";
|
||||
echo " OMG Job: {$parsed['omg_job_number']}\n";
|
||||
echo " Brand: {$parsed['brand_code']}\n";
|
||||
echo " Country: {$parsed['country_code']}\n";
|
||||
echo " Language: {$parsed['language_code']}\n";
|
||||
echo " Subject: {$parsed['subject_title']}\n";
|
||||
echo " Asset Type: {$parsed['asset_type']}\n";
|
||||
echo " Spot Version: {$parsed['spot_version']}\n";
|
||||
echo " Duration: {$parsed['seconds']}S\n";
|
||||
echo " Aspect Ratio: {$parsed['aspect_ratio']}\n";
|
||||
echo " Tracking ID: " . ($parsed['tracking_id'] ?? 'None') . "\n";
|
||||
|
||||
// Show clean filename
|
||||
$cleanFilename = $parser->stripUploadComponents($test['filename']);
|
||||
echo " Clean Filename: $cleanFilename\n";
|
||||
} else {
|
||||
echo " Validation Errors:\n";
|
||||
foreach ($parsed['validation_errors'] as $error) {
|
||||
echo " - $error\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parsed['warnings'])) {
|
||||
echo " Warnings:\n";
|
||||
foreach ($parsed['warnings'] as $warning) {
|
||||
echo " - $warning\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "✗ FAILED - Expected valid=$expectedValid, got valid=$isValid\n";
|
||||
$failed++;
|
||||
|
||||
if (!empty($parsed['validation_errors'])) {
|
||||
echo " Validation Errors:\n";
|
||||
foreach ($parsed['validation_errors'] as $error) {
|
||||
echo " - $error\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "✗ FAILED - Could not parse filename\n";
|
||||
$failed++;
|
||||
}
|
||||
|
||||
echo "\n" . str_repeat('-', 70) . "\n\n";
|
||||
}
|
||||
|
||||
// Summary
|
||||
echo "=== TEST SUMMARY ===\n";
|
||||
echo "Total: " . count($testCases) . "\n";
|
||||
echo "Passed: $passed\n";
|
||||
echo "Failed: $failed\n";
|
||||
echo "\n";
|
||||
|
||||
if ($failed === 0) {
|
||||
echo "✓ All tests passed!\n";
|
||||
} else {
|
||||
echo "✗ Some tests failed\n";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue