fixed import

This commit is contained in:
Simon Pocrnjič
2025-12-28 13:55:09 +01:00
parent 84b75143df
commit 36b63a180d
9 changed files with 548 additions and 34 deletions
+266 -6
View File
@@ -8,6 +8,7 @@
use App\Models\ImportRow;
use App\Services\Import\Contracts\EntityHandlerInterface;
use App\Services\Import\DateNormalizer;
use App\Services\Import\DecimalNormalizer;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -75,6 +76,9 @@ public function process(Import $import, ?Authenticatable $user = null): array
throw new \RuntimeException("File not found: {$filePath}");
}
// Check if this is a retry (import_rows already exist)
$isRetry = ImportRow::where('import_id', $import->id)->exists();
$fullPath = Storage::disk($import->disk ?? 'local')->path($filePath);
$fh = fopen($fullPath, 'r');
@@ -98,7 +102,82 @@ public function process(Import $import, ?Authenticatable $user = null): array
$isPg = DB::connection()->getDriverName() === 'pgsql';
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
// If retry mode, only process failed/invalid rows
if ($isRetry) {
$failedRows = ImportRow::where('import_id', $import->id)
->whereIn('status', ['invalid', 'failed'])
->orderBy('row_number')
->get();
foreach ($failedRows as $importRow) {
$total++;
try {
$rawAssoc = $importRow->raw_data;
$mapped = $importRow->mapped_data;
// Process entities in priority order within a transaction
$context = ['import' => $import, 'user' => $user, 'import_row' => $importRow];
DB::beginTransaction();
try {
$results = $this->processRow($import, $mapped, $rawAssoc, $context);
// If processing succeeded, commit the transaction
if ($results['status'] === 'imported' || $results['status'] === 'skipped') {
DB::commit();
} else {
DB::rollBack();
}
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// Collect entity details from results
$entityData = $this->collectEntityDetails($results);
$entityDetails = $entityData['details'];
$hasErrors = $entityData['hasErrors'];
$hasWarnings = $entityData['hasWarnings'];
// Handle different result statuses
if ($results['status'] === 'imported') {
$imported++;
$importRow->update([
'status' => 'imported',
'entity_type' => $results['entity_type'] ?? null,
'entity_id' => $results['entity_id'] ?? null,
]);
$this->createRowProcessedEvent($import, $user, $importRow->row_number, $entityDetails, $hasWarnings, $rawAssoc);
} elseif ($results['status'] === 'skipped') {
$skipped++;
$importRow->update(['status' => 'skipped']);
$this->createRowSkippedEvent($import, $user, $importRow->row_number, $entityDetails, $rawAssoc);
} else {
$invalid++;
$importRow->update([
'status' => 'invalid',
'errors' => $results['errors'] ?? ['Processing failed'],
]);
$this->createRowFailedEvent(
$import,
$user,
$importRow->row_number,
$results['errors'] ?? ['Processing failed'],
$entityDetails,
$rawAssoc
);
}
} catch (\Throwable $e) {
$invalid++;
$this->handleRowException($import, $user, $importRow->row_number, $e);
}
}
fclose($fh);
} else {
// Normal mode: process all rows from CSV
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
$rowNum++;
$total++;
@@ -180,9 +259,10 @@ public function process(Import $import, ?Authenticatable $user = null): array
$invalid++;
$this->handleRowException($import, $user, $rowNum, $e);
}
}
}
fclose($fh);
fclose($fh);
}
$this->finalizeImport($import, $user, $total, $imported, $skipped, $invalid);
} catch (\Throwable $e) {
@@ -272,6 +352,12 @@ protected function applyMappings(array $raw, $mappings): array
}
foreach ($groupedMappings as $targetField => $fieldMappings) {
// Special handling for meta fields: contracts.meta or other_entity.meta
if (str_ends_with($targetField, '.meta')) {
$this->applyMetaMappings($mapped, $targetField, $fieldMappings, $raw);
continue;
}
// Group by group number from options
$valuesByGroup = [];
@@ -323,6 +409,89 @@ protected function applyMappings(array $raw, $mappings): array
return $mapped;
}
/**
* Apply meta mappings with special structure: entity.meta[group][key] = {title, value, type}
*/
protected function applyMetaMappings(array &$mapped, string $targetField, array $fieldMappings, array $raw): void
{
// Extract entity from target field: contracts.meta -> contracts
$entity = str_replace('.meta', '', $targetField);
foreach ($fieldMappings as $mapping) {
$sourceCol = $mapping->source_column;
if (!isset($raw[$sourceCol])) {
continue;
}
$value = $raw[$sourceCol];
// Apply transform
if ($mapping->transform) {
$value = $this->applyTransform($value, $mapping->transform);
}
// Get options
$options = $mapping->options ? json_decode($mapping->options, true) : [];
$metaKey = $options['key'] ?? null;
$metaType = $options['type'] ?? 'string';
$group = $options['group'] ?? '1';
if (!$metaKey) {
continue;
}
// Coerce value based on type
$coerced = $value;
if ($metaType === 'number') {
if (is_string($coerced)) {
$norm = DecimalNormalizer::normalize($coerced);
$coerced = is_numeric($norm) ? (float) $norm : $coerced;
}
} elseif ($metaType === 'boolean') {
if (is_string($coerced)) {
$lc = strtolower(trim($coerced));
if (in_array($lc, ['1', 'true', 'yes', 'y'], true)) {
$coerced = true;
} elseif (in_array($lc, ['0', 'false', 'no', 'n'], true)) {
$coerced = false;
}
} else {
$coerced = (bool) $coerced;
}
} elseif ($metaType === 'date') {
$coerced = is_scalar($coerced) ? $this->normalizeDate((string) $coerced) : null;
} else {
// string or unspecified: cast scalars to string
if (is_scalar($coerced)) {
$coerced = (string) $coerced;
}
}
// Initialize structure if needed
if (!isset($mapped[$entity])) {
$mapped[$entity] = [];
}
if (!isset($mapped[$entity]['meta']) || !is_array($mapped[$entity]['meta'])) {
$mapped[$entity]['meta'] = [];
}
if (!isset($mapped[$entity]['meta'][$group])) {
$mapped[$entity]['meta'][$group] = [];
}
// Store as structure with title, value and type
$entry = [
'title' => $sourceCol,
'value' => $coerced,
];
if ($metaType) {
$entry['type'] = $metaType;
}
$mapped[$entity]['meta'][$group][$metaKey] = $entry;
}
}
/**
* Apply transform to value.
@@ -457,6 +626,19 @@ protected function processRow(Import $import, array $mapped, array $raw, array $
$lastEntityType = $handler->getEntityClass();
$lastEntityId = $result['entity']?->id ?? null;
}
// Post-contract actions (segment attachment, activity creation)
if ($root === 'contract' && in_array($result['action'] ?? null, ['inserted', 'updated', 'reactivated'])) {
try {
$this->postContractActions($import, $result['entity'], $context);
} catch (\Throwable $e) {
Log::warning('Post-contract action failed', [
'import_id' => $import->id,
'contract_id' => $result['entity']->id ?? null,
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$hasErrors = true;
@@ -714,8 +896,13 @@ protected function finalizeImport(
int $skipped,
int $invalid
): void {
// If there are any invalid rows, mark import as failed, not completed
$status = $invalid > 0 ? 'failed' : 'completed';
$eventLevel = $invalid > 0 ? 'error' : 'info';
$eventName = $invalid > 0 ? 'processing_failed' : 'processing_completed';
$import->update([
'status' => 'completed',
'status' => $status,
'finished_at' => now(),
'total_rows' => $total,
'imported_rows' => $imported,
@@ -726,8 +913,8 @@ protected function finalizeImport(
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'processing_completed',
'level' => 'info',
'event' => $eventName,
'level' => $eventLevel,
'message' => "Processed {$total} rows: {$imported} imported, {$skipped} skipped, {$invalid} invalid",
]);
}
@@ -756,4 +943,77 @@ protected function handleFatalException(
'message' => $e->getMessage(),
]);
}
/**
* Post-contract actions: attach segment, create activity with decision.
* Matches ImportProcessor::postContractActions() behavior.
*/
protected function postContractActions(Import $import, $contract, array $context = []): void
{
$meta = $import->meta ?? [];
$segmentId = (int) ($meta['segment_id'] ?? 0);
$decisionId = (int) ($meta['decision_id'] ?? 0);
$templateName = (string) ($meta['template_name'] ?? optional($import->template)->name ?? '');
$actionId = (int) ($meta['action_id'] ?? 0);
// Attach segment to contract as the main (active) segment if provided
if ($segmentId > 0) {
// Ensure the segment exists on the client case and is active
$ccSeg = \DB::table('client_case_segment')
->where('client_case_id', $contract->client_case_id)
->where('segment_id', $segmentId)
->first();
if (! $ccSeg) {
\DB::table('client_case_segment')->insert([
'client_case_id' => $contract->client_case_id,
'segment_id' => $segmentId,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
} elseif (! $ccSeg->active) {
\DB::table('client_case_segment')
->where('id', $ccSeg->id)
->update(['active' => true, 'updated_at' => now()]);
}
// Deactivate all other segments for this contract to make this the main one
\DB::table('contract_segment')
->where('contract_id', $contract->id)
->where('segment_id', '!=', $segmentId)
->update(['active' => false, 'updated_at' => now()]);
// Upsert the selected segment as active for this contract
$pivot = \DB::table('contract_segment')
->where('contract_id', $contract->id)
->where('segment_id', $segmentId)
->first();
if ($pivot) {
if (! $pivot->active) {
\DB::table('contract_segment')
->where('id', $pivot->id)
->update(['active' => true, 'updated_at' => now()]);
}
} else {
\DB::table('contract_segment')->insert([
'contract_id' => $contract->id,
'segment_id' => $segmentId,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// Create activity if decision provided
if ($decisionId > 0) {
\App\Models\Activity::create([
'decision_id' => $decisionId,
'action_id' => $actionId > 0 ? $actionId : null,
'contract_id' => $contract->id,
'client_case_id' => $contract->client_case_id,
'note' => trim('Imported via template'.($templateName ? ': '.$templateName : '')),
]);
}
}
}