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,379 @@
|
||||
# Sistema Pre-Discovery per Associazioni Esistenti
|
||||
|
||||
## Data: 21 Ottobre 2025
|
||||
|
||||
## 🎯 Obiettivo
|
||||
|
||||
Implementato un sistema di **pre-discovery automatico** che, prima di creare nuovi record, verifica se esistono già record nella destinazione che corrispondono ai dati sorgente. Se trovati, crea automaticamente un'associazione e procede con l'aggiornamento invece della creazione.
|
||||
|
||||
## 📋 Funzionamento
|
||||
|
||||
### Flusso Operativo
|
||||
|
||||
Quando l'utente clicca su "Avvia trasferimento dati", per ogni record sorgente il sistema esegue i seguenti controlli:
|
||||
|
||||
```
|
||||
1. Verifica Associazione Locale
|
||||
↓
|
||||
2. Se NON esiste → PRE-DISCOVERY nella Destinazione
|
||||
↓
|
||||
3a. Trovato → Crea Associazione + Aggiorna Record
|
||||
↓
|
||||
3b. NON Trovato → Crea Nuovo Record + Associazione
|
||||
```
|
||||
|
||||
### Dettaglio Step
|
||||
|
||||
#### Step 1: Verifica Associazione Locale
|
||||
|
||||
```csharp
|
||||
// Cerca nella tabella KeyAssociations
|
||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||
sourceKey, // Es: "C00001"
|
||||
currentEntityName, // Es: "Account"
|
||||
currentCredentialName // Es: "Salesforce_Prod"
|
||||
);
|
||||
```
|
||||
|
||||
**Risultato possibile:**
|
||||
- ✅ **Trovata**: Procedi con aggiornamento del record esistente
|
||||
- ❌ **Non trovata**: Passa allo Step 2
|
||||
|
||||
#### Step 2: Pre-Discovery nella Destinazione
|
||||
|
||||
Se non esiste associazione locale, cerca direttamente nella destinazione REST:
|
||||
|
||||
```csharp
|
||||
// Prepara ricerca usando il campo mappato
|
||||
var searchFields = new Dictionary<string, object>
|
||||
{
|
||||
{ mappedDestinationFieldName, sourceKey }
|
||||
// Es: { "cardcode__c", "C00001" }
|
||||
};
|
||||
|
||||
// Cerca nella REST API
|
||||
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
|
||||
currentEntityName, // "Account"
|
||||
searchFields // { "cardcode__c": "C00001" }
|
||||
);
|
||||
```
|
||||
|
||||
**Risultato possibile:**
|
||||
- ✅ **Trovato record**: Vai allo Step 3a
|
||||
- ❌ **Non trovato**: Vai allo Step 3b
|
||||
|
||||
#### Step 3a: Creazione Associazione + Aggiornamento
|
||||
|
||||
Se il record esiste nella destinazione:
|
||||
|
||||
```csharp
|
||||
// 1. Crea associazione
|
||||
var newAssociation = new KeyAssociation
|
||||
{
|
||||
KeyValue = sourceKey, // "C00001"
|
||||
SourceKeyField = sourceKeyField, // "CardCode"
|
||||
DestinationKeyField = "Id", // Campo ID standard
|
||||
MappedDestinationField = mappedDestinationField,// "cardcode__c"
|
||||
DestinationEntity = currentEntityName, // "Account"
|
||||
DestinationId = destinationId, // "001xx000003DGb2AAG"
|
||||
RestCredentialName = currentCredentialName, // "Salesforce_Prod"
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
Data_Hash = currentDataHash, // Hash dei dati mappati
|
||||
AdditionalInfo = JSON con metadata "PreDiscovery"
|
||||
};
|
||||
|
||||
// 2. Salva associazione
|
||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation);
|
||||
|
||||
// 3. Procedi con AGGIORNAMENTO del record esistente
|
||||
```
|
||||
|
||||
#### Step 3b: Creazione Normale
|
||||
|
||||
Se il record NON esiste:
|
||||
|
||||
```csharp
|
||||
// 1. Crea nuovo record nella destinazione
|
||||
var result = await restClient.CreateEntityAsync(entityName, data);
|
||||
|
||||
// 2. Crea associazione con il nuovo ID
|
||||
var association = new KeyAssociation { ... };
|
||||
await CredentialService.SaveKeyAssociationParallelAsync(association);
|
||||
```
|
||||
|
||||
## 🔧 Implementazione Tecnica
|
||||
|
||||
### File Modificati
|
||||
|
||||
1. **`Data_Coupler/Pages/DataCoupler.razor.cs`**
|
||||
- Metodo `StartDataTransferWithComposite()` - linea ~2620
|
||||
- Metodo `StartDataTransferOriginal()` - linea ~1290
|
||||
|
||||
### Codice Chiave
|
||||
|
||||
```csharp
|
||||
// 🔍 PRE-DISCOVERY: Se non esiste associazione, cerca nella destinazione
|
||||
if (existingAssociation == null)
|
||||
{
|
||||
Logger.LogDebug("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...", sourceKey);
|
||||
|
||||
// Cerca il campo destinazione mappato al campo chiave sorgente
|
||||
if (fieldMappings.TryGetValue(sourceKeyField, out var mappedDestinationFieldName))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Prepara i campi di ricerca
|
||||
var searchFields = new Dictionary<string, object>
|
||||
{
|
||||
{ mappedDestinationFieldName, sourceKey }
|
||||
};
|
||||
|
||||
// Cerca nella destinazione REST
|
||||
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
|
||||
currentEntityName, searchFields);
|
||||
|
||||
if (existingEntities != null && existingEntities.Count > 0)
|
||||
{
|
||||
var foundEntity = existingEntities[0];
|
||||
var destinationId = foundEntity.ContainsKey("Id")
|
||||
? foundEntity["Id"]?.ToString()
|
||||
: foundEntity.ContainsKey("id")
|
||||
? foundEntity["id"]?.ToString()
|
||||
: null;
|
||||
|
||||
if (!string.IsNullOrEmpty(destinationId))
|
||||
{
|
||||
// Crea associazione e usa per aggiornamento
|
||||
var newAssociation = new KeyAssociation { ... };
|
||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation);
|
||||
existingAssociation = newAssociation;
|
||||
existingAssociation.Id = associationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception discEx)
|
||||
{
|
||||
Logger.LogWarning(discEx, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", sourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Esempio Pratico
|
||||
|
||||
### Scenario: SAP Business One → Salesforce
|
||||
|
||||
**Record Sorgente:**
|
||||
```
|
||||
CardCode: "C00001"
|
||||
CardName: "Acme Corp"
|
||||
City: "Milano"
|
||||
```
|
||||
|
||||
**Mappings Configurati:**
|
||||
```
|
||||
CardCode → cardcode__c
|
||||
CardName → Name
|
||||
City → BillingCity
|
||||
```
|
||||
|
||||
**Campo Chiave Selezionato:** `CardCode`
|
||||
|
||||
### Caso 1: Prima Esecuzione (Record Non Esiste)
|
||||
|
||||
```
|
||||
1. Cerca associazione → ❌ Non trovata
|
||||
2. PRE-DISCOVERY in Salesforce → ❌ Non trovato
|
||||
3. CREA nuovo Account in Salesforce
|
||||
4. Crea associazione: CardCode="C00001" → Id="001xx000003DGb2AAG"
|
||||
```
|
||||
|
||||
**Risultato:**
|
||||
- Account creato con ID `001xx000003DGb2AAG`
|
||||
- Associazione salvata in KeyAssociations
|
||||
|
||||
### Caso 2: Seconda Esecuzione (Record Già Esiste)
|
||||
|
||||
```
|
||||
1. Cerca associazione → ✅ TROVATA!
|
||||
2. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG)
|
||||
```
|
||||
|
||||
**Risultato:**
|
||||
- Account aggiornato
|
||||
- Nessun duplicato
|
||||
|
||||
### Caso 3: Record Esiste ma Senza Associazione
|
||||
|
||||
**Situazione:** Il record è stato creato manualmente in Salesforce con `cardcode__c = "C00001"` ma non esiste associazione.
|
||||
|
||||
```
|
||||
1. Cerca associazione → ❌ Non trovata
|
||||
2. PRE-DISCOVERY in Salesforce → ✅ TROVATO! (ID: 001xx000003DGb2AAG)
|
||||
3. CREA associazione automatica
|
||||
4. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG)
|
||||
```
|
||||
|
||||
**Risultato:**
|
||||
- Associazione creata automaticamente
|
||||
- Account aggiornato invece di creare duplicato
|
||||
- ✨ Sincronizzazione senza duplicati!
|
||||
|
||||
## 🎁 Vantaggi
|
||||
|
||||
### 1. **Prevenzione Duplicati Automatica**
|
||||
- Il sistema trova record esistenti anche se creati manualmente
|
||||
- Non richiede pre-caricamento delle associazioni
|
||||
|
||||
### 2. **Sincronizzazione Intelligente**
|
||||
- Aggiorna invece di creare se il record esiste
|
||||
- Mantiene la coerenza tra sistemi
|
||||
|
||||
### 3. **Recupero Dati Legacy**
|
||||
- Se hai già dati in Salesforce, il sistema li "scopre" e crea le associazioni automaticamente
|
||||
- Utile per migrazioni da sistemi esistenti
|
||||
|
||||
### 4. **Performance Ottimizzate**
|
||||
- Ricerca parallela thread-safe
|
||||
- Cache delle associazioni già trovate
|
||||
|
||||
### 5. **Logging Dettagliato**
|
||||
- Traccia completa del processo di discovery
|
||||
- Debug facilitato con log prefissati `PRE-DISCOVERY:`
|
||||
|
||||
## 📋 Log di Esempio
|
||||
|
||||
### Discovery Successful
|
||||
```
|
||||
[Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione...
|
||||
[Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001'
|
||||
[Info] PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: 'C00001' -> DestinationId: '001xx000003DGb2AAG'
|
||||
[Info] PRE-DISCOVERY: Associazione creata con ID: 42
|
||||
[Info] COMPOSITE PARALLEL: Record 1 marcato per aggiornamento (EntityId: 001xx000003DGb2AAG)
|
||||
```
|
||||
|
||||
### Discovery Not Found
|
||||
```
|
||||
[Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione...
|
||||
[Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001'
|
||||
[Debug] PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: 'C00001'
|
||||
[Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione
|
||||
```
|
||||
|
||||
### Discovery Error
|
||||
```
|
||||
[Warning] PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: 'C00001'
|
||||
System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request)
|
||||
[Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione
|
||||
```
|
||||
|
||||
## ⚙️ Configurazione
|
||||
|
||||
### Requisiti
|
||||
|
||||
1. **Campo Chiave Sorgente**: Deve essere selezionato e mappato
|
||||
2. **Sistema Associazioni**: Deve essere abilitato (`useRecordAssociations = true`)
|
||||
3. **Mapping Valido**: Il campo chiave deve essere presente nei field mappings
|
||||
|
||||
### Quando Si Attiva
|
||||
|
||||
La pre-discovery si attiva **automaticamente** quando:
|
||||
- ✅ Il sistema di associazioni è abilitato
|
||||
- ✅ Non esiste associazione per il valore chiave
|
||||
- ✅ Il campo chiave è mappato a un campo destinazione
|
||||
- ✅ Il client REST supporta `FindEntitiesByKeysAsync`
|
||||
|
||||
### Quando NON Si Attiva
|
||||
|
||||
- ❌ Associazione già presente (non serve discovery)
|
||||
- ❌ Campo chiave non mappato
|
||||
- ❌ Sistema associazioni disabilitato
|
||||
- ❌ Errore durante la ricerca (fallback a creazione normale)
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test Case 1: Record Già in Destinazione
|
||||
|
||||
**Setup:**
|
||||
1. Crea manualmente un Account in Salesforce con `cardcode__c = "TEST001"`
|
||||
2. Configura mapping: `CardCode → cardcode__c`
|
||||
3. Seleziona `CardCode` come campo chiave
|
||||
|
||||
**Esecuzione:**
|
||||
- Trasferisci record sorgente con `CardCode = "TEST001"`
|
||||
|
||||
**Risultato Atteso:**
|
||||
- ✅ Pre-discovery trova il record
|
||||
- ✅ Associazione creata automaticamente
|
||||
- ✅ Record aggiornato (non creato duplicato)
|
||||
|
||||
### Test Case 2: Record Non Esiste
|
||||
|
||||
**Setup:**
|
||||
1. Salesforce NON contiene Account con `cardcode__c = "NEW001"`
|
||||
|
||||
**Esecuzione:**
|
||||
- Trasferisci record sorgente con `CardCode = "NEW001"`
|
||||
|
||||
**Risultato Atteso:**
|
||||
- ❌ Pre-discovery non trova nulla
|
||||
- ✅ Nuovo record creato
|
||||
- ✅ Associazione creata con nuovo ID
|
||||
|
||||
### Test Case 3: Più Trasferimenti
|
||||
|
||||
**Setup:**
|
||||
1. Esegui trasferimento 1 volta (crea record)
|
||||
|
||||
**Esecuzione:**
|
||||
- Esegui trasferimento 2° volta con stessi dati
|
||||
|
||||
**Risultato Atteso:**
|
||||
- ✅ Associazione trovata al primo controllo
|
||||
- ✅ Record aggiornato senza pre-discovery
|
||||
- ✅ Nessun duplicato creato
|
||||
|
||||
## 🔒 Sicurezza
|
||||
|
||||
### Thread-Safety
|
||||
- Usa `SaveKeyAssociationParallelAsync` per gestire race conditions
|
||||
- Le ricerche REST sono stateless e thread-safe
|
||||
|
||||
### Gestione Errori
|
||||
- Errori in pre-discovery non bloccano il trasferimento
|
||||
- Fallback automatico a creazione normale
|
||||
- Logging warning per diagnosi
|
||||
|
||||
### Validazione Dati
|
||||
- Verifica presenza ID valido prima di creare associazione
|
||||
- Controlla compatibilità Entity e Credential
|
||||
- Supporta varianti case-sensitive di "Id" field
|
||||
|
||||
## 📚 Riferimenti
|
||||
|
||||
### Metodi Utilizzati
|
||||
|
||||
- `FindKeyAssociationByValueParallelAsync()`
|
||||
- `FindEntitiesByKeysAsync()`
|
||||
- `SaveKeyAssociationParallelAsync()`
|
||||
- `UpdateEntityAsync()`
|
||||
|
||||
### Entità Coinvolte
|
||||
|
||||
- `KeyAssociation` - Tabella associazioni
|
||||
- `fieldMappings` - Dictionary mapping sorgente→destinazione
|
||||
- `IRestServiceClient` - Interfaccia REST per ricerca
|
||||
|
||||
## ✅ Status: IMPLEMENTATO
|
||||
|
||||
Il sistema è completamente funzionale e attivo in entrambi i metodi di trasferimento:
|
||||
- ✅ `StartDataTransferWithComposite()` (Salesforce Composite API)
|
||||
- ✅ `StartDataTransferOriginal()` (REST API standard)
|
||||
|
||||
---
|
||||
|
||||
**Versione**: 1.0
|
||||
**Data Implementazione**: 21 Ottobre 2025
|
||||
**Autore**: Sistema Data Coupler
|
||||
Reference in New Issue
Block a user