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

343 lines
11 KiB
PHP

<?php
namespace App\Services\Import\Handlers;
use App\Models\ClientCase;
use App\Models\Contract;
use App\Models\Import;
use App\Services\Import\BaseEntityHandler;
use App\Services\Import\EntityResolutionService;
use Illuminate\Support\Facades\Log;
class ContractHandler extends BaseEntityHandler
{
protected EntityResolutionService $resolutionService;
public function __construct($entityConfig = null)
{
parent::__construct($entityConfig);
$this->resolutionService = new EntityResolutionService();
}
public function getEntityClass(): string
{
return Contract::class;
}
public function resolve(array $mapped, array $context = []): mixed
{
$reference = $mapped['reference'] ?? null;
if (! $reference) {
return null;
}
$query = Contract::query();
// Scope by client if available
if ($clientId = $context['import']->client_id) {
$query->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
->where('client_cases.client_id', $clientId)
->select('contracts.*');
}
return $query->where('contracts.reference', $reference)->first();
}
public function process(Import $import, array $mapped, array $raw, array $context = []): array
{
// PHASE 4: Check for existing Contract early to prevent duplicate creation
$reference = $mapped['reference'] ?? null;
if ($reference) {
$existingContract = $this->resolutionService->getExistingContract(
$import->client_id,
$reference
);
if ($existingContract) {
Log::info('ContractHandler: Found existing Contract by reference', [
'contract_id' => $existingContract->id,
'reference' => $reference,
]);
$mode = $this->getOption('update_mode', 'update');
if ($mode === 'skip') {
return [
'action' => 'skipped',
'entity' => $existingContract,
'message' => 'Contract already exists (skip mode)',
];
}
// Update existing contract
$payload = $this->buildPayload($mapped, $existingContract);
$payload = $this->mergeJsonFields($payload, $existingContract);
$appliedFields = $this->trackAppliedFields($existingContract, $payload);
if (empty($appliedFields)) {
return [
'action' => 'skipped',
'entity' => $existingContract,
'message' => 'No changes detected',
];
}
$existingContract->fill($payload);
$existingContract->save();
return [
'action' => 'updated',
'entity' => $existingContract,
'applied_fields' => $appliedFields,
];
}
}
$existing = $this->resolve($mapped, $context);
// Check for reactivation request
$reactivate = $this->shouldReactivate($context);
// Handle reactivation if entity is soft-deleted or inactive
if ($existing && $reactivate && $this->needsReactivation($existing)) {
$reactivated = $this->attemptReactivation($existing, $context);
if ($reactivated) {
return [
'action' => 'reactivated',
'entity' => $existing,
'message' => 'Contract reactivated',
];
}
}
// Determine if we should update or skip based on mode
$mode = $this->getOption('update_mode', 'update');
if ($existing) {
if ($mode === 'skip') {
return [
'action' => 'skipped',
'entity' => $existing,
'message' => 'Contract already exists (skip mode)',
];
}
// Update
$payload = $this->buildPayload($mapped, $existing);
// Merge JSON fields instead of overwriting
$payload = $this->mergeJsonFields($payload, $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 contract
$contract = new Contract;
$payload = $this->buildPayload($mapped, $contract);
// Get client_case_id from context or mapped data
$clientCaseId = $mapped['client_case_id']
?? $context['client_case']?->entity?->id
?? null;
// If no client_case_id, try to create/find one automatically (using EntityResolutionService)
if (!$clientCaseId) {
// Add mapped data to context for EntityResolutionService
$context['mapped'] = $mapped;
$clientCaseId = $this->findOrCreateClientCaseId($context);
}
if (!$clientCaseId) {
return [
'action' => 'invalid',
'message' => 'Contract requires client_case_id (import must have client_id)',
];
}
$payload['client_case_id'] = $clientCaseId;
// Ensure required defaults
if (!isset($payload['type_id'])) {
$payload['type_id'] = $this->getDefaultContractTypeId();
}
if (!isset($payload['start_date'])) {
$payload['start_date'] = now()->toDateString();
}
$contract->fill($payload);
$contract->save();
return [
'action' => 'inserted',
'entity' => $contract,
'applied_fields' => array_keys($payload),
];
}
protected function buildPayload(array $mapped, $model): array
{
$payload = [];
// Map fields according to contract schema
$fieldMap = [
'reference' => 'reference',
'title' => 'title',
'description' => 'description',
'amount' => 'amount',
'currency' => 'currency',
'start_date' => 'start_date',
'end_date' => 'end_date',
'active' => 'active',
'type_id' => 'type_id',
'client_case_id' => 'client_case_id',
];
foreach ($fieldMap as $source => $target) {
if (array_key_exists($source, $mapped)) {
$payload[$target] = $mapped[$source];
}
}
return $payload;
}
private function getDefaultContractTypeId(): int
{
return (int) (\App\Models\ContractType::min('id') ?? 1);
}
/**
* Check if reactivation should be attempted.
*/
protected function shouldReactivate(array $context): bool
{
// Row-level reactivate column takes precedence
if (isset($context['raw']['reactivate'])) {
return filter_var($context['raw']['reactivate'], FILTER_VALIDATE_BOOLEAN);
}
// Then import-level
if (isset($context['import']->reactivate)) {
return (bool) $context['import']->reactivate;
}
// Finally template-level
if (isset($context['import']->template?->reactivate)) {
return (bool) $context['import']->template->reactivate;
}
return false;
}
/**
* Check if entity needs reactivation.
*/
protected function needsReactivation($entity): bool
{
return $entity->active == 0 || $entity->deleted_at !== null;
}
/**
* Attempt to reactivate soft-deleted or inactive contract.
*/
protected function attemptReactivation(Contract $contract, array $context): bool
{
if (! $this->getOption('supports_reactivation', false)) {
return false;
}
try {
if ($contract->trashed()) {
$contract->restore();
}
$contract->update(['active' => 1]);
return true;
} catch (\Throwable $e) {
\Log::error('Contract reactivation failed', [
'contract_id' => $contract->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Merge JSON fields instead of overwriting.
*/
protected function mergeJsonFields(array $payload, $existing): array
{
$mergeFields = $this->getOption('merge_json_fields', []);
foreach ($mergeFields as $field) {
if (isset($payload[$field]) && isset($existing->{$field})) {
$existingData = is_array($existing->{$field}) ? $existing->{$field} : [];
$newData = is_array($payload[$field]) ? $payload[$field] : [];
$payload[$field] = array_merge($existingData, $newData);
}
}
return $payload;
}
/**
* Find or create a ClientCase for this contract (using EntityResolutionService).
*/
protected function findOrCreateClientCaseId(array $context): ?int
{
$import = $context['import'] ?? null;
$mapped = $context['mapped'] ?? [];
$clientId = $import?->client_id ?? null;
if (!$clientId) {
return null;
}
// PHASE 4: Use EntityResolutionService to resolve or create ClientCase
// This will reuse existing ClientCase when possible
$clientCaseId = $this->resolutionService->resolveOrCreateClientCaseForContract(
$import,
$mapped,
$context
);
if ($clientCaseId) {
Log::info('ContractHandler: Resolved/Created ClientCase for Contract', [
'client_case_id' => $clientCaseId,
]);
}
return $clientCaseId;
}
/**
* Generate a unique client_ref.
*/
protected function generateClientRef(int $clientId): string
{
$timestamp = now()->format('ymdHis');
$random = substr(md5(uniqid()), 0, 4);
return "C{$clientId}-{$timestamp}-{$random}";
}
}