Teren-app/app/Services/Import/EntityResolutionService.php
Simon Pocrnjič dea7432deb changes
2025-12-26 22:39:58 +01:00

395 lines
13 KiB
PHP

<?php
namespace App\Services\Import;
use App\Models\ClientCase;
use App\Models\Contract;
use App\Models\Email;
use App\Models\Import;
use App\Models\Person\Person;
use App\Models\Person\PersonAddress;
use App\Models\Person\PersonPhone;
use Illuminate\Support\Facades\Log;
/**
* EntityResolutionService - Resolves existing entities to prevent duplication.
*
* This service checks for existing entities before creating new ones,
* following the V1 deduplication hierarchy:
* 1. Contract reference → ClientCase → Person
* 2. ClientCase client_ref → Person
* 3. Contact values (email/phone/address) → Person
* 4. Person identifiers (tax_number/ssn) → Person
*/
class EntityResolutionService
{
/**
* Resolve Person ID from import context (existing entities).
* Returns Person ID if found, null otherwise.
*
* @param Import $import
* @param array $mapped Mapped data from CSV row
* @param array $context Processing context with previously processed entities
* @return int|null Person ID if found, null if should create new
*/
public function resolvePersonFromContext(Import $import, array $mapped, array $context): ?int
{
// 1. Check if Contract already processed in this row
if ($contract = $context['contract']['entity'] ?? null) {
$personId = $this->getPersonFromContract($contract);
if ($personId) {
Log::info('EntityResolutionService: Found Person from processed Contract', [
'person_id' => $personId,
'contract_id' => $contract->id,
]);
return $personId;
}
}
// 2. Check if ClientCase already processed in this row
if ($clientCase = $context['client_case']['entity'] ?? null) {
if ($clientCase->person_id) {
Log::info('EntityResolutionService: Found Person from processed ClientCase', [
'person_id' => $clientCase->person_id,
'client_case_id' => $clientCase->id,
]);
return $clientCase->person_id;
}
}
// 3. Check for existing Contract by reference (before it's processed)
if ($contractRef = $mapped['contract']['reference'] ?? null) {
$personId = $this->getPersonFromContractReference($import->client_id, $contractRef);
if ($personId) {
Log::info('EntityResolutionService: Found Person from existing Contract reference', [
'person_id' => $personId,
'contract_reference' => $contractRef,
]);
return $personId;
}
}
// 4. Check for existing ClientCase by client_ref (before it's processed)
if ($clientRef = $mapped['client_case']['client_ref'] ?? null) {
$personId = $this->getPersonFromClientRef($import->client_id, $clientRef);
if ($personId) {
Log::info('EntityResolutionService: Found Person from existing ClientCase client_ref', [
'person_id' => $personId,
'client_ref' => $clientRef,
]);
return $personId;
}
}
// 5. Check for existing Person by contact values (email/phone/address)
$personId = $this->resolvePersonByContacts($mapped);
if ($personId) {
Log::info('EntityResolutionService: Found Person from contact values', [
'person_id' => $personId,
]);
return $personId;
}
// No existing Person found
return null;
}
/**
* Check if ClientCase exists for this client_ref.
*
* @param int|null $clientId
* @param string $clientRef
* @return bool
*/
public function clientCaseExists(?int $clientId, string $clientRef): bool
{
if (!$clientId || !$clientRef) {
return false;
}
return ClientCase::where('client_id', $clientId)
->where('client_ref', $clientRef)
->exists();
}
/**
* Check if Contract exists for this reference.
*
* @param int|null $clientId
* @param string $reference
* @return bool
*/
public function contractExists(?int $clientId, string $reference): bool
{
if (!$clientId || !$reference) {
return false;
}
return Contract::query()
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
->where('client_cases.client_id', $clientId)
->where('contracts.reference', $reference)
->exists();
}
/**
* Get existing ClientCase by client_ref.
*
* @param int|null $clientId
* @param string $clientRef
* @return ClientCase|null
*/
public function getExistingClientCase(?int $clientId, string $clientRef): ?ClientCase
{
if (!$clientId || !$clientRef) {
return null;
}
return ClientCase::where('client_id', $clientId)
->where('client_ref', $clientRef)
->first();
}
/**
* Get existing Contract by reference for this client.
*
* @param int|null $clientId
* @param string $reference
* @return Contract|null
*/
public function getExistingContract(?int $clientId, string $reference): ?Contract
{
if (!$clientId || !$reference) {
return null;
}
return 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();
}
/**
* Get Person ID from a Contract entity.
*
* @param Contract $contract
* @return int|null
*/
protected function getPersonFromContract(Contract $contract): ?int
{
if ($contract->client_case_id) {
return ClientCase::where('id', $contract->client_case_id)
->value('person_id');
}
return null;
}
/**
* Get Person ID from existing Contract by reference.
*
* @param int|null $clientId
* @param string $reference
* @return int|null
*/
protected function getPersonFromContractReference(?int $clientId, string $reference): ?int
{
if (!$clientId) {
return null;
}
$clientCaseId = Contract::query()
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
->where('client_cases.client_id', $clientId)
->where('contracts.reference', $reference)
->value('contracts.client_case_id');
if ($clientCaseId) {
return ClientCase::where('id', $clientCaseId)
->value('person_id');
}
return null;
}
/**
* Get Person ID from existing ClientCase by client_ref.
*
* @param int|null $clientId
* @param string $clientRef
* @return int|null
*/
protected function getPersonFromClientRef(?int $clientId, string $clientRef): ?int
{
if (!$clientId) {
return null;
}
return ClientCase::where('client_id', $clientId)
->where('client_ref', $clientRef)
->value('person_id');
}
/**
* Resolve Person by contact values (email, phone, address).
* Checks existing contact records and returns associated Person ID.
*
* @param array $mapped
* @return int|null
*/
protected function resolvePersonByContacts(array $mapped): ?int
{
// Check email (support both single and array formats)
$email = $this->extractContactValue($mapped, 'email', 'value', 'emails');
if ($email) {
$personId = Email::where('value', trim($email))->value('person_id');
if ($personId) {
return $personId;
}
}
// Check phone (support both single and array formats)
$phone = $this->extractContactValue($mapped, 'phone', 'nu', 'person_phones');
if ($phone) {
$personId = PersonPhone::where('nu', trim($phone))->value('person_id');
if ($personId) {
return $personId;
}
}
// Check address (support both single and array formats)
$address = $this->extractContactValue($mapped, 'address', 'address', 'person_addresses');
if ($address) {
$personId = PersonAddress::where('address', trim($address))->value('person_id');
if ($personId) {
return $personId;
}
}
return null;
}
/**
* Extract contact value from mapped data, supporting multiple formats.
*
* @param array $mapped
* @param string $singularKey e.g., 'email', 'phone', 'address'
* @param string $field Field name within the contact data
* @param string $pluralKey e.g., 'emails', 'person_phones', 'person_addresses'
* @return string|null
*/
protected function extractContactValue(array $mapped, string $singularKey, string $field, string $pluralKey): ?string
{
// Try singular key first (e.g., 'email')
if (isset($mapped[$singularKey][$field])) {
return $mapped[$singularKey][$field];
}
// Try plural key (e.g., 'emails')
if (isset($mapped[$pluralKey])) {
// If it's an array of contacts
if (is_array($mapped[$pluralKey])) {
// Try first element if it's an indexed array
if (isset($mapped[$pluralKey][0][$field])) {
return $mapped[$pluralKey][0][$field];
}
// Try direct field access if it's a single hash
if (isset($mapped[$pluralKey][$field])) {
return $mapped[$pluralKey][$field];
}
}
}
return null;
}
/**
* Check if this row should skip Person creation based on existing entities.
* Used by PersonHandler to determine if Person already exists via chain.
*
* @param Import $import
* @param array $mapped
* @param array $context
* @return bool True if Person should be skipped (already exists)
*/
public function shouldSkipPersonCreation(Import $import, array $mapped, array $context): bool
{
// If we can resolve existing Person, we should skip creation
$personId = $this->resolvePersonFromContext($import, $mapped, $context);
return $personId !== null;
}
/**
* Get or create ClientCase for Contract creation.
* Reuses existing ClientCase if found by client_ref.
*
* @param Import $import
* @param array $mapped
* @param array $context
* @return int|null ClientCase ID
*/
public function resolveOrCreateClientCaseForContract(Import $import, array $mapped, array $context): ?int
{
$clientId = $import->client_id;
if (!$clientId) {
return null;
}
// If ClientCase already processed in this row, use it
if ($clientCaseId = $context['client_case']['entity']?->id ?? null) {
return $clientCaseId;
}
// Try to find by client_ref
$clientRef = $mapped['client_case']['client_ref'] ?? $mapped['client_ref'] ?? null;
if ($clientRef) {
$existing = $this->getExistingClientCase($clientId, $clientRef);
if ($existing) {
Log::info('EntityResolutionService: Reusing existing ClientCase for Contract', [
'client_case_id' => $existing->id,
'client_ref' => $clientRef,
]);
return $existing->id;
}
}
// Need to create new ClientCase
// Get Person from context (should be processed before Contract now)
$personId = $context['person']['entity']?->id ?? null;
if (!$personId) {
// Person wasn't in import or wasn't found, try to resolve
$personId = $this->resolvePersonFromContext($import, $mapped, $context);
if (!$personId) {
// Create minimal Person as last resort
$personId = Person::create(['type_id' => 1])->id;
Log::info('EntityResolutionService: Created minimal Person for new ClientCase', [
'person_id' => $personId,
]);
}
}
$clientCase = ClientCase::create([
'client_id' => $clientId,
'person_id' => $personId,
'client_ref' => $clientRef,
]);
Log::info('EntityResolutionService: Created new ClientCase', [
'client_case_id' => $clientCase->id,
'person_id' => $personId,
'client_ref' => $clientRef,
]);
return $clientCase->id;
}
}