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:
2025-10-21 00:48:03 +02:00
parent 5d9b9756cf
commit 39d7124ce1
6 changed files with 1330 additions and 39 deletions
@@ -504,6 +504,7 @@ namespace DataConnection.REST.Implementations
}
} /// <summary>
/// Finds entities by their key fields in Salesforce.
/// Uses External ID GET when possible (single field), otherwise uses SOQL query.
/// </summary>
/// <param name="entityName">The name of the SObject to search (e.g., "Account", "Contact").</param>
/// <param name="keyFields">The key fields and their values to match.</param>
@@ -522,40 +523,127 @@ namespace DataConnection.REST.Implementations
return new List<Dictionary<string, object>>();
}
// Costruisci la query SOQL
var whereConditions = keyFields.Select(kvp =>
// 🔍 IMPORTANTE: L'approccio External ID GET funziona SOLO per campi marcati come External ID in Salesforce
// Per la maggior parte dei campi, è più affidabile usare direttamente SOQL query
// Se vuoi abilitare il tentativo External ID, decommenta il blocco sotto
/* EXTERNAL ID GET - DISABILITATO PER DEFAULT
if (keyFields.Count == 1)
{
var value = kvp.Value?.ToString() ?? "";
// Se il valore è una stringa, aggiungi le virgolette
if (kvp.Value is string)
var kvp = keyFields.First();
var fieldName = kvp.Key;
var fieldValue = kvp.Value?.ToString() ?? "";
try
{
// Tentativo 1: GET con External ID (funziona SOLO per campi External ID)
// Endpoint: /sobjects/{objectType}/{fieldName}/{fieldValue}
var externalIdEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/{entityName}/{fieldName}/{Uri.EscapeDataString(fieldValue)}";
Console.WriteLine($"⚠️ Tentativo GET con External ID: {externalIdEndpoint}");
Console.WriteLine($"⚠️ NOTA: Questo funziona SOLO se '{fieldName}' è marcato come External ID in Salesforce");
var getResponse = await _httpClient.GetAsync(externalIdEndpoint, cancellationToken);
if (getResponse.IsSuccessStatusCode)
{
var entity = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>(cancellationToken: cancellationToken);
if (entity != null)
{
Console.WriteLine($"✅ Trovato tramite External ID GET: Id={entity.GetValueOrDefault("Id")}");
return new List<Dictionary<string, object>> { entity };
}
}
else if (getResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine($"⚠️ External ID GET ha restituito 404 - Il campo '{fieldName}' probabilmente non è External ID");
Console.WriteLine($" Uso SOQL query come fallback (metodo universale)");
}
else
{
Console.WriteLine($"External ID GET non disponibile (Status: {getResponse.StatusCode}), uso SOQL query");
}
}
catch (Exception externalIdEx)
{
Console.WriteLine($"External ID GET fallito: {externalIdEx.Message}, uso SOQL query");
}
}
*/
// 🔍 SOQL Query: metodo universale che funziona per TUTTI i campi (External ID o no)
Console.WriteLine("📋 Usando SOQL Query per la ricerca (metodo universale)...");
// Costruisci le condizioni WHERE
var whereConditions = new List<string>();
foreach (var kvp in keyFields)
{
var fieldName = kvp.Key;
var fieldValue = kvp.Value;
Console.WriteLine($" 🔎 Ricerca per campo: {fieldName} = {fieldValue}");
var value = fieldValue?.ToString() ?? "";
// Se il valore è una stringa, aggiungi le virgolette ed escape
if (fieldValue is string)
{
value = $"'{value.Replace("'", "\\'")}'"; // Escape delle virgolette
}
return $"{kvp.Key} = {value}";
});
whereConditions.Add($"{fieldName} = {value}");
}
var query = $"SELECT Id FROM {entityName} WHERE {string.Join(" AND ", whereConditions)}";
Console.WriteLine($"SOQL Query: {query}");
var encodedQuery = Uri.EscapeDataString(query);
var queryEndpoint = $"/services/data/v59.0/query/?q={encodedQuery}"; var response = await GetAsync<SalesforceQueryResponse>(queryEndpoint, cancellationToken);
// Costruisci la query: seleziona tutti i campi forniti + Id
var fieldsToSelect = new List<string> { "Id" };
fieldsToSelect.AddRange(keyFields.Keys.Where(k => k != "Id"));
if (response?.Records != null)
var query = $"SELECT {string.Join(", ", fieldsToSelect)} FROM {entityName} WHERE {string.Join(" AND ", whereConditions)}";
Console.WriteLine($"📝 SOQL Query: {query}");
// Usa l'endpoint query con autenticazione corretta
var encodedQuery = Uri.EscapeDataString(query);
var queryEndpoint = $"{_instanceUrl}/services/data/v60.0/query/?q={encodedQuery}";
Console.WriteLine($"Query Endpoint: {queryEndpoint}");
// Usa GET con autenticazione inclusa nell'HttpClient (già configurato)
var response = await _httpClient.GetAsync(queryEndpoint, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var results = response.Records.Select(record =>
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"SOQL Query failed: {response.StatusCode}");
Console.WriteLine($"Error details: {errorContent}");
return new List<Dictionary<string, object>>();
}
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"SOQL Response: {responseContent}");
var queryResponse = JsonSerializer.Deserialize<SalesforceQueryResponse>(responseContent, SalesforceJsonOptions);
if (queryResponse?.Records != null && queryResponse.Records.Any())
{
var results = queryResponse.Records.Select(record =>
record as Dictionary<string, object> ?? new Dictionary<string, object>()
).ToList();
Console.WriteLine($"Found {results.Count} entities matching the key fields");
Console.WriteLine($"✅ Trovati {results.Count} record tramite SOQL");
foreach (var result in results)
{
Console.WriteLine($" - Id: {result.GetValueOrDefault("Id")}, Campi: {string.Join(", ", result.Keys)}");
}
return results;
}
Console.WriteLine("No entities found matching the key fields");
Console.WriteLine("Nessun record trovato");
return new List<Dictionary<string, object>>();
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity search: {ex.Message}");
Console.WriteLine($"Errore durante la ricerca Salesforce: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
return new List<Dictionary<string, object>>();
}
}