loreal-global-kickoff/process-csv.php
DJP 64b99c7a58 Add comprehensive application logging and activity tracking system
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>
2025-11-18 09:39:21 -05:00

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()
]);
}