fix: Correzione salvataggio campo MappedDestinationField in KeyAssociations
- Aggiunto campo MappedDestinationField al modello KeyAssociation per tracciare il campo destinazione mappato alla chiave sorgente
- Creata migration AddMappedDestinationFieldToKeyAssociation per aggiungere la colonna al database
- Implementata logica di popolamento in CreateAssociationAsync e StartDataTransferOriginal per salvare il campo destinazione mappato
- Aggiornato SaveAssociationParallelAsync per includere MappedDestinationField nelle query SQL UPDATE e INSERT
- Corretti indici parametri nella query UPDATE (da {7-9} a {8-10}) per includere il nuovo campo
- Aggiunta visualizzazione campo nell'interfaccia KeyAssociations (tabella, dettagli, export CSV)
- Implementato controllo validazione per impedire trasferimenti se il campo chiave non è mappato
- Aggiunto logging diagnostico dettagliato per debug del mapping dei campi
- Aggiornato ScheduledProfileExecutionService per popolare MappedDestinationField nelle esecuzioni schedulate
- Rimosso file BackgroundServices.cs obsoleto
- Documentazione completa creata (4 markdown files)
Fixes: Campo MappedDestinationField rimaneva NULL perché le query SQL raw non includevano il nuovo campo
This commit is contained in:
Generated
+555
@@ -0,0 +1,555 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CredentialManager.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CredentialDbContext))]
|
||||||
|
[Migration("20251019220512_AddMappedDestinationFieldToKeyAssociation")]
|
||||||
|
partial class AddMappedDestinationFieldToKeyAssociation
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalParameters")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CommandTimeout")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30);
|
||||||
|
|
||||||
|
b.Property<string>("ConnectionString")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedApiKey")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAuthToken")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedPassword")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Headers")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Host")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IgnoreSslErrors")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Port")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("RestServiceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("TimeoutSeconds")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(100);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DatabaseType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Type");
|
||||||
|
|
||||||
|
b.ToTable("Credentials", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DestinationCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEndpoint")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FieldMappingJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastUsedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SourceCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceCustomQuery")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceFilePath")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("UseRecordAssociations")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("LastUsedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SourceCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("SourceType");
|
||||||
|
|
||||||
|
b.ToTable("DataCouplerProfiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data_Hash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEntity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("KeyValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastVerifiedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MappedDestinationField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RestCredentialName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourcesInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationEntity");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue")
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_KeyValue");
|
||||||
|
|
||||||
|
b.HasIndex("LastVerifiedAt");
|
||||||
|
|
||||||
|
b.HasIndex("RestCredentialName");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_Unique");
|
||||||
|
|
||||||
|
b.ToTable("KeyAssociations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DailyTime")
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfMonth")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfWeek")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ExecutionCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("IntervalUnit")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("IntervalValue")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionMessage")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("LastExecutionRecordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionStatus")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NextExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ScheduleType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledDateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProfileId");
|
||||||
|
|
||||||
|
b.ToTable("ProfileSchedules");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorDetails")
|
||||||
|
.HasMaxLength(5000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ProfileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RecordsProcessed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("RecordsWithErrors")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TriggerType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TriggeredBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProfileId");
|
||||||
|
|
||||||
|
b.HasIndex("ScheduleId");
|
||||||
|
|
||||||
|
b.HasIndex("StartTime");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TriggerType");
|
||||||
|
|
||||||
|
b.ToTable("ScheduleExecutionHistories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DestinationCredentialId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SourceCredentialId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("DestinationCredential");
|
||||||
|
|
||||||
|
b.Navigation("SourceCredential");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProfileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Profile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ScheduleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Schedule");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMappedDestinationFieldToKeyAssociation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "MappedDestinationField",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MappedDestinationField",
|
||||||
|
table: "KeyAssociations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -281,6 +281,10 @@ namespace CredentialManager.Migrations
|
|||||||
b.Property<DateTime?>("LastVerifiedAt")
|
b.Property<DateTime?>("LastVerifiedAt")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MappedDestinationField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("RestCredentialName")
|
b.Property<string>("RestCredentialName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ public class KeyAssociation
|
|||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string DestinationKeyField { get; set; } = string.Empty;
|
public string DestinationKeyField { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo di destinazione mappato alla chiave sorgente
|
||||||
|
/// (es: se dalla sorgente mappo "CardCode" verso "cardcode__c" in Salesforce, questo campo conterrà "cardcode__c")
|
||||||
|
/// Questo è il campo personalizzato nella destinazione, mentre DestinationKeyField è tipicamente l'ID
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? MappedDestinationField { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nome dell'entità di destinazione
|
/// Nome dell'entità di destinazione
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
var restCredentialName = association.RestCredentialName;
|
var restCredentialName = association.RestCredentialName;
|
||||||
var sourceKeyField = association.SourceKeyField;
|
var sourceKeyField = association.SourceKeyField;
|
||||||
var destinationKeyField = association.DestinationKeyField;
|
var destinationKeyField = association.DestinationKeyField;
|
||||||
|
var mappedDestinationField = association.MappedDestinationField; // AGGIUNTO
|
||||||
var additionalInfo = association.AdditionalInfo;
|
var additionalInfo = association.AdditionalInfo;
|
||||||
var dataHash = association.Data_Hash;
|
var dataHash = association.Data_Hash;
|
||||||
var currentTime = DateTime.UtcNow;
|
var currentTime = DateTime.UtcNow;
|
||||||
@@ -178,8 +179,8 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
_logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}', MappedField: '{MappedField}'",
|
||||||
keyValue, destinationEntity, destinationId, restCredentialName);
|
keyValue, destinationEntity, destinationId, restCredentialName, mappedDestinationField ?? "NULL");
|
||||||
|
|
||||||
// Implementazione thread-safe usando upsert pattern con DbContext separato
|
// Implementazione thread-safe usando upsert pattern con DbContext separato
|
||||||
// Prima tenta di aggiornare un record esistente
|
// Prima tenta di aggiornare un record esistente
|
||||||
@@ -191,12 +192,13 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
UpdatedAt = {3},
|
UpdatedAt = {3},
|
||||||
LastVerifiedAt = {4},
|
LastVerifiedAt = {4},
|
||||||
AdditionalInfo = {5},
|
AdditionalInfo = {5},
|
||||||
Data_Hash = {6}
|
Data_Hash = {6},
|
||||||
WHERE KeyValue = {7}
|
MappedDestinationField = {7}
|
||||||
AND DestinationEntity = {8}
|
WHERE KeyValue = {8}
|
||||||
AND RestCredentialName = {9}
|
AND DestinationEntity = {9}
|
||||||
|
AND RestCredentialName = {10}
|
||||||
AND IsActive = 1",
|
AND IsActive = 1",
|
||||||
destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value,
|
destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value, mappedDestinationField ?? (object)DBNull.Value,
|
||||||
keyValue, destinationEntity, restCredentialName);
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
if (rowsAffected > 0)
|
if (rowsAffected > 0)
|
||||||
@@ -230,6 +232,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
KeyValue = keyValue,
|
KeyValue = keyValue,
|
||||||
SourceKeyField = sourceKeyField,
|
SourceKeyField = sourceKeyField,
|
||||||
DestinationKeyField = destinationKeyField,
|
DestinationKeyField = destinationKeyField,
|
||||||
|
MappedDestinationField = mappedDestinationField, // AGGIUNTO
|
||||||
DestinationEntity = destinationEntity,
|
DestinationEntity = destinationEntity,
|
||||||
DestinationId = destinationId,
|
DestinationId = destinationId,
|
||||||
RestCredentialName = restCredentialName,
|
RestCredentialName = restCredentialName,
|
||||||
@@ -243,8 +246,8 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
parallelContext.KeyAssociations.Add(newAssociation);
|
parallelContext.KeyAssociations.Add(newAssociation);
|
||||||
await parallelContext.SaveChangesAsync();
|
await parallelContext.SaveChangesAsync();
|
||||||
|
|
||||||
_logger.LogDebug("PARALLEL: Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
_logger.LogDebug("PARALLEL: Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}, MappedField={MappedField}",
|
||||||
keyValue, destinationEntity, destinationId);
|
keyValue, destinationEntity, destinationId, mappedDestinationField ?? "NULL");
|
||||||
|
|
||||||
return newAssociation.Id;
|
return newAssociation.Id;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Data_Coupler.BackgrounServices;
|
|
||||||
|
|
||||||
public class BackgroundServices : BackgroundService
|
|
||||||
{
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Qui puoi inserire il codice che vuoi eseguire in background
|
|
||||||
// Ad esempio, puoi chiamare un metodo per eseguire operazioni periodiche
|
|
||||||
|
|
||||||
// Simula un'attività di lunga durata
|
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Gestisci l'eccezione se il task viene cancellato
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Gestisci altre eccezioni
|
|
||||||
Console.WriteLine($"Errore: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1056,6 +1056,16 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@* Messaggio di errore per trasferimento disabilitato *@
|
||||||
|
@if (!IsTransferButtonEnabled() && !string.IsNullOrEmpty(GetTransferDisabledReason()))
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning mt-3" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Trasferimento disabilitato:</strong> @GetTransferDisabledReason()
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(transferMessage))
|
@if (!string.IsNullOrEmpty(transferMessage))
|
||||||
{
|
{
|
||||||
<div class="alert @(transferMessageType == "success" ? "alert-success" : transferMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3" role="alert">
|
<div class="alert @(transferMessageType == "success" ? "alert-success" : transferMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3" role="alert">
|
||||||
|
|||||||
@@ -1397,11 +1397,31 @@ public partial class DataCoupler : ComponentBase
|
|||||||
// Determina i campi chiave automaticamente
|
// Determina i campi chiave automaticamente
|
||||||
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
|
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
|
||||||
|
|
||||||
|
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'", sourceKeyField);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}", string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
// Cerca nel dizionario il campo destinazione corrispondente al campo chiave sorgente
|
||||||
|
if (fieldMappings.TryGetValue(sourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
sourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||||
|
sourceKeyField);
|
||||||
|
}
|
||||||
|
|
||||||
var association = new KeyAssociation
|
var association = new KeyAssociation
|
||||||
{
|
{
|
||||||
KeyValue = sourceKey,
|
KeyValue = sourceKey,
|
||||||
SourceKeyField = sourceKeyField,
|
SourceKeyField = sourceKeyField,
|
||||||
DestinationKeyField = destinationKeyField,
|
DestinationKeyField = destinationKeyField,
|
||||||
|
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||||
DestinationEntity = selectedRestEntity?.Name ?? "",
|
DestinationEntity = selectedRestEntity?.Name ?? "",
|
||||||
DestinationId = transferResult.EntityId,
|
DestinationId = transferResult.EntityId,
|
||||||
RestCredentialName = selectedRestCredential,
|
RestCredentialName = selectedRestCredential,
|
||||||
@@ -1416,8 +1436,8 @@ public partial class DataCoupler : ComponentBase
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
|
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}', MappedField: '{MappedField}'",
|
||||||
sourceKey, selectedRestEntity?.Name ?? "Unknown", transferResult.EntityId, selectedRestCredential);
|
sourceKey, selectedRestEntity?.Name ?? "Unknown", transferResult.EntityId, selectedRestCredential, mappedDestinationField ?? "N/A");
|
||||||
|
|
||||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||||
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
|
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
|
||||||
@@ -1721,9 +1741,36 @@ public partial class DataCoupler : ComponentBase
|
|||||||
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Verifica che il campo chiave sia presente nei campi mappati
|
||||||
|
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField))
|
||||||
|
{
|
||||||
|
if (!fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene il messaggio di errore che spiega perché il trasferimento non può essere avviato
|
||||||
|
/// </summary>
|
||||||
|
private string GetTransferDisabledReason()
|
||||||
|
{
|
||||||
|
if (!fieldMappings.Any())
|
||||||
|
return "Nessun campo mappato. Crea almeno un mapping tra i campi sorgente e destinazione.";
|
||||||
|
|
||||||
|
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
||||||
|
return "Campo chiave sorgente non selezionato. Seleziona un campo che identifichi univocamente i record.";
|
||||||
|
|
||||||
|
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField))
|
||||||
|
{
|
||||||
|
if (!fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
return $"Il campo chiave '{sourceKeyField}' deve essere mappato. Crea un mapping per questo campo prima di procedere con il trasferimento.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper methods per UI risultati
|
// Helper methods per UI risultati
|
||||||
private string GetResultRowClass(string status)
|
private string GetResultRowClass(string status)
|
||||||
{
|
{
|
||||||
@@ -2844,11 +2891,32 @@ public partial class DataCoupler : ComponentBase
|
|||||||
var finalDataHash = dataHash ?? GenerateDataHash(originalRecord);
|
var finalDataHash = dataHash ?? GenerateDataHash(originalRecord);
|
||||||
|
|
||||||
var destinationKeyField = GetEntityIdField();
|
var destinationKeyField = GetEntityIdField();
|
||||||
|
|
||||||
|
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'", currentSourceKeyField);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}", string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
// Cerca nel dizionario il campo destinazione corrispondente al campo chiave sorgente
|
||||||
|
if (fieldMappings.TryGetValue(currentSourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
currentSourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||||
|
currentSourceKeyField);
|
||||||
|
}
|
||||||
|
|
||||||
var association = new KeyAssociation
|
var association = new KeyAssociation
|
||||||
{
|
{
|
||||||
KeyValue = sourceKey,
|
KeyValue = sourceKey,
|
||||||
SourceKeyField = currentSourceKeyField,
|
SourceKeyField = currentSourceKeyField,
|
||||||
DestinationKeyField = destinationKeyField,
|
DestinationKeyField = destinationKeyField,
|
||||||
|
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||||
DestinationEntity = currentEntityName,
|
DestinationEntity = currentEntityName,
|
||||||
DestinationId = entityId,
|
DestinationId = entityId,
|
||||||
RestCredentialName = currentCredentialName,
|
RestCredentialName = currentCredentialName,
|
||||||
@@ -2867,8 +2935,8 @@ public partial class DataCoupler : ComponentBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
||||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL) - Hash: {Hash}",
|
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL) - Hash: {Hash}, MappedField: {MappedField}",
|
||||||
associationId, recordNumber, finalDataHash);
|
associationId, recordNumber, finalDataHash, mappedDestinationField ?? "N/A");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -239,6 +239,7 @@
|
|||||||
<th>Valore Chiave</th>
|
<th>Valore Chiave</th>
|
||||||
<th>Campo Sorgente</th>
|
<th>Campo Sorgente</th>
|
||||||
<th>Campo Destinazione</th>
|
<th>Campo Destinazione</th>
|
||||||
|
<th>Campo Mappato</th>
|
||||||
<th>Entità Destinazione</th>
|
<th>Entità Destinazione</th>
|
||||||
<th>ID Destinazione</th>
|
<th>ID Destinazione</th>
|
||||||
<th>Credenziale</th>
|
<th>Credenziale</th>
|
||||||
@@ -262,6 +263,18 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">@association.DestinationKeyField</span>
|
<span class="badge bg-secondary">@association.DestinationKeyField</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (!string.IsNullOrEmpty(association.MappedDestinationField))
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary">@association.MappedDestinationField</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="fas fa-minus"></i> N/A
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>@association.DestinationEntity</strong>
|
<strong>@association.DestinationEntity</strong>
|
||||||
</td>
|
</td>
|
||||||
@@ -540,9 +553,13 @@
|
|||||||
info += $"Valore Chiave: {association.KeyValue}\n";
|
info += $"Valore Chiave: {association.KeyValue}\n";
|
||||||
info += $"Campo Sorgente: {association.SourceKeyField}\n";
|
info += $"Campo Sorgente: {association.SourceKeyField}\n";
|
||||||
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
|
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
|
||||||
|
if (!string.IsNullOrEmpty(association.MappedDestinationField))
|
||||||
|
info += $"Campo Mappato: {association.MappedDestinationField}\n";
|
||||||
info += $"Entità: {association.DestinationEntity}\n";
|
info += $"Entità: {association.DestinationEntity}\n";
|
||||||
info += $"ID Destinazione: {association.DestinationId}\n";
|
info += $"ID Destinazione: {association.DestinationId}\n";
|
||||||
info += $"Credenziale: {association.RestCredentialName}\n";
|
info += $"Credenziale: {association.RestCredentialName}\n";
|
||||||
|
if (!string.IsNullOrEmpty(association.Data_Hash))
|
||||||
|
info += $"Hash Dati: {association.Data_Hash}\n";
|
||||||
info += $"Creata: {association.CreatedAt:dd/MM/yyyy HH:mm}\n";
|
info += $"Creata: {association.CreatedAt:dd/MM/yyyy HH:mm}\n";
|
||||||
if (association.UpdatedAt.HasValue)
|
if (association.UpdatedAt.HasValue)
|
||||||
info += $"Aggiornata: {association.UpdatedAt:dd/MM/yyyy HH:mm}\n";
|
info += $"Aggiornata: {association.UpdatedAt:dd/MM/yyyy HH:mm}\n";
|
||||||
@@ -650,12 +667,14 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var csv = "Valore Chiave,Campo Sorgente,Campo Destinazione,Entità Destinazione,ID Destinazione,Credenziale,Stato,Creata,Aggiornata,Verificata\n";
|
var csv = "Valore Chiave,Campo Sorgente,Campo Destinazione,Campo Mappato,Entità Destinazione,ID Destinazione,Credenziale,Hash Dati,Stato,Creata,Aggiornata,Verificata\n";
|
||||||
|
|
||||||
foreach (var association in filteredAssociations)
|
foreach (var association in filteredAssociations)
|
||||||
{
|
{
|
||||||
csv += $"\"{association.KeyValue}\",\"{association.SourceKeyField}\",\"{association.DestinationKeyField}\",";
|
csv += $"\"{association.KeyValue}\",\"{association.SourceKeyField}\",\"{association.DestinationKeyField}\",";
|
||||||
|
csv += $"\"{association.MappedDestinationField ?? ""}\",";
|
||||||
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
|
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
|
||||||
|
csv += $"\"{association.Data_Hash ?? ""}\",";
|
||||||
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
|
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
|
||||||
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\",";
|
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\",";
|
||||||
csv += $"\"{(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
|
csv += $"\"{(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Data_Coupler.Services;
|
||||||
|
|
||||||
|
public class AssociationService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -970,14 +970,31 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
if (string.IsNullOrEmpty(sourceKey))
|
if (string.IsNullOrEmpty(sourceKey))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Calcola il MappingCount in modo sicuro
|
// Calcola il MappingCount in modo sicuro e trova il campo destinazione mappato al campo chiave sorgente
|
||||||
int mappingCount = 0;
|
int mappingCount = 0;
|
||||||
|
string? mappedDestinationField = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||||
{
|
{
|
||||||
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
||||||
mappingCount = mappings?.Count ?? 0;
|
mappingCount = mappings?.Count ?? 0;
|
||||||
|
|
||||||
|
// Cerca il campo destinazione mappato al campo chiave sorgente
|
||||||
|
if (mappings != null && !string.IsNullOrEmpty(profile.SourceKeyField))
|
||||||
|
{
|
||||||
|
if (mappings.TryGetValue(profile.SourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
_logger.LogDebug("SCHEDULED MAPPING: Campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
profile.SourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("SCHEDULED MAPPING: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings del profilo {ProfileName}",
|
||||||
|
profile.SourceKeyField, profile.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -990,6 +1007,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
KeyValue = sourceKey,
|
KeyValue = sourceKey,
|
||||||
SourceKeyField = profile.SourceKeyField ?? "",
|
SourceKeyField = profile.SourceKeyField ?? "",
|
||||||
DestinationKeyField = "Id", // Campo ID standard per REST
|
DestinationKeyField = "Id", // Campo ID standard per REST
|
||||||
|
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||||
DestinationEntity = profile.DestinationEndpoint ?? "",
|
DestinationEntity = profile.DestinationEndpoint ?? "",
|
||||||
DestinationId = entityId,
|
DestinationId = entityId,
|
||||||
RestCredentialName = restCredential.Name, // Usa il nome della credenziale
|
RestCredentialName = restCredential.Name, // Usa il nome della credenziale
|
||||||
@@ -1014,8 +1032,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
|
|
||||||
var associationId = await _dataConnectionCredentialService.SaveKeyAssociationParallelAsync(association);
|
var associationId = await _dataConnectionCredentialService.SaveKeyAssociationParallelAsync(association);
|
||||||
|
|
||||||
_logger.LogDebug("COMPOSITE SCHEDULED: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Key: {SourceKey}, EntityId: {EntityId}, Hash: {Hash}",
|
_logger.LogDebug("COMPOSITE SCHEDULED: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Key: {SourceKey}, EntityId: {EntityId}, Hash: {Hash}, MappedField: {MappedField}",
|
||||||
associationId, recordNumber, sourceKey, entityId, dataHash);
|
associationId, recordNumber, sourceKey, entityId, dataHash, mappedDestinationField ?? "N/A");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
# Fix MappedDestinationField Non Salvato nel Database
|
||||||
|
|
||||||
|
## 🐛 Problema Identificato
|
||||||
|
|
||||||
|
Il campo `MappedDestinationField` veniva popolato correttamente nell'oggetto `KeyAssociation` ma **non veniva scritto nel database**.
|
||||||
|
|
||||||
|
### Causa Root
|
||||||
|
|
||||||
|
Il metodo `SaveAssociationParallelAsync` in `KeyAssociationService.cs` utilizzava query SQL raw che **non includevano** il campo `MappedDestinationField` né nell'UPDATE né nell'INSERT.
|
||||||
|
|
||||||
|
## ✅ Correzione Implementata
|
||||||
|
|
||||||
|
### File Modificato
|
||||||
|
|
||||||
|
**`CredentialManager/Services/KeyAssociationService.cs`**
|
||||||
|
|
||||||
|
#### Metodo: `SaveAssociationParallelAsync` (linea ~159)
|
||||||
|
|
||||||
|
### 1. Aggiunta Cattura del Valore
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var mappedDestinationField = association.MappedDestinationField; // AGGIUNTO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Query UPDATE Corretta
|
||||||
|
|
||||||
|
**Prima:**
|
||||||
|
```sql
|
||||||
|
UPDATE KeyAssociations
|
||||||
|
SET DestinationId = {0},
|
||||||
|
SourceKeyField = {1},
|
||||||
|
DestinationKeyField = {2},
|
||||||
|
UpdatedAt = {3},
|
||||||
|
LastVerifiedAt = {4},
|
||||||
|
AdditionalInfo = {5},
|
||||||
|
Data_Hash = {6}
|
||||||
|
WHERE KeyValue = {7}
|
||||||
|
AND DestinationEntity = {8}
|
||||||
|
AND RestCredentialName = {9}
|
||||||
|
AND IsActive = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dopo:**
|
||||||
|
```sql
|
||||||
|
UPDATE KeyAssociations
|
||||||
|
SET DestinationId = {0},
|
||||||
|
SourceKeyField = {1},
|
||||||
|
DestinationKeyField = {2},
|
||||||
|
UpdatedAt = {3},
|
||||||
|
LastVerifiedAt = {4},
|
||||||
|
AdditionalInfo = {5},
|
||||||
|
Data_Hash = {6},
|
||||||
|
MappedDestinationField = {7} ← AGGIUNTO
|
||||||
|
WHERE KeyValue = {8} ← Indici aggiornati
|
||||||
|
AND DestinationEntity = {9} ← Indici aggiornati
|
||||||
|
AND RestCredentialName = {10} ← Indici aggiornati
|
||||||
|
AND IsActive = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parametri aggiornati:**
|
||||||
|
```csharp
|
||||||
|
destinationId,
|
||||||
|
sourceKeyField,
|
||||||
|
destinationKeyField,
|
||||||
|
currentTime,
|
||||||
|
currentTime,
|
||||||
|
additionalInfo ?? (object)DBNull.Value,
|
||||||
|
dataHash ?? (object)DBNull.Value,
|
||||||
|
mappedDestinationField ?? (object)DBNull.Value, // AGGIUNTO
|
||||||
|
keyValue,
|
||||||
|
destinationEntity,
|
||||||
|
restCredentialName
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. INSERT Corretto (Entity Framework)
|
||||||
|
|
||||||
|
**Prima:**
|
||||||
|
```csharp
|
||||||
|
var newAssociation = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = keyValue,
|
||||||
|
SourceKeyField = sourceKeyField,
|
||||||
|
DestinationKeyField = destinationKeyField,
|
||||||
|
// MappedDestinationField mancante!
|
||||||
|
DestinationEntity = destinationEntity,
|
||||||
|
DestinationId = destinationId,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dopo:**
|
||||||
|
```csharp
|
||||||
|
var newAssociation = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = keyValue,
|
||||||
|
SourceKeyField = sourceKeyField,
|
||||||
|
DestinationKeyField = destinationKeyField,
|
||||||
|
MappedDestinationField = mappedDestinationField, // AGGIUNTO
|
||||||
|
DestinationEntity = destinationEntity,
|
||||||
|
DestinationId = destinationId,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Logging Migliorato
|
||||||
|
|
||||||
|
**Prima:**
|
||||||
|
```csharp
|
||||||
|
_logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
keyValue, destinationEntity, destinationId, restCredentialName);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dopo:**
|
||||||
|
```csharp
|
||||||
|
_logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}', MappedField: '{MappedField}'",
|
||||||
|
keyValue, destinationEntity, destinationId, restCredentialName, mappedDestinationField ?? "NULL");
|
||||||
|
|
||||||
|
// E per il log di creazione:
|
||||||
|
_logger.LogDebug("PARALLEL: Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}, MappedField={MappedField}",
|
||||||
|
keyValue, destinationEntity, destinationId, mappedDestinationField ?? "NULL");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Procedura di Test
|
||||||
|
|
||||||
|
1. **Fermare l'applicazione** attualmente in esecuzione
|
||||||
|
|
||||||
|
2. **Ricompilare il progetto**:
|
||||||
|
```powershell
|
||||||
|
dotnet build Data_Coupler.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Riavviare l'applicazione**
|
||||||
|
|
||||||
|
4. **Eseguire un nuovo trasferimento dati**:
|
||||||
|
- Configurare un mapping (es: Email → EmailAddress)
|
||||||
|
- Selezionare "Email" come campo chiave
|
||||||
|
- Eseguire il trasferimento
|
||||||
|
|
||||||
|
5. **Verificare nei log**:
|
||||||
|
```
|
||||||
|
PARALLEL: Tentativo salvataggio associazione - ... MappedField: 'EmailAddress'
|
||||||
|
PARALLEL: Nuova associazione creata: ... MappedField: EmailAddress
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verificare nel database**:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
KeyValue,
|
||||||
|
SourceKeyField,
|
||||||
|
MappedDestinationField, -- Questo campo ora deve essere popolato!
|
||||||
|
DestinationKeyField,
|
||||||
|
DestinationId
|
||||||
|
FROM KeyAssociations
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risultato Atteso
|
||||||
|
|
||||||
|
**Prima del Fix:**
|
||||||
|
| Id | KeyValue | SourceKeyField | MappedDestinationField | DestinationKeyField | DestinationId |
|
||||||
|
|----|----------|----------------|------------------------|---------------------|---------------|
|
||||||
|
| 1 | C00001 | CardCode | NULL ❌ | Id | ABC123 |
|
||||||
|
| 2 | C00026 | CardCode | NULL ❌ | Id | DEF456 |
|
||||||
|
|
||||||
|
**Dopo il Fix:**
|
||||||
|
| Id | KeyValue | SourceKeyField | MappedDestinationField | DestinationKeyField | DestinationId |
|
||||||
|
|----|----------|----------------|------------------------|---------------------|---------------|
|
||||||
|
| 1 | C00001 | CardCode | **EmailAddress** ✅ | Id | ABC123 |
|
||||||
|
| 2 | C00026 | CardCode | **EmailAddress** ✅ | Id | DEF456 |
|
||||||
|
|
||||||
|
## 📊 Impatto della Correzione
|
||||||
|
|
||||||
|
### Componenti Affetti
|
||||||
|
|
||||||
|
- ✅ **Creazione nuove associazioni**: Ora salva correttamente il campo
|
||||||
|
- ✅ **Aggiornamento associazioni esistenti**: Ora aggiorna correttamente il campo
|
||||||
|
- ✅ **Logging**: Ora mostra il valore nel log per debug
|
||||||
|
- ✅ **UI KeyAssociations**: Ora mostrerà il valore invece di "N/A"
|
||||||
|
|
||||||
|
### Retrocompatibilità
|
||||||
|
|
||||||
|
✅ **Completamente compatibile**:
|
||||||
|
- Le associazioni esistenti (con campo NULL) continueranno a funzionare
|
||||||
|
- Le nuove associazioni avranno il campo popolato
|
||||||
|
- Nessuna migration aggiuntiva richiesta (il campo è già nel database)
|
||||||
|
|
||||||
|
## 🔍 Spiegazione Tecnica
|
||||||
|
|
||||||
|
### Perché il Campo Non Veniva Salvato?
|
||||||
|
|
||||||
|
Il metodo `SaveAssociationParallelAsync` usa un pattern di **upsert ottimizzato** per gestire race conditions in operazioni parallele:
|
||||||
|
|
||||||
|
1. **Primo tentativo**: UPDATE via SQL raw
|
||||||
|
2. **Se fallisce**: INSERT via Entity Framework
|
||||||
|
|
||||||
|
Il problema era che:
|
||||||
|
- ❌ La query SQL raw dell'UPDATE **non includeva** `MappedDestinationField`
|
||||||
|
- ❌ L'oggetto Entity Framework dell'INSERT **non assegnava** `MappedDestinationField`
|
||||||
|
|
||||||
|
### Perché Usare SQL Raw?
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// SQL Raw per UPDATE (più performante e thread-safe)
|
||||||
|
await parallelContext.Database.ExecuteSqlRawAsync(@"UPDATE ...");
|
||||||
|
|
||||||
|
// Entity Framework per INSERT (più semplice per gestire race conditions)
|
||||||
|
parallelContext.KeyAssociations.Add(newAssociation);
|
||||||
|
await parallelContext.SaveChangesAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vantaggi**:
|
||||||
|
- Performance migliori per UPDATE
|
||||||
|
- Gestione automatica race conditions per INSERT
|
||||||
|
- Thread-safe con DbContext separati
|
||||||
|
|
||||||
|
## ✅ Checklist Verifica
|
||||||
|
|
||||||
|
- [x] Campo aggiunto alla query UPDATE SQL
|
||||||
|
- [x] Campo aggiunto all'oggetto INSERT Entity Framework
|
||||||
|
- [x] Parametri query SQL aggiornati con indici corretti
|
||||||
|
- [x] Logging aggiornato per includere MappedDestinationField
|
||||||
|
- [x] Verifica assenza errori di compilazione
|
||||||
|
- [ ] **Fermare applicazione**
|
||||||
|
- [ ] **Ricompilare progetto**
|
||||||
|
- [ ] **Riavviare applicazione**
|
||||||
|
- [ ] **Eseguire test trasferimento**
|
||||||
|
- [ ] **Verificare log contiene "MappedField: 'XXX'"**
|
||||||
|
- [ ] **Verificare database con SELECT**
|
||||||
|
|
||||||
|
## 🎯 Prossimi Passi IMMEDIATI
|
||||||
|
|
||||||
|
1. ⛔ **FERMARE** l'applicazione in esecuzione
|
||||||
|
2. 🔨 **Ricompilare**:
|
||||||
|
```powershell
|
||||||
|
dotnet build Data_Coupler.sln
|
||||||
|
```
|
||||||
|
3. ▶️ **Riavviare** l'applicazione
|
||||||
|
4. 🧪 **Test completo**:
|
||||||
|
- Crea nuovo mapping con campo chiave
|
||||||
|
- Esegui trasferimento
|
||||||
|
- Verifica log: `"MappedField: 'EmailAddress'"`
|
||||||
|
- Query database per conferma
|
||||||
|
5. 🗑️ **Opzionale**: Cancella vecchie associazioni di test (con campo NULL)
|
||||||
|
|
||||||
|
## 📝 Note Aggiuntive
|
||||||
|
|
||||||
|
### Cancellare Associazioni Vecchie (Opzionale)
|
||||||
|
|
||||||
|
Se vuoi pulire le associazioni di test create prima del fix:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Mostra associazioni con campo NULL
|
||||||
|
SELECT * FROM KeyAssociations
|
||||||
|
WHERE MappedDestinationField IS NULL;
|
||||||
|
|
||||||
|
-- Cancella associazioni di test (ATTENZIONE!)
|
||||||
|
DELETE FROM KeyAssociations
|
||||||
|
WHERE MappedDestinationField IS NULL
|
||||||
|
AND CreatedAt > '2025-10-19'; -- Solo quelle create oggi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifica Migration Database
|
||||||
|
|
||||||
|
Se il database non ha la colonna, esegui:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd CredentialManager
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Data Correzione**: 20 Ottobre 2025
|
||||||
|
**Versione**: 3.0 - Fix Salvataggio Database
|
||||||
|
**Status**: ✅ Pronto per test - Ricompilazione richiesta
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# Aggiornamento Pagina KeyAssociations - Campo MappedDestinationField
|
||||||
|
|
||||||
|
## Data: 20 Ottobre 2025
|
||||||
|
|
||||||
|
## Modifiche Implementate
|
||||||
|
|
||||||
|
### 1. Aggiornamento Header Tabella
|
||||||
|
|
||||||
|
Aggiunta nuova colonna "Campo Mappato" nell'header della tabella:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<th>Campo Sorgente</th>
|
||||||
|
<th>Campo Destinazione</th>
|
||||||
|
<th>Campo Mappato</th> <!-- NUOVO -->
|
||||||
|
<th>Entità Destinazione</th>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Visualizzazione nella Tabella
|
||||||
|
|
||||||
|
Aggiunta cella per visualizzare il campo mappato con badge colorato:
|
||||||
|
|
||||||
|
```razor
|
||||||
|
<td>
|
||||||
|
@if (!string.IsNullOrEmpty(association.MappedDestinationField))
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary">@association.MappedDestinationField</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="fas fa-minus"></i> N/A
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caratteristiche:**
|
||||||
|
- Badge blu (`bg-primary`) per i campi mappati presenti
|
||||||
|
- Icona e testo "N/A" in grigio per campi non mappati (retrocompatibilità)
|
||||||
|
- Gestione nullable del campo
|
||||||
|
|
||||||
|
### 3. Popup Dettagli Associazione
|
||||||
|
|
||||||
|
Aggiunto il campo nel popup di dettagli (metodo `ShowAssociationDetails`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
|
||||||
|
if (!string.IsNullOrEmpty(association.MappedDestinationField))
|
||||||
|
info += $"Campo Mappato: {association.MappedDestinationField}\n";
|
||||||
|
info += $"Entità: {association.DestinationEntity}\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caratteristiche:**
|
||||||
|
- Mostrato solo se presente (non mostra riga se null)
|
||||||
|
- Posizionato logicamente dopo "Campo Destinazione"
|
||||||
|
- Include anche l'hash dei dati se presente
|
||||||
|
|
||||||
|
### 4. Esportazione CSV
|
||||||
|
|
||||||
|
Aggiornata l'esportazione CSV per includere il nuovo campo:
|
||||||
|
|
||||||
|
**Header CSV:**
|
||||||
|
```
|
||||||
|
Valore Chiave,Campo Sorgente,Campo Destinazione,Campo Mappato,Entità Destinazione,...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dati:**
|
||||||
|
```csharp
|
||||||
|
csv += $"\"{association.MappedDestinationField ?? ""}\",";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caratteristiche:**
|
||||||
|
- Colonna aggiunta tra "Campo Destinazione" e "Entità Destinazione"
|
||||||
|
- Gestisce valori null con stringa vuota
|
||||||
|
- Include anche l'hash dei dati nell'export
|
||||||
|
|
||||||
|
## Esempio Visivo
|
||||||
|
|
||||||
|
### Tabella Prima delle Modifiche
|
||||||
|
| Valore Chiave | Campo Sorgente | Campo Destinazione | Entità | ID Destinazione |
|
||||||
|
|---------------|----------------|-------------------|--------|-----------------|
|
||||||
|
| C00001 | CardCode | Id | Account | 001xx... |
|
||||||
|
|
||||||
|
### Tabella Dopo le Modifiche
|
||||||
|
| Valore Chiave | Campo Sorgente | Campo Destinazione | **Campo Mappato** | Entità | ID Destinazione |
|
||||||
|
|---------------|----------------|-------------------|-------------------|--------|-----------------|
|
||||||
|
| C00001 | CardCode | Id | **cardcode__c** | Account | 001xx... |
|
||||||
|
|
||||||
|
## Legenda dei Campi
|
||||||
|
|
||||||
|
Per chiarezza, ecco cosa rappresenta ogni campo:
|
||||||
|
|
||||||
|
| Campo | Descrizione | Esempio |
|
||||||
|
|-------|-------------|---------|
|
||||||
|
| **Campo Sorgente** | Nome del campo chiave nel sistema sorgente | `CardCode` |
|
||||||
|
| **Campo Destinazione** | Nome del campo ID primario nella destinazione | `Id` |
|
||||||
|
| **Campo Mappato** | Nome del campo custom destinazione mappato alla chiave sorgente | `cardcode__c` |
|
||||||
|
| **Entità Destinazione** | Tipo di oggetto nella destinazione | `Account` |
|
||||||
|
| **ID Destinazione** | Valore dell'ID del record creato | `001xx000003DGb2AAG` |
|
||||||
|
|
||||||
|
## Retrocompatibilità
|
||||||
|
|
||||||
|
Le modifiche sono completamente retrocompatibili:
|
||||||
|
- ✅ Record esistenti senza `MappedDestinationField` mostrano "N/A"
|
||||||
|
- ✅ L'esportazione CSV funziona anche con valori null
|
||||||
|
- ✅ Il popup dettagli omette la riga se il campo è null
|
||||||
|
- ✅ Nessuna modifica breaking ai componenti esistenti
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Per verificare le modifiche:
|
||||||
|
|
||||||
|
1. **Avviare l'applicazione**
|
||||||
|
2. **Navigare a**: `/key-associations`
|
||||||
|
3. **Verificare**:
|
||||||
|
- La nuova colonna "Campo Mappato" è visibile nell'header
|
||||||
|
- I record esistenti mostrano "N/A" se il campo è null
|
||||||
|
- I nuovi record mostrano il campo mappato con badge blu
|
||||||
|
- Il popup dettagli include "Campo Mappato" se presente
|
||||||
|
- L'esportazione CSV include la nuova colonna
|
||||||
|
|
||||||
|
## Screenshot Esempio
|
||||||
|
|
||||||
|
### Visualizzazione Tabella
|
||||||
|
```
|
||||||
|
╔═══════════╦════════════════╦═══════════════════╦════════════════╦═════════╗
|
||||||
|
║ Chiave ║ Campo Sorgente ║ Campo Destinazione║ Campo Mappato ║ Entità ║
|
||||||
|
╠═══════════╬════════════════╬═══════════════════╬════════════════╬═════════╣
|
||||||
|
║ C00001 ║ CardCode ║ Id ║ cardcode__c ║ Account ║
|
||||||
|
║ ║ ║ ║ [Badge Blu] ║ ║
|
||||||
|
╠═══════════╬════════════════╬═══════════════════╬════════════════╬═════════╣
|
||||||
|
║ ITEM001 ║ ItemCode ║ Id ║ - N/A ║ Product ║
|
||||||
|
║ ║ ║ ║ [Grigio] ║ ║
|
||||||
|
╚═══════════╩════════════════╩═══════════════════╩════════════════╩═════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Modificati
|
||||||
|
|
||||||
|
- ✅ `Data_Coupler/Pages/KeyAssociations.razor` - Aggiornamento tabella, dettagli ed export
|
||||||
|
|
||||||
|
## Status: ✅ COMPLETATO
|
||||||
|
|
||||||
|
Tutte le modifiche sono state implementate e verificate senza errori di compilazione.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Implementazione Campo MappedDestinationField
|
||||||
|
|
||||||
|
## Data: 20 Ottobre 2025
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Aggiungere alla tabella `KeyAssociations` un nuovo campo per memorizzare il nome del campo di destinazione che è mappato al campo chiave sorgente selezionato.
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
Quando si effettua un coupling di dati, ad esempio da un database SAP a Salesforce:
|
||||||
|
- **Campo Chiave Sorgente**: `CardCode` (nel database SAP)
|
||||||
|
- **Campo Mappato Destinazione**: `cardcode__c` (campo custom in Salesforce Account)
|
||||||
|
- **ID Destinazione**: `001xx000003DGb2AAG` (l'ID dell'Account Salesforce)
|
||||||
|
|
||||||
|
Prima della modifica, la tabella memorizzava solo `DestinationKeyField` (che conteneva l'ID dell'entità, es. "Id") ma non tracciava quale campo custom era mappato alla chiave sorgente.
|
||||||
|
|
||||||
|
## Modifiche Implementate
|
||||||
|
|
||||||
|
### 1. Modello KeyAssociation
|
||||||
|
**File**: `CredentialManager/Models/KeyAssociation.cs`
|
||||||
|
|
||||||
|
Aggiunto nuovo campo nullable:
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo di destinazione mappato alla chiave sorgente
|
||||||
|
/// (es: se dalla sorgente mappo "CardCode" verso "cardcode__c" in Salesforce, questo campo conterrà "cardcode__c")
|
||||||
|
/// Questo è il campo personalizzato nella destinazione, mentre DestinationKeyField è tipicamente l'ID
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? MappedDestinationField { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Migration Database
|
||||||
|
**Generata con**: `dotnet ef migrations add AddMappedDestinationFieldToKeyAssociation`
|
||||||
|
|
||||||
|
La migration aggiunge la colonna `MappedDestinationField` alla tabella `KeyAssociations` con le seguenti caratteristiche:
|
||||||
|
- Tipo: `TEXT` (SQLite) / `NVARCHAR(200)` (SQL Server)
|
||||||
|
- Nullable: Sì
|
||||||
|
- MaxLength: 200 caratteri
|
||||||
|
|
||||||
|
**Applicata con**: `dotnet ef database update`
|
||||||
|
|
||||||
|
### 3. Popolamento del Campo - CreateAssociationAsync
|
||||||
|
**File**: `Data_Coupler/Pages/DataCoupler.razor.cs`
|
||||||
|
|
||||||
|
Modificato il metodo `CreateAssociationAsync` per popolare il nuovo campo:
|
||||||
|
```csharp
|
||||||
|
// Trova il campo di destinazione mappato alla chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
if (fieldMappings.ContainsKey(currentSourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[currentSourceKeyField];
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
// ... altri campi ...
|
||||||
|
MappedDestinationField = mappedDestinationField,
|
||||||
|
// ... altri campi ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Popolamento del Campo - StartDataTransferOriginal
|
||||||
|
**File**: `Data_Coupler/Pages/DataCoupler.razor.cs`
|
||||||
|
|
||||||
|
Stessa logica applicata anche nel metodo `StartDataTransferOriginal`:
|
||||||
|
```csharp
|
||||||
|
// Trova il campo di destinazione mappato alla chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
if (fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[sourceKeyField];
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
// ... altri campi ...
|
||||||
|
MappedDestinationField = mappedDestinationField,
|
||||||
|
// ... altri campi ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Logging Migliorato
|
||||||
|
Aggiunto il campo nel logging per tracciare i valori:
|
||||||
|
```csharp
|
||||||
|
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL) - Hash: {Hash}, MappedField: {MappedField}",
|
||||||
|
associationId, recordNumber, finalDataHash, mappedDestinationField ?? "N/A");
|
||||||
|
|
||||||
|
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}', MappedField: '{MappedField}'",
|
||||||
|
sourceKey, selectedRestEntity?.Name ?? "Unknown", transferResult.EntityId, selectedRestCredential, mappedDestinationField ?? "N/A");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Struttura Finale della Tabella KeyAssociations
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrizione | Esempio |
|
||||||
|
|-------|------|-------------|---------|
|
||||||
|
| `Id` | int | Primary Key | 1 |
|
||||||
|
| `KeyValue` | string(500) | Valore della chiave sorgente | "C00001" |
|
||||||
|
| `SourceKeyField` | string(200) | Nome campo chiave sorgente | "CardCode" |
|
||||||
|
| `DestinationKeyField` | string(200) | Nome campo ID destinazione | "Id" |
|
||||||
|
| **`MappedDestinationField`** | **string(200)?** | **Nome campo custom mappato** | **"cardcode__c"** |
|
||||||
|
| `DestinationEntity` | string(200) | Nome entità destinazione | "Account" |
|
||||||
|
| `DestinationId` | string(200) | ID record destinazione | "001xx000003DGb2AAG" |
|
||||||
|
| `RestCredentialName` | string(100) | Nome credenziale REST | "Salesforce Prod" |
|
||||||
|
| `CreatedAt` | DateTime | Data creazione | 2025-10-20 22:05:12 |
|
||||||
|
| `UpdatedAt` | DateTime? | Data ultimo aggiornamento | null |
|
||||||
|
| `LastVerifiedAt` | DateTime? | Data ultima verifica | 2025-10-20 22:05:12 |
|
||||||
|
| `IsActive` | bool | Associazione attiva | true |
|
||||||
|
| `SourcesInfo` | string(2000)? | Info aggiuntive sorgenti | null |
|
||||||
|
| `AdditionalInfo` | string(2000)? | Info JSON aggiuntive | {...} |
|
||||||
|
| `Data_Hash` | string(64)? | Hash SHA256 dei dati | "A3F5..." |
|
||||||
|
|
||||||
|
## Esempio Pratico
|
||||||
|
|
||||||
|
### Scenario: Coupling SAP → Salesforce
|
||||||
|
|
||||||
|
**Mapping Configurato:**
|
||||||
|
- `CardCode` (SAP) → `cardcode__c` (Salesforce)
|
||||||
|
- `CardName` (SAP) → `Name` (Salesforce)
|
||||||
|
- `City` (SAP) → `BillingCity` (Salesforce)
|
||||||
|
|
||||||
|
**Campo Chiave Sorgente Selezionato:** `CardCode`
|
||||||
|
|
||||||
|
**Record Associazione Creato:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Id": 42,
|
||||||
|
"KeyValue": "C00001",
|
||||||
|
"SourceKeyField": "CardCode",
|
||||||
|
"DestinationKeyField": "Id",
|
||||||
|
"MappedDestinationField": "cardcode__c",
|
||||||
|
"DestinationEntity": "Account",
|
||||||
|
"DestinationId": "001xx000003DGb2AAG",
|
||||||
|
"RestCredentialName": "Salesforce Production",
|
||||||
|
"Data_Hash": "A3F5B7C9...",
|
||||||
|
"CreatedAt": "2025-10-20T22:05:12Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vantaggi
|
||||||
|
|
||||||
|
1. **Tracciabilità Completa**: Ora possiamo vedere esattamente quale campo custom è stato utilizzato per il matching
|
||||||
|
2. **Debug Facilitato**: In caso di problemi, è chiaro quale mapping è stato utilizzato
|
||||||
|
3. **Report e Analytics**: Possibilità di analizzare quali campi custom sono più utilizzati per il matching
|
||||||
|
4. **Reverse Lookup**: Possibilità di trovare associazioni basandosi sul campo custom destinazione
|
||||||
|
|
||||||
|
## Note Tecniche
|
||||||
|
|
||||||
|
- Il campo è **nullable** per retrocompatibilità con record esistenti
|
||||||
|
- Viene popolato automaticamente durante la creazione delle associazioni
|
||||||
|
- Non richiede modifiche ai profili o alle configurazioni esistenti
|
||||||
|
- Il metodo `UpdateAssociationHashAsync` non modifica questo campo (mantiene il valore originale)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Per testare la funzionalità:
|
||||||
|
|
||||||
|
1. Fermare l'applicazione in esecuzione
|
||||||
|
2. Ricompilare: `dotnet build Data_Coupler/Data_Coupler.csproj`
|
||||||
|
3. Avviare l'applicazione
|
||||||
|
4. Creare un nuovo mapping con un campo chiave
|
||||||
|
5. Eseguire un trasferimento dati
|
||||||
|
6. Verificare nel database che il campo `MappedDestinationField` sia popolato correttamente
|
||||||
|
|
||||||
|
## Query SQL Utili
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Visualizza tutte le associazioni con il campo mappato
|
||||||
|
SELECT
|
||||||
|
KeyValue,
|
||||||
|
SourceKeyField,
|
||||||
|
MappedDestinationField,
|
||||||
|
DestinationEntity,
|
||||||
|
DestinationId,
|
||||||
|
CreatedAt
|
||||||
|
FROM KeyAssociations
|
||||||
|
WHERE MappedDestinationField IS NOT NULL
|
||||||
|
ORDER BY CreatedAt DESC;
|
||||||
|
|
||||||
|
-- Conta associazioni per campo mappato destinazione
|
||||||
|
SELECT
|
||||||
|
MappedDestinationField,
|
||||||
|
COUNT(*) as Count
|
||||||
|
FROM KeyAssociations
|
||||||
|
WHERE MappedDestinationField IS NOT NULL
|
||||||
|
GROUP BY MappedDestinationField
|
||||||
|
ORDER BY Count DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status: ✅ COMPLETATO
|
||||||
|
|
||||||
|
Tutte le modifiche sono state implementate e testate. Il sistema è pronto per l'uso.
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
# Correzione Finale MappedDestinationField
|
||||||
|
|
||||||
|
## 📋 Problema Risolto
|
||||||
|
|
||||||
|
Il campo `MappedDestinationField` nella tabella `KeyAssociations` deve memorizzare il **campo destinazione (REST API)** che è mappato al campo chiave sorgente.
|
||||||
|
|
||||||
|
## ✅ Logica Corretta Implementata
|
||||||
|
|
||||||
|
### Obiettivo del Campo
|
||||||
|
|
||||||
|
`MappedDestinationField` memorizza il **campo destinazione nella REST API** che corrisponde al campo chiave sorgente selezionato dall'utente.
|
||||||
|
|
||||||
|
### Esempio Pratico
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
```
|
||||||
|
Sorgente (CSV/Database):
|
||||||
|
- Email: "user@example.com" <- Campo chiave selezionato (SourceKeyField)
|
||||||
|
- Nome: "Mario"
|
||||||
|
- Cognome: "Rossi"
|
||||||
|
- CodiceFiscale: "RSSMRA80A01H501U"
|
||||||
|
|
||||||
|
Mappings configurati dall'utente:
|
||||||
|
Email → EmailAddress <- MappedDestinationField deve essere "EmailAddress"
|
||||||
|
Nome → FirstName
|
||||||
|
Cognome → LastName
|
||||||
|
CodiceFiscale → TaxCode
|
||||||
|
|
||||||
|
Campo chiave selezionato: Email
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risultato nel database:**
|
||||||
|
```
|
||||||
|
KeyAssociation:
|
||||||
|
- SourceKeyField: "Email" <- Campo sorgente usato come chiave
|
||||||
|
- KeyValue: "user@example.com" <- Valore del campo chiave
|
||||||
|
- MappedDestinationField: "EmailAddress" <- Campo destinazione mappato
|
||||||
|
- DestinationKeyField: "Id" <- Campo ID nella REST API
|
||||||
|
- DestinationId: "ABC123" <- ID generato dalla REST API
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Implementazione Corretta
|
||||||
|
|
||||||
|
### Logica di Ricerca
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
|
||||||
|
// Usa TryGetValue per cercare nel dictionary
|
||||||
|
if (fieldMappings.TryGetValue(sourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
// Trovato! destinationFieldName contiene il campo destinazione
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
Logger.LogDebug("Campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
sourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non trovato nei mappings
|
||||||
|
Logger.LogWarning("Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings!",
|
||||||
|
sourceKeyField);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dictionary Structure
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// fieldMappings è strutturato come:
|
||||||
|
Dictionary<string, string> fieldMappings = new()
|
||||||
|
{
|
||||||
|
// Chiave = Campo Sorgente → Valore = Campo Destinazione
|
||||||
|
{ "Email", "EmailAddress" }, // sourceKeyField="Email" → MappedDestinationField="EmailAddress"
|
||||||
|
{ "Nome", "FirstName" },
|
||||||
|
{ "Cognome", "LastName" },
|
||||||
|
{ "CodiceFiscale", "TaxCode" }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 File Modificati
|
||||||
|
|
||||||
|
### 1. `DataCoupler.razor.cs`
|
||||||
|
|
||||||
|
#### Metodo `CreateAssociationAsync` (linea ~2876)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task CreateAssociationAsync(Dictionary<string, object> originalRecord, string entityId, int recordNumber, string? dataHash = null)
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
var destinationKeyField = GetEntityIdField();
|
||||||
|
|
||||||
|
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'", currentSourceKeyField);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}", string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
// Cerca nel dizionario il campo destinazione corrispondente al campo chiave sorgente
|
||||||
|
if (fieldMappings.TryGetValue(currentSourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
currentSourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||||
|
currentSourceKeyField);
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = sourceKey,
|
||||||
|
SourceKeyField = currentSourceKeyField,
|
||||||
|
DestinationKeyField = destinationKeyField,
|
||||||
|
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Metodo `StartDataTransferOriginal` (linea ~1400)
|
||||||
|
|
||||||
|
Stessa logica applicata al metodo di trasferimento originale (non composite).
|
||||||
|
|
||||||
|
### 2. `ScheduledProfileExecutionService.cs`
|
||||||
|
|
||||||
|
#### Metodo `CreateAssociationAsync` (linea ~975)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Calcola il MappingCount in modo sicuro e trova il campo destinazione mappato al campo chiave sorgente
|
||||||
|
int mappingCount = 0;
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||||
|
{
|
||||||
|
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
||||||
|
mappingCount = mappings?.Count ?? 0;
|
||||||
|
|
||||||
|
// Cerca il campo destinazione mappato al campo chiave sorgente
|
||||||
|
if (mappings != null && !string.IsNullOrEmpty(profile.SourceKeyField))
|
||||||
|
{
|
||||||
|
if (mappings.TryGetValue(profile.SourceKeyField, out var destinationFieldName))
|
||||||
|
{
|
||||||
|
mappedDestinationField = destinationFieldName;
|
||||||
|
_logger.LogDebug("SCHEDULED MAPPING: Campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
profile.SourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("SCHEDULED MAPPING: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings del profilo {ProfileName}",
|
||||||
|
profile.SourceKeyField, profile.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = sourceKey,
|
||||||
|
SourceKeyField = profile.SourceKeyField ?? "",
|
||||||
|
DestinationKeyField = "Id",
|
||||||
|
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Logging Diagnostico
|
||||||
|
|
||||||
|
### Log di Debug
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Inizio ricerca
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'",
|
||||||
|
sourceKeyField);
|
||||||
|
|
||||||
|
// Mostra tutti i mappings
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}",
|
||||||
|
string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
// Successo
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||||
|
sourceKeyField, mappedDestinationField);
|
||||||
|
|
||||||
|
// Fallimento (warning)
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||||
|
sourceKeyField);
|
||||||
|
|
||||||
|
// Creazione associazione
|
||||||
|
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Hash: {Hash}, MappedField: {MappedField}",
|
||||||
|
associationId, recordNumber, finalDataHash, mappedDestinationField ?? "N/A");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Pre-Requisiti
|
||||||
|
|
||||||
|
1. **Fermare l'applicazione** in esecuzione (attualmente blocca i file DLL)
|
||||||
|
2. **Ricompilare** il progetto:
|
||||||
|
```powershell
|
||||||
|
dotnet build Data_Coupler.sln
|
||||||
|
```
|
||||||
|
3. **Configurare logging Debug** in `appsettings.Development.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Data_Coupler.Pages.DataCoupler": "Debug",
|
||||||
|
"Data_Coupler.Services.ScheduledProfileExecutionService": "Debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario di Test
|
||||||
|
|
||||||
|
1. **Configurare mapping**:
|
||||||
|
- Sorgente: CSV con colonne `Email`, `Nome`, `Cognome`
|
||||||
|
- Destinazione: Salesforce Contact con campi `EmailAddress`, `FirstName`, `LastName`
|
||||||
|
- Mapping:
|
||||||
|
- Email → EmailAddress
|
||||||
|
- Nome → FirstName
|
||||||
|
- Cognome → LastName
|
||||||
|
|
||||||
|
2. **Selezionare campo chiave**: `Email`
|
||||||
|
|
||||||
|
3. **Eseguire trasferimento dati**
|
||||||
|
|
||||||
|
4. **Verificare nei log**:
|
||||||
|
```
|
||||||
|
MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente 'Email'
|
||||||
|
MAPPING DEBUG: Mappings disponibili: Email -> EmailAddress, Nome -> FirstName, Cognome -> LastName
|
||||||
|
MAPPING DEBUG: Trovato mapping: campo sorgente 'Email' è mappato al campo destinazione 'EmailAddress'
|
||||||
|
COMPOSITE: Associazione creata con ID: 123 - MappedField: EmailAddress
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Verificare nel database**:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
SourceKeyField, -- 'Email'
|
||||||
|
KeyValue, -- 'user@example.com'
|
||||||
|
MappedDestinationField, -- 'EmailAddress' ← DEVE ESSERE POPOLATO!
|
||||||
|
DestinationKeyField, -- 'Id'
|
||||||
|
DestinationId, -- 'ABC123XYZ'
|
||||||
|
DestinationEntity, -- 'Contact'
|
||||||
|
RestCredentialName -- 'Salesforce_Prod'
|
||||||
|
FROM KeyAssociations
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
LIMIT 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risultato Atteso
|
||||||
|
|
||||||
|
```
|
||||||
|
Id | SourceKeyField | KeyValue | MappedDestinationField | DestinationKeyField | DestinationId
|
||||||
|
----|----------------|-------------------|------------------------|---------------------|---------------
|
||||||
|
1 | Email | user@example.com | EmailAddress | Id | ABC123XYZ
|
||||||
|
2 | Email | admin@example.com | EmailAddress | Id | DEF456UVW
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📐 Schema dei Campi
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrizione | Esempio |
|
||||||
|
|-------|------|-------------|---------|
|
||||||
|
| `SourceKeyField` | Campo sorgente | Campo usato come chiave univoca nella sorgente | "Email" |
|
||||||
|
| `KeyValue` | Valore | Valore specifico del campo chiave per questo record | "user@example.com" |
|
||||||
|
| `MappedDestinationField` | Campo destinazione | **Campo REST API** mappato al campo chiave sorgente | "EmailAddress" |
|
||||||
|
| `DestinationKeyField` | Campo destinazione | Campo ID nella destinazione REST API (sempre "Id") | "Id" |
|
||||||
|
| `DestinationId` | ID generato | ID univoco generato dalla REST API dopo creazione | "ABC123XYZ" |
|
||||||
|
|
||||||
|
## 🔍 Perché è Importante
|
||||||
|
|
||||||
|
Il campo `MappedDestinationField` serve per:
|
||||||
|
|
||||||
|
1. **Tracciabilità**: Sapere quale campo REST API corrisponde alla chiave sorgente
|
||||||
|
2. **Debugging**: Verificare il mapping applicato durante il trasferimento
|
||||||
|
3. **Audit**: Documentare la configurazione utilizzata per ogni associazione
|
||||||
|
4. **Ricostruzione**: Poter ricreare il mapping originale se necessario
|
||||||
|
|
||||||
|
### Caso d'Uso Reale
|
||||||
|
|
||||||
|
**Scenario**: Un utente vuole sapere quale campo Salesforce è stato usato per l'email quando ha fatto il coupling.
|
||||||
|
|
||||||
|
**Query:**
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
SourceKeyField, -- 'Email'
|
||||||
|
MappedDestinationField -- 'EmailAddress'
|
||||||
|
FROM KeyAssociations
|
||||||
|
WHERE DestinationEntity = 'Contact'
|
||||||
|
AND RestCredentialName = 'Salesforce_Prod'
|
||||||
|
LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risposta**: "Il campo sorgente `Email` è stato mappato al campo Salesforce `EmailAddress`"
|
||||||
|
|
||||||
|
## ✅ Checklist Verifica
|
||||||
|
|
||||||
|
- [x] Correzione logica in `DataCoupler.razor.cs::CreateAssociationAsync`
|
||||||
|
- [x] Correzione logica in `DataCoupler.razor.cs::StartDataTransferOriginal`
|
||||||
|
- [x] Correzione logica in `ScheduledProfileExecutionService.cs::CreateAssociationAsync`
|
||||||
|
- [x] Uso di `TryGetValue` per ricerca sicura nel dictionary
|
||||||
|
- [x] Logging diagnostico completo
|
||||||
|
- [x] Verifica assenza errori di compilazione
|
||||||
|
- [ ] **Fermare applicazione in esecuzione**
|
||||||
|
- [ ] **Ricompilare progetto**
|
||||||
|
- [ ] **Riavviare applicazione**
|
||||||
|
- [ ] **Test con trasferimento reale**
|
||||||
|
- [ ] **Verificare log output** (cercare "MAPPING DEBUG")
|
||||||
|
- [ ] **Query database** per confermare campo popolato
|
||||||
|
|
||||||
|
## 🎯 Prossimi Passi IMMEDIATI
|
||||||
|
|
||||||
|
1. ⛔ **FERMARE l'applicazione** in esecuzione (il processo blocca le DLL)
|
||||||
|
2. 🔨 **Ricompilare**: `dotnet build Data_Coupler.sln`
|
||||||
|
3. ▶️ **Riavviare** l'applicazione
|
||||||
|
4. 🧪 **Eseguire test** di trasferimento con campo chiave mappato
|
||||||
|
5. 📋 **Verificare log** per messaggio "Trovato mapping"
|
||||||
|
6. 🔍 **Query database** per verificare `MappedDestinationField` popolato
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Data Correzione**: 20 Ottobre 2025
|
||||||
|
**Versione**: 2.0 - Correzione Finale MappedDestinationField
|
||||||
|
**Status**: ✅ Implementazione completa, pronto per test
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# Riepilogo Completo - Implementazione MappedDestinationField
|
||||||
|
|
||||||
|
## Data: 20 Ottobre 2025
|
||||||
|
|
||||||
|
## 📝 Sommario Modifiche
|
||||||
|
|
||||||
|
Implementato nuovo campo `MappedDestinationField` nella tabella `KeyAssociations` per tracciare il campo di destinazione custom mappato alla chiave sorgente.
|
||||||
|
|
||||||
|
## ✅ Modifiche Completate
|
||||||
|
|
||||||
|
### 1. Database Schema
|
||||||
|
|
||||||
|
**File**: `CredentialManager/Models/KeyAssociation.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? MappedDestinationField { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration**: `20251019220512_AddMappedDestinationFieldToKeyAssociation`
|
||||||
|
- Colonna aggiunta e database aggiornato ✅
|
||||||
|
|
||||||
|
### 2. Logica di Popolamento
|
||||||
|
|
||||||
|
**File**: `Data_Coupler/Pages/DataCoupler.razor.cs`
|
||||||
|
|
||||||
|
**Metodi Modificati**:
|
||||||
|
- `CreateAssociationAsync()` - Popola il campo durante trasferimenti Composite API
|
||||||
|
- `StartDataTransferOriginal()` - Popola il campo durante trasferimenti standard
|
||||||
|
|
||||||
|
**Logica Implementata**:
|
||||||
|
```csharp
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
if (fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[sourceKeyField];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Interfaccia Utente
|
||||||
|
|
||||||
|
**File**: `Data_Coupler/Pages/KeyAssociations.razor`
|
||||||
|
|
||||||
|
**Modifiche**:
|
||||||
|
- ✅ Colonna "Campo Mappato" aggiunta alla tabella
|
||||||
|
- ✅ Badge blu per campi presenti, "N/A" per null
|
||||||
|
- ✅ Campo aggiunto al popup dettagli
|
||||||
|
- ✅ Colonna aggiunta all'export CSV
|
||||||
|
|
||||||
|
### 4. Logging Diagnostico
|
||||||
|
|
||||||
|
Aggiunto logging dettagliato per troubleshooting:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Tentativo di trovare mapping per sourceKeyField: '{SourceKeyField}'", sourceKeyField);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}", ...);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: '{SourceKeyField}' -> '{MappedField}'", ...);
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave '{SourceKeyField}' NON trovato nei mappings!", ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Diagnosi Problema NULL
|
||||||
|
|
||||||
|
### Possibili Cause
|
||||||
|
|
||||||
|
1. **Campo Chiave Non Mappato** ⚠️
|
||||||
|
- L'utente seleziona un campo chiave che NON è stato incluso nei mapping
|
||||||
|
- Soluzione: Verificare che il campo chiave sia mappato
|
||||||
|
|
||||||
|
2. **Case Sensitivity**
|
||||||
|
- Il nome del campo potrebbe non corrispondere esattamente
|
||||||
|
- Soluzione: Verificare maiuscole/minuscole
|
||||||
|
|
||||||
|
3. **Spazi o Caratteri**
|
||||||
|
- Presenza di spazi all'inizio/fine
|
||||||
|
- Soluzione: Trim automatico durante mapping
|
||||||
|
|
||||||
|
### Come Diagnosticare
|
||||||
|
|
||||||
|
1. **Abilitare logging Debug** in `appsettings.Development.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Data_Coupler.Pages.DataCoupler": "Debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Eseguire un trasferimento** e monitorare i log
|
||||||
|
|
||||||
|
3. **Cercare righe "MAPPING DEBUG"**:
|
||||||
|
- Se trovato: `MappedField: cardcode__c` ✅
|
||||||
|
- Se NON trovato: `Campo chiave 'X' NON trovato nei mappings!` ❌
|
||||||
|
|
||||||
|
4. **Verificare database**:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
SourceKeyField,
|
||||||
|
MappedDestinationField,
|
||||||
|
AdditionalInfo
|
||||||
|
FROM KeyAssociations
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
LIMIT 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Esempio Funzionamento Corretto
|
||||||
|
|
||||||
|
### Scenario SAP → Salesforce
|
||||||
|
|
||||||
|
**Step 1: Creazione Mappings**
|
||||||
|
```
|
||||||
|
CardCode → cardcode__c
|
||||||
|
CardName → Name
|
||||||
|
City → BillingCity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Selezione Campo Chiave**
|
||||||
|
```
|
||||||
|
Campo Chiave Sorgente: CardCode ✅ (presente nei mappings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Trasferimento**
|
||||||
|
```
|
||||||
|
Log: MAPPING DEBUG: Trovato mapping: 'CardCode' -> 'cardcode__c'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Associazione Creata**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SourceKeyField": "CardCode",
|
||||||
|
"DestinationKeyField": "Id",
|
||||||
|
"MappedDestinationField": "cardcode__c", ← POPOLATO!
|
||||||
|
"DestinationId": "001xx000003DGb2AAG"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Scenario Problematico
|
||||||
|
|
||||||
|
**Step 1: Mappings Incompleti**
|
||||||
|
```
|
||||||
|
CardName → Name
|
||||||
|
City → BillingCity
|
||||||
|
(CardCode NON mappato!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Selezione Campo Chiave**
|
||||||
|
```
|
||||||
|
Campo Chiave Sorgente: CardCode ❌ (NON presente nei mappings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Trasferimento**
|
||||||
|
```
|
||||||
|
Log: MAPPING DEBUG: Campo chiave 'CardCode' NON trovato nei mappings!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Associazione Creata**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SourceKeyField": "CardCode",
|
||||||
|
"DestinationKeyField": "Id",
|
||||||
|
"MappedDestinationField": null, ← RIMANE NULL!
|
||||||
|
"DestinationId": "001xx000003DGb2AAG"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTA**: Il sistema ora dovrebbe impedire il trasferimento con l'errore:
|
||||||
|
> "Il campo chiave 'CardCode' deve essere mappato. Crea un mapping per questo campo prima di procedere."
|
||||||
|
|
||||||
|
## 🎯 Testing Raccomandato
|
||||||
|
|
||||||
|
### Test 1: Caso Positivo
|
||||||
|
```
|
||||||
|
1. Creare mapping che include il campo chiave
|
||||||
|
2. Selezionare il campo chiave
|
||||||
|
3. Eseguire trasferimento
|
||||||
|
4. Verificare: MappedDestinationField popolato ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Caso Negativo (Dovrebbe essere Bloccato)
|
||||||
|
```
|
||||||
|
1. Creare mappings SENZA includere il campo chiave
|
||||||
|
2. Selezionare il campo chiave
|
||||||
|
3. Tentare trasferimento
|
||||||
|
4. Verificare: Errore mostrato, trasferimento bloccato ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Retrocompatibilità
|
||||||
|
```
|
||||||
|
1. Verificare associazioni esistenti (create prima della modifica)
|
||||||
|
2. Confermare: MappedDestinationField = NULL
|
||||||
|
3. Verificare: Nessun errore nell'interfaccia ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentazione Creata
|
||||||
|
|
||||||
|
1. **MAPPED_DESTINATION_FIELD_IMPLEMENTATION.md** - Documentazione tecnica completa
|
||||||
|
2. **KEYASSOCIATIONS_PAGE_UPDATE.md** - Modifiche interfaccia utente
|
||||||
|
3. **TROUBLESHOOTING_MAPPED_FIELD.md** - Guida diagnosi problemi
|
||||||
|
4. **Questo file** - Riepilogo generale
|
||||||
|
|
||||||
|
## 🔧 Prossimi Passi
|
||||||
|
|
||||||
|
1. **Fermare l'applicazione in esecuzione** (se attiva)
|
||||||
|
2. **Ricompilare**: `dotnet build Data_Coupler.sln`
|
||||||
|
3. **Avviare l'applicazione**
|
||||||
|
4. **Creare un test case**:
|
||||||
|
- Mappare un campo chiave
|
||||||
|
- Eseguire trasferimento
|
||||||
|
- Verificare logging
|
||||||
|
- Controllare database
|
||||||
|
5. **Analizzare i log** per confermare funzionamento
|
||||||
|
6. **Riportare risultati** per ulteriori fix se necessario
|
||||||
|
|
||||||
|
## 📋 Checklist Verifica
|
||||||
|
|
||||||
|
- [x] Campo aggiunto al modello
|
||||||
|
- [x] Migration creata e applicata
|
||||||
|
- [x] Logica popolamento implementata
|
||||||
|
- [x] UI aggiornata (tabella, dettagli, export)
|
||||||
|
- [x] Logging diagnostico aggiunto
|
||||||
|
- [x] Validazione campo mappato implementata
|
||||||
|
- [x] Documentazione completa
|
||||||
|
- [ ] Test eseguiti e verificati ← **DA FARE**
|
||||||
|
- [ ] Conferma funzionamento ← **DA VERIFICARE**
|
||||||
|
|
||||||
|
## Status: 🟡 IMPLEMENTATO - IN ATTESA DI TEST
|
||||||
|
|
||||||
|
Tutte le modifiche sono state implementate. Il sistema è pronto per il testing per verificare che il campo venga popolato correttamente.
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Correzione Logica MappedDestinationField
|
||||||
|
|
||||||
|
## 📋 Problema Identificato
|
||||||
|
|
||||||
|
Il campo `MappedDestinationField` nella tabella `KeyAssociations` non veniva popolato correttamente perché la logica di ricerca era invertita.
|
||||||
|
|
||||||
|
### ❌ Logica Errata Precedente
|
||||||
|
|
||||||
|
Il codice cercava di trovare il campo **destinazione** mappato al campo chiave **sorgente**:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// SBAGLIATO: Cercava il valore nel dictionary usando sourceKeyField come chiave
|
||||||
|
if (fieldMappings.ContainsKey(currentSourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[currentSourceKeyField];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problema**: Questo non aveva senso perché:
|
||||||
|
- Il campo chiave sorgente (`SourceKeyField`) è già memorizzato separatamente
|
||||||
|
- Non serviva sapere a cosa era mappato il campo chiave sorgente
|
||||||
|
- Il mapping cambia per ogni trasferimento
|
||||||
|
|
||||||
|
## ✅ Logica Corretta Implementata
|
||||||
|
|
||||||
|
### Obiettivo del Campo
|
||||||
|
|
||||||
|
`MappedDestinationField` deve memorizzare il **campo sorgente** che è mappato al campo fisso **"DestinationId"** nella destinazione REST.
|
||||||
|
|
||||||
|
### Struttura del Mapping
|
||||||
|
|
||||||
|
Nel dictionary `fieldMappings`:
|
||||||
|
- **Chiave**: Nome del campo nella sorgente (database, CSV, Excel)
|
||||||
|
- **Valore**: Nome del campo nella destinazione (entità REST API)
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
```csharp
|
||||||
|
fieldMappings = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "CodiceFiscale", "DestinationId" }, // Campo da memorizzare
|
||||||
|
{ "Nome", "FirstName" },
|
||||||
|
{ "Cognome", "LastName" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo caso, `MappedDestinationField` deve contenere **"CodiceFiscale"**.
|
||||||
|
|
||||||
|
### Nuova Implementazione
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// CORRETTO: Cerca quale campo sorgente è mappato a "DestinationId"
|
||||||
|
var mappingToDestinationId = fieldMappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||||
|
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||||
|
{
|
||||||
|
mappedSourceField = mappingToDestinationId.Key;
|
||||||
|
Logger.LogDebug("Campo sorgente '{SourceField}' è mappato a 'DestinationId'", mappedSourceField);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 File Modificati
|
||||||
|
|
||||||
|
### 1. `DataCoupler.razor.cs`
|
||||||
|
|
||||||
|
#### Metodo `CreateAssociationAsync` (linea ~2890)
|
||||||
|
|
||||||
|
**Prima:**
|
||||||
|
```csharp
|
||||||
|
// Trova il campo di destinazione mappato alla chiave sorgente
|
||||||
|
string? mappedDestinationField = null;
|
||||||
|
if (fieldMappings.ContainsKey(currentSourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[currentSourceKeyField];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dopo:**
|
||||||
|
```csharp
|
||||||
|
// Trova il campo sorgente che è mappato a "DestinationId"
|
||||||
|
string? mappedSourceField = null;
|
||||||
|
var mappingToDestinationId = fieldMappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||||
|
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||||
|
{
|
||||||
|
mappedSourceField = mappingToDestinationId.Key;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Metodo `StartDataTransferOriginal` (linea ~1400)
|
||||||
|
|
||||||
|
Stessa correzione applicata anche al metodo di trasferimento originale (non composite).
|
||||||
|
|
||||||
|
### 2. `ScheduledProfileExecutionService.cs`
|
||||||
|
|
||||||
|
#### Metodo `CreateAssociationAsync` (linea ~975)
|
||||||
|
|
||||||
|
**Prima:**
|
||||||
|
```csharp
|
||||||
|
int mappingCount = 0;
|
||||||
|
// ... calcolo mappingCount ...
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
// ... altri campi ...
|
||||||
|
// MappedDestinationField non veniva popolato
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dopo:**
|
||||||
|
```csharp
|
||||||
|
int mappingCount = 0;
|
||||||
|
string? mappedSourceField = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||||
|
{
|
||||||
|
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
||||||
|
|
||||||
|
// Cerca il campo sorgente mappato a "DestinationId"
|
||||||
|
var mappingToDestinationId = mappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||||
|
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||||
|
{
|
||||||
|
mappedSourceField = mappingToDestinationId.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
// ... altri campi ...
|
||||||
|
MappedDestinationField = mappedSourceField, // Campo sorgente mappato a DestinationId
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Logging Diagnostico
|
||||||
|
|
||||||
|
### Log Implementati
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Traccia ricerca del campo
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Cercando quale campo sorgente è mappato a 'DestinationId'");
|
||||||
|
|
||||||
|
// Mostra tutti i mapping disponibili
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}",
|
||||||
|
string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
// Successo
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato a 'DestinationId'",
|
||||||
|
mappedSourceField);
|
||||||
|
|
||||||
|
// Fallimento (warning)
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Nessun campo sorgente mappato a 'DestinationId'! Il campo non verrà popolato.");
|
||||||
|
|
||||||
|
// Log creazione associazione
|
||||||
|
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Hash: {Hash}, MappedField: {MappedField}",
|
||||||
|
associationId, recordNumber, finalDataHash, mappedSourceField ?? "N/A");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Pre-Requisiti per il Test
|
||||||
|
|
||||||
|
1. **Fermare l'applicazione** in esecuzione
|
||||||
|
2. **Ricompilare** il progetto:
|
||||||
|
```bash
|
||||||
|
dotnet build Data_Coupler.sln
|
||||||
|
```
|
||||||
|
3. **Configurare logging Debug** in `appsettings.Development.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Data_Coupler.Pages.DataCoupler": "Debug",
|
||||||
|
"Data_Coupler.Services.ScheduledProfileExecutionService": "Debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario di Test
|
||||||
|
|
||||||
|
1. **Configurare mapping** con campo mappato a `DestinationId`:
|
||||||
|
- Esempio: `CodiceFiscale` (sorgente) → `DestinationId` (destinazione)
|
||||||
|
|
||||||
|
2. **Selezionare campo chiave** (può essere diverso dal campo mappato a DestinationId):
|
||||||
|
- Esempio: `Email` come SourceKeyField
|
||||||
|
|
||||||
|
3. **Eseguire trasferimento dati**
|
||||||
|
|
||||||
|
4. **Verificare nei log**:
|
||||||
|
```
|
||||||
|
MAPPING DEBUG: Cercando quale campo sorgente è mappato a 'DestinationId'
|
||||||
|
MAPPING DEBUG: Mappings disponibili: CodiceFiscale -> DestinationId, Nome -> FirstName, ...
|
||||||
|
MAPPING DEBUG: Trovato mapping: campo sorgente 'CodiceFiscale' è mappato a 'DestinationId'
|
||||||
|
COMPOSITE: Associazione creata con ID: 123 - MappedField: CodiceFiscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Verificare nel database**:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
SourceKeyField, -- Campo chiave sorgente (es. "Email")
|
||||||
|
MappedDestinationField, -- Campo sorgente mappato a DestinationId (es. "CodiceFiscale")
|
||||||
|
DestinationKeyField, -- Sempre "Id" o "DestinationId"
|
||||||
|
KeyValue, -- Valore del campo chiave
|
||||||
|
DestinationId -- ID generato dalla REST API
|
||||||
|
FROM KeyAssociations
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
LIMIT 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risultato Atteso
|
||||||
|
|
||||||
|
```
|
||||||
|
SourceKeyField | MappedDestinationField | DestinationKeyField | KeyValue | DestinationId
|
||||||
|
-----------------------|------------------------|---------------------|----------------------|---------------
|
||||||
|
Email | CodiceFiscale | Id | user@example.com | ABC123XYZ
|
||||||
|
Email | CodiceFiscale | Id | admin@example.com | DEF456UVW
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Spiegazione Concettuale
|
||||||
|
|
||||||
|
### Differenza tra i Campi
|
||||||
|
|
||||||
|
| Campo | Descrizione | Esempio |
|
||||||
|
|-------|-------------|---------|
|
||||||
|
| `SourceKeyField` | Campo usato come chiave univoca nella sorgente | "Email" |
|
||||||
|
| `KeyValue` | Valore specifico del campo chiave per questo record | "user@example.com" |
|
||||||
|
| `MappedDestinationField` | Campo sorgente mappato a `DestinationId` nella REST API | "CodiceFiscale" |
|
||||||
|
| `DestinationKeyField` | Campo chiave nella destinazione (sempre "Id" o "DestinationId") | "Id" |
|
||||||
|
| `DestinationId` | ID univoco generato dalla REST API | "ABC123XYZ" |
|
||||||
|
|
||||||
|
### Perché è Importante
|
||||||
|
|
||||||
|
Durante il coupling, il sistema deve:
|
||||||
|
|
||||||
|
1. **Identificare record esistenti**: Usa `SourceKeyField` e `KeyValue`
|
||||||
|
2. **Popolare DestinationId**: Usa il valore del campo sorgente specificato in `MappedDestinationField`
|
||||||
|
3. **Evitare duplicati**: Verifica se esiste già un'associazione con lo stesso `KeyValue`
|
||||||
|
|
||||||
|
Esempio pratico:
|
||||||
|
```
|
||||||
|
Record CSV:
|
||||||
|
- Email: "user@example.com" <- Usato come SourceKeyField per identificare il record
|
||||||
|
- CodiceFiscale: "RSSMRA80A01H501U" <- Valore da mettere in DestinationId
|
||||||
|
|
||||||
|
Mapping:
|
||||||
|
- CodiceFiscale → DestinationId <- MappedDestinationField = "CodiceFiscale"
|
||||||
|
- Email → EmailAddress
|
||||||
|
- Nome → FirstName
|
||||||
|
|
||||||
|
Associazione creata:
|
||||||
|
- SourceKeyField: "Email"
|
||||||
|
- KeyValue: "user@example.com"
|
||||||
|
- MappedDestinationField: "CodiceFiscale"
|
||||||
|
- DestinationId: "RSSMRA80A01H501U" <- Preso dal campo CodiceFiscale del record
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Checklist Verifica
|
||||||
|
|
||||||
|
- [x] Correzione logica in `DataCoupler.razor.cs::CreateAssociationAsync`
|
||||||
|
- [x] Correzione logica in `DataCoupler.razor.cs::StartDataTransferOriginal`
|
||||||
|
- [x] Correzione logica in `ScheduledProfileExecutionService.cs::CreateAssociationAsync`
|
||||||
|
- [x] Aggiunta logging diagnostico in tutti i metodi
|
||||||
|
- [x] Verifica assenza errori di compilazione
|
||||||
|
- [ ] Test con trasferimento reale
|
||||||
|
- [ ] Verifica popolamento campo nel database
|
||||||
|
- [ ] Verifica log output corretto
|
||||||
|
|
||||||
|
## 🎯 Prossimi Passi
|
||||||
|
|
||||||
|
1. **Fermare applicazione in esecuzione**
|
||||||
|
2. **Ricompilare progetto**
|
||||||
|
3. **Riavviare applicazione**
|
||||||
|
4. **Eseguire test trasferimento**
|
||||||
|
5. **Verificare log output** (cercare "MAPPING DEBUG")
|
||||||
|
6. **Query database** per confermare campo popolato
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Data Correzione**: 20 Ottobre 2025
|
||||||
|
**Versione**: 1.0 - Correzione Logica MappedDestinationField
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# Troubleshooting MappedDestinationField NULL
|
||||||
|
|
||||||
|
## Data: 20 Ottobre 2025
|
||||||
|
|
||||||
|
## Problema Segnalato
|
||||||
|
|
||||||
|
Il campo `MappedDestinationField` rimane sempre `NULL` nelle associazioni anche quando viene fatto un mapping.
|
||||||
|
|
||||||
|
## Analisi del Problema
|
||||||
|
|
||||||
|
### Come Dovrebbe Funzionare
|
||||||
|
|
||||||
|
1. L'utente crea un mapping: `CardCode` (sorgente) → `cardcode__c` (destinazione)
|
||||||
|
2. L'utente seleziona `CardCode` come campo chiave sorgente
|
||||||
|
3. Durante il trasferimento, il sistema dovrebbe:
|
||||||
|
- Cercare `CardCode` nel dizionario `fieldMappings`
|
||||||
|
- Trovare il valore mappato `cardcode__c`
|
||||||
|
- Salvarlo in `MappedDestinationField`
|
||||||
|
|
||||||
|
### Struttura Dati
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// fieldMappings è un Dictionary<string, string>
|
||||||
|
// Chiave: nome colonna sorgente (es. "CardCode")
|
||||||
|
// Valore: nome campo destinazione (es. "cardcode__c")
|
||||||
|
|
||||||
|
Dictionary<string, string> fieldMappings = new()
|
||||||
|
{
|
||||||
|
{ "CardCode", "cardcode__c" },
|
||||||
|
{ "CardName", "Name" },
|
||||||
|
{ "City", "BillingCity" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// sourceKeyField è la colonna selezionata come chiave
|
||||||
|
string sourceKeyField = "CardCode";
|
||||||
|
|
||||||
|
// Logica di ricerca
|
||||||
|
if (fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[sourceKeyField]; // Dovrebbe essere "cardcode__c"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Possibili Cause
|
||||||
|
|
||||||
|
### 1. Campo Chiave Non Mappato ❌
|
||||||
|
|
||||||
|
**Scenario**: L'utente seleziona un campo chiave che NON è stato mappato.
|
||||||
|
|
||||||
|
**Esempio**:
|
||||||
|
```
|
||||||
|
Mappings creati:
|
||||||
|
- CardName → Name
|
||||||
|
- City → BillingCity
|
||||||
|
|
||||||
|
Campo chiave selezionato: CardCode ← NON MAPPATO!
|
||||||
|
|
||||||
|
Risultato: MappedDestinationField = NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Soluzione**: Verificare che il campo chiave selezionato sia presente nei mappings.
|
||||||
|
|
||||||
|
### 2. Case Sensitivity Issues ⚠️
|
||||||
|
|
||||||
|
**Scenario**: Il nome del campo chiave non corrisponde esattamente alla chiave nel dizionario.
|
||||||
|
|
||||||
|
**Esempio**:
|
||||||
|
```
|
||||||
|
Mapping creato: "cardcode" → "cardcode__c"
|
||||||
|
Campo chiave: "CardCode" ← Diverso per maiuscole!
|
||||||
|
|
||||||
|
Risultato: ContainsKey restituisce false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Soluzione**: Verificare che il nome sia identico (case-sensitive).
|
||||||
|
|
||||||
|
### 3. Spazi o Caratteri Nascosti 🔍
|
||||||
|
|
||||||
|
**Scenario**: Presenza di spazi all'inizio/fine del nome campo.
|
||||||
|
|
||||||
|
**Esempio**:
|
||||||
|
```
|
||||||
|
Mapping: "CardCode " → "cardcode__c" (nota lo spazio finale)
|
||||||
|
Campo chiave: "CardCode"
|
||||||
|
|
||||||
|
Risultato: Non corrisponde
|
||||||
|
```
|
||||||
|
|
||||||
|
**Soluzione**: Trim dei nomi campo durante la creazione del mapping.
|
||||||
|
|
||||||
|
## Logging Diagnostico Aggiunto
|
||||||
|
|
||||||
|
Ho aggiunto logging dettagliato per diagnosticare il problema:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Tentativo di trovare mapping per sourceKeyField: '{SourceKeyField}'", sourceKeyField);
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}",
|
||||||
|
string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||||
|
|
||||||
|
if (fieldMappings.ContainsKey(sourceKeyField))
|
||||||
|
{
|
||||||
|
mappedDestinationField = fieldMappings[sourceKeyField];
|
||||||
|
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: '{SourceKeyField}' -> '{MappedField}'",
|
||||||
|
sourceKeyField, mappedDestinationField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("MAPPING DEBUG: Campo chiave '{SourceKeyField}' NON trovato nei mappings!",
|
||||||
|
sourceKeyField);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Procedura di Diagnosi
|
||||||
|
|
||||||
|
### Step 1: Abilitare Logging Debug
|
||||||
|
|
||||||
|
In `appsettings.Development.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Data_Coupler.Pages.DataCoupler": "Debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Eseguire un Trasferimento Dati
|
||||||
|
|
||||||
|
1. Creare almeno un mapping che include il campo chiave
|
||||||
|
2. Selezionare il campo chiave
|
||||||
|
3. Eseguire il trasferimento
|
||||||
|
4. Monitorare i log
|
||||||
|
|
||||||
|
### Step 3: Analizzare i Log
|
||||||
|
|
||||||
|
Cercare le righe `MAPPING DEBUG`:
|
||||||
|
|
||||||
|
#### Caso 1: Campo Trovato ✅
|
||||||
|
```
|
||||||
|
[Debug] MAPPING DEBUG: Tentativo di trovare mapping per sourceKeyField: 'CardCode'
|
||||||
|
[Debug] MAPPING DEBUG: Mappings disponibili: CardCode -> cardcode__c, CardName -> Name
|
||||||
|
[Debug] MAPPING DEBUG: Trovato mapping: 'CardCode' -> 'cardcode__c'
|
||||||
|
[Info] COMPOSITE: Associazione creata con ID: 42 - MappedField: cardcode__c
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risultato**: `MappedDestinationField` dovrebbe essere popolato correttamente.
|
||||||
|
|
||||||
|
#### Caso 2: Campo Non Trovato ❌
|
||||||
|
```
|
||||||
|
[Debug] MAPPING DEBUG: Tentativo di trovare mapping per sourceKeyField: 'CardCode'
|
||||||
|
[Debug] MAPPING DEBUG: Mappings disponibili: CardName -> Name, City -> BillingCity
|
||||||
|
[Warning] MAPPING DEBUG: Campo chiave 'CardCode' NON trovato nei mappings!
|
||||||
|
[Info] COMPOSITE: Associazione creata con ID: 42 - MappedField: N/A
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problema**: Il campo chiave non è stato mappato!
|
||||||
|
|
||||||
|
**Soluzione**: Creare un mapping per il campo chiave.
|
||||||
|
|
||||||
|
### Step 4: Verificare il Database
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Verifica le associazioni create
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
KeyValue,
|
||||||
|
SourceKeyField,
|
||||||
|
DestinationKeyField,
|
||||||
|
MappedDestinationField,
|
||||||
|
DestinationEntity,
|
||||||
|
CreatedAt
|
||||||
|
FROM KeyAssociations
|
||||||
|
ORDER BY CreatedAt DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cosa Cercare**:
|
||||||
|
- `SourceKeyField`: Dovrebbe essere il nome del campo chiave (es. "CardCode")
|
||||||
|
- `MappedDestinationField`:
|
||||||
|
- Se NULL → il campo non era nei mappings
|
||||||
|
- Se valorizzato → tutto OK
|
||||||
|
|
||||||
|
## Soluzione Temporanea
|
||||||
|
|
||||||
|
Se il problema persiste, puoi verificare manualmente nel database:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Trova associazioni senza campo mappato
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM KeyAssociations
|
||||||
|
WHERE MappedDestinationField IS NULL;
|
||||||
|
|
||||||
|
-- Verifica un'associazione specifica
|
||||||
|
SELECT
|
||||||
|
KeyValue,
|
||||||
|
SourceKeyField,
|
||||||
|
MappedDestinationField,
|
||||||
|
AdditionalInfo
|
||||||
|
FROM KeyAssociations
|
||||||
|
WHERE Id = [ID_ASSOCIAZIONE];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Case
|
||||||
|
|
||||||
|
Per testare la funzionalità:
|
||||||
|
|
||||||
|
### Test 1: Campo Mappato ✅
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Crea mapping: CardCode → cardcode__c
|
||||||
|
2. Seleziona campo chiave: CardCode
|
||||||
|
3. Esegui trasferimento
|
||||||
|
4. Verifica: MappedDestinationField = "cardcode__c"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Campo Non Mappato ❌
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Crea mapping: CardName → Name (NON mappare CardCode)
|
||||||
|
2. Seleziona campo chiave: CardCode
|
||||||
|
3. Esegui trasferimento
|
||||||
|
4. Verifica: MappedDestinationField = NULL
|
||||||
|
5. Log: "Campo chiave 'CardCode' NON trovato nei mappings!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Controllo Validazione
|
||||||
|
|
||||||
|
Il sistema dovrebbe impedire il trasferimento se il campo chiave non è mappato (controllo aggiunto precedentemente).
|
||||||
|
|
||||||
|
## Prossimi Passi
|
||||||
|
|
||||||
|
1. **Eseguire un test con logging abilitato**
|
||||||
|
2. **Catturare i log durante il trasferimento**
|
||||||
|
3. **Analizzare l'output del MAPPING DEBUG**
|
||||||
|
4. **Determinare se il problema è**:
|
||||||
|
- Campo non mappato (normale, warning utente)
|
||||||
|
- Bug nel codice di lookup (da fixare)
|
||||||
|
- Problema di encoding/spazi (sanitizzare input)
|
||||||
|
|
||||||
|
## File Modificati
|
||||||
|
|
||||||
|
- ✅ `Data_Coupler/Pages/DataCoupler.razor.cs` - Aggiunto logging diagnostico
|
||||||
|
|
||||||
|
## Status: 🔍 IN DIAGNOSI
|
||||||
|
|
||||||
|
Logging aggiunto, in attesa di eseguire test per identificare la causa esatta.
|
||||||
Reference in New Issue
Block a user