Changes to import and notifications
This commit is contained in:
@@ -20,6 +20,11 @@
|
||||
*/
|
||||
class ImportSimulationService
|
||||
{
|
||||
/**
|
||||
* Optional client scoping for lookups during a simulation run.
|
||||
*/
|
||||
private ?int $clientId = null;
|
||||
|
||||
/**
|
||||
* Public entry: simulate import applying mappings to first $limit rows.
|
||||
* Keeps existing machine keys for backward compatibility, but adds Slovenian
|
||||
@@ -27,6 +32,8 @@ class ImportSimulationService
|
||||
*/
|
||||
public function simulate(Import $import, int $limit = 100, bool $verbose = false): array
|
||||
{
|
||||
// Store client context for the duration of this simulation
|
||||
$this->clientId = $import->client_id ?: null;
|
||||
$meta = $import->meta ?? [];
|
||||
$hasHeader = (bool) ($meta['has_header'] ?? true);
|
||||
$delimiter = $meta['forced_delimiter'] ?? $meta['detected_delimiter'] ?? ',';
|
||||
@@ -70,9 +77,14 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
$translatedStatuses = $this->statusTranslations();
|
||||
|
||||
$simRows = [];
|
||||
// Determine keyref behavior for contract.reference from mappings/template
|
||||
$tplMeta = optional($import->template)->meta ?? [];
|
||||
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
|
||||
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
|
||||
foreach ($rows as $idx => $rawValues) {
|
||||
$assoc = $this->associateRow($columns, $rawValues);
|
||||
$rowEntities = [];
|
||||
$keyrefSkipRow = false; // if true, downstream creations are skipped for this row
|
||||
|
||||
// Reactivation intent detection (row > import > template)
|
||||
$rowReactivate = false;
|
||||
@@ -139,6 +151,24 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
$contractEntity['action'] = 'reactivate';
|
||||
$contractEntity['reactivation'] = true;
|
||||
}
|
||||
// Keyref enforcement: if mapping is keyref (or template says reference) and contract doesn't exist, skip row creations
|
||||
$ref = $contractEntity['reference'] ?? null;
|
||||
if (($contractRefMode === 'keyref' || $contractKeyModeTpl === 'reference')
|
||||
&& ($contractEntity['action'] === 'create')
|
||||
) {
|
||||
// Adjust summaries: revert create -> invalid
|
||||
if (isset($summaries['contract'])) {
|
||||
if (($summaries['contract']['create'] ?? 0) > 0) {
|
||||
$summaries['contract']['create']--;
|
||||
}
|
||||
$summaries['contract']['invalid'] = ($summaries['contract']['invalid'] ?? 0) + 1;
|
||||
}
|
||||
$contractEntity['original_action'] = 'create';
|
||||
$contractEntity['action'] = 'skip';
|
||||
$contractEntity['warning'] = 'Contract reference '.(string) $ref.' does not exist (keyref); row skipped.';
|
||||
$contractEntity['skipped_due_to_keyref'] = true;
|
||||
$keyrefSkipRow = true;
|
||||
}
|
||||
// Attach contract meta preview from mappings (group-aware)
|
||||
$metaGroups = [];
|
||||
// Grouped contract.meta.* via groupedLookup
|
||||
@@ -199,6 +229,19 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
}
|
||||
}
|
||||
[$accountEntity, $summaries, $accountCache] = $this->simulateAccount($val, $summaries, $accountCache, $rawAccountRef);
|
||||
// If row is being skipped due to keyref missing contract, do not create accounts
|
||||
if ($keyrefSkipRow && ($accountEntity['action'] ?? null) === 'create') {
|
||||
if (isset($summaries['account'])) {
|
||||
if (($summaries['account']['create'] ?? 0) > 0) {
|
||||
$summaries['account']['create']--;
|
||||
}
|
||||
$summaries['account']['invalid'] = ($summaries['account']['invalid'] ?? 0) + 1;
|
||||
}
|
||||
$accountEntity['original_action'] = 'create';
|
||||
$accountEntity['action'] = 'skip';
|
||||
$accountEntity['warning'] = 'Skipped due to missing contract.reference in keyref mode.';
|
||||
$accountEntity['skipped_due_to_keyref'] = true;
|
||||
}
|
||||
if ($inherited) {
|
||||
$accountEntity['inherited_reference'] = true;
|
||||
}
|
||||
@@ -212,6 +255,10 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
|
||||
// Generic roots (person, address, email, phone, client_case, etc.) excluding already handled ones
|
||||
foreach (array_keys($entityRoots) as $rootKey) {
|
||||
// If keyref skip is active for this row, suppress downstream creations (person, client_case, etc.)
|
||||
if ($keyrefSkipRow && ! in_array($rootKey, ['contract', 'account', 'payment'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($rootKey, ['contract', 'account', 'payment'], true)) {
|
||||
continue; // already simulated explicitly
|
||||
}
|
||||
@@ -220,6 +267,16 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
if ($existingContract && in_array($rootKey, ['person', 'client_case'], true)) {
|
||||
continue;
|
||||
}
|
||||
// Special-case: when contract exists and email/phone/address mapping uses the same column as contract.reference,
|
||||
// treat it as a root declaration only and defer to chain attachments instead of generic simulation.
|
||||
if ($existingContract && in_array($rootKey, ['email', 'phone', 'address'], true)) {
|
||||
$crSrc = $targetToSource['contract.reference'] ?? null;
|
||||
$rk = $rootKey === 'email' ? 'email.value' : ($rootKey === 'phone' ? 'phone.nu' : 'address.address');
|
||||
$rkSrc = $targetToSource[$rk] ?? null;
|
||||
if ($crSrc !== null && $rkSrc !== null && $crSrc === $rkSrc) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$reference = $val($rootKey.'.reference');
|
||||
$identityCandidates = $this->genericIdentityCandidates($rootKey, $val);
|
||||
if (isset($multiRoots[$rootKey]) && $multiRoots[$rootKey] === true) {
|
||||
@@ -237,12 +294,18 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
$verbose,
|
||||
$targetToSource
|
||||
);
|
||||
// Add action labels and attach
|
||||
$rowEntities[$rootKey] = array_map(function ($ent) use ($translatedActions) {
|
||||
// Add action labels
|
||||
$items = array_map(function ($ent) use ($translatedActions) {
|
||||
$ent['action_label'] = $translatedActions[$ent['action']] ?? $ent['action'];
|
||||
|
||||
return $ent;
|
||||
}, $items);
|
||||
// If only a single, ungrouped item, flatten to a single entity for convenience
|
||||
if (count($items) === 1 && (($items[0]['group'] ?? '') === '')) {
|
||||
$rowEntities[$rootKey] = $items[0];
|
||||
} else {
|
||||
$rowEntities[$rootKey] = $items;
|
||||
}
|
||||
} else {
|
||||
[$genericEntity, $summaries, $genericCaches, $genericExistingIdentities, $genericSeenIdentities]
|
||||
= $this->simulateGenericRoot(
|
||||
@@ -491,6 +554,10 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||
if (isset($rowEntities['payment']['status']) && $rowEntities['payment']['status'] !== 'ok') {
|
||||
$rowStatus = $rowEntities['payment']['status'];
|
||||
}
|
||||
// If we skipped due to keyref, surface a warning status for the row if not already set
|
||||
if ($rowStatus === 'ok' && $keyrefSkipRow) {
|
||||
$rowStatus = 'warning';
|
||||
}
|
||||
$simRows[] = [
|
||||
'index' => $idx + 1,
|
||||
'entities' => $rowEntities,
|
||||
@@ -806,7 +873,14 @@ private function simulateContract(callable $val, array $summaries, array $cache,
|
||||
if (array_key_exists($reference, $cache)) {
|
||||
$contract = $cache[$reference];
|
||||
} else {
|
||||
$contract = Contract::query()->where('reference', $reference)->first(['id', 'reference', 'client_case_id', 'active', 'deleted_at']);
|
||||
$q = Contract::query()->where('reference', $reference);
|
||||
// Scope to selected client when available
|
||||
if (! is_null($this->clientId)) {
|
||||
$q->whereHas('clientCase', function ($qq): void {
|
||||
$qq->where('client_id', $this->clientId);
|
||||
});
|
||||
}
|
||||
$contract = $q->first(['id', 'reference', 'client_case_id', 'active', 'deleted_at']);
|
||||
$cache[$reference] = $contract; // may be null
|
||||
}
|
||||
}
|
||||
@@ -838,10 +912,16 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
||||
if (array_key_exists($reference, $cache)) {
|
||||
$account = $cache[$reference];
|
||||
} else {
|
||||
$account = Account::query()
|
||||
$q = Account::query()
|
||||
->where('reference', $reference)
|
||||
->where('active', 1)
|
||||
->first(['id', 'reference', 'balance_amount']);
|
||||
->where('active', 1);
|
||||
// Scope to selected client when available via contract -> clientCase
|
||||
if (! is_null($this->clientId)) {
|
||||
$q->whereHas('contract.clientCase', function ($qq): void {
|
||||
$qq->where('client_id', $this->clientId);
|
||||
});
|
||||
}
|
||||
$account = $q->first(['id', 'reference', 'balance_amount']);
|
||||
$cache[$reference] = $account;
|
||||
}
|
||||
}
|
||||
@@ -897,6 +977,39 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
||||
return [$entity, $summaries, $cache];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup apply_mode for a mapping target field on this import.
|
||||
* Returns lowercased mode like 'insert', 'update', 'both', 'keyref', or null if not found.
|
||||
*/
|
||||
private function mappingModeForImport(Import $import, string $targetField): ?string
|
||||
{
|
||||
$rows = \DB::table('import_mappings')
|
||||
->where('import_id', $import->id)
|
||||
->get(['target_field', 'apply_mode']);
|
||||
foreach ($rows as $row) {
|
||||
$tf = (string) ($row->target_field ?? '');
|
||||
if ($tf === '') {
|
||||
continue;
|
||||
}
|
||||
// Normalize root part to match canonical keys like contract.reference
|
||||
if (str_contains($tf, '.')) {
|
||||
[$root, $rest] = explode('.', $tf, 2);
|
||||
} else {
|
||||
$root = $tf;
|
||||
$rest = null;
|
||||
}
|
||||
$norm = $this->normalizeRoot($root);
|
||||
$tfNorm = $rest !== null ? ($norm.'.'.$rest) : $norm;
|
||||
if ($tfNorm === $targetField) {
|
||||
$mode = $row->apply_mode ?? null;
|
||||
|
||||
return is_string($mode) ? strtolower($mode) : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function simulateImplicitAccount(int $contractId, array $summaries, array $cache): array
|
||||
{
|
||||
$acct = Account::query()->where('contract_id', $contractId)->orderBy('id')->first(['id', 'reference', 'balance_amount']);
|
||||
@@ -1070,7 +1183,12 @@ private function simulateGenericRoot(
|
||||
// Try/catch to avoid issues if column doesn't exist
|
||||
try {
|
||||
if (\Schema::hasColumn((new $modelClass)->getTable(), 'reference')) {
|
||||
$record = $modelClass::query()->where('reference', $reference)->first(['id', 'reference']);
|
||||
$qb = $modelClass::query()->where('reference', $reference);
|
||||
// Scope client_case lookups to selected client
|
||||
if ($modelClass === \App\Models\ClientCase::class && ! is_null($this->clientId)) {
|
||||
$qb->where('client_id', $this->clientId);
|
||||
}
|
||||
$record = $qb->first(['id', 'reference']);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$record = null;
|
||||
@@ -1471,7 +1589,11 @@ private function simulateGenericRootMulti(
|
||||
} elseif ($modelClass && class_exists($modelClass)) {
|
||||
try {
|
||||
if (\Schema::hasColumn((new $modelClass)->getTable(), 'reference')) {
|
||||
$record = $modelClass::query()->where('reference', $reference)->first(['id', 'reference']);
|
||||
$qb = $modelClass::query()->where('reference', $reference);
|
||||
if ($modelClass === \App\Models\ClientCase::class && ! is_null($this->clientId)) {
|
||||
$qb->where('client_id', $this->clientId);
|
||||
}
|
||||
$record = $qb->first(['id', 'reference']);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$record = null;
|
||||
|
||||
Reference in New Issue
Block a user