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

View File

@ -0,0 +1,83 @@
<?php
namespace App\Services\Import;
class DecimalNormalizer
{
/**
* Normalize a raw decimal string to a standard format (period as decimal separator).
* Handles European format (comma as decimal) and American format (period as decimal).
*
* Examples:
* - "958,31" => "958.31"
* - "1.234,56" => "1234.56"
* - "1,234.56" => "1234.56"
* - "1234" => "1234"
*
* Based on ImportProcessor::normalizeDecimal()
*/
public static function normalize(?string $raw): ?string
{
if ($raw === null) {
return null;
}
// Keep digits, comma, dot, and minus to detect separators
$s = preg_replace('/[^0-9,\.-]/', '', $raw) ?? '';
$s = trim($s);
if ($s === '') {
return null;
}
$lastComma = strrpos($s, ',');
$lastDot = strrpos($s, '.');
// Determine decimal separator by last occurrence
$decimalSep = null;
if ($lastComma !== false || $lastDot !== false) {
if ($lastComma === false) {
$decimalSep = '.';
} elseif ($lastDot === false) {
$decimalSep = ',';
} else {
$decimalSep = $lastComma > $lastDot ? ',' : '.';
}
}
// Remove all thousand separators and unify decimal to '.'
if ($decimalSep === ',') {
// Remove all dots (thousand separators)
$s = str_replace('.', '', $s);
// Replace last comma with dot
$pos = strrpos($s, ',');
if ($pos !== false) {
$s[$pos] = '.';
}
// Remove any remaining commas (unlikely)
$s = str_replace(',', '', $s);
} elseif ($decimalSep === '.') {
// Remove all commas (thousand separators)
$s = str_replace(',', '', $s);
// Dot is already decimal separator
} else {
// No decimal separator: remove commas/dots entirely
$s = str_replace([',', '.'], '', $s);
}
// Handle negative numbers
$s = ltrim($s, '+');
$neg = false;
if (str_starts_with($s, '-')) {
$neg = true;
$s = ltrim($s, '-');
}
// Remove any stray minus signs
$s = str_replace('-', '', $s);
if ($neg) {
$s = '-' . $s;
}
return $s;
}
}

View File

@ -370,9 +370,14 @@ public function resolveOrCreateClientCaseForContract(Import $import, array $mapp
if (!$personId) { if (!$personId) {
// Create minimal Person as last resort // Create minimal Person as last resort
$personId = Person::create(['type_id' => 1])->id; $defaultGroupId = (int) (\App\Models\Person\PersonGroup::min('id') ?? 1);
$personId = Person::create([
'type_id' => 1,
'group_id' => $defaultGroupId,
])->id;
Log::info('EntityResolutionService: Created minimal Person for new ClientCase', [ Log::info('EntityResolutionService: Created minimal Person for new ClientCase', [
'person_id' => $personId, 'person_id' => $personId,
'group_id' => $defaultGroupId,
]); ]);
} }
} }

View File

@ -5,6 +5,7 @@
use App\Models\Account; use App\Models\Account;
use App\Models\Import; use App\Models\Import;
use App\Services\Import\BaseEntityHandler; use App\Services\Import\BaseEntityHandler;
use App\Services\Import\DecimalNormalizer;
class AccountHandler extends BaseEntityHandler class AccountHandler extends BaseEntityHandler
{ {
@ -13,10 +14,38 @@ public function getEntityClass(): string
return Account::class; return Account::class;
} }
/**
* Override validate to handle contract_id and reference from context.
* Both contract_id and reference are populated in process() (reference defaults to contract reference).
*/
public function validate(array $mapped): array
{
// Remove contract_id and reference from validation - both will be populated in process()
// Reference defaults to contract.reference if not set (matching v1 behavior)
$rules = $this->entityConfig?->validation_rules ?? [];
unset($rules['contract_id'], $rules['reference']);
if (empty($rules)) {
return ['valid' => true, 'errors' => []];
}
$validator = \Illuminate\Support\Facades\Validator::make($mapped, $rules);
if ($validator->fails()) {
return [
'valid' => false,
'errors' => $validator->errors()->all(),
];
}
return ['valid' => true, 'errors' => []];
}
public function resolve(array $mapped, array $context = []): mixed public function resolve(array $mapped, array $context = []): mixed
{ {
$reference = $mapped['reference'] ?? null; $reference = $mapped['reference'] ?? null;
$contractId = $mapped['contract_id'] ?? $context['contract']?->entity?->id ?? null; $contractId = $mapped['contract_id'] ?? $context['contract']['entity']->id ?? null;
if (! $reference || ! $contractId) { if (! $reference || ! $contractId) {
return null; return null;
@ -37,7 +66,15 @@ public function process(Import $import, array $mapped, array $raw, array $contex
]; ];
} }
$contractId = $context['contract']->entity->id; // Fallback: if account.reference is empty, use contract.reference (matching v1 behavior)
if (empty($mapped['reference'])) {
$contractReference = $context['contract']['entity']->reference ?? null;
if ($contractReference) {
$mapped['reference'] = preg_replace('/\s+/', '', trim((string) $contractReference));
}
}
$contractId = $context['contract']['entity']->id;
$mapped['contract_id'] = $contractId; $mapped['contract_id'] = $contractId;
$existing = $this->resolve($mapped, $context); $existing = $this->resolve($mapped, $context);
@ -75,6 +112,12 @@ public function process(Import $import, array $mapped, array $raw, array $contex
// Create new account // Create new account
$account = new Account; $account = new Account;
$payload = $this->buildPayload($mapped, $account); $payload = $this->buildPayload($mapped, $account);
// Ensure required defaults for new accounts
if (!isset($payload['type_id'])) {
$payload['type_id'] = $this->getDefaultAccountTypeId();
}
$account->fill($payload); $account->fill($payload);
$account->save(); $account->save();
@ -100,7 +143,14 @@ protected function buildPayload(array $mapped, $model): array
foreach ($fieldMap as $source => $target) { foreach ($fieldMap as $source => $target) {
if (array_key_exists($source, $mapped)) { if (array_key_exists($source, $mapped)) {
$payload[$target] = $mapped[$source]; $value = $mapped[$source];
// Normalize decimal fields (convert comma to period)
if (in_array($source, ['balance_amount', 'initial_amount']) && is_string($value)) {
$value = DecimalNormalizer::normalize($value);
}
$payload[$target] = $value;
} }
} }
@ -155,4 +205,12 @@ protected function createBalanceChangeActivity(Account $account, float $oldBalan
]); ]);
} }
} }
/**
* Get default account type ID.
*/
protected function getDefaultAccountTypeId(): int
{
return (int) (\App\Models\AccountType::min('id') ?? 1);
}
} }

View File

@ -217,6 +217,22 @@ protected function buildPayload(array $mapped, $model): array
} }
} }
// Handle meta field - merge grouped meta into flat structure
if (!empty($mapped['meta']) && is_array($mapped['meta'])) {
$metaData = [];
foreach ($mapped['meta'] as $grp => $entries) {
if (!is_array($entries)) {
continue;
}
foreach ($entries as $k => $v) {
$metaData[$k] = $v;
}
}
if (!empty($metaData)) {
$payload['meta'] = $metaData;
}
}
return $payload; return $payload;
} }

View File

@ -14,7 +14,8 @@ public function getEntityClass(): string
} }
/** /**
* Override validate to skip validation if email is empty. * Override validate to skip validation if email is empty or invalid.
* Invalid emails should be skipped, not cause transaction rollback.
*/ */
public function validate(array $mapped): array public function validate(array $mapped): array
{ {
@ -23,6 +24,12 @@ public function validate(array $mapped): array
return ['valid' => true, 'errors' => []]; return ['valid' => true, 'errors' => []];
} }
// Validate email format - if invalid, mark as valid to skip instead of failing
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Return valid=true but we'll skip it in process()
return ['valid' => true, 'errors' => []];
}
return parent::validate($mapped); return parent::validate($mapped);
} }
@ -48,6 +55,14 @@ public function process(Import $import, array $mapped, array $raw, array $contex
]; ];
} }
// Skip if email format is invalid
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return [
'action' => 'skipped',
'message' => 'Invalid email format',
];
}
// Resolve person_id from context // Resolve person_id from context
$personId = $mapped['person_id'] ?? $context['person']['entity']?->id ?? null; $personId = $mapped['person_id'] ?? $context['person']['entity']?->id ?? null;

View File

@ -128,7 +128,20 @@ public function process(Import $import, array $mapped, array $raw, array $contex
$payload['type_id'] = $this->getDefaultPersonTypeId(); $payload['type_id'] = $this->getDefaultPersonTypeId();
} }
Log::debug('PersonHandler: Payload before fill', [
'payload' => $payload,
'has_group_id' => isset($payload['group_id']),
'group_id_value' => $payload['group_id'] ?? null,
]);
$person->fill($payload); $person->fill($payload);
Log::debug('PersonHandler: Person attributes after fill', [
'attributes' => $person->getAttributes(),
'has_group_id' => isset($person->group_id),
'group_id_value' => $person->group_id ?? null,
]);
$person->save(); $person->save();
Log::info('PersonHandler: Created new Person', [ Log::info('PersonHandler: Created new Person', [

View File

@ -8,6 +8,7 @@
use App\Models\ImportRow; use App\Models\ImportRow;
use App\Services\Import\Contracts\EntityHandlerInterface; use App\Services\Import\Contracts\EntityHandlerInterface;
use App\Services\Import\DateNormalizer; use App\Services\Import\DateNormalizer;
use App\Services\Import\DecimalNormalizer;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; 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}"); 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); $fullPath = Storage::disk($import->disk ?? 'local')->path($filePath);
$fh = fopen($fullPath, 'r'); $fh = fopen($fullPath, 'r');
@ -98,6 +102,81 @@ public function process(Import $import, ?Authenticatable $user = null): array
$isPg = DB::connection()->getDriverName() === 'pgsql'; $isPg = DB::connection()->getDriverName() === 'pgsql';
// 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) { while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
$rowNum++; $rowNum++;
$total++; $total++;
@ -183,6 +262,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
} }
fclose($fh); fclose($fh);
}
$this->finalizeImport($import, $user, $total, $imported, $skipped, $invalid); $this->finalizeImport($import, $user, $total, $imported, $skipped, $invalid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -272,6 +352,12 @@ protected function applyMappings(array $raw, $mappings): array
} }
foreach ($groupedMappings as $targetField => $fieldMappings) { 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 // Group by group number from options
$valuesByGroup = []; $valuesByGroup = [];
@ -324,6 +410,89 @@ protected function applyMappings(array $raw, $mappings): array
return $mapped; 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. * Apply transform to value.
*/ */
@ -457,6 +626,19 @@ protected function processRow(Import $import, array $mapped, array $raw, array $
$lastEntityType = $handler->getEntityClass(); $lastEntityType = $handler->getEntityClass();
$lastEntityId = $result['entity']?->id ?? null; $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) { } catch (\Throwable $e) {
$hasErrors = true; $hasErrors = true;
@ -714,8 +896,13 @@ protected function finalizeImport(
int $skipped, int $skipped,
int $invalid int $invalid
): void { ): 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([ $import->update([
'status' => 'completed', 'status' => $status,
'finished_at' => now(), 'finished_at' => now(),
'total_rows' => $total, 'total_rows' => $total,
'imported_rows' => $imported, 'imported_rows' => $imported,
@ -726,8 +913,8 @@ protected function finalizeImport(
ImportEvent::create([ ImportEvent::create([
'import_id' => $import->id, 'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(), 'user_id' => $user?->getAuthIdentifier(),
'event' => 'processing_completed', 'event' => $eventName,
'level' => 'info', 'level' => $eventLevel,
'message' => "Processed {$total} rows: {$imported} imported, {$skipped} skipped, {$invalid} invalid", 'message' => "Processed {$total} rows: {$imported} imported, {$skipped} skipped, {$invalid} invalid",
]); ]);
} }
@ -756,4 +943,77 @@ protected function handleFatalException(
'message' => $e->getMessage(), '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 : '')),
]);
}
}
} }

View File

@ -258,7 +258,17 @@ protected function simulateRow(Import $import, array $mapped, array $raw, array
// Resolve existing entity (uses EntityResolutionService internally) // Resolve existing entity (uses EntityResolutionService internally)
// Pass accumulated entityResults as context for chain resolution // Pass accumulated entityResults as context for chain resolution
try {
$existingEntity = $handler->resolve($entityData, array_merge($context, $entityResults)); $existingEntity = $handler->resolve($entityData, array_merge($context, $entityResults));
} catch (\Throwable $resolutionError) {
// In simulation mode, resolution may fail due to simulated entities
// Just treat as new entity
\Log::debug("ImportSimulation: Resolution failed (treating as new)", [
'entity' => $root,
'error' => $resolutionError->getMessage(),
]);
$existingEntity = null;
}
if ($existingEntity) { if ($existingEntity) {
// Would update existing // Would update existing
@ -284,9 +294,14 @@ protected function simulateRow(Import $import, array $mapped, array $raw, array
]; ];
// Simulate entity creation for context (no actual ID) // Simulate entity creation for context (no actual ID)
// Mark as simulated so resolution service knows not to use model methods
$simulatedEntity = (object) $entityData;
$simulatedEntity->_simulated = true;
$entityResults[$root] = [ $entityResults[$root] = [
'entity' => (object) $entityData, 'entity' => $simulatedEntity,
'action' => 'inserted', 'action' => 'inserted',
'_simulated' => true,
]; ];
} }
} }
@ -420,16 +435,27 @@ protected function groupMappedDataByEntity(array $mapped): array
// Handle array values // Handle array values
if (is_array($value)) { if (is_array($value)) {
// Special case: activity.note should be kept as array in single instance
if ($entity === 'activity' || $entity === 'activities') {
if (!isset($grouped[$entity])) {
$grouped[$entity] = [];
}
$grouped[$entity][$field] = $value; // Keep as array
} else {
// For other entities, only create multiple instances if:
// 1. Entity doesn't exist yet, OR
// 2. Entity has no other fields yet (is empty array)
if (!isset($grouped[$entity])) { if (!isset($grouped[$entity])) {
$grouped[$entity] = []; $grouped[$entity] = [];
} }
// Special case: activity.note should be kept as array in single instance // If entity already has string-keyed fields, just set the array as field value
if ($entity === 'activity' || $entity === 'activities') { // Otherwise, create separate instances
if (!isset($grouped[$entity][0])) { $hasStringKeys = !empty($grouped[$entity]) && isset(array_keys($grouped[$entity])[0]) && is_string(array_keys($grouped[$entity])[0]);
$grouped[$entity][0] = [];
} if ($hasStringKeys) {
$grouped[$entity][0][$field] = $value; // Keep as array // Entity has fields already - don't split, keep array as-is
$grouped[$entity][$field] = $value;
} else { } else {
// Create separate entity instances for each array value // Create separate entity instances for each array value
foreach ($value as $idx => $val) { foreach ($value as $idx => $val) {
@ -439,20 +465,24 @@ protected function groupMappedDataByEntity(array $mapped): array
$grouped[$entity][$idx][$field] = $val; $grouped[$entity][$idx][$field] = $val;
} }
} }
}
} else { } else {
// Single value // Single value
if (!isset($grouped[$entity])) { if (!isset($grouped[$entity])) {
$grouped[$entity] = []; $grouped[$entity] = [];
} }
// If entity is already an array (from previous grouped field), add to all instances // Check if entity is already an array of instances (from previous grouped field)
if (isset($grouped[$entity][0]) && is_array($grouped[$entity][0])) { if (!empty($grouped[$entity]) && is_int(array_key_first($grouped[$entity]))) {
// Entity has multiple instances - add field to all instances
foreach ($grouped[$entity] as &$instance) { foreach ($grouped[$entity] as &$instance) {
if (is_array($instance)) {
$instance[$field] = $value; $instance[$field] = $value;
} }
}
unset($instance); unset($instance);
} else { } else {
// Simple associative array // Simple associative array - add field
$grouped[$entity][$field] = $value; $grouped[$entity][$field] = $value;
} }
} }
@ -634,7 +664,7 @@ protected function rowIsEffectivelyEmpty(array $assoc): bool
* *
* Updated to match ImportServiceV2: * Updated to match ImportServiceV2:
* - Supports group option for concatenating multiple sources * - Supports group option for concatenating multiple sources
* - Uses setNestedValue for proper array handling * - Returns flat array with "entity.field" keys (no nesting)
*/ */
protected function applyMappings(array $raw, array $mappings): array protected function applyMappings(array $raw, array $mappings): array
{ {
@ -685,16 +715,32 @@ protected function applyMappings(array $raw, array $mappings): array
} }
} }
// Now set the values (same logic as ImportServiceV2) // Now set the values - KEEP FLAT, DON'T NEST
foreach ($valuesByGroup as $values) { foreach ($valuesByGroup as $values) {
if (count($values) === 1) { if (count($values) === 1) {
// Single value - set directly // Single value - add to array if key exists, otherwise set directly
$this->setNestedValue($mapped, $targetField, $values[0]); if (isset($mapped[$targetField])) {
// Convert to array and append
if (!is_array($mapped[$targetField])) {
$mapped[$targetField] = [$mapped[$targetField]];
}
$mapped[$targetField][] = $values[0];
} else {
$mapped[$targetField] = $values[0];
}
} else { } else {
// Multiple values in same group - concatenate with newline // Multiple values in same group - concatenate with newline
$concatenated = implode("\n", array_filter($values, fn($v) => !empty($v) && trim((string)$v) !== '')); $concatenated = implode("\n", array_filter($values, fn($v) => !empty($v) && trim((string)$v) !== ''));
if (!empty($concatenated)) { if (!empty($concatenated)) {
$this->setNestedValue($mapped, $targetField, $concatenated); if (isset($mapped[$targetField])) {
// Convert to array and append
if (!is_array($mapped[$targetField])) {
$mapped[$targetField] = [$mapped[$targetField]];
}
$mapped[$targetField][] = $concatenated;
} else {
$mapped[$targetField] = $concatenated;
}
} }
} }
} }

18
mark-import-failed.php Normal file
View File

@ -0,0 +1,18 @@
<?php
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$uuid = '68c7b8f8-fdf0-4575-9cbc-3ab2b3544d8f';
$import = \App\Models\Import::where('uuid', $uuid)->first();
if ($import) {
$import->status = 'failed';
$import->save();
echo "Import {$uuid} marked as failed\n";
echo "Current status: {$import->status}\n";
} else {
echo "Import not found\n";
}