config = $appConfig['global_to_local']; $this->isoCodes = $this->config['iso_codes']; } /** * Validate uploaded CSV file */ public function validateUpload($file) { $errors = []; // Check if file was uploaded if (!isset($file['tmp_name']) || empty($file['tmp_name'])) { $errors[] = 'No file was uploaded'; return ['valid' => false, 'errors' => $errors]; } // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'File upload error: ' . $this->getUploadErrorMessage($file['error']); return ['valid' => false, 'errors' => $errors]; } // Check file extension $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($ext !== 'csv') { $errors[] = 'Invalid file type. Expected .csv, got .' . $ext; return ['valid' => false, 'errors' => $errors]; } // Check file size if ($file['size'] > $this->config['max_file_size']) { $maxMB = round($this->config['max_file_size'] / 1048576, 2); $sizeMB = round($file['size'] / 1048576, 2); $errors[] = "File too large. Maximum size: {$maxMB}MB, uploaded: {$sizeMB}MB"; return ['valid' => false, 'errors' => $errors]; } // Check if file is empty if ($file['size'] === 0) { $errors[] = 'File is empty'; return ['valid' => false, 'errors' => $errors]; } return ['valid' => true, 'filename' => $file['name']]; } /** * Get upload error message */ private function getUploadErrorMessage($code) { switch ($code) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return 'File exceeds maximum allowed size'; case UPLOAD_ERR_PARTIAL: return 'File was only partially uploaded'; case UPLOAD_ERR_NO_FILE: return 'No file was uploaded'; case UPLOAD_ERR_NO_TMP_DIR: return 'Missing temporary folder'; case UPLOAD_ERR_CANT_WRITE: return 'Failed to write file to disk'; default: return 'Unknown upload error'; } } /** * Parse CSV and validate structure */ public function parseCSV($filePath) { try { $csv = Reader::createFromPath($filePath, 'r'); $csv->setHeaderOffset(0); // First row is headers $headers = $csv->getHeader(); $records = iterator_to_array($csv->getRecords()); // Validate column count (should have at least 12 columns based on blueprint) if (count($headers) < 12) { return [ 'success' => false, 'error' => 'Invalid CSV structure', 'details' => 'Expected at least 12 columns, found ' . count($headers), 'action' => 'Verify you uploaded the correct CSV file' ]; } // Check if CSV has data rows if (count($records) === 0) { return [ 'success' => false, 'error' => 'CSV has no data rows', 'details' => 'CSV contains only headers, no data to process', 'action' => 'Upload a CSV file with data rows' ]; } return [ 'success' => true, 'headers' => $headers, 'rows' => $records, 'rowCount' => count($records) ]; } catch (Exception $e) { return [ 'success' => false, 'error' => 'CSV parsing failed', 'details' => $e->getMessage(), 'action' => 'Check if the file is a valid CSV with comma delimiters' ]; } } /** * Extract campaign number from filename * Expected pattern: *_CAMPAIGNNUM_*.csv */ public function extractCampaignNumber($filename) { // Remove .csv extension $nameWithoutExt = preg_replace('/\.csv$/i', '', $filename); // Split by underscore $parts = explode('_', $nameWithoutExt); // Try to find campaign number (should be in part 2 based on blueprint) if (isset($parts[2]) && is_numeric($parts[2])) { return [ 'success' => true, 'campaignNumber' => $parts[2] ]; } // Try to find any numeric part foreach ($parts as $part) { if (is_numeric($part) && strlen($part) >= 4) { return [ 'success' => true, 'campaignNumber' => $part, 'warning' => 'Campaign number found but not in expected position' ]; } } return [ 'success' => false, 'error' => 'Campaign number not found in filename', 'details' => "Expected pattern: *_CAMPAIGN#_*.csv, got: $filename", 'action' => 'Rename file to include campaign number (e.g., Global_123456_Assets.csv)' ]; } /** * Transform CSV data per Make.com blueprint logic */ public function transformData($rows, $campaignNumber, $businessUnit) { $transformedRows = []; $this->errors = []; $this->warnings = []; $rowNumber = 1; // Start from 1 (after header) foreach ($rows as $inputRow) { $rowNumber++; // Get columns by position (0-indexed) $cols = array_values($inputRow); // Validate required columns exist if (count($cols) < 12) { $this->errors[] = [ 'row' => $rowNumber, 'error' => 'Insufficient columns', 'details' => 'Row has ' . count($cols) . ' columns, expected at least 12' ]; continue; } // Extract data from columns $productId = $cols[1] ?? ''; // col2 $category = $cols[3] ?? ''; // col4 $media = $cols[4] ?? ''; // col5 $subMedia = $cols[5] ?? ''; // col6 $destination = $cols[6] ?? ''; // col7 $format = $cols[7] ?? ''; // col8 $supplyDate = $cols[8] ?? ''; // col9 $liveDate = $cols[9] ?? ''; // col10 $endDate = $cols[10] ?? ''; // col11 $specialInstructions = $cols[11] ?? ''; // col12 // Validate required fields if (empty($productId)) { $this->warnings[] = [ 'row' => $rowNumber, 'warning' => 'Missing Product ID', 'severity' => 'high' ]; } // Transform dates (parse and add 1 month per blueprint) $transformedSupplyDate = $this->transformDate($supplyDate, $rowNumber, 'Supply date'); $transformedLiveDate = $this->transformDate($liveDate, $rowNumber, 'Live date'); $transformedEndDate = $this->transformDate($endDate, $rowNumber, 'End date'); // Create one output row for each ISO code (16 markets) foreach ($this->isoCodes as $isoCode) { $isoCode = trim($isoCode); // Extract country code (2nd part after dash) $parts = explode('-', $isoCode); $country = isset($parts[1]) ? $parts[1] : ''; // Build output row per blueprint mapping $transformedRow = [ 'Title' => $productId . '_' . $isoCode, 'Status' => 'Booked', 'Category' => $category, 'Media' => $media, 'Sub media' => $subMedia, 'Destination' => $destination, 'Format' => $format, 'Supply date' => $transformedSupplyDate, 'Live date' => $transformedLiveDate, 'End date' => $transformedEndDate, 'Special instructions' => $specialInstructions, 'Language' => $isoCode, 'Country' => $country, 'Quantity' => '1', 'Pos Code' => '', 'Duplicate' => '', 'Product ID' => $productId, 'Creative Execution' => $productId ]; $transformedRows[] = $transformedRow; } } return [ 'success' => count($this->errors) === 0, 'rows' => $transformedRows, 'inputRowCount' => count($rows), 'outputRowCount' => count($transformedRows), 'errors' => $this->errors, 'warnings' => $this->warnings ]; } /** * Transform date: parse, add 1 month, format DD/MM/YYYY */ private function transformDate($dateString, $rowNumber, $fieldName) { if (empty($dateString)) { $this->warnings[] = [ 'row' => $rowNumber, 'warning' => "Empty $fieldName", 'severity' => 'medium' ]; return ''; } try { // Try parsing with different formats per blueprint logic // Format 1: "DD MMM YYYY" (length check in blueprint: length = 17) if (strlen($dateString) >= 11 && preg_match('/\d{1,2}\s+[A-Za-z]{3}\s+\d{4}/', $dateString)) { $date = Carbon::createFromFormat('d M Y', $dateString); } // Format 2: "DD/MM/YYYY" else if (preg_match('/\d{1,2}\/\d{1,2}\/\d{4}/', $dateString)) { $date = Carbon::createFromFormat('d/m/Y', $dateString); } // Try ISO format else { $date = Carbon::parse($dateString); } // Add 1 month per blueprint $date->addMonth(); // Format as DD/MM/YYYY return $date->format('d/m/Y'); } catch (Exception $e) { $this->errors[] = [ 'row' => $rowNumber, 'error' => "Invalid $fieldName format", 'details' => "Unable to parse '$dateString' - " . $e->getMessage(), 'action' => 'Use format DD/MM/YYYY or DD MMM YYYY' ]; return $dateString; // Return original if can't parse } } /** * Generate output filename per blueprint */ public function generateFilename($campaignNumber, $businessUnit, $isoCode) { $timestamp = time(); // Extract country from ISO code $parts = explode('-', $isoCode); $country = isset($parts[1]) ? $parts[1] : ''; // Format: OMG{campaign}_GlobalACIngest_{BU}-{Country}_{timestamp}.csv return "OMG{$campaignNumber}_GlobalACIngest_{$businessUnit}-{country}_{$timestamp}.csv"; } /** * Get errors */ public function getErrors() { return $this->errors; } /** * Get warnings */ public function getWarnings() { return $this->warnings; } /** * Has errors */ public function hasErrors() { return count($this->errors) > 0; } /** * Has warnings */ public function hasWarnings() { return count($this->warnings) > 0; } }