ApplicationLogger Class: - Structured JSON logging to logs/application.log - Tracks all actions: master_asset_submission, global_to_local_transform, box_upload, omg_api_lookup - Captures: timestamp, user email/name, status, detailed data, IP address, user agent - Methods: getRecentLogs(), getLogsByAction(), getLogsByUser(), getStatistics() Logging Integration: - submit.php: Logs all asset submissions (success/failure) - process-csv.php: Logs all CSV transformations - upload-to-box.php: Logs all Box uploads - Tracks campaign numbers, business units, file counts, dates Logs Viewer (logs-viewer.php): - New tab "Activity Logs" in navigation - Statistics dashboard (total actions, success rate, errors, unique users) - Filterable table (by action type, user, date range) - View detailed data for each log entry (expandable JSON) - Export to CSV functionality - Shows last 100 entries by default (configurable) Email Service Enhancement: - Added SMTP support (in addition to Mailgun API) - Configured for smtp.mailgun.org with provided credentials - Sends notifications to logged-in user email - Proper SMTP protocol implementation with AUTH LOGIN OMG API Configuration: - Added 'enabled' flag (currently false due to 403 error) - Added 'fallback_business_unit' for when OMG disabled - Uses X-API-Key header format - Comprehensive error logging - When API permissions are fixed, set enabled=true Security: - logs/ directory excluded from git - .gitkeep file to preserve directory structure - Protected by .htaccess (log files already blocked) Usage: - All activity automatically logged - View reports at /logs-viewer.php - Export logs as CSV for analysis - Filter by action type, user, or time period - Monitor system health and usage patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
287 lines
10 KiB
PHP
287 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Process CSV Upload
|
|
* Handles CSV transformation with detailed progress tracking and error reporting
|
|
*/
|
|
|
|
session_start();
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0); // Disable display_errors to prevent HTML output breaking JSON
|
|
ini_set('log_errors', 1);
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
error_log('========== PROCESS-CSV.PHP VERSION 2.0 - OMG API ENABLED ==========');
|
|
|
|
try {
|
|
// Load dependencies
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
require_once __DIR__ . '/AuthMiddleware.php';
|
|
require_once __DIR__ . '/CSVTransformer.php';
|
|
require_once __DIR__ . '/OMGService.php';
|
|
require_once __DIR__ . '/EmailService.php';
|
|
require_once __DIR__ . '/ApplicationLogger.php';
|
|
|
|
// Authenticate
|
|
$auth = new AuthMiddleware();
|
|
$user = $auth->requireAuth();
|
|
|
|
// Initialize logger
|
|
$logger = new ApplicationLogger();
|
|
|
|
// Debug: Log request
|
|
error_log('process-csv.php: Started processing');
|
|
error_log('FILES: ' . json_encode(array_keys($_FILES)));
|
|
|
|
// Check if file was uploaded
|
|
if (!isset($_FILES['csvFile'])) {
|
|
error_log('process-csv.php: No file in $_FILES');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'upload',
|
|
'error' => 'No file uploaded',
|
|
'details' => 'Please select a CSV file to upload',
|
|
'debug' => array_keys($_FILES)
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$file = $_FILES['csvFile'];
|
|
$transformer = new CSVTransformer();
|
|
$omgService = new OMGService();
|
|
$emailService = new EmailService();
|
|
|
|
$progress = [];
|
|
|
|
// STAGE 1: Validate Upload
|
|
$progress[] = ['stage' => 'upload', 'status' => 'processing', 'message' => 'Validating file upload...'];
|
|
|
|
$validation = $transformer->validateUpload($file);
|
|
|
|
if (!$validation['valid']) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'upload',
|
|
'error' => 'File validation failed',
|
|
'details' => implode('; ', $validation['errors']),
|
|
'action' => 'Please upload a valid CSV file (max 5MB)',
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$filename = $validation['filename'];
|
|
$progress[] = ['stage' => 'upload', 'status' => 'success', 'message' => 'File uploaded: ' . $filename];
|
|
|
|
// STAGE 2: Parse CSV
|
|
$progress[] = ['stage' => 'parse', 'status' => 'processing', 'message' => 'Parsing CSV file...'];
|
|
|
|
$parseResult = $transformer->parseCSV($file['tmp_name']);
|
|
|
|
if (!$parseResult['success']) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'parse',
|
|
'error' => $parseResult['error'],
|
|
'details' => $parseResult['details'] ?? '',
|
|
'action' => $parseResult['action'] ?? 'Check CSV format',
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$rowCount = $parseResult['rowCount'];
|
|
$progress[] = ['stage' => 'parse', 'status' => 'success', 'message' => "Parsed $rowCount data rows"];
|
|
|
|
// STAGE 3: Extract Campaign Number
|
|
$progress[] = ['stage' => 'campaign', 'status' => 'processing', 'message' => 'Extracting campaign number...'];
|
|
|
|
$campaignResult = $transformer->extractCampaignNumber($filename);
|
|
|
|
if (!$campaignResult['success']) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'campaign',
|
|
'error' => $campaignResult['error'],
|
|
'details' => $campaignResult['details'] ?? '',
|
|
'action' => $campaignResult['action'] ?? 'Fix filename format',
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$campaignNumber = $campaignResult['campaignNumber'];
|
|
$message = 'Campaign number: ' . $campaignNumber;
|
|
if (isset($campaignResult['warning'])) {
|
|
$message .= ' (warning: ' . $campaignResult['warning'] . ')';
|
|
}
|
|
$progress[] = ['stage' => 'campaign', 'status' => 'success', 'message' => $message];
|
|
|
|
// STAGE 4: Call OMG API (if enabled)
|
|
$appConfig = require __DIR__ . '/config.php';
|
|
$omgEnabled = $appConfig['omg_api']['enabled'] ?? false;
|
|
|
|
if (!$omgEnabled) {
|
|
// OMG API disabled - use fallback
|
|
$businessUnit = $appConfig['omg_api']['fallback_business_unit'] ?? 'TESTING';
|
|
$progress[] = ['stage' => 'omg_api', 'status' => 'success', 'message' => 'Using fallback business unit (OMG API disabled)'];
|
|
$progress[] = ['stage' => 'business_unit', 'status' => 'success', 'message' => 'Business unit: ' . $businessUnit];
|
|
error_log('OMG API disabled in config - using fallback: ' . $businessUnit);
|
|
} else {
|
|
// OMG API enabled - make the call
|
|
$progress[] = ['stage' => 'omg_api', 'status' => 'processing', 'message' => 'Looking up campaign in OMG...'];
|
|
|
|
error_log('=== OMG API LOOKUP ===');
|
|
error_log('Campaign Number: ' . $campaignNumber);
|
|
|
|
$projectResult = $omgService->getProject($campaignNumber);
|
|
|
|
error_log('OMG API Result: ' . json_encode($projectResult));
|
|
|
|
if (!$projectResult['success']) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'omg_api',
|
|
'error' => $projectResult['error'],
|
|
'details' => $projectResult['details'] ?? '',
|
|
'action' => $projectResult['action'] ?? 'Check OMG API configuration',
|
|
'httpCode' => $projectResult['httpCode'] ?? 0,
|
|
'endpoint' => 'https://api2.omg.oliver.solutions/loreal/v1/getProject?project_number=' . $campaignNumber,
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$projectData = $projectResult['data'];
|
|
$progress[] = ['stage' => 'omg_api', 'status' => 'success', 'message' => 'Retrieved campaign data from OMG'];
|
|
|
|
// STAGE 5: Map Business Unit
|
|
$progress[] = ['stage' => 'business_unit', 'status' => 'processing', 'message' => 'Mapping business unit...'];
|
|
|
|
$businessArea = $omgService->getBusinessArea($projectData);
|
|
|
|
error_log('Extracted Business Area: ' . ($businessArea ?? 'NULL'));
|
|
|
|
if (empty($businessArea)) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'business_unit',
|
|
'error' => 'Business area not found in OMG response',
|
|
'details' => 'The OMG API response did not contain a business_area field',
|
|
'action' => 'Check if campaign data is complete in OMG system',
|
|
'omgResponse' => $projectData,
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$mappingResult = $omgService->mapBusinessUnit($businessArea);
|
|
|
|
error_log('Business Unit Mapping Result: ' . json_encode($mappingResult));
|
|
|
|
if (!$mappingResult['success']) {
|
|
// Non-fatal warning - continue with "ERROR" value
|
|
$progress[] = [
|
|
'stage' => 'business_unit',
|
|
'status' => 'warning',
|
|
'message' => 'Business unit "' . $businessArea . '" not recognized - using "ERROR"',
|
|
'warning' => $mappingResult['error']
|
|
];
|
|
$businessUnit = 'ERROR';
|
|
} else {
|
|
$businessUnit = $mappingResult['businessUnit'];
|
|
$progress[] = [
|
|
'stage' => 'business_unit',
|
|
'status' => 'success',
|
|
'message' => 'Business unit: ' . $businessUnit
|
|
];
|
|
}
|
|
}
|
|
|
|
// STAGE 6: Transform Data
|
|
$progress[] = ['stage' => 'transform', 'status' => 'processing', 'message' => 'Transforming data for 16 markets...'];
|
|
|
|
$transformResult = $transformer->transformData($parseResult['rows'], $campaignNumber, $businessUnit);
|
|
|
|
if (!$transformResult['success']) {
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'transform',
|
|
'error' => 'Data transformation failed',
|
|
'details' => count($transformResult['errors']) . ' error(s) found',
|
|
'errors' => $transformResult['errors'],
|
|
'warnings' => $transformResult['warnings'],
|
|
'action' => 'Review errors and fix CSV data',
|
|
'progress' => $progress
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$fileCount = $transformResult['fileCount'];
|
|
$totalRows = $transformResult['totalOutputRows'];
|
|
$progress[] = [
|
|
'stage' => 'transform',
|
|
'status' => 'success',
|
|
'message' => "Created {$fileCount} CSV files with {$rowCount} rows each ({$totalRows} total rows)"
|
|
];
|
|
|
|
// Store in session for later upload
|
|
$_SESSION['processed_csv'] = [
|
|
'files' => $transformResult['csvFiles'],
|
|
'campaignNumber' => $campaignNumber,
|
|
'businessUnit' => $businessUnit,
|
|
'inputRowCount' => $rowCount,
|
|
'fileCount' => $fileCount
|
|
];
|
|
|
|
// Prepare preview data (show first CSV file as sample)
|
|
$firstCSV = $transformResult['csvFiles'][0];
|
|
$previewRows = [];
|
|
|
|
// Parse first CSV for preview (show all rows)
|
|
$csvReader = \League\Csv\Reader::createFromString($firstCSV['content']);
|
|
$csvReader->setHeaderOffset(0);
|
|
$previewRows = iterator_to_array($csvReader->getRecords()); // Show all rows
|
|
|
|
// Log successful transformation
|
|
$logger->logGlobalToLocal($user, $campaignNumber, $businessUnit, $rowCount, $fileCount, 'success');
|
|
|
|
// Return success with preview data
|
|
echo json_encode([
|
|
'success' => true,
|
|
'stage' => 'complete',
|
|
'data' => [
|
|
'inputRows' => $rowCount,
|
|
'fileCount' => $fileCount,
|
|
'totalRows' => $totalRows,
|
|
'campaignNumber' => $campaignNumber,
|
|
'businessUnit' => $businessUnit,
|
|
'files' => array_map(function($file) {
|
|
return [
|
|
'filename' => $file['filename'],
|
|
'isoCode' => $file['isoCode'],
|
|
'country' => $file['country'],
|
|
'rowCount' => $file['rowCount']
|
|
];
|
|
}, $transformResult['csvFiles']),
|
|
'previewFile' => $firstCSV['filename'],
|
|
'preview' => $previewRows,
|
|
'warnings' => $transformResult['warnings']
|
|
],
|
|
'progress' => $progress
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('Process CSV exception: ' . $e->getMessage());
|
|
error_log('Stack trace: ' . $e->getTraceAsString());
|
|
|
|
echo json_encode([
|
|
'success' => false,
|
|
'stage' => 'exception',
|
|
'error' => 'Unexpected error occurred',
|
|
'details' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine()
|
|
]);
|
|
}
|