Files
Data-Coupler/PRE_DISCOVERY_SYSTEM.md
Alessio 39d7124ce1 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
2025-10-21 00:48:03 +02:00

380 lines
11 KiB
Markdown

# 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