using CredentialManager.Models;
using DataConnection.CredentialManagement.Interfaces;
using DataConnection.REST.Implementations;
using DataConnection.REST.Interfaces;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.Services;
///
/// Interfaccia per il servizio di sincronizzazione delle cancellazioni
///
public interface IDeletionSyncService
{
///
/// Sincronizza le cancellazioni dalla sorgente alla destinazione
///
Task SyncDeletionsAsync(
List currentSourceKeyValues,
string destinationEntity,
string restCredentialName,
IRestServiceClient restClient,
DeletionSyncOptions options);
}
///
/// Servizio per la sincronizzazione delle cancellazioni dalla sorgente alla destinazione
///
public class DeletionSyncService : IDeletionSyncService
{
private readonly IDataConnectionCredentialService _credentialService;
private readonly ILogger _logger;
public DeletionSyncService(
IDataConnectionCredentialService credentialService,
ILogger logger)
{
_credentialService = credentialService ?? throw new ArgumentNullException(nameof(credentialService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// Sincronizza le cancellazioni dalla sorgente alla destinazione
///
public async Task SyncDeletionsAsync(
List currentSourceKeyValues,
string destinationEntity,
string restCredentialName,
IRestServiceClient restClient,
DeletionSyncOptions options)
{
var result = new DeletionSyncResult
{
StartTime = DateTime.Now
};
try
{
_logger.LogInformation("Inizio sincronizzazione cancellazioni per {Entity} - {Credential}",
destinationEntity, restCredentialName);
// Step 1: Marca le associazioni come cancellate se non sono più presenti nella sorgente
var markedCount = await _credentialService.MarkDeletedAssociationsAsync(
currentSourceKeyValues, destinationEntity, restCredentialName);
result.DeletedRecordsDetected = markedCount;
_logger.LogInformation("Rilevati {Count} record cancellati dalla sorgente", markedCount);
if (markedCount == 0)
{
result.IsSuccess = true;
result.Message = "Nessun record cancellato rilevato";
result.EndTime = DateTime.Now;
return result;
}
// Step 2: Ottieni le cancellazioni in attesa di sincronizzazione
var pendingDeletions = await _credentialService.GetPendingDeletionsAsync(
destinationEntity, restCredentialName);
_logger.LogInformation("Trovate {Count} cancellazioni in attesa di sincronizzazione",
pendingDeletions.Count);
// Step 3: Esegui le cancellazioni nella destinazione.
// Per Salesforce usiamo le Composite API in batch (ceil(N/25) HTTP call invece di N);
// per gli altri client REST manteniamo il loop sequenziale (nessun batch supportato).
if (restClient is SalesforceServiceClient salesforceClient)
{
await ExecuteBatchedSalesforceDeletionsAsync(
salesforceClient, destinationEntity, pendingDeletions, options, result);
}
else
{
await ExecuteSequentialDeletionsAsync(
restClient, destinationEntity, pendingDeletions, options, result);
}
result.IsSuccess = result.SyncErrors == 0;
result.Message = result.IsSuccess
? $"Sincronizzazione completata: {result.DeletedRecordsSynced} record cancellati"
: $"Sincronizzazione completata con errori: {result.DeletedRecordsSynced} sincronizzati, {result.SyncErrors} errori";
_logger.LogInformation(result.Message);
}
catch (Exception ex)
{
result.IsSuccess = false;
result.Message = $"Errore durante la sincronizzazione delle cancellazioni: {ex.Message}";
result.Errors.Add(ex.Message);
_logger.LogError(ex, "Errore durante la sincronizzazione delle cancellazioni");
}
result.EndTime = DateTime.Now;
return result;
}
///
/// Esegue le cancellazioni in batch via Salesforce Composite API.
/// Riduce N round-trip HTTP a ceil(N/25) batch in parallelo.
///
private async Task ExecuteBatchedSalesforceDeletionsAsync(
SalesforceServiceClient salesforceClient,
string destinationEntity,
List pendingDeletions,
DeletionSyncOptions options,
DeletionSyncResult result)
{
// Per Mark serve MarkField e MarkValue: validazione preventiva (un solo log)
if (options.Action == DeletionAction.Mark &&
(string.IsNullOrEmpty(options.MarkField) || string.IsNullOrEmpty(options.MarkValue)))
{
const string err = "MarkField e MarkValue devono essere specificati per DeletionAction.Mark";
_logger.LogWarning(err);
foreach (var d in pendingDeletions)
{
result.SyncErrors++;
result.Errors.Add($"KeyValue: {d.KeyValue} - {err}");
}
return;
}
// Mappa entityId → KeyAssociation per ricostruire l'associazione dal risultato batch
var deletionsById = pendingDeletions
.Where(d => !string.IsNullOrEmpty(d.DestinationId))
.GroupBy(d => d.DestinationId)
.ToDictionary(g => g.Key, g => g.First()); // se duplicati, prima occorrenza
var entityIds = deletionsById.Keys.ToList();
if (entityIds.Count == 0)
return;
_logger.LogInformation("DELETION SYNC (Salesforce batched): {Count} record, action={Action}",
entityIds.Count, options.Action);
List batchResults;
try
{
switch (options.Action)
{
case DeletionAction.Delete:
batchResults = await salesforceClient.BatchDeleteEntitiesAsync(destinationEntity, entityIds);
break;
case DeletionAction.Deactivate:
// Aggiorna IsActive/Active = false in batch.
// Non sappiamo a priori quale dei due campi esista sull'SObject: proviamo IsActive,
// se Salesforce ritorna errore il record verrà segnalato come fallito.
var deactivateUpdates = entityIds.ToDictionary(
id => id,
_ => (Dictionary)new Dictionary
{
{ "IsActive", false }
});
batchResults = await salesforceClient.BatchUpdateEntitiesAsync(destinationEntity, deactivateUpdates);
break;
case DeletionAction.Mark:
batchResults = await salesforceClient.BatchPatchSingleFieldAsync(
destinationEntity, entityIds, options.MarkField!, options.MarkValue!);
break;
default:
_logger.LogWarning("DELETION SYNC: azione non supportata: {Action}", options.Action);
foreach (var d in pendingDeletions)
{
result.SyncErrors++;
result.Errors.Add($"KeyValue: {d.KeyValue} - Azione non supportata: {options.Action}");
}
return;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "DELETION SYNC: errore nell'esecuzione del batch Salesforce");
foreach (var d in pendingDeletions)
{
result.SyncErrors++;
result.Errors.Add($"KeyValue: {d.KeyValue} - {ex.Message}");
}
return;
}
// Aggiorna lo stato delle cancellazioni in DB in parallelo per i record sincronizzati con successo
var markSyncedTasks = new List();
foreach (var br in batchResults)
{
if (!deletionsById.TryGetValue(br.EntityId ?? string.Empty, out var deletion))
continue;
if (br.Success)
{
result.DeletedRecordsSynced++;
markSyncedTasks.Add(_credentialService.MarkDeletionSyncedAsync(deletion.Id));
_logger.LogDebug(
"DELETION SYNC: KeyValue={KeyValue}, DestinationId={DestinationId}, Action={Action} OK",
deletion.KeyValue, deletion.DestinationId, options.Action);
}
else
{
result.SyncErrors++;
var msg = $"KeyValue: {deletion.KeyValue} - {br.ErrorMessage ?? "Unknown error"}";
result.Errors.Add(msg);
_logger.LogWarning("DELETION SYNC fallita: {Msg}", msg);
}
}
if (markSyncedTasks.Count > 0)
await Task.WhenAll(markSyncedTasks);
}
///
/// Fallback sequenziale per client REST non Salesforce.
///
private async Task ExecuteSequentialDeletionsAsync(
IRestServiceClient restClient,
string destinationEntity,
List pendingDeletions,
DeletionSyncOptions options,
DeletionSyncResult result)
{
foreach (var deletion in pendingDeletions)
{
try
{
bool syncSuccess = false;
string errorMessage = "";
switch (options.Action)
{
case DeletionAction.Delete:
syncSuccess = await DeleteRecordAsync(restClient, destinationEntity, deletion.DestinationId);
break;
case DeletionAction.Deactivate:
syncSuccess = await DeactivateRecordAsync(restClient, destinationEntity, deletion.DestinationId);
break;
case DeletionAction.Mark:
if (string.IsNullOrEmpty(options.MarkField) || string.IsNullOrEmpty(options.MarkValue))
{
errorMessage = "MarkField e MarkValue devono essere specificati per DeletionAction.Mark";
_logger.LogWarning(errorMessage);
result.Errors.Add($"KeyValue: {deletion.KeyValue} - {errorMessage}");
continue;
}
syncSuccess = await MarkRecordAsync(restClient, destinationEntity, deletion.DestinationId,
options.MarkField, options.MarkValue);
break;
default:
errorMessage = $"Azione di cancellazione non supportata: {options.Action}";
_logger.LogWarning(errorMessage);
result.Errors.Add($"KeyValue: {deletion.KeyValue} - {errorMessage}");
continue;
}
if (syncSuccess)
{
await _credentialService.MarkDeletionSyncedAsync(deletion.Id);
result.DeletedRecordsSynced++;
}
else
{
result.SyncErrors++;
result.Errors.Add($"Errore nella sincronizzazione della cancellazione per KeyValue: {deletion.KeyValue}");
}
}
catch (Exception ex)
{
result.SyncErrors++;
result.Errors.Add($"KeyValue: {deletion.KeyValue} - {ex.Message}");
_logger.LogError(ex, "Errore nella sincronizzazione della cancellazione per {KeyValue}", deletion.KeyValue);
}
}
}
///
/// Elimina fisicamente un record dalla destinazione
///
private async Task DeleteRecordAsync(
IRestServiceClient restClient,
string entityName,
string entityId)
{
try
{
return await restClient.DeleteEntityAsync(entityName, entityId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'eliminazione del record {EntityId} dall'entità {Entity}",
entityId, entityName);
return false;
}
}
///
/// Marca un record come inattivo nella destinazione
///
private async Task DeactivateRecordAsync(
IRestServiceClient restClient,
string entityName,
string entityId)
{
try
{
var updateData = new Dictionary
{
{ "IsActive", false },
{ "Active", false } // Prova entrambi i campi comuni
};
var result = await restClient.UpdateEntityAsync(entityName, entityId, updateData);
return result != null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella disattivazione del record {EntityId} dall'entità {Entity}",
entityId, entityName);
return false;
}
}
///
/// Imposta un campo personalizzato per marcare un record come cancellato
///
private async Task MarkRecordAsync(
IRestServiceClient restClient,
string entityName,
string entityId,
string markField,
string markValue)
{
try
{
var updateData = new Dictionary
{
{ markField, markValue }
};
var result = await restClient.UpdateEntityAsync(entityName, entityId, updateData);
return result != null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella marcatura del record {EntityId} dall'entità {Entity}",
entityId, entityName);
return false;
}
}
}
///
/// Opzioni per la sincronizzazione delle cancellazioni
///
public class DeletionSyncOptions
{
public DeletionAction Action { get; set; } = DeletionAction.Delete;
public string? MarkField { get; set; }
public string? MarkValue { get; set; }
}
///
/// Azione da eseguire per i record cancellati
///
public enum DeletionAction
{
///
/// Elimina fisicamente il record
///
Delete,
///
/// Marca il record come inattivo
///
Deactivate,
///
/// Imposta un campo personalizzato
///
Mark
}
///
/// Risultato della sincronizzazione delle cancellazioni
///
public class DeletionSyncResult
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = "";
public int DeletedRecordsDetected { get; set; }
public int DeletedRecordsSynced { get; set; }
public int SyncErrors { get; set; }
public List Errors { get; set; } = new();
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public TimeSpan Duration => EndTime - StartTime;
}