Teren-app/app/Services/Import/ImportServiceV2.php
Simon Pocrnjič 36b63a180d fixed import
2025-12-28 13:55:09 +01:00

1020 lines
36 KiB
PHP

<?php
namespace App\Services\Import;
use App\Models\Import;
use App\Models\ImportEntity;
use App\Models\ImportEvent;
use App\Models\ImportRow;
use App\Services\Import\Contracts\EntityHandlerInterface;
use App\Services\Import\DateNormalizer;
use App\Services\Import\DecimalNormalizer;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
/**
* ImportServiceV2 - Generic, database-driven import processor.
*
* Refactored from ImportProcessor to use entity handlers and config from import_entities table.
*
* PHASE 6: EntityResolutionService is integrated via handler constructors.
* Each handler (PersonHandler, ContractHandler, ClientCaseHandler) instantiates
* the service and uses it to prevent duplicate Person creation.
*/
class ImportServiceV2
{
protected array $handlers = [];
protected array $entityConfigs = [];
protected array $templateMeta = [];
protected bool $paymentsImport = false;
protected bool $historyImport = false;
protected ?string $contractKeyMode = null;
/**
* Process an import using v2 architecture.
*/
public function process(Import $import, ?Authenticatable $user = null): array
{
$started = now();
$total = 0;
$skipped = 0;
$imported = 0;
$invalid = 0;
try {
// Load template meta flags
$this->loadTemplateMeta($import);
// Load entity configurations and handlers
$this->loadEntityConfigurations();
// Only CSV/TSV supported for now
if (! in_array($import->source_type, ['csv', 'txt'])) {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'processing_skipped',
'level' => 'warning',
'message' => 'Only CSV/TXT supported in v2 processor.',
]);
$import->update(['status' => 'completed', 'finished_at' => now()]);
return compact('total', 'imported', 'skipped', 'invalid');
}
$import->update(['status' => 'processing', 'started_at' => $started]);
$filePath = $import->path;
if (! Storage::disk($import->disk ?? 'local')->exists($filePath)) {
throw new \RuntimeException("File not found: {$filePath}");
}
// Check if this is a retry (import_rows already exist)
$isRetry = ImportRow::where('import_id', $import->id)->exists();
$fullPath = Storage::disk($import->disk ?? 'local')->path($filePath);
$fh = fopen($fullPath, 'r');
if (! $fh) {
throw new \RuntimeException("Could not open file: {$filePath}");
}
$meta = $import->meta ?? [];
$hasHeader = (bool) ($meta['has_header'] ?? true);
$delimiter = $meta['forced_delimiter'] ?? $meta['detected_delimiter'] ?? ',';
$mappings = $this->loadMappings($import);
$header = null;
$rowNum = 0;
// Read header if present
if ($hasHeader) {
$header = fgetcsv($fh, 0, $delimiter);
$rowNum++;
}
$isPg = DB::connection()->getDriverName() === 'pgsql';
// If retry mode, only process failed/invalid rows
if ($isRetry) {
$failedRows = ImportRow::where('import_id', $import->id)
->whereIn('status', ['invalid', 'failed'])
->orderBy('row_number')
->get();
foreach ($failedRows as $importRow) {
$total++;
try {
$rawAssoc = $importRow->raw_data;
$mapped = $importRow->mapped_data;
// Process entities in priority order within a transaction
$context = ['import' => $import, 'user' => $user, 'import_row' => $importRow];
DB::beginTransaction();
try {
$results = $this->processRow($import, $mapped, $rawAssoc, $context);
// If processing succeeded, commit the transaction
if ($results['status'] === 'imported' || $results['status'] === 'skipped') {
DB::commit();
} else {
DB::rollBack();
}
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// Collect entity details from results
$entityData = $this->collectEntityDetails($results);
$entityDetails = $entityData['details'];
$hasErrors = $entityData['hasErrors'];
$hasWarnings = $entityData['hasWarnings'];
// Handle different result statuses
if ($results['status'] === 'imported') {
$imported++;
$importRow->update([
'status' => 'imported',
'entity_type' => $results['entity_type'] ?? null,
'entity_id' => $results['entity_id'] ?? null,
]);
$this->createRowProcessedEvent($import, $user, $importRow->row_number, $entityDetails, $hasWarnings, $rawAssoc);
} elseif ($results['status'] === 'skipped') {
$skipped++;
$importRow->update(['status' => 'skipped']);
$this->createRowSkippedEvent($import, $user, $importRow->row_number, $entityDetails, $rawAssoc);
} else {
$invalid++;
$importRow->update([
'status' => 'invalid',
'errors' => $results['errors'] ?? ['Processing failed'],
]);
$this->createRowFailedEvent(
$import,
$user,
$importRow->row_number,
$results['errors'] ?? ['Processing failed'],
$entityDetails,
$rawAssoc
);
}
} catch (\Throwable $e) {
$invalid++;
$this->handleRowException($import, $user, $importRow->row_number, $e);
}
}
fclose($fh);
} else {
// Normal mode: process all rows from CSV
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
$rowNum++;
$total++;
try {
$rawAssoc = $this->buildRowAssoc($row, $header);
// Skip empty rows
if ($this->rowIsEffectivelyEmpty($rawAssoc)) {
$skipped++;
continue;
}
$mapped = $this->applyMappings($rawAssoc, $mappings);
$rawSha1 = sha1(json_encode($rawAssoc));
$importRow = ImportRow::create([
'import_id' => $import->id,
'row_number' => $rowNum,
'record_type' => $this->determineRecordType($mapped),
'raw_data' => $rawAssoc,
'mapped_data' => $mapped,
'status' => 'valid',
'raw_sha1' => $rawSha1,
]);
// Process entities in priority order within a transaction
$context = ['import' => $import, 'user' => $user, 'import_row' => $importRow];
DB::beginTransaction();
try {
$results = $this->processRow($import, $mapped, $rawAssoc, $context);
// If processing succeeded, commit the transaction
if ($results['status'] === 'imported' || $results['status'] === 'skipped') {
DB::commit();
} else {
DB::rollBack();
}
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// Collect entity details from results
$entityData = $this->collectEntityDetails($results);
$entityDetails = $entityData['details'];
$hasErrors = $entityData['hasErrors'];
$hasWarnings = $entityData['hasWarnings'];
// Handle different result statuses
if ($results['status'] === 'imported') {
$imported++;
$importRow->update([
'status' => 'imported',
'entity_type' => $results['entity_type'] ?? null,
'entity_id' => $results['entity_id'] ?? null,
]);
$this->createRowProcessedEvent($import, $user, $rowNum, $entityDetails, $hasWarnings, $rawAssoc);
} elseif ($results['status'] === 'skipped') {
$skipped++;
$importRow->update(['status' => 'skipped']);
$this->createRowSkippedEvent($import, $user, $rowNum, $entityDetails, $rawAssoc);
} else {
$invalid++;
$importRow->update([
'status' => 'invalid',
'errors' => $results['errors'] ?? ['Processing failed'],
]);
$this->createRowFailedEvent(
$import,
$user,
$rowNum,
$results['errors'] ?? ['Processing failed'],
$entityDetails,
$rawAssoc
);
}
} catch (\Throwable $e) {
$invalid++;
$this->handleRowException($import, $user, $rowNum, $e);
}
}
fclose($fh);
}
$this->finalizeImport($import, $user, $total, $imported, $skipped, $invalid);
} catch (\Throwable $e) {
$this->handleFatalException($import, $user, $e);
throw $e;
}
return compact('total', 'imported', 'skipped', 'invalid');
}
/**
* Load entity configurations from database.
*/
protected function loadEntityConfigurations(): void
{
$entities = ImportEntity::where('is_active', true)
->orderBy('priority', 'desc')
->get();
foreach ($entities as $entity) {
$this->entityConfigs[$entity->canonical_root] = $entity;
// Instantiate handler if specified
if ($entity->handler_class && class_exists($entity->handler_class)) {
$this->handlers[$entity->canonical_root] = new $entity->handler_class($entity);
}
}
}
/**
* Load mappings for import.
*/
protected function loadMappings(Import $import)
{
return DB::table('import_mappings')
->where('import_id', $import->id)
->orderBy('position')
->get();
}
/**
* Build associative array from row.
*/
protected function buildRowAssoc(array $row, ?array $header): array
{
if ($header) {
$result = [];
foreach ($header as $idx => $col) {
$result[$col] = $row[$idx] ?? null;
}
return $result;
}
return array_combine(range(0, count($row) - 1), $row);
}
/**
* Check if row is effectively empty.
*/
protected function rowIsEffectivelyEmpty(array $raw): bool
{
foreach ($raw as $val) {
if (! is_null($val) && trim((string) $val) !== '') {
return false;
}
}
return true;
}
/**
* Apply mappings to raw data.
*/
protected function applyMappings(array $raw, $mappings): array
{
$mapped = [];
// Group mappings by target field to handle concatenation
$groupedMappings = [];
foreach ($mappings as $mapping) {
$targetField = $mapping->target_field;
if (!isset($groupedMappings[$targetField])) {
$groupedMappings[$targetField] = [];
}
$groupedMappings[$targetField][] = $mapping;
}
foreach ($groupedMappings as $targetField => $fieldMappings) {
// Special handling for meta fields: contracts.meta or other_entity.meta
if (str_ends_with($targetField, '.meta')) {
$this->applyMetaMappings($mapped, $targetField, $fieldMappings, $raw);
continue;
}
// Group by group number from options
$valuesByGroup = [];
foreach ($fieldMappings as $mapping) {
$sourceCol = $mapping->source_column;
if (!isset($raw[$sourceCol])) {
continue;
}
$value = $raw[$sourceCol];
// Apply transform
if ($mapping->transform) {
$value = $this->applyTransform($value, $mapping->transform);
}
// Get group from options
$options = $mapping->options ? json_decode($mapping->options, true) : [];
$group = $options['group'] ?? null;
// Group values by their group number
if ($group !== null) {
// Same group = concatenate
if (!isset($valuesByGroup[$group])) {
$valuesByGroup[$group] = [];
}
$valuesByGroup[$group][] = $value;
} else {
// No group = each gets its own group
$valuesByGroup[] = [$value];
}
}
// Now set the values
foreach ($valuesByGroup as $values) {
if (count($values) === 1) {
// Single value - set directly
$this->setNestedValue($mapped, $targetField, $values[0]);
} else {
// Multiple values in same group - concatenate with newline
$concatenated = implode("\n", array_filter($values, fn($v) => !empty($v) && trim((string)$v) !== ''));
if (!empty($concatenated)) {
$this->setNestedValue($mapped, $targetField, $concatenated);
}
}
}
}
return $mapped;
}
/**
* Apply meta mappings with special structure: entity.meta[group][key] = {title, value, type}
*/
protected function applyMetaMappings(array &$mapped, string $targetField, array $fieldMappings, array $raw): void
{
// Extract entity from target field: contracts.meta -> contracts
$entity = str_replace('.meta', '', $targetField);
foreach ($fieldMappings as $mapping) {
$sourceCol = $mapping->source_column;
if (!isset($raw[$sourceCol])) {
continue;
}
$value = $raw[$sourceCol];
// Apply transform
if ($mapping->transform) {
$value = $this->applyTransform($value, $mapping->transform);
}
// Get options
$options = $mapping->options ? json_decode($mapping->options, true) : [];
$metaKey = $options['key'] ?? null;
$metaType = $options['type'] ?? 'string';
$group = $options['group'] ?? '1';
if (!$metaKey) {
continue;
}
// Coerce value based on type
$coerced = $value;
if ($metaType === 'number') {
if (is_string($coerced)) {
$norm = DecimalNormalizer::normalize($coerced);
$coerced = is_numeric($norm) ? (float) $norm : $coerced;
}
} elseif ($metaType === 'boolean') {
if (is_string($coerced)) {
$lc = strtolower(trim($coerced));
if (in_array($lc, ['1', 'true', 'yes', 'y'], true)) {
$coerced = true;
} elseif (in_array($lc, ['0', 'false', 'no', 'n'], true)) {
$coerced = false;
}
} else {
$coerced = (bool) $coerced;
}
} elseif ($metaType === 'date') {
$coerced = is_scalar($coerced) ? $this->normalizeDate((string) $coerced) : null;
} else {
// string or unspecified: cast scalars to string
if (is_scalar($coerced)) {
$coerced = (string) $coerced;
}
}
// Initialize structure if needed
if (!isset($mapped[$entity])) {
$mapped[$entity] = [];
}
if (!isset($mapped[$entity]['meta']) || !is_array($mapped[$entity]['meta'])) {
$mapped[$entity]['meta'] = [];
}
if (!isset($mapped[$entity]['meta'][$group])) {
$mapped[$entity]['meta'][$group] = [];
}
// Store as structure with title, value and type
$entry = [
'title' => $sourceCol,
'value' => $coerced,
];
if ($metaType) {
$entry['type'] = $metaType;
}
$mapped[$entity]['meta'][$group][$metaKey] = $entry;
}
}
/**
* Apply transform to value.
*/
protected function applyTransform(mixed $value, string $transform): mixed
{
return match (strtolower($transform)) {
'trim' => is_string($value) ? trim($value) : $value,
'upper' => is_string($value) ? strtoupper($value) : $value,
'lower' => is_string($value) ? strtolower($value) : $value,
'date' => $this->normalizeDate($value),
default => $value,
};
}
/**
* Normalize date value.
*/
protected function normalizeDate(mixed $value): ?string
{
if (empty($value)) {
return null;
}
try {
return DateNormalizer::toDate((string) $value);
} catch (\Throwable $e) {
return null;
}
}
/**
* Set nested value in array using dot notation.
* If the key already exists, convert to array and append the new value.
*/
protected function setNestedValue(array &$array, string $key, mixed $value): void
{
$keys = explode('.', $key);
$current = &$array;
foreach ($keys as $i => $k) {
if ($i === count($keys) - 1) {
// If key already exists, convert to array and append
if (isset($current[$k])) {
// Convert existing single value to array if needed
if (!is_array($current[$k])) {
$current[$k] = [$current[$k]];
}
// Append new value
$current[$k][] = $value;
} else {
// Set as single value
$current[$k] = $value;
}
} else {
if (! isset($current[$k]) || ! is_array($current[$k])) {
$current[$k] = [];
}
$current = &$current[$k];
}
}
}
/**
* Determine record type from mapped data.
*/
protected function determineRecordType(array $mapped): string
{
if (isset($mapped['payment'])) {
return 'payment';
}
if (isset($mapped['activity'])) {
return 'activity';
}
if (isset($mapped['contract'])) {
return 'contract';
}
if (isset($mapped['account'])) {
return 'account';
}
return 'contact';
}
/**
* Process a single row through all entity handlers.
*/
protected function processRow(Import $import, array $mapped, array $raw, array $context): array
{
$entityResults = [];
$lastEntityType = null;
$lastEntityId = null;
$hasErrors = false;
// Process entities in configured priority order
foreach ($this->entityConfigs as $root => $config) {
// Check if this entity exists in mapped data (support aliases)
$mappedKey = $this->findMappedKey($mapped, $root, $config);
if (!$mappedKey || !isset($mapped[$mappedKey])) {
continue;
}
$handler = $this->handlers[$root] ?? null;
if (! $handler) {
continue;
}
try {
// Validate before processing
$validation = $handler->validate($mapped[$mappedKey]);
if (! $validation['valid']) {
$entityResults[$root] = [
'action' => 'invalid',
'errors' => $validation['errors'],
'level' => 'error',
];
$hasErrors = true;
// Don't stop processing, continue to other entities to collect all errors
continue;
}
// Pass previous results as context
$result = $handler->process($import, $mapped[$mappedKey], $raw, array_merge($context, $entityResults));
$entityResults[$root] = $result;
// Track last successful entity for row status
if (in_array($result['action'] ?? null, ['inserted', 'updated'])) {
$lastEntityType = $handler->getEntityClass();
$lastEntityId = $result['entity']?->id ?? null;
}
// Post-contract actions (segment attachment, activity creation)
if ($root === 'contract' && in_array($result['action'] ?? null, ['inserted', 'updated', 'reactivated'])) {
try {
$this->postContractActions($import, $result['entity'], $context);
} catch (\Throwable $e) {
Log::warning('Post-contract action failed', [
'import_id' => $import->id,
'contract_id' => $result['entity']->id ?? null,
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$hasErrors = true;
Log::error("Handler failed for entity {$root}", [
'import_id' => $import->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$entityResults[$root] = [
'action' => 'failed',
'level' => 'error',
'errors' => [$e->getMessage()],
'exception' => [
'message' => $e->getMessage(),
'file' => basename($e->getFile()),
'line' => $e->getLine(),
'class' => get_class($e),
],
];
// Continue to process other entities to collect all errors
continue;
}
}
// If we had errors, return invalid status
if ($hasErrors) {
$allErrors = [];
foreach ($entityResults as $root => $result) {
if (isset($result['errors'])) {
$allErrors[] = "{$root}: " . implode(', ', $result['errors']);
}
}
return [
'status' => 'invalid',
'errors' => $allErrors,
'results' => $entityResults,
];
}
return [
'status' => $lastEntityId ? 'imported' : 'skipped',
'entity_type' => $lastEntityType,
'entity_id' => $lastEntityId,
'results' => $entityResults,
];
}
/**
* Find the key in mapped data that corresponds to this canonical root.
*/
protected function findMappedKey(array $mapped, string $canonicalRoot, $config): ?string
{
// First check canonical_root itself
if (isset($mapped[$canonicalRoot])) {
return $canonicalRoot;
}
// Then check key (e.g., 'contracts', 'person_addresses')
if (isset($mapped[$config->key])) {
return $config->key;
}
// Then check aliases
$aliases = $config->aliases ?? [];
foreach ($aliases as $alias) {
if (isset($mapped[$alias])) {
return $alias;
}
}
return null;
}
/**
* Load template meta flags for special processing modes.
*/
protected function loadTemplateMeta(Import $import): void
{
$this->templateMeta = optional($import->template)->meta ?? [];
$this->paymentsImport = (bool) ($this->templateMeta['payments_import'] ?? false);
$this->historyImport = (bool) ($this->templateMeta['history_import'] ?? false);
$this->contractKeyMode = $this->templateMeta['contract_key_mode'] ?? null;
}
/**
* Collect entity details from processing results.
*/
protected function collectEntityDetails(array $results): array
{
$entityDetails = [];
$hasErrors = false;
$hasWarnings = false;
if (isset($results['results']) && is_array($results['results'])) {
foreach ($results['results'] as $entityKey => $result) {
$action = $result['action'] ?? 'unknown';
$message = $result['message'] ?? null;
$count = $result['count'] ?? 1;
$detail = [
'entity' => $entityKey,
'action' => $action,
'count' => $count,
];
if ($message) {
$detail['message'] = $message;
}
if ($action === 'invalid' || isset($result['errors'])) {
$detail['level'] = 'error';
$detail['errors'] = $result['errors'] ?? [];
$hasErrors = true;
} elseif ($action === 'skipped') {
$detail['level'] = 'warning';
$hasWarnings = true;
} else {
$detail['level'] = 'info';
}
if (isset($result['exception'])) {
$detail['exception'] = $result['exception'];
$hasErrors = true;
}
$entityDetails[] = $detail;
}
}
return [
'details' => $entityDetails,
'hasErrors' => $hasErrors,
'hasWarnings' => $hasWarnings,
];
}
/**
* Create a success event for a processed row.
*/
protected function createRowProcessedEvent(
Import $import,
?Authenticatable $user,
int $rowNum,
array $entityDetails,
bool $hasWarnings,
array $rawData = []
): void {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'row_processed',
'level' => $hasWarnings ? 'warning' : 'info',
'message' => "Row {$rowNum} processed successfully",
'context' => [
'row' => $rowNum,
'entity_details' => $entityDetails,
'raw_data' => $rawData,
],
]);
}
/**
* Create a skip event for a skipped row.
*/
protected function createRowSkippedEvent(
Import $import,
?Authenticatable $user,
int $rowNum,
array $entityDetails,
array $rawData = []
): void {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'row_skipped',
'level' => 'warning',
'message' => "Row {$rowNum} skipped",
'context' => [
'row' => $rowNum,
'entity_details' => $entityDetails,
'raw_data' => $rawData,
],
]);
}
/**
* Create a failure event for a failed row.
*/
protected function createRowFailedEvent(
Import $import,
?Authenticatable $user,
int $rowNum,
array $errors,
array $entityDetails,
array $rawData = []
): void {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'row_failed',
'level' => 'error',
'message' => "Row {$rowNum} failed: " . implode(', ', $errors),
'context' => [
'row' => $rowNum,
'errors' => $errors,
'entity_details' => $entityDetails,
'raw_data' => $rawData,
],
]);
}
/**
* Handle row processing exception.
*/
protected function handleRowException(
Import $import,
?Authenticatable $user,
int $rowNum,
\Throwable $e
): void {
Log::error('ImportServiceV2 row processing failed', [
'import_id' => $import->id,
'row' => $rowNum,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'row_failed',
'level' => 'error',
'message' => "Row {$rowNum} exception: {$e->getMessage()}",
'context' => [
'row' => $rowNum,
'exception' => [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
],
],
]);
}
/**
* Finalize import with completion event.
*/
protected function finalizeImport(
Import $import,
?Authenticatable $user,
int $total,
int $imported,
int $skipped,
int $invalid
): void {
// If there are any invalid rows, mark import as failed, not completed
$status = $invalid > 0 ? 'failed' : 'completed';
$eventLevel = $invalid > 0 ? 'error' : 'info';
$eventName = $invalid > 0 ? 'processing_failed' : 'processing_completed';
$import->update([
'status' => $status,
'finished_at' => now(),
'total_rows' => $total,
'imported_rows' => $imported,
'valid_rows' => $imported,
'invalid_rows' => $invalid,
]);
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => $eventName,
'level' => $eventLevel,
'message' => "Processed {$total} rows: {$imported} imported, {$skipped} skipped, {$invalid} invalid",
]);
}
/**
* Handle fatal processing exception.
*/
protected function handleFatalException(
Import $import,
?Authenticatable $user,
\Throwable $e
): void {
Log::error('ImportServiceV2 processing failed', [
'import_id' => $import->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$import->update(['status' => 'failed', 'finished_at' => now()]);
ImportEvent::create([
'import_id' => $import->id,
'user_id' => $user?->getAuthIdentifier(),
'event' => 'processing_failed',
'level' => 'error',
'message' => $e->getMessage(),
]);
}
/**
* Post-contract actions: attach segment, create activity with decision.
* Matches ImportProcessor::postContractActions() behavior.
*/
protected function postContractActions(Import $import, $contract, array $context = []): void
{
$meta = $import->meta ?? [];
$segmentId = (int) ($meta['segment_id'] ?? 0);
$decisionId = (int) ($meta['decision_id'] ?? 0);
$templateName = (string) ($meta['template_name'] ?? optional($import->template)->name ?? '');
$actionId = (int) ($meta['action_id'] ?? 0);
// Attach segment to contract as the main (active) segment if provided
if ($segmentId > 0) {
// Ensure the segment exists on the client case and is active
$ccSeg = \DB::table('client_case_segment')
->where('client_case_id', $contract->client_case_id)
->where('segment_id', $segmentId)
->first();
if (! $ccSeg) {
\DB::table('client_case_segment')->insert([
'client_case_id' => $contract->client_case_id,
'segment_id' => $segmentId,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
} elseif (! $ccSeg->active) {
\DB::table('client_case_segment')
->where('id', $ccSeg->id)
->update(['active' => true, 'updated_at' => now()]);
}
// Deactivate all other segments for this contract to make this the main one
\DB::table('contract_segment')
->where('contract_id', $contract->id)
->where('segment_id', '!=', $segmentId)
->update(['active' => false, 'updated_at' => now()]);
// Upsert the selected segment as active for this contract
$pivot = \DB::table('contract_segment')
->where('contract_id', $contract->id)
->where('segment_id', $segmentId)
->first();
if ($pivot) {
if (! $pivot->active) {
\DB::table('contract_segment')
->where('id', $pivot->id)
->update(['active' => true, 'updated_at' => now()]);
}
} else {
\DB::table('contract_segment')->insert([
'contract_id' => $contract->id,
'segment_id' => $segmentId,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// Create activity if decision provided
if ($decisionId > 0) {
\App\Models\Activity::create([
'decision_id' => $decisionId,
'action_id' => $actionId > 0 ? $actionId : null,
'contract_id' => $contract->id,
'client_case_id' => $contract->client_case_id,
'note' => trim('Imported via template'.($templateName ? ': '.$templateName : '')),
]);
}
}
}