diff --git a/CSVTransformer.php b/CSVTransformer.php
index 375d160..720527a 100644
--- a/CSVTransformer.php
+++ b/CSVTransformer.php
@@ -213,11 +213,20 @@ class CSVTransformer {
foreach ($rows as $inputRow) {
$rowNumber++;
+ // Store original title for Creative Execution
+ $originalTitle = $inputRow['Title'] ?? '';
+
// Replace Language and Country fields
$outputRow = $inputRow;
$outputRow['Language'] = $language;
$outputRow['Country'] = $country;
+ // Add language/country to Title (append to end)
+ $outputRow['Title'] = $originalTitle . '_' . $language;
+
+ // Add Creative Execution column (contains the original global title)
+ $outputRow['Creative Execution'] = $originalTitle;
+
// Transform dates if needed (add 1 month per blueprint)
if (isset($outputRow['Supply date'])) {
$outputRow['Supply date'] = $this->transformDate($outputRow['Supply date'], $rowNumber, 'Supply date');
diff --git a/EmailService.php b/EmailService.php
index bee3028..57377f3 100644
--- a/EmailService.php
+++ b/EmailService.php
@@ -12,12 +12,15 @@ class EmailService {
$appConfig = require __DIR__ . '/config.php';
$this->config = $appConfig['email'];
$this->enabled = $this->config['enabled'] ?? false;
+
+ // Load email templates
+ require_once __DIR__ . '/EmailTemplates.php';
}
/**
* Send email via Mailgun API or SMTP
*/
- public function send($to, $subject, $text) {
+ public function send($to, $subject, $text, $html = null) {
if (!$this->enabled) {
error_log('Email not sent (service disabled): ' . $subject);
return ['success' => true, 'message' => 'Email disabled'];
@@ -26,12 +29,28 @@ class EmailService {
$service = $this->config['service'] ?? 'mailgun';
if ($service === 'smtp') {
- return $this->sendViaSMTP($to, $subject, $text);
+ return $this->sendViaSMTP($to, $subject, $text, $html);
} else {
- return $this->sendViaMailgunAPI($to, $subject, $text);
+ return $this->sendViaMailgunAPI($to, $subject, $text, $html);
}
}
+ /**
+ * Send templated email
+ */
+ public function sendTemplate($to, $templateName, $data) {
+ $html = EmailTemplates::getTemplate($templateName, $data);
+
+ // Extract subject from HTML title
+ preg_match('/
]*>(.*?)<\/h1>/', $html, $matches);
+ $subject = $matches[1] ?? 'L\'Oréal OMG Assistant Notification';
+
+ // Create plain text version from data
+ $text = strip_tags($subject) . "\n\n" . json_encode($data, JSON_PRETTY_PRINT);
+
+ return $this->send($to, $subject, $text, $html);
+ }
+
/**
* Send email via Mailgun API
*/
@@ -101,7 +120,7 @@ class EmailService {
/**
* Send email via SMTP
*/
- private function sendViaSMTP($to, $subject, $text) {
+ private function sendViaSMTP($to, $subject, $text, $html = null) {
try {
$from = $this->config['from'];
$host = $this->config['smtp_host'];
@@ -109,12 +128,19 @@ class EmailService {
$username = $this->config['smtp_username'];
$password = $this->config['smtp_password'];
- // Build email message
+ // Build email message with multipart if HTML provided
+ $boundary = md5(time());
+
$headers = "From: {$from}\r\n";
$headers .= "Reply-To: {$from}\r\n";
$headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
- $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
+
+ if ($html) {
+ $headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
+ } else {
+ $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
+ }
// Create SMTP connection
$socket = fsockopen($host, $port, $errno, $errstr, 10);
@@ -142,7 +168,21 @@ class EmailService {
$message = "Subject: {$subject}\r\n";
$message .= $headers;
$message .= "\r\n";
- $message .= $text . "\r\n";
+
+ if ($html) {
+ // Multipart message
+ $message .= "--{$boundary}\r\n";
+ $message .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
+ $message .= $text . "\r\n\r\n";
+ $message .= "--{$boundary}\r\n";
+ $message .= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
+ $message .= $html . "\r\n\r\n";
+ $message .= "--{$boundary}--\r\n";
+ } else {
+ // Plain text only
+ $message .= $text . "\r\n";
+ }
+
$message .= ".\r\n";
$this->smtpCommand($socket, $message, 250);
@@ -196,34 +236,35 @@ class EmailService {
/**
* Send process started notification
*/
- public function notifyStarted($userEmail, $filename) {
- $subject = 'Regional Hotfolder - Started';
- $text = "Document: $filename has been picked up for processing.";
-
- return $this->send($userEmail, $subject, $text);
+ public function notifyStarted($userEmail, $data) {
+ return $this->sendTemplate($userEmail, 'global_to_local_started', $data);
}
/**
* Send process completed notification
*/
- public function notifyCompleted($userEmail, $filename, $outputCount) {
- $subject = 'Regional Hotfolder - Completed';
- $text = "Document: $filename has completed processing.\n\n";
- $text .= "Generated $outputCount regional CSV files.\n";
- $text .= "Files should appear in OMG within 5 minutes.";
-
- return $this->send($userEmail, $subject, $text);
+ public function notifyCompleted($userEmail, $data) {
+ return $this->sendTemplate($userEmail, 'global_to_local_complete', $data);
}
/**
* Send error notification
*/
- public function notifyError($userEmail, $filename, $errorMessage) {
- $subject = 'Error - Regional Hotfolder';
- $text = "An error occurred while processing: $filename\n\n";
- $text .= "Error: $errorMessage\n\n";
- $text .= "Please check the file and try again.";
+ public function notifyError($userEmail, $data) {
+ return $this->sendTemplate($userEmail, 'global_to_local_failed', $data);
+ }
- return $this->send($userEmail, $subject, $text);
+ /**
+ * Send asset submission success notification
+ */
+ public function notifyAssetSubmissionSuccess($userEmail, $data) {
+ return $this->sendTemplate($userEmail, 'asset_submission_success', $data);
+ }
+
+ /**
+ * Send asset submission failed notification
+ */
+ public function notifyAssetSubmissionFailed($userEmail, $data) {
+ return $this->sendTemplate($userEmail, 'asset_submission_failed', $data);
}
}
diff --git a/EmailTemplates.php b/EmailTemplates.php
new file mode 100644
index 0000000..9552ea5
--- /dev/null
+++ b/EmailTemplates.php
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+HTML;
+ }
+
+ /**
+ * Master Asset Submission - Success
+ */
+ public static function assetSubmissionSuccess($data) {
+ $content = <<
+
✅ Master Asset Submission Complete
+
+
+
+
Box ID: {$data['box_id']}
+
Campaign Number: {$data['campaign_number']}
+
Folder: {$data['folder_name']}
+
Total Assets: {$data['asset_count']} items
+
+
+Campaign Dates:
+
+
Supply Date: {$data['supply_date']}
+
Live Date: {$data['live_date']}
+
End Date: {$data['end_date']}
+
+
+
+
✓ Status: Asset metadata successfully submitted to Make.com workflow.
+
Your assets are now being processed in the OMG system.
+
+HTML;
+
+ return self::wrapTemplate('✅ Asset Submission Complete', $content, '#28a745');
+ }
+
+ /**
+ * Master Asset Submission - Failed
+ */
+ public static function assetSubmissionFailed($data) {
+ $content = <<
+ ❌ Asset Submission Failed
+
+
+
+
Box ID: {$data['box_id']}
+
Campaign Number: {$data['campaign_number']}
+
+
+Error Details:
+
+
Error: {$data['error']}
+
+
+
+
📌 Action Required: Please review the error and try again.
+
If the issue persists, contact the system administrator.
+
+HTML;
+
+ return self::wrapTemplate('❌ Asset Submission Failed', $content, '#d32f2f');
+ }
+
+ /**
+ * Global to Local - Processing Started
+ */
+ public static function globalToLocalStarted($data) {
+ $content = <<
+ 📄 File: {$data['filename']}
+ Campaign Number: {$data['campaign_number']}
+
+
+Processing Steps:
+
+
✓ File uploaded and validated
+
🔄 Extracting campaign information...
+
🔄 Calling OMG API for business unit...
+
🔄 Transforming data for 16 regional markets...
+
+
+
+
⏳ Please wait: You will receive another email when processing is complete.
+
+HTML;
+
+ return self::wrapTemplate('🔄 CSV Processing Started', $content);
+ }
+
+ /**
+ * Global to Local - Processing Complete
+ */
+ public static function globalToLocalComplete($data) {
+ $content = <<
+ ✅ CSV Transformation Complete
+
+
+
+
Campaign Number: {$data['campaign_number']}
+
Business Unit: {$data['business_unit']}
+
Input Rows: {$data['input_rows']}
+
Output Files Created: {$data['file_count']}
+
Total Output Rows: {$data['total_rows']}
+
+
+Files Created (16 Regional CSVs):
+
+
+ | ISO Code |
+ Country |
+ Rows |
+
+HTML;
+
+ // Add sample rows (first 5 files)
+ $isoSample = ['en-GB', 'es-ES', 'pt-PT', 'en-IE', 'fr-CH'];
+ foreach ($isoSample as $iso) {
+ $parts = explode('-', $iso);
+ $country = $parts[1] ?? '';
+ $content .= "| {$iso} | {$country} | {$data['input_rows']} |
";
+ }
+
+ $content .= <<... and 11 more files |
+
+
+
+
✓ Complete: All {$data['file_count']} CSV files have been uploaded to Box.
+
Files should appear in OMG within 5 minutes.
+
+
+
+HTML;
+
+ return self::wrapTemplate('✅ Global to Local Complete', $content, '#28a745');
+ }
+
+ /**
+ * Global to Local - Processing Failed
+ */
+ public static function globalToLocalFailed($data) {
+ $content = <<
+ ❌ CSV Processing Failed
+
+
+
+
File: {$data['filename']}
+ {$data['campaign_number'] ? "
Campaign Number: {$data['campaign_number']}
" : ''}
+
+
+Error Details:
+
+
Stage: {$data['stage']}
+
Error: {$data['error']}
+
+
+
+
📌 Action Required:
+
+ - Review the error details above
+ - Check your CSV file format and data
+ - Verify campaign number is in the filename
+ - Try uploading again after corrections
+
+
+HTML;
+
+ return self::wrapTemplate('❌ CSV Processing Failed', $content, '#d32f2f');
+ }
+
+ /**
+ * Box Upload - Success
+ */
+ public static function boxUploadSuccess($data) {
+ $content = <<
+ ✅ Files Uploaded to Box Successfully
+
+
+
+
Files Uploaded: {$data['file_count']}
+
Campaign Number: {$data['campaign_number']}
+
Business Unit: {$data['business_unit']}
+
+
+Upload Summary:
+
+
All {$data['file_count']} regional CSV files have been successfully uploaded to the Box output folder.
+
These files are now available in the OMG system for further processing.
+
+
+
+HTML;
+
+ return self::wrapTemplate('✅ Box Upload Complete', $content, '#28a745');
+ }
+
+ /**
+ * Get template by name
+ */
+ public static function getTemplate($templateName, $data) {
+ switch ($templateName) {
+ case 'asset_submission_success':
+ return self::assetSubmissionSuccess($data);
+
+ case 'asset_submission_failed':
+ return self::assetSubmissionFailed($data);
+
+ case 'global_to_local_started':
+ return self::globalToLocalStarted($data);
+
+ case 'global_to_local_complete':
+ return self::globalToLocalComplete($data);
+
+ case 'global_to_local_failed':
+ return self::globalToLocalFailed($data);
+
+ case 'box_upload_success':
+ return self::boxUploadSuccess($data);
+
+ default:
+ // Fallback simple template
+ return self::wrapTemplate('Notification', '' . json_encode($data) . '
');
+ }
+ }
+}
diff --git a/upload-to-box.php b/upload-to-box.php
index c559e44..cb3cd2d 100644
--- a/upload-to-box.php
+++ b/upload-to-box.php
@@ -150,7 +150,14 @@ try {
}
// Send completion email
- $emailService->notifyCompleted($user['email'], "{$fileCount} regional CSV files", count($uploadedFiles));
+ $emailService->notifyCompleted($user['email'], [
+ 'campaign_number' => $campaignNumber,
+ 'business_unit' => $businessUnit,
+ 'input_rows' => $processedData['inputRowCount'],
+ 'file_count' => $fileCount,
+ 'total_rows' => $processedData['inputRowCount'] * $fileCount,
+ 'folder_url' => $folderUrl
+ ]);
// Log successful upload
$logger->logBoxUpload($user, $fileCount, $outputFolderId, 'success');
@@ -175,7 +182,12 @@ try {
error_log('Box upload exception: ' . $e->getMessage());
// Send error email
- $emailService->notifyError($user['email'], $filename, $e->getMessage());
+ $emailService->notifyError($user['email'], [
+ 'filename' => $processedData['files'][0]['filename'] ?? 'Unknown',
+ 'campaign_number' => $campaignNumber,
+ 'stage' => 'box_upload',
+ 'error' => $e->getMessage()
+ ]);
echo json_encode([
'success' => false,