source_type, ['csv', 'txt'])) { ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'event' => 'processing_skipped', 'level' => 'warning', 'message' => 'Only CSV/TXT supported in this pass.', ]); $import->update(['status' => 'completed', 'finished_at' => now()]); return ['ok' => true, 'status' => $import->status, 'counts' => compact('total', 'skipped', 'imported', 'invalid')]; } // Get mappings for this import (with apply_mode) $mappings = DB::table('import_mappings') ->where('import_id', $import->id) ->get(['source_column', 'target_field', 'transform', 'apply_mode', 'options']); $header = $import->meta['columns'] ?? null; $delimiter = $import->meta['detected_delimiter'] ?? ','; $hasHeader = (bool) ($import->meta['has_header'] ?? true); $path = Storage::disk($import->disk)->path($import->path); // Parse file and create import_rows with mapped_data $fh = @fopen($path, 'r'); if (! $fh) { ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'event' => 'processing_failed', 'level' => 'error', 'message' => 'Unable to open file for reading.', ]); $import->update(['status' => 'failed', 'failed_at' => now()]); return ['ok' => false, 'status' => $import->status]; } try { DB::beginTransaction(); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'event' => 'processing_started', 'level' => 'info', 'message' => 'Processing started.', ]); $rowNum = 0; if ($hasHeader) { $first = fgetcsv($fh, 0, $delimiter); $rowNum++; // use actual detected header if not already stored if (! $header) { $header = array_map(fn ($v) => trim((string) $v), $first ?: []); } } while (($row = fgetcsv($fh, 0, $delimiter)) !== false) { $rowNum++; $total++; $rawAssoc = $this->buildRowAssoc($row, $header); [$recordType, $mapped] = $this->applyMappings($rawAssoc, $mappings); $importRow = ImportRow::create([ 'import_id' => $import->id, 'row_number' => $rowNum, 'record_type' => $recordType, 'raw_data' => $rawAssoc, 'mapped_data' => $mapped, 'status' => 'valid', ]); // Contracts $contractResult = null; if (isset($mapped['contract'])) { $contractResult = $this->upsertContractChain($import, $mapped, $mappings); if ($contractResult['action'] === 'skipped') { $skipped++; $importRow->update(['status' => 'skipped']); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id, 'event' => 'row_skipped', 'level' => 'info', 'message' => $contractResult['message'] ?? 'Skipped contract (no changes).', ]); } elseif (in_array($contractResult['action'], ['inserted', 'updated'])) { $imported++; $importRow->update([ 'status' => 'imported', 'entity_type' => Contract::class, 'entity_id' => $contractResult['contract']->id, ]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id, 'event' => 'row_imported', 'level' => 'info', 'message' => ucfirst($contractResult['action']).' contract', 'context' => ['id' => $contractResult['contract']->id], ]); } else { $invalid++; $importRow->update(['status' => 'invalid', 'errors' => [$contractResult['message'] ?? 'Contract processing failed']]); } } // Accounts $accountResult = null; if (isset($mapped['account'])) { $accountResult = $this->upsertAccount($import, $mapped, $mappings); if ($accountResult['action'] === 'skipped') { $skipped++; $importRow->update(['status' => 'skipped']); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id, 'event' => 'row_skipped', 'level' => 'info', 'message' => $accountResult['message'] ?? 'Skipped (no changes).', ]); } elseif ($accountResult['action'] === 'inserted' || $accountResult['action'] === 'updated') { $imported++; $importRow->update([ 'status' => 'imported', 'entity_type' => Account::class, 'entity_id' => $accountResult['account']->id, ]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id, 'event' => 'row_imported', 'level' => 'info', 'message' => ucfirst($accountResult['action']).' account', 'context' => ['id' => $accountResult['account']->id], ]); } else { $invalid++; $importRow->update(['status' => 'invalid', 'errors' => ['Unhandled result']]); } } // Contacts: resolve person strictly via Contract -> ClientCase -> Person, contacts, or identifiers $personIdForRow = null; // Prefer person from contract created/updated above if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) { $ccId = $contractResult['contract']->client_case_id; $personIdForRow = ClientCase::where('id', $ccId)->value('person_id'); } // If we have a contract reference, resolve existing contract for this client and derive person if (! $personIdForRow && $import->client_id && ! empty($mapped['contract']['reference'] ?? null)) { $existingContract = Contract::query() ->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id') ->where('client_cases.client_id', $import->client_id) ->where('contracts.reference', $mapped['contract']['reference']) ->select('contracts.client_case_id') ->first(); if ($existingContract) { $personIdForRow = ClientCase::where('id', $existingContract->client_case_id)->value('person_id'); } } // If account processing created/resolved a contract, derive person via its client_case if (! $personIdForRow && $accountResult) { if (isset($accountResult['contract']) && $accountResult['contract'] instanceof Contract) { $ccId = $accountResult['contract']->client_case_id; $personIdForRow = ClientCase::where('id', $ccId)->value('person_id'); } elseif (isset($accountResult['contract_id'])) { $ccId = Contract::where('id', $accountResult['contract_id'])->value('client_case_id'); if ($ccId) { $personIdForRow = ClientCase::where('id', $ccId)->value('person_id'); } } } // Resolve by contact values next if (! $personIdForRow) { $emailVal = trim((string) ($mapped['email']['value'] ?? '')); $phoneNu = trim((string) ($mapped['phone']['nu'] ?? '')); $addrLine = trim((string) ($mapped['address']['address'] ?? '')); // Try to resolve by existing contacts first if ($emailVal !== '') { $personIdForRow = Email::where('value', $emailVal)->value('person_id'); } if (! $personIdForRow && $phoneNu !== '') { $personIdForRow = PersonPhone::where('nu', $phoneNu)->value('person_id'); } if (! $personIdForRow && $addrLine !== '') { $personIdForRow = PersonAddress::where('address', $addrLine)->value('person_id'); } // If still no person but we have any contact value, auto-create a minimal person if (! $personIdForRow && ($emailVal !== '' || $phoneNu !== '' || $addrLine !== '')) { $personIdForRow = $this->createMinimalPersonId(); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id ?? null, 'event' => 'person_autocreated_for_contacts', 'level' => 'info', 'message' => 'Created minimal person to attach contact data (email/phone/address).', 'context' => [ 'email' => $emailVal ?: null, 'phone' => $phoneNu ?: null, 'address' => $addrLine ?: null, 'person_id' => $personIdForRow, ], ]); } } // Try identifiers from mapped person (no creation yet) if (! $personIdForRow && ! empty($mapped['person'] ?? [])) { $personIdForRow = $this->findPersonIdByIdentifiers($mapped['person']); } // Finally, if still unknown and person fields provided, create if (! $personIdForRow && ! empty($mapped['person'] ?? [])) { $personIdForRow = $this->findOrCreatePersonId($mapped['person']); } // At this point, personIdForRow is either resolved or remains null (no contacts/person data) $contactChanged = false; if ($personIdForRow) { if (! empty($mapped['email'] ?? [])) { $r = $this->upsertEmail($personIdForRow, $mapped['email'], $mappings); if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) { $contactChanged = true; } } if (! empty($mapped['address'] ?? [])) { $r = $this->upsertAddress($personIdForRow, $mapped['address'], $mappings); if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) { $contactChanged = true; } } if (! empty($mapped['phone'] ?? [])) { $r = $this->upsertPhone($personIdForRow, $mapped['phone'], $mappings); if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) { $contactChanged = true; } } } if (! isset($mapped['contract']) && ! isset($mapped['account'])) { if ($contactChanged) { $imported++; $importRow->update([ 'status' => 'imported', 'entity_type' => Person::class, 'entity_id' => $personIdForRow, ]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'import_row_id' => $importRow->id, 'event' => 'row_imported', 'level' => 'info', 'message' => 'Contacts upserted', 'context' => ['person_id' => $personIdForRow], ]); } else { $skipped++; $importRow->update(['status' => 'skipped']); } } } fclose($fh); $import->update([ 'status' => 'completed', 'finished_at' => now(), 'total_rows' => $total, 'imported_rows' => $imported, 'invalid_rows' => $invalid, 'valid_rows' => $total - $invalid, ]); DB::commit(); return [ 'ok' => true, 'status' => $import->status, 'counts' => compact('total', 'skipped', 'imported', 'invalid'), ]; } catch (\Throwable $e) { if (is_resource($fh)) { @fclose($fh); } DB::rollBack(); // Mark failed and log after rollback (so no partial writes persist) $import->refresh(); $import->update(['status' => 'failed', 'failed_at' => now()]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), 'event' => 'processing_failed', 'level' => 'error', 'message' => $e->getMessage(), ]); return ['ok' => false, 'status' => 'failed', 'error' => $e->getMessage()]; } } private function buildRowAssoc(array $row, ?array $header): array { if (! $header) { // positional mapping: 0..N-1 $assoc = []; foreach ($row as $i => $v) { $assoc[(string) $i] = $v; } return $assoc; } $assoc = []; foreach ($header as $i => $name) { $assoc[$name] = $row[$i] ?? null; } return $assoc; } private function applyMappings(array $raw, $mappings): array { $recordType = null; $mapped = []; foreach ($mappings as $map) { $src = $map->source_column; $target = $map->target_field; if (! $target) { continue; } $value = $raw[$src] ?? null; // very basic transforms if ($map->transform === 'trim') { $value = is_string($value) ? trim($value) : $value; } if ($map->transform === 'upper') { $value = is_string($value) ? strtoupper($value) : $value; } // detect record type from first segment, e.g., "account.balance_amount" $parts = explode('.', $target); if (! $recordType && isset($parts[0])) { $recordType = $parts[0]; } // build nested array by dot notation $this->arraySetDot($mapped, $target, $value); } return [$recordType, $mapped]; } private function arraySetDot(array &$arr, string $path, $value): void { $keys = explode('.', $path); $ref = &$arr; foreach ($keys as $k) { if (! isset($ref[$k]) || ! is_array($ref[$k])) { $ref[$k] = []; } $ref = &$ref[$k]; } $ref = $value; } private function upsertAccount(Import $import, array $mapped, $mappings): array { $clientId = $import->client_id; // may be null, used for contract lookup/creation $acc = $mapped['account'] ?? []; $contractId = $acc['contract_id'] ?? null; $reference = $acc['reference'] ?? null; // If contract_id not provided, attempt to resolve by contract reference for the selected client if (! $contractId) { $contractRef = $acc['contract_reference'] ?? ($mapped['contract']['reference'] ?? null); if ($clientId && $contractRef) { // 1) Search existing contract by reference for that client (across its client cases) $existingContract = Contract::query() ->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id') ->where('client_cases.client_id', $clientId) ->where('contracts.reference', $contractRef) ->select('contracts.*') ->first(); if ($existingContract) { $contractId = $existingContract->id; } else { // 2) Not found: attempt to resolve debtor via identifiers or provided person, then create case+contract // Try strong identifiers first $personId = $this->findPersonIdByIdentifiers($mapped['person'] ?? []); // Create from provided person data if unresolved if (! $personId) { $personId = $this->findOrCreatePersonId($mapped['person'] ?? []); } // Last resort, create minimal if (! $personId) { $personId = $this->createMinimalPersonId(); } // Use the selected client for this import to tie the case/contract if (! $clientId) { return ['action' => 'skipped', 'message' => 'Client required to create contract']; } $resolvedClientId = $clientId; $clientCaseId = $this->findOrCreateClientCaseId($resolvedClientId, $personId); // Build minimal/new contract $contractFields = $mapped['contract'] ?? []; $newContractData = [ 'client_case_id' => $clientCaseId, 'reference' => $contractRef, ]; foreach (['start_date', 'end_date', 'description', 'type_id'] as $k) { if (array_key_exists($k, $contractFields) && ! is_null($contractFields[$k])) { $newContractData[$k] = $contractFields[$k]; } } // ensure required fields on contracts $newContractData['start_date'] = $newContractData['start_date'] ?? now()->toDateString(); $newContractData['type_id'] = $newContractData['type_id'] ?? $this->getDefaultContractTypeId(); $createdContract = Contract::create($newContractData); $contractId = $createdContract->id; } if ($contractId) { $acc['contract_id'] = $contractId; $mapped['account'] = $acc; } } } // Default account.reference to contract reference if missing if (! $reference) { $contractRef = $acc['contract_reference'] ?? ($mapped['contract']['reference'] ?? null); if ($contractRef) { $reference = $contractRef; $acc['reference'] = $reference; $mapped['account'] = $acc; } } if (! $contractId || ! $reference) { return ['action' => 'skipped', 'message' => 'Missing contract_id/reference']; } $existing = Account::query() ->where('contract_id', $contractId) ->where('reference', $reference) ->first(); // Build applyable data based on apply_mode $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { if (! $map->target_field) { continue; } $parts = explode('.', $map->target_field); if ($parts[0] !== 'account') { continue; } $field = $parts[1] ?? null; if (! $field) { continue; } $value = $acc[$field] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { $applyInsert[$field] = $value; } if (in_array($mode, ['update', 'both'])) { $applyUpdate[$field] = $value; } } if ($existing) { if (empty($applyUpdate)) { return ['action' => 'skipped', 'message' => 'No fields marked for update']; } // Only update fields that are set; skip nulls to avoid wiping unintentionally $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); if (empty($changes)) { return ['action' => 'skipped', 'message' => 'No non-null changes']; } $existing->fill($changes); $existing->save(); // also include contract hints for downstream contact resolution return ['action' => 'updated', 'account' => $existing, 'contract_id' => $contractId]; } else { if (empty($applyInsert)) { return ['action' => 'skipped', 'message' => 'No fields marked for insert']; } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['contract_id'] = $contractId; $data['reference'] = $reference; // ensure required defaults $data['type_id'] = $data['type_id'] ?? $this->getDefaultAccountTypeId(); if (! array_key_exists('active', $data)) { $data['active'] = 1; } $created = Account::create($data); return ['action' => 'inserted', 'account' => $created, 'contract_id' => $contractId]; } } private function findPersonIdByIdentifiers(array $p): ?int { $tax = $p['tax_number'] ?? null; if ($tax) { $found = Person::where('tax_number', $tax)->first(); if ($found) { return $found->id; } } $ssn = $p['social_security_number'] ?? null; if ($ssn) { $found = Person::where('social_security_number', $ssn)->first(); if ($found) { return $found->id; } } return null; } private function upsertContractChain(Import $import, array $mapped, $mappings): array { $contractData = $mapped['contract'] ?? []; $reference = $contractData['reference'] ?? null; if (! $reference) { return ['action' => 'invalid', 'message' => 'Missing contract.reference']; } // Determine client_case_id: prefer provided, else derive via person/client $clientCaseId = $contractData['client_case_id'] ?? null; $clientId = $import->client_id; // may be null // Try to find existing contract EARLY by (client_id, reference) across all cases to prevent duplicates $existing = null; if ($clientId) { $existing = Contract::query() ->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id') ->where('client_cases.client_id', $clientId) ->where('contracts.reference', $reference) ->select('contracts.*') ->first(); } // If not found by client+reference and a specific client_case_id is provided, try that too if (! $existing && $clientCaseId) { $existing = Contract::query() ->where('client_case_id', $clientCaseId) ->where('reference', $reference) ->first(); } // If we still need to insert, we must resolve clientCaseId, but avoid creating new person/case unless necessary if (! $existing && ! $clientCaseId) { // Resolve by identifiers or provided person; do not use Client->person $personId = null; if (! empty($mapped['person'] ?? [])) { $personId = $this->findPersonIdByIdentifiers($mapped['person']); if (! $personId) { $personId = $this->findOrCreatePersonId($mapped['person']); } } // As a last resort, create a minimal person for this client if ($clientId && ! $personId) { $personId = $this->createMinimalPersonId(); } if ($clientId && $personId) { $clientCaseId = $this->findOrCreateClientCaseId($clientId, $personId); } elseif ($personId) { // require an import client to attach case/contract return ['action' => 'invalid', 'message' => 'Import must be linked to a client to create a case']; } else { return ['action' => 'invalid', 'message' => 'Unable to resolve client_case (need import client)']; } } // Build applyable data based on apply_mode $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { if (! $map->target_field) { continue; } $parts = explode('.', $map->target_field); if ($parts[0] !== 'contract') { continue; } $field = $parts[1] ?? null; if (! $field) { continue; } $value = $contractData[$field] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { $applyInsert[$field] = $value; } if (in_array($mode, ['update', 'both'])) { $applyUpdate[$field] = $value; } } if ($existing) { if (empty($applyUpdate)) { return ['action' => 'skipped', 'message' => 'No contract fields marked for update']; } $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); if (empty($changes)) { return ['action' => 'skipped', 'message' => 'No non-null contract changes']; } $existing->fill($changes); $existing->save(); return ['action' => 'updated', 'contract' => $existing]; } else { if (empty($applyInsert)) { return ['action' => 'skipped', 'message' => 'No contract fields marked for insert']; } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['client_case_id'] = $clientCaseId; $data['reference'] = $reference; // ensure required defaults $data['start_date'] = $data['start_date'] ?? now()->toDateString(); $data['type_id'] = $data['type_id'] ?? $this->getDefaultContractTypeId(); $created = Contract::create($data); return ['action' => 'inserted', 'contract' => $created]; } } private function findOrCreatePersonId(array $p): ?int { // Basic dedup: by tax_number, ssn, else full_name $query = Person::query(); if (! empty($p['tax_number'] ?? null)) { $found = $query->where('tax_number', $p['tax_number'])->first(); if ($found) { return $found->id; } } if (! empty($p['social_security_number'] ?? null)) { $found = Person::where('social_security_number', $p['social_security_number'])->first(); if ($found) { return $found->id; } } // Do NOT use full_name as an identifier // Create person if any fields present; ensure required foreign keys if (! empty($p)) { $data = []; foreach (['first_name', 'last_name', 'full_name', 'tax_number', 'social_security_number', 'birthday', 'gender', 'description', 'group_id', 'type_id'] as $k) { if (array_key_exists($k, $p)) { $data[$k] = $p[$k]; } } // derive full_name if missing if (empty($data['full_name'])) { $fn = trim((string) ($data['first_name'] ?? '')); $ln = trim((string) ($data['last_name'] ?? '')); if ($fn || $ln) { $data['full_name'] = trim($fn.' '.$ln); } } // ensure required group/type ids $data['group_id'] = $data['group_id'] ?? $this->getDefaultPersonGroupId(); $data['type_id'] = $data['type_id'] ?? $this->getDefaultPersonTypeId(); $created = Person::create($data); return $created->id; } return null; } private function createMinimalPersonId(): int { return Person::create([ 'group_id' => $this->getDefaultPersonGroupId(), 'type_id' => $this->getDefaultPersonTypeId(), // names and identifiers can be null; 'nu' will be auto-generated (unique 6-char) ])->id; } private function getDefaultPersonGroupId(): int { return (int) (PersonGroup::min('id') ?? 1); } private function getDefaultPersonTypeId(): int { return (int) (PersonType::min('id') ?? 1); } private function getDefaultContractTypeId(): int { return (int) (ContractType::min('id') ?? 1); } private function getDefaultAccountTypeId(): int { return (int) (AccountType::min('id') ?? 1); } private function getDefaultAddressTypeId(): int { return (int) (AddressType::min('id') ?? 1); } private function getDefaultPhoneTypeId(): int { return (int) (PhoneType::min('id') ?? 1); } // Removed getExistingPersonIdForClient: resolution should come from Contract -> ClientCase -> Person or identifiers private function findOrCreateClientId(int $personId): int { $client = Client::where('person_id', $personId)->first(); if ($client) { return $client->id; } return Client::create(['person_id' => $personId])->id; } private function findOrCreateClientCaseId(int $clientId, int $personId): int { $cc = ClientCase::where('client_id', $clientId)->where('person_id', $personId)->first(); if ($cc) { return $cc->id; } return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId])->id; } private function upsertEmail(int $personId, array $emailData, $mappings): array { $value = trim((string) ($emailData['value'] ?? '')); if ($value === '') { return ['action' => 'skipped', 'message' => 'No email value']; } $existing = Email::where('person_id', $personId)->where('value', $value)->first(); $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { if (! $map->target_field) { continue; } $parts = explode('.', $map->target_field); if ($parts[0] !== 'email') { continue; } $field = $parts[1] ?? null; if (! $field) { continue; } $val = $emailData[$field] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { $applyInsert[$field] = $val; } if (in_array($mode, ['update', 'both'])) { $applyUpdate[$field] = $val; } } if ($existing) { $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); if (empty($changes)) { return ['action' => 'skipped', 'message' => 'No email updates']; } $existing->fill($changes); $existing->save(); return ['action' => 'updated', 'email' => $existing]; } else { if (empty($applyInsert)) { return ['action' => 'skipped', 'message' => 'No email fields for insert']; } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['person_id'] = $personId; if (! array_key_exists('is_active', $data)) { $data['is_active'] = true; } $created = Email::create($data); return ['action' => 'inserted', 'email' => $created]; } } private function upsertAddress(int $personId, array $addrData, $mappings): array { $addressLine = trim((string) ($addrData['address'] ?? '')); if ($addressLine === '') { return ['action' => 'skipped', 'message' => 'No address value']; } // Default country SLO if not provided if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') { $addrData['country'] = 'SLO'; } $existing = PersonAddress::where('person_id', $personId)->where('address', $addressLine)->first(); $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { if (! $map->target_field) { continue; } $parts = explode('.', $map->target_field); if ($parts[0] !== 'address') { continue; } $field = $parts[1] ?? null; if (! $field) { continue; } $val = $addrData[$field] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { $applyInsert[$field] = $val; } if (in_array($mode, ['update', 'both'])) { $applyUpdate[$field] = $val; } } if ($existing) { $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); if (empty($changes)) { return ['action' => 'skipped', 'message' => 'No address updates']; } $existing->fill($changes); $existing->save(); return ['action' => 'updated', 'address' => $existing]; } else { if (empty($applyInsert)) { return ['action' => 'skipped', 'message' => 'No address fields for insert']; } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['person_id'] = $personId; $data['country'] = $data['country'] ?? 'SLO'; $data['type_id'] = $data['type_id'] ?? $this->getDefaultAddressTypeId(); $created = PersonAddress::create($data); return ['action' => 'inserted', 'address' => $created]; } } private function upsertPhone(int $personId, array $phoneData, $mappings): array { $nu = trim((string) ($phoneData['nu'] ?? '')); if ($nu === '') { return ['action' => 'skipped', 'message' => 'No phone value']; } $existing = PersonPhone::where('person_id', $personId)->where('nu', $nu)->first(); $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { if (! $map->target_field) { continue; } $parts = explode('.', $map->target_field); if ($parts[0] !== 'phone') { continue; } $field = $parts[1] ?? null; if (! $field) { continue; } $val = $phoneData[$field] ?? null; $mode = $map->apply_mode ?? 'both'; if (in_array($mode, ['insert', 'both'])) { $applyInsert[$field] = $val; } if (in_array($mode, ['update', 'both'])) { $applyUpdate[$field] = $val; } } if ($existing) { $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); if (empty($changes)) { return ['action' => 'skipped', 'message' => 'No phone updates']; } $existing->fill($changes); $existing->save(); return ['action' => 'updated', 'phone' => $existing]; } else { if (empty($applyInsert)) { return ['action' => 'skipped', 'message' => 'No phone fields for insert']; } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['person_id'] = $personId; $data['type_id'] = $data['type_id'] ?? $this->getDefaultPhoneTypeId(); $created = PersonPhone::create($data); return ['action' => 'inserted', 'phone' => $created]; } } }