resolutionService = new EntityResolutionService(); } public function getEntityClass(): string { return Person::class; } public function resolve(array $mapped, array $context = []): mixed { // PHASE 3: Use EntityResolutionService to check chain-based deduplication // This prevents creating duplicate Persons when Contract/ClientCase already exists $import = $context['import'] ?? null; if ($import) { $personId = $this->resolutionService->resolvePersonFromContext($import, $mapped, $context); if ($personId) { $person = Person::find($personId); if ($person) { Log::info('PersonHandler: Resolved existing Person via chain', [ 'person_id' => $personId, 'resolution_method' => 'EntityResolutionService', ]); return $person; } } } // Fall back to configured deduplication fields (tax_number, SSN) $dedupeBy = $this->getOption('deduplicate_by', ['tax_number', 'social_security_number']); foreach ($dedupeBy as $field) { if (! empty($mapped[$field])) { $person = Person::where($field, $mapped[$field])->first(); if ($person) { Log::info('PersonHandler: Resolved existing Person by identifier', [ 'person_id' => $person->id, 'field' => $field, ]); return $person; } } } return null; } public function process(Import $import, array $mapped, array $raw, array $context = []): array { // Add import to context for EntityResolutionService $context['import'] = $import; $existing = $this->resolve($mapped, $context); if ($existing) { // Update if configured $mode = $this->getOption('update_mode', 'update'); if ($mode === 'skip') { return [ 'action' => 'skipped', 'entity' => $existing, 'message' => 'Person already exists (skip mode)', ]; } $payload = $this->buildPayload($mapped, $existing); $appliedFields = $this->trackAppliedFields($existing, $payload); if (empty($appliedFields)) { return [ 'action' => 'skipped', 'entity' => $existing, 'message' => 'No changes detected', ]; } $existing->fill($payload); $existing->save(); return [ 'action' => 'updated', 'entity' => $existing, 'applied_fields' => $appliedFields, ]; } // Create new person Log::info('PersonHandler: Creating new Person (no existing entity found)', [ 'has_tax_number' => !empty($mapped['tax_number']), 'has_ssn' => !empty($mapped['social_security_number']), 'has_contract' => isset($context['contract']), 'has_client_case' => isset($context['client_case']), ]); $person = new Person; $payload = $this->buildPayload($mapped, $person); // Ensure required foreign keys have defaults if (!isset($payload['group_id'])) { $payload['group_id'] = $this->getDefaultPersonGroupId(); } if (!isset($payload['type_id'])) { $payload['type_id'] = $this->getDefaultPersonTypeId(); } $person->fill($payload); $person->save(); Log::info('PersonHandler: Created new Person', [ 'person_id' => $person->id, ]); return [ 'action' => 'inserted', 'entity' => $person, 'applied_fields' => array_keys($payload), ]; } protected function buildPayload(array $mapped, $model): array { $payload = []; $fieldMap = [ 'first_name' => 'first_name', 'last_name' => 'last_name', 'full_name' => 'full_name', 'gender' => 'gender', 'birthday' => 'birthday', 'tax_number' => 'tax_number', 'social_security_number' => 'social_security_number', 'description' => 'description', 'group_id' => 'group_id', 'type_id' => 'type_id', ]; foreach ($fieldMap as $source => $target) { if (array_key_exists($source, $mapped)) { $value = $mapped[$source]; // Normalize date fields if ($source === 'birthday' && $value) { $value = DateNormalizer::toDate((string) $value); } $payload[$target] = $value; } } return $payload; } private function getDefaultPersonGroupId(): int { return (int) (PersonGroup::min('id') ?? 1); } private function getDefaultPersonTypeId(): int { return (int) (PersonType::min('id') ?? 1); } }