feat: Implementato sistema Pre-Discovery con supporto esecuzioni schedulate
- Fix FindEntitiesByKeysAsync: SOQL query universale al posto di External ID GET * Disabilitato External ID GET (funziona solo per campi marcati External ID) * Query SOQL come metodo primario funzionante per tutti i campi * Logging dettagliato per diagnostica ricerche Salesforce - Pre-Discovery nei trasferimenti manuali (DataCoupler.razor.cs) * Ricerca automatica nella destinazione quando KeyAssociation non esiste * Creazione associazione con marker CreatedBy="PreDiscovery" * Aggiornamento forzato senza controllo hash per associazioni Pre-Discovery * Supporto parallel processing thread-safe - Pre-Discovery nei trasferimenti schedulati (ScheduledProfileExecutionService.cs) * Logica identica a trasferimenti manuali * Marker aggiuntivo ScheduledTransfer=true per tracciabilità * Aggiornamento forzato per prima sincronizzazione - Sistema aggiornamento forzato * Verifica AdditionalInfo per identificare associazioni Pre-Discovery * Skip controllo hash per associazioni appena scoperte * Garantisce sincronizzazione dati al primo trasferimento Vantaggi: - Prevenzione automatica duplicati in Salesforce - Recupero record esistenti senza associazioni - Parità funzionale tra esecuzioni manuali e schedulate - Performance ottimizzate con controllo hash per esecuzioni successive Docs: PRE_DISCOVERY_SYSTEM.md, PRE_DISCOVERY_FORCED_UPDATE.md, SALESFORCE_FIND_ENTITIES_FIX.md
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# Pre-Discovery: Aggiornamento Forzato
|
||||
|
||||
## 📋 Requisito Implementato
|
||||
|
||||
Quando il **Pre-Discovery** trova un record esistente nella destinazione e crea una nuova associazione, il sistema deve **forzare l'aggiornamento** del record **saltando il controllo hash**.
|
||||
|
||||
## 🎯 Rationale
|
||||
|
||||
Quando un'associazione viene creata dal Pre-Discovery significa che:
|
||||
1. ✅ Il record **esiste nella destinazione** Salesforce
|
||||
2. ❌ **Non esisteva** un'associazione in `KeyAssociations`
|
||||
3. 🔍 I dati potrebbero essere **diversi** tra sorgente e destinazione
|
||||
4. 🔄 Vogliamo **sincronizzare** i dati al primo trasferimento
|
||||
|
||||
Quindi ha senso **forzare l'aggiornamento** senza controllare l'hash, per assicurarci che i dati siano allineati.
|
||||
|
||||
## 🔧 Implementazione
|
||||
|
||||
### Identificazione Associazioni Pre-Discovery
|
||||
|
||||
Le associazioni create dal Pre-Discovery hanno un marker nel campo `AdditionalInfo`:
|
||||
|
||||
```json
|
||||
{
|
||||
"CreatedBy": "PreDiscovery",
|
||||
"DiscoveredAt": "2025-10-21T10:30:00Z",
|
||||
"MappingCount": 5
|
||||
}
|
||||
```
|
||||
|
||||
### Logica di Controllo
|
||||
|
||||
Il sistema verifica il campo `AdditionalInfo` per determinare se l'associazione proviene dal Pre-Discovery:
|
||||
|
||||
```csharp
|
||||
// Verifica se l'associazione è stata creata dal Pre-Discovery
|
||||
var isPreDiscoveryAssociation = false;
|
||||
if (!string.IsNullOrEmpty(existingAssociation.AdditionalInfo))
|
||||
{
|
||||
try
|
||||
{
|
||||
var additionalInfo = JsonSerializer.Deserialize<Dictionary<string, object>>(existingAssociation.AdditionalInfo);
|
||||
if (additionalInfo != null && additionalInfo.ContainsKey("CreatedBy"))
|
||||
{
|
||||
var createdBy = additionalInfo["CreatedBy"]?.ToString();
|
||||
isPreDiscoveryAssociation = createdBy == "PreDiscovery";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignora errori di parsing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Flusso di Decisione
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Esiste associazione attiva? │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
├─ NO ──> Crea nuovo record
|
||||
│
|
||||
└─ SI ──> È da Pre-Discovery?
|
||||
│
|
||||
├─ SI ──> ✅ FORZA AGGIORNAMENTO (skip hash)
|
||||
│
|
||||
└─ NO ──> Controlla hash
|
||||
│
|
||||
├─ Hash identico ──> Skip record
|
||||
│
|
||||
└─ Hash diverso ──> Aggiorna record
|
||||
```
|
||||
|
||||
### Codice Modificato
|
||||
|
||||
**File**: `Data_Coupler/Pages/DataCoupler.razor.cs`
|
||||
**Metodo**: `StartDataTransferWithComposite()` (linea ~2840-2890)
|
||||
|
||||
```csharp
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
// Verifica se l'associazione è stata creata dal Pre-Discovery
|
||||
var isPreDiscoveryAssociation = false;
|
||||
if (!string.IsNullOrEmpty(existingAssociation.AdditionalInfo))
|
||||
{
|
||||
try
|
||||
{
|
||||
var additionalInfo = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(existingAssociation.AdditionalInfo);
|
||||
if (additionalInfo != null && additionalInfo.ContainsKey("CreatedBy"))
|
||||
{
|
||||
var createdBy = additionalInfo["CreatedBy"]?.ToString();
|
||||
isPreDiscoveryAssociation = createdBy == "PreDiscovery";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignora errori di parsing
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 PRE-DISCOVERY: Se l'associazione è stata appena creata dal Pre-Discovery, FORZA l'aggiornamento
|
||||
if (isPreDiscoveryAssociation)
|
||||
{
|
||||
// Forza aggiornamento senza controllo hash
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber, currentDataHash));
|
||||
Logger.LogInformation("COMPOSITE PARALLEL: Record {RecordNumber} marcato per AGGIORNAMENTO FORZATO (Pre-Discovery) - EntityId: {EntityId}",
|
||||
recordNumber, existingAssociation.DestinationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// CONTROLLO HASH: Verifica se i dati sono cambiati (solo per associazioni esistenti)
|
||||
var existingHash = existingAssociation.Data_Hash;
|
||||
|
||||
if (!string.IsNullOrEmpty(existingHash) && existingHash.Equals(currentDataHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// I dati non sono cambiati, salta questo record
|
||||
recordsSkipped.Add((record, recordNumber, "Dati non modificati (hash identico)"));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} saltato - hash identico: {Hash}",
|
||||
recordNumber, currentDataHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
// I dati sono cambiati o l'hash è vuoto, procedi con l'aggiornamento
|
||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber, currentDataHash));
|
||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId}) - hash diverso: old={OldHash}, new={NewHash}",
|
||||
recordNumber, existingAssociation.DestinationId, existingHash ?? "NULL", currentDataHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Logging
|
||||
|
||||
Il sistema ora produce log specifici per aggiornamenti forzati:
|
||||
|
||||
```log
|
||||
COMPOSITE PARALLEL: Record 42 marcato per AGGIORNAMENTO FORZATO (Pre-Discovery) - EntityId: 003xxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
vs. aggiornamenti normali:
|
||||
|
||||
```log
|
||||
COMPOSITE PARALLEL: Record 42 marcato per aggiornamento (EntityId: 003xxxxxxxxxxxxx) - hash diverso: old=abc123, new=def456
|
||||
```
|
||||
|
||||
## 🎯 Comportamento Atteso
|
||||
|
||||
### Scenario 1: Prima Esecuzione con Pre-Discovery
|
||||
|
||||
**Setup**:
|
||||
- ❌ Tabella `KeyAssociations` vuota
|
||||
- ✅ Record già presenti in Salesforce
|
||||
|
||||
**Risultato**:
|
||||
1. Pre-Discovery trova i record esistenti
|
||||
2. Crea associazioni con `CreatedBy: "PreDiscovery"`
|
||||
3. **Forza l'aggiornamento** di tutti i record trovati (ignora hash)
|
||||
4. Aggiorna `Data_Hash` con il nuovo valore
|
||||
5. Record sincronizzati tra sorgente e destinazione
|
||||
|
||||
### Scenario 2: Seconda Esecuzione (Associazioni Esistenti)
|
||||
|
||||
**Setup**:
|
||||
- ✅ Tabella `KeyAssociations` popolata (dalla prima esecuzione)
|
||||
- ✅ Record già sincronizzati
|
||||
|
||||
**Risultato**:
|
||||
1. Trova associazioni esistenti (NON da Pre-Discovery)
|
||||
2. **Controlla l'hash** dei dati
|
||||
3. Se hash identico → **Skip record** (performance ottimizzata)
|
||||
4. Se hash diverso → Aggiorna record normalmente
|
||||
|
||||
### Scenario 3: Record Modificato in Sorgente
|
||||
|
||||
**Setup**:
|
||||
- ✅ Associazione esistente
|
||||
- 🔄 Dati modificati in sorgente
|
||||
|
||||
**Risultato**:
|
||||
1. Trova associazione esistente
|
||||
2. Calcola nuovo hash: `new_hash ≠ old_hash`
|
||||
3. **Aggiorna il record** in destinazione
|
||||
4. Salva il nuovo hash
|
||||
|
||||
## 🔄 Ciclo di Vita Associazione Pre-Discovery
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 1° TRASFERIMENTO (KeyAssociations vuota) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Pre-Discovery: Cerca record in Salesforce │
|
||||
│ ✅ Trovato → Crea associazione con CreatedBy="PreDiscovery" │
|
||||
│ 🔄 AGGIORNAMENTO FORZATO (skip hash check) │
|
||||
│ 💾 Salva nuovo hash │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 2° TRASFERIMENTO (Associazione esistente) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Trova associazione (CreatedBy="PreDiscovery") │
|
||||
│ 🔍 CONTROLLO HASH │
|
||||
│ • Hash identico → ⏭️ Skip (performance) │
|
||||
│ • Hash diverso → 🔄 Aggiorna normalmente │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ TRASFERIMENTI SUCCESSIVI │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Comportamento normale con controllo hash │
|
||||
│ (l'associazione non è più "nuova" dal Pre-Discovery) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## ⚠️ Note Importanti
|
||||
|
||||
### Metodo Original (Non-Composite)
|
||||
|
||||
Il metodo `StartDataTransferOriginal()` **non ha controllo hash**. Tenta sempre l'aggiornamento direttamente, quindi non necessita di modifiche per il Pre-Discovery.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Prima esecuzione**: Tutti i record trovati dal Pre-Discovery vengono aggiornati forzatamente
|
||||
- **Esecuzioni successive**: Controllo hash ottimizza le performance saltando record non modificati
|
||||
|
||||
### Pulizia Marker
|
||||
|
||||
Il marker `CreatedBy: "PreDiscovery"` rimane nell'associazione per tutta la sua vita. Questo permette di:
|
||||
1. Tracciare l'origine dell'associazione
|
||||
2. Debugging più semplice
|
||||
3. Statistiche su quante associazioni sono state scoperte automaticamente
|
||||
|
||||
## 🧪 Test Case
|
||||
|
||||
### Test 1: Pre-Discovery con Aggiornamento Forzato
|
||||
|
||||
```
|
||||
SETUP:
|
||||
- KeyAssociations: vuota
|
||||
- Salesforce: Record con Email="test@example.com"
|
||||
- Sorgente: Record con Email="test@example.com" + dati aggiornati
|
||||
|
||||
EXPECTED:
|
||||
1. Pre-Discovery trova record in Salesforce
|
||||
2. Crea associazione con CreatedBy="PreDiscovery"
|
||||
3. FORZA aggiornamento (salta controllo hash)
|
||||
4. Log: "AGGIORNAMENTO FORZATO (Pre-Discovery)"
|
||||
5. Record aggiornato in Salesforce
|
||||
```
|
||||
|
||||
### Test 2: Secondo Trasferimento con Hash Check
|
||||
|
||||
```
|
||||
SETUP:
|
||||
- KeyAssociations: popolata (da Test 1)
|
||||
- Sorgente: Dati NON modificati
|
||||
|
||||
EXPECTED:
|
||||
1. Trova associazione esistente
|
||||
2. Calcola hash: identico
|
||||
3. Skip record (performance)
|
||||
4. Log: "saltato - hash identico"
|
||||
5. Nessuna chiamata API a Salesforce
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Data Implementazione**: 21 Ottobre 2025
|
||||
**File Modificato**: `Data_Coupler/Pages/DataCoupler.razor.cs`
|
||||
**Metodo**: `StartDataTransferWithComposite()`
|
||||
**Linee**: ~2840-2890
|
||||
Reference in New Issue
Block a user