395 lines
13 KiB
PHP
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;
|
|
}
|
|
}
|