316 lines
9.6 KiB
PHP
316 lines
9.6 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
|
|
{
|
|
// Check for existing contract (using resolve method which handles client scoping)
|
|
$existing = $this->resolve($mapped, $context);
|
|
|
|
if ($existing) {
|
|
|
|
|
|
// Check for reactivation FIRST (before update_mode check)
|
|
$reactivate = $this->shouldReactivate($context);
|
|
|
|
Log::info('ContractHandler: Found existing Contract', [
|
|
'contract_id' => $existing->id,
|
|
'reference' => $mapped['reference'] ?? null,
|
|
'context' => $context['import']
|
|
]);
|
|
|
|
if ($reactivate && $this->needsReactivation($existing)) {
|
|
$reactivated = $this->attemptReactivation($existing, $context);
|
|
Log::info('ContractHandler: Reactivate', ['reactivated' => $reactivated]);
|
|
if ($reactivated) {
|
|
return [
|
|
'action' => 'reactivated',
|
|
'entity' => $existing,
|
|
'message' => 'Contract reactivated',
|
|
];
|
|
}
|
|
}
|
|
|
|
// Check update mode
|
|
$mode = $this->getOption('update_mode', 'update');
|
|
if ($mode === 'skip') {
|
|
return [
|
|
'action' => 'skipped',
|
|
'entity' => $existing,
|
|
'message' => 'Contract already exists (skip mode)',
|
|
];
|
|
}
|
|
|
|
// Update existing contract
|
|
$payload = $this->buildPayload($mapped, $existing);
|
|
$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',
|
|
'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];
|
|
}
|
|
}
|
|
|
|
// Handle meta field - merge grouped meta into flat structure
|
|
if (!empty($mapped['meta']) && is_array($mapped['meta'])) {
|
|
$metaData = [];
|
|
foreach ($mapped['meta'] as $grp => $entries) {
|
|
if (!is_array($entries)) {
|
|
continue;
|
|
}
|
|
foreach ($entries as $k => $v) {
|
|
$metaData[$k] = $v;
|
|
}
|
|
}
|
|
if (!empty($metaData)) {
|
|
$payload['meta'] = $metaData;
|
|
}
|
|
}
|
|
|
|
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->active = 1;
|
|
|
|
$contract->save();
|
|
|
|
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}";
|
|
}
|
|
} |