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}"; } }