changes 0328092025
This commit is contained in:
+287
-135
@@ -2,23 +2,23 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Account;
|
||||
use App\Models\AccountType;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\ContractType;
|
||||
use App\Models\Email;
|
||||
use App\Models\Import;
|
||||
use App\Models\ImportEvent;
|
||||
use App\Models\ImportRow;
|
||||
use App\Models\Account;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Email;
|
||||
use App\Models\Person\Person;
|
||||
use App\Models\Person\PersonGroup;
|
||||
use App\Models\Person\PersonType;
|
||||
use App\Models\Person\PersonAddress;
|
||||
use App\Models\Person\PersonPhone;
|
||||
use App\Models\Person\AddressType;
|
||||
use App\Models\Person\Person;
|
||||
use App\Models\Person\PersonAddress;
|
||||
use App\Models\Person\PersonGroup;
|
||||
use App\Models\Person\PersonPhone;
|
||||
use App\Models\Person\PersonType;
|
||||
use App\Models\Person\PhoneType;
|
||||
use App\Models\ContractType;
|
||||
use App\Models\AccountType;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -32,11 +32,14 @@ class ImportProcessor
|
||||
public function process(Import $import, ?Authenticatable $user = null): array
|
||||
{
|
||||
$started = now();
|
||||
$total = 0; $skipped = 0; $imported = 0; $invalid = 0;
|
||||
$total = 0;
|
||||
$skipped = 0;
|
||||
$imported = 0;
|
||||
$invalid = 0;
|
||||
$fh = null;
|
||||
|
||||
// Only CSV/TSV supported in this pass
|
||||
if (!in_array($import->source_type, ['csv','txt'])) {
|
||||
if (! in_array($import->source_type, ['csv', 'txt'])) {
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
@@ -45,13 +48,14 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'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') ];
|
||||
|
||||
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']);
|
||||
->get(['source_column', 'target_field', 'transform', 'apply_mode', 'options']);
|
||||
|
||||
$header = $import->meta['columns'] ?? null;
|
||||
$delimiter = $import->meta['detected_delimiter'] ?? ',';
|
||||
@@ -60,7 +64,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
|
||||
// Parse file and create import_rows with mapped_data
|
||||
$fh = @fopen($path, 'r');
|
||||
if (!$fh) {
|
||||
if (! $fh) {
|
||||
ImportEvent::create([
|
||||
'import_id' => $import->id,
|
||||
'user_id' => $user?->getAuthIdentifier(),
|
||||
@@ -69,7 +73,8 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'message' => 'Unable to open file for reading.',
|
||||
]);
|
||||
$import->update(['status' => 'failed', 'failed_at' => now()]);
|
||||
return [ 'ok' => false, 'status' => $import->status ];
|
||||
|
||||
return ['ok' => false, 'status' => $import->status];
|
||||
}
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
@@ -87,8 +92,8 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
$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 ?: []);
|
||||
if (! $header) {
|
||||
$header = array_map(fn ($v) => trim((string) $v), $first ?: []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +128,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'level' => 'info',
|
||||
'message' => $contractResult['message'] ?? 'Skipped contract (no changes).',
|
||||
]);
|
||||
} elseif (in_array($contractResult['action'], ['inserted','updated'])) {
|
||||
} elseif (in_array($contractResult['action'], ['inserted', 'updated'])) {
|
||||
$imported++;
|
||||
$importRow->update([
|
||||
'status' => 'imported',
|
||||
@@ -137,7 +142,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'event' => 'row_imported',
|
||||
'level' => 'info',
|
||||
'message' => ucfirst($contractResult['action']).' contract',
|
||||
'context' => [ 'id' => $contractResult['contract']->id ],
|
||||
'context' => ['id' => $contractResult['contract']->id],
|
||||
]);
|
||||
} else {
|
||||
$invalid++;
|
||||
@@ -174,7 +179,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'event' => 'row_imported',
|
||||
'level' => 'info',
|
||||
'message' => ucfirst($accountResult['action']).' account',
|
||||
'context' => [ 'id' => $accountResult['account']->id ],
|
||||
'context' => ['id' => $accountResult['account']->id],
|
||||
]);
|
||||
} else {
|
||||
$invalid++;
|
||||
@@ -190,7 +195,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
$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)) {
|
||||
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)
|
||||
@@ -202,7 +207,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
}
|
||||
}
|
||||
// If account processing created/resolved a contract, derive person via its client_case
|
||||
if (!$personIdForRow && $accountResult) {
|
||||
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');
|
||||
@@ -214,50 +219,75 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
}
|
||||
}
|
||||
// Resolve by contact values next
|
||||
if (!$personIdForRow) {
|
||||
$emailVal = trim((string)($mapped['email']['value'] ?? ''));
|
||||
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 = trim((string)($mapped['phone']['nu'] ?? ''));
|
||||
if ($phoneNu !== '') {
|
||||
if (! $personIdForRow && $phoneNu !== '') {
|
||||
$personIdForRow = PersonPhone::where('nu', $phoneNu)->value('person_id');
|
||||
}
|
||||
}
|
||||
if (!$personIdForRow) {
|
||||
$addrLine = trim((string)($mapped['address']['address'] ?? ''));
|
||||
if ($addrLine !== '') {
|
||||
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'] ?? [])) {
|
||||
if (! $personIdForRow && ! empty($mapped['person'] ?? [])) {
|
||||
$personIdForRow = $this->findPersonIdByIdentifiers($mapped['person']);
|
||||
}
|
||||
// Finally, if still unknown and person fields provided, create
|
||||
if (!$personIdForRow && !empty($mapped['person'] ?? [])) {
|
||||
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'] ?? [])) {
|
||||
if (! empty($mapped['email'] ?? [])) {
|
||||
$r = $this->upsertEmail($personIdForRow, $mapped['email'], $mappings);
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted','updated'])) { $contactChanged = true; }
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) {
|
||||
$contactChanged = true;
|
||||
}
|
||||
}
|
||||
if (!empty($mapped['address'] ?? [])) {
|
||||
if (! empty($mapped['address'] ?? [])) {
|
||||
$r = $this->upsertAddress($personIdForRow, $mapped['address'], $mappings);
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted','updated'])) { $contactChanged = true; }
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) {
|
||||
$contactChanged = true;
|
||||
}
|
||||
}
|
||||
if (!empty($mapped['phone'] ?? [])) {
|
||||
if (! empty($mapped['phone'] ?? [])) {
|
||||
$r = $this->upsertPhone($personIdForRow, $mapped['phone'], $mappings);
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted','updated'])) { $contactChanged = true; }
|
||||
if (in_array($r['action'] ?? 'skipped', ['inserted', 'updated'])) {
|
||||
$contactChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($mapped['contract']) && !isset($mapped['account'])) {
|
||||
if (! isset($mapped['contract']) && ! isset($mapped['account'])) {
|
||||
if ($contactChanged) {
|
||||
$imported++;
|
||||
$importRow->update([
|
||||
@@ -272,7 +302,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'event' => 'row_imported',
|
||||
'level' => 'info',
|
||||
'message' => 'Contacts upserted',
|
||||
'context' => [ 'person_id' => $personIdForRow ],
|
||||
'context' => ['person_id' => $personIdForRow],
|
||||
]);
|
||||
} else {
|
||||
$skipped++;
|
||||
@@ -297,10 +327,12 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => $import->status,
|
||||
'counts' => compact('total','skipped','imported','invalid'),
|
||||
'counts' => compact('total', 'skipped', 'imported', 'invalid'),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
if (is_resource($fh)) { @fclose($fh); }
|
||||
if (is_resource($fh)) {
|
||||
@fclose($fh);
|
||||
}
|
||||
DB::rollBack();
|
||||
// Mark failed and log after rollback (so no partial writes persist)
|
||||
$import->refresh();
|
||||
@@ -312,22 +344,27 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||
'level' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return [ 'ok' => false, 'status' => 'failed', 'error' => $e->getMessage() ];
|
||||
|
||||
return ['ok' => false, 'status' => 'failed', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function buildRowAssoc(array $row, ?array $header): array
|
||||
{
|
||||
if (!$header) {
|
||||
if (! $header) {
|
||||
// positional mapping: 0..N-1
|
||||
$assoc = [];
|
||||
foreach ($row as $i => $v) { $assoc[(string)$i] = $v; }
|
||||
foreach ($row as $i => $v) {
|
||||
$assoc[(string) $i] = $v;
|
||||
}
|
||||
|
||||
return $assoc;
|
||||
}
|
||||
$assoc = [];
|
||||
foreach ($header as $i => $name) {
|
||||
$assoc[$name] = $row[$i] ?? null;
|
||||
}
|
||||
|
||||
return $assoc;
|
||||
}
|
||||
|
||||
@@ -338,31 +375,40 @@ private function applyMappings(array $raw, $mappings): array
|
||||
foreach ($mappings as $map) {
|
||||
$src = $map->source_column;
|
||||
$target = $map->target_field;
|
||||
if (!$target) continue;
|
||||
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; }
|
||||
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])) {
|
||||
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;
|
||||
$ref = &$arr;
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($ref[$k]) || !is_array($ref[$k])) { $ref[$k] = []; }
|
||||
$ref =& $ref[$k];
|
||||
if (! isset($ref[$k]) || ! is_array($ref[$k])) {
|
||||
$ref[$k] = [];
|
||||
}
|
||||
$ref = &$ref[$k];
|
||||
}
|
||||
$ref = $value;
|
||||
}
|
||||
@@ -374,7 +420,7 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
$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) {
|
||||
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)
|
||||
@@ -391,15 +437,15 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
// Try strong identifiers first
|
||||
$personId = $this->findPersonIdByIdentifiers($mapped['person'] ?? []);
|
||||
// Create from provided person data if unresolved
|
||||
if (!$personId) {
|
||||
if (! $personId) {
|
||||
$personId = $this->findOrCreatePersonId($mapped['person'] ?? []);
|
||||
}
|
||||
// Last resort, create minimal
|
||||
if (!$personId) {
|
||||
if (! $personId) {
|
||||
$personId = $this->createMinimalPersonId();
|
||||
}
|
||||
// Use the selected client for this import to tie the case/contract
|
||||
if (!$clientId) {
|
||||
if (! $clientId) {
|
||||
return ['action' => 'skipped', 'message' => 'Client required to create contract'];
|
||||
}
|
||||
$resolvedClientId = $clientId;
|
||||
@@ -410,8 +456,8 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
'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])) {
|
||||
foreach (['start_date', 'end_date', 'description', 'type_id'] as $k) {
|
||||
if (array_key_exists($k, $contractFields) && ! is_null($contractFields[$k])) {
|
||||
$newContractData[$k] = $contractFields[$k];
|
||||
}
|
||||
}
|
||||
@@ -428,7 +474,7 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
}
|
||||
}
|
||||
// Default account.reference to contract reference if missing
|
||||
if (!$reference) {
|
||||
if (! $reference) {
|
||||
$contractRef = $acc['contract_reference'] ?? ($mapped['contract']['reference'] ?? null);
|
||||
if ($contractRef) {
|
||||
$reference = $contractRef;
|
||||
@@ -436,7 +482,7 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
$mapped['account'] = $acc;
|
||||
}
|
||||
}
|
||||
if (!$contractId || !$reference) {
|
||||
if (! $contractId || ! $reference) {
|
||||
return ['action' => 'skipped', 'message' => 'Missing contract_id/reference'];
|
||||
}
|
||||
|
||||
@@ -449,15 +495,25 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
$applyInsert = [];
|
||||
$applyUpdate = [];
|
||||
foreach ($mappings as $map) {
|
||||
if (!$map->target_field) continue;
|
||||
if (! $map->target_field) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $map->target_field);
|
||||
if ($parts[0] !== 'account') continue;
|
||||
if ($parts[0] !== 'account') {
|
||||
continue;
|
||||
}
|
||||
$field = $parts[1] ?? null;
|
||||
if (!$field) continue;
|
||||
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 (in_array($mode, ['insert', 'both'])) {
|
||||
$applyInsert[$field] = $value;
|
||||
}
|
||||
if (in_array($mode, ['update', 'both'])) {
|
||||
$applyUpdate[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
@@ -465,25 +521,29 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||
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));
|
||||
$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 = 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; }
|
||||
if (! array_key_exists('active', $data)) {
|
||||
$data['active'] = 1;
|
||||
}
|
||||
$created = Account::create($data);
|
||||
|
||||
return ['action' => 'inserted', 'account' => $created, 'contract_id' => $contractId];
|
||||
}
|
||||
}
|
||||
@@ -493,13 +553,18 @@ 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;
|
||||
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;
|
||||
if ($found) {
|
||||
return $found->id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -507,7 +572,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
{
|
||||
$contractData = $mapped['contract'] ?? [];
|
||||
$reference = $contractData['reference'] ?? null;
|
||||
if (!$reference) {
|
||||
if (! $reference) {
|
||||
return ['action' => 'invalid', 'message' => 'Missing contract.reference'];
|
||||
}
|
||||
|
||||
@@ -527,7 +592,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
}
|
||||
|
||||
// If not found by client+reference and a specific client_case_id is provided, try that too
|
||||
if (!$existing && $clientCaseId) {
|
||||
if (! $existing && $clientCaseId) {
|
||||
$existing = Contract::query()
|
||||
->where('client_case_id', $clientCaseId)
|
||||
->where('reference', $reference)
|
||||
@@ -535,17 +600,17 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
}
|
||||
|
||||
// If we still need to insert, we must resolve clientCaseId, but avoid creating new person/case unless necessary
|
||||
if (!$existing && !$clientCaseId) {
|
||||
if (! $existing && ! $clientCaseId) {
|
||||
// Resolve by identifiers or provided person; do not use Client->person
|
||||
$personId = null;
|
||||
if (!empty($mapped['person'] ?? [])) {
|
||||
if (! empty($mapped['person'] ?? [])) {
|
||||
$personId = $this->findPersonIdByIdentifiers($mapped['person']);
|
||||
if (!$personId) {
|
||||
if (! $personId) {
|
||||
$personId = $this->findOrCreatePersonId($mapped['person']);
|
||||
}
|
||||
}
|
||||
// As a last resort, create a minimal person for this client
|
||||
if ($clientId && !$personId) {
|
||||
if ($clientId && ! $personId) {
|
||||
$personId = $this->createMinimalPersonId();
|
||||
}
|
||||
|
||||
@@ -563,39 +628,51 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||
$applyInsert = [];
|
||||
$applyUpdate = [];
|
||||
foreach ($mappings as $map) {
|
||||
if (!$map->target_field) continue;
|
||||
if (! $map->target_field) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $map->target_field);
|
||||
if ($parts[0] !== 'contract') continue;
|
||||
if ($parts[0] !== 'contract') {
|
||||
continue;
|
||||
}
|
||||
$field = $parts[1] ?? null;
|
||||
if (!$field) continue;
|
||||
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 (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));
|
||||
$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 = 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];
|
||||
}
|
||||
}
|
||||
@@ -604,33 +681,43 @@ private function findOrCreatePersonId(array $p): ?int
|
||||
{
|
||||
// Basic dedup: by tax_number, ssn, else full_name
|
||||
$query = Person::query();
|
||||
if (!empty($p['tax_number'] ?? null)) {
|
||||
if (! empty($p['tax_number'] ?? null)) {
|
||||
$found = $query->where('tax_number', $p['tax_number'])->first();
|
||||
if ($found) return $found->id;
|
||||
if ($found) {
|
||||
return $found->id;
|
||||
}
|
||||
}
|
||||
if (!empty($p['social_security_number'] ?? null)) {
|
||||
if (! empty($p['social_security_number'] ?? null)) {
|
||||
$found = Person::where('social_security_number', $p['social_security_number'])->first();
|
||||
if ($found) return $found->id;
|
||||
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)) {
|
||||
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];
|
||||
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);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -678,117 +765,182 @@ private function getDefaultPhoneTypeId(): int
|
||||
private function findOrCreateClientId(int $personId): int
|
||||
{
|
||||
$client = Client::where('person_id', $personId)->first();
|
||||
if ($client) return $client->id;
|
||||
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;
|
||||
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'];
|
||||
$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;
|
||||
if (! $map->target_field) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $map->target_field);
|
||||
if ($parts[0] !== 'email') continue;
|
||||
$field = $parts[1] ?? null; if (!$field) continue;
|
||||
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 (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'];
|
||||
$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));
|
||||
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;
|
||||
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'];
|
||||
$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'] === '') {
|
||||
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;
|
||||
if (! $map->target_field) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $map->target_field);
|
||||
if ($parts[0] !== 'address') continue;
|
||||
$field = $parts[1] ?? null; if (!$field) continue;
|
||||
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 (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'];
|
||||
$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));
|
||||
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'];
|
||||
$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;
|
||||
if (! $map->target_field) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $map->target_field);
|
||||
if ($parts[0] !== 'phone') continue;
|
||||
$field = $parts[1] ?? null; if (!$field) continue;
|
||||
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 (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'];
|
||||
$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));
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user