using Omika.Core.Compatibility; using Omika.Core.Domain.Entities.Clients; using Omika.Core.Domain.ValueObjects; using Omika.Data.Mapping.Common; using BatikModels = Omika.Data.HfSql.Models.Batik; using OmikaModels = Omika.Data.HfSql.Models.Omika; using BatikMappers = Omika.Data.Mapping.Batik; using OmikaMappers = Omika.Data.Mapping.Omika; namespace Omika.Tests.Mapping; /// /// Tests unitaires des premiers mappers physiques → canonique (Clients, /// Adresses et valeurs de référentiel). Aucune base réelle, aucun HFSQL : /// uniquement des objets en mémoire. /// public class PhysicalToCanonicalMappingTests { // ---- 1. Client Batik minimal ------------------------------------------------- [Fact] public void ClientBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue() { var row = new BatikModels.ClientRow { CLPKID = 1 }; CanonicalMappingResult result = BatikMappers.BatikClientMapper.Map(row); Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion); Assert.NotNull(result.Value); Assert.Null(result.Value.Nom); Assert.Null(result.Value.Type); Assert.Null(result.Value.Statut); Assert.Null(result.Value.Coordonnees); Assert.Null(result.Value.AdressePrincipale); Assert.False(result.Value.EstArchive); Assert.NotNull(result.Value.SourceReference); Assert.Equal(1, result.Value.SourceReference!.NumericId); Assert.Equal(DatabaseSchemaVersion.Batik, result.Value.SourceReference!.Source); Assert.Null(result.Value.SourceReference!.EntityGuid); Assert.Empty(result.Warnings); } // ---- 2. Client Batik complet ------------------------------------------------- [Fact] public void ClientBatik_Full_MapsAllCanonicalProperties() { var row = new BatikModels.ClientRow { CLPKID = 42, CLCodeClient = "C-001", CLNom = "Ville de Lyon", CLIDType = 2, CLIDStatut = 3, CLFormeJuridique = "Collectivité", CLCodeTVA = "FR12345678901", CLCodeComptable = "411LYON", CLSIREN = "123456789", CLSIRET = "12345678900012", CLEmail = "contact@ville-lyon.fr", CLTel = "0472000000", CLFax = "0472000001", CLPays = "FR", CLInfoSup = "Client principal", CLBlocage = true, ClRaisonBlocage = "Litige en cours", Archive = true, CLIDAdressePrincipale = 10 }; var referenceRows = new Dictionary { [2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Régie" }, [3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "Actif" } }; var adresses = new Dictionary { [10] = new() { ADPKID = 10, ADEtiquette = "Mairie", ADLigne1 = "1 place de la Comédie", ADVille = "Lyon", ADCodePostal = "69001" } }; CanonicalMappingResult result = BatikMappers.BatikClientMapper.Map(row, referenceRows, adresses); Client client = result.Value; Assert.Equal("C-001", client.Code); Assert.Equal("Ville de Lyon", client.Nom); Assert.Equal("Régie", client.Type!.Label); Assert.Equal(2, client.Type!.LegacyId); Assert.Equal("T", client.Type!.Code); Assert.Equal("Actif", client.Statut!.Label); Assert.Equal(3, client.Statut!.LegacyId); Assert.Equal("Collectivité", client.FormeJuridique!.Label); Assert.Equal("FR12345678901", client.CodeTva); Assert.Equal("411LYON", client.CodeComptable); Assert.Equal("123456789", client.Siren); Assert.Equal("12345678900012", client.Siret); Assert.Equal("contact@ville-lyon.fr", client.Coordonnees!.Email); Assert.Equal("0472000000", client.Coordonnees!.Phone); Assert.Equal("0472000001", client.Coordonnees!.Fax); Assert.Equal("FR", client.Pays); Assert.Equal("Client principal", client.InformationsComplementaires); Assert.True(client.EstBloque); Assert.Equal("Litige en cours", client.RaisonBlocage); Assert.True(client.EstArchive); Assert.NotNull(client.AdressePrincipale); Assert.True(client.AdressePrincipale!.EstPrincipale); Assert.Equal("Mairie", client.AdressePrincipale!.Etiquette); Assert.Equal("69001", client.AdressePrincipale!.AdressePostale!.PostalCode); Assert.Equal(42, client.SourceReference!.NumericId); Assert.Equal(DatabaseSchemaVersion.Batik, client.SourceReference!.Source); } // ---- 3. Adresse Batik --------------------------------------------------------- [Fact] public void AdresseBatik_MapsLinesAndFillsLine3FromLine4Line5WhenEmpty() { var complete = new BatikModels.AdresseRow { ADPKID = 5, ADEtiquette = "Siège", ADLigne1 = "M. Dupont", ADLigne2 = "Service comptabilité", ADLigne3 = "Bâtiment A", ADLigne4 = "12 rue de la Paix", ADLigne5 = "ZI Nord", ADCodePostal = "69001", ADVille = "Lyon", ADPresentation = "bloc courrier" }; CanonicalMappingResult result = BatikMappers.BatikAdresseMapper.Map(complete); Adresse adresse = result.Value; Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion); Assert.Equal("Siège", adresse.Etiquette); Assert.Equal("M. Dupont", adresse.AdressePostale!.Line1); Assert.Equal("Service comptabilité", adresse.AdressePostale!.Line2); Assert.Equal("Bâtiment A", adresse.AdressePostale!.Line3); Assert.Equal("69001", adresse.AdressePostale!.PostalCode); Assert.Equal("Lyon", adresse.AdressePostale!.City); Assert.Equal(5, adresse.SourceReference!.NumericId); Assert.Contains(result.IgnoredFields, f => f == "AD_Presentation"); var withoutLine3 = new BatikModels.AdresseRow { ADPKID = 6, ADLigne1 = "M. Dupont", ADLigne4 = "12 rue de la Paix", ADLigne5 = "ZI Nord", ADCodePostal = "69001", ADVille = "Lyon" }; CanonicalMappingResult result2 = BatikMappers.BatikAdresseMapper.Map(withoutLine3); Assert.Equal("12 rue de la Paix ZI Nord", result2.Value.AdressePostale!.Line3); } // ---- 4. Client Omika avec GUID valide ---------------------------------------- [Fact] public void ClientOmika_ValidGuid_KeepsEntityGuidAndPhysicalGuid() { var row = new OmikaModels.ClientRow { CLPKID = 7, GUID = "550e8400-e29b-41d4-a716-446655440000", CLNom = "SARL Omika" }; CanonicalMappingResult result = OmikaMappers.OmikaClientMapper.Map(row); Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion); Assert.NotNull(result.Value.SourceReference!.EntityGuid); Assert.Equal( "550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.EntityGuid!.Value.ToString("D")); Assert.Equal("550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.PhysicalGuid); Assert.Equal(7, result.Value.SourceReference!.NumericId); Assert.Equal("SARL Omika", result.Value.Nom); } // ---- 5. Client Omika avec GUID sans tirets ------------------------------------ [Fact] public void ClientOmika_GuidWithoutDashes_IsParsedByTryParse() { var row = new OmikaModels.ClientRow { CLPKID = 8, GUID = "550e8400e29b41d4a716446655440000" }; CanonicalMappingResult result = OmikaMappers.OmikaClientMapper.Map(row); Assert.NotNull(result.Value.SourceReference!.EntityGuid); Assert.Equal( "550e8400-e29b-41d4-a716-446655440000", result.Value.SourceReference!.EntityGuid!.Value.ToString("D")); Assert.Empty(result.Warnings); } // ---- 6. Client Omika avec GUID invalide --------------------------------------- [Fact] public void ClientOmika_InvalidGuid_ProducesNullGuidAndWarningWithoutException() { var row = new OmikaModels.ClientRow { CLPKID = 9, GUID = "not-a-guid", CLNom = "Société inconnue" }; CanonicalMappingResult result = OmikaMappers.OmikaClientMapper.Map(row); Assert.Null(result.Value.SourceReference!.EntityGuid); Assert.Equal("not-a-guid", result.Value.SourceReference!.PhysicalGuid); Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.InvalidPhysicalGuid, StringComparison.Ordinal)); Assert.Equal("Société inconnue", result.Value.Nom); } // ---- 7. Adresse Omika ---------------------------------------------------------- [Fact] public void AdresseOmika_NativeMemoIsPreferredOverZombieLines() { var row = new OmikaModels.AdresseRow { ADPKID = 11, GUID = "1f0e8400-e29b-41d4-a716-446655441111", ADEtiquette = "Agence Lyon", ADAdresse = "12 rue de la Paix", ADLigne1 = "ancienne ligne 1", ADCodePostal = "69001", ADVille = "Lyon" }; CanonicalMappingResult result = OmikaMappers.OmikaAdresseMapper.Map(row); Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion); Assert.Equal("12 rue de la Paix", result.Value.AdressePostale!.Line1); Assert.Equal("Agence Lyon", result.Value.Etiquette); Assert.Equal("69001", result.Value.AdressePostale!.PostalCode); Assert.Equal("Lyon", result.Value.AdressePostale!.City); Assert.DoesNotContain(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal)); Assert.NotNull(result.Value.SourceReference!.EntityGuid); } [Fact] public void AdresseOmika_WithoutNativeMemo_UsesZombieLinesAsDocumentedFallback() { var row = new OmikaModels.AdresseRow { ADPKID = 12, ADLigne1 = "M. Dupont", ADLigne2 = "Service comptabilité", ADCodePostal = "69002", ADVille = "Lyon" }; CanonicalMappingResult result = OmikaMappers.OmikaAdresseMapper.Map(row); Assert.Equal("M. Dupont", result.Value.AdressePostale!.Line1); Assert.Equal("Service comptabilité", result.Value.AdressePostale!.Line2); Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal)); } // ---- 8. Priorité de la clé métier Omika ---------------------------------------- [Fact] public void ClientOmika_BusinessKeyIsPreferredAndLegacyIdIsKeptAsCompatibility() { var referenceRows = new Dictionary { ["ENT"] = new() { PAPKID = 50, PACLE = "ENT", PALibelle = "Entreprise", PACategorie = "TYPE" } }; var row = new OmikaModels.ClientRow { CLPKID = 13, CLCleType = "ENT", CLIDType = 99, // zombie : ancien identifiant CLCleStatut = null, CLIDStatut = 5 // zombie : ancien identifiant, pas de clé métier }; CanonicalMappingResult result = OmikaMappers.OmikaClientMapper.Map(row, referenceRows); Assert.Equal("ENT", result.Value.Type!.Key); Assert.Equal("Entreprise", result.Value.Type!.Label); Assert.Equal(99, result.Value.Type!.LegacyId); Assert.NotNull(result.Value.Statut); Assert.Equal(5, result.Value.Statut!.LegacyId); Assert.Null(result.Value.Statut!.Key); Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal)); } // ---- 9. Conservation des identifiants sources ---------------------------------- [Fact] public void SourceIdentifiers_ArePreservedOnBothSchemas() { var batik = new BatikModels.ClientRow { CLPKID = 1234, CLIDImport = "IMP-B" }; CanonicalMappingResult batikResult = BatikMappers.BatikClientMapper.Map(batik); Assert.Equal(DatabaseSchemaVersion.Batik, batikResult.Value.SourceReference!.Source); Assert.Equal(1234, batikResult.Value.SourceReference!.NumericId); Assert.Equal("IMP-B", batikResult.Value.SourceReference!.ImportId); Assert.Null(batikResult.Value.SourceReference!.EntityGuid); var omika = new OmikaModels.ClientRow { CLPKID = 5678, GUID = "550e8400-e29b-41d4-a716-446655440000", CLIDImport = "IMP-O" }; CanonicalMappingResult omikaResult = OmikaMappers.OmikaClientMapper.Map(omika); Assert.Equal(DatabaseSchemaVersion.Omika, omikaResult.Value.SourceReference!.Source); Assert.Equal(5678, omikaResult.Value.SourceReference!.NumericId); Assert.Equal("IMP-O", omikaResult.Value.SourceReference!.ImportId); Assert.NotNull(omikaResult.Value.SourceReference!.EntityGuid); } // ---- 10. Aucune exception sur les valeurs historiques ou nulles ---------------- [Fact] public void Mapping_HistoricalOrNullValues_NeverThrow() { var batikClient = new BatikModels.ClientRow(); BatikMappers.BatikClientMapper.Map(batikClient); var batikAdresse = new BatikModels.AdresseRow(); BatikMappers.BatikAdresseMapper.Map(batikAdresse); var batikRef = new BatikModels.ParametreMetierRow(); BatikMappers.BatikReferenceValueMapper.Map(batikRef); var omikaClient = new OmikaModels.ClientRow(); OmikaMappers.OmikaClientMapper.Map(omikaClient); var omikaAdresse = new OmikaModels.AdresseRow(); OmikaMappers.OmikaAdresseMapper.Map(omikaAdresse); var omikaRef = new OmikaModels.ParametreMetierRow(); OmikaMappers.OmikaReferenceValueMapper.Map(omikaRef); var omikaClientLegacy = new OmikaModels.ClientRow { CLIDType = 12, CLIDStatut = 0 }; CanonicalMappingResult legacyResult = OmikaMappers.OmikaClientMapper.Map(omikaClientLegacy); Assert.NotNull(legacyResult.Value.Type); Assert.Equal(12, legacyResult.Value.Type!.LegacyId); var omikaClientNullGuid = new OmikaModels.ClientRow { GUID = null! }; Assert.NotNull(OmikaMappers.OmikaClientMapper.Map(omikaClientNullGuid).Value); } // ---- 11. Présence des avertissements attendus ---------------------------------- [Fact] public void Warnings_AreRaisedForInvalidGuidAndUnresolvedReference() { // GUID invalide. var invalidGuid = new OmikaModels.ClientRow { CLPKID = 21, GUID = "zzz" }; CanonicalMappingResult invalidGuidResult = OmikaMappers.OmikaClientMapper.Map(invalidGuid); Assert.Contains(invalidGuidResult.Warnings, w => w.StartsWith(MappingWarningCodes.InvalidPhysicalGuid, StringComparison.Ordinal)); // Référentiel Batik non résolu. var unresolved = new BatikModels.ClientRow { CLPKID = 22, CLIDType = 77 }; CanonicalMappingResult unresolvedResult = BatikMappers.BatikClientMapper.Map(unresolved); Assert.Contains(unresolvedResult.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal)); // Clé métier absente sur une valeur de référentiel Omika. var noKey = new OmikaModels.ParametreMetierRow { PAPKID = 30, PALibelle = "Actif" }; CanonicalMappingResult noKeyResult = OmikaMappers.OmikaReferenceValueMapper.Map(noKey); Assert.Contains(noKeyResult.Warnings, w => w.StartsWith(MappingWarningCodes.MissingBusinessKey, StringComparison.Ordinal)); Assert.Equal("Actif", noKeyResult.Value.Label); // Libellé absent : jamais inventé, simple avertissement. var noLabel = new BatikModels.ParametreMetierRow { PAPKID = 31, PACode = "T", PAValeur = "1" }; CanonicalMappingResult noLabelResult = BatikMappers.BatikReferenceValueMapper.Map(noLabel); Assert.Contains(noLabelResult.Warnings, w => w.StartsWith(MappingWarningCodes.EmptyReferenceLabel, StringComparison.Ordinal)); Assert.Equal(string.Empty, noLabelResult.Value.Label); Assert.Equal(31, noLabelResult.Value.LegacyId); } // ---- 12. Absence de noms HFSQL dans le domaine canonique ------------------------ [Fact] public void CanonicalDomain_DoesNotExposeHfSqlNames() { var row = new OmikaModels.ClientRow { CLPKID = 40, GUID = "550e8400-e29b-41d4-a716-446655440000", CLNom = "SARL Test", CLIDType = 0, CLCleType = null, CLIDStatut = 0, CLCodeTVA = "FR123", CLEmail = "test@test.fr", Archive = false }; CanonicalMappingResult result = OmikaMappers.OmikaClientMapper.Map(row); Client client = result.Value; string[] forbidden = [ "CL_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "Ligne_", "GUID" ]; AssertHasNoPhysicalNames(client, forbidden); AssertHasNoPhysicalNames(client.AdressePrincipale ?? new Adresse(), forbidden); if (client.Coordonnees is not null) { AssertHasNoPhysicalNames(client.Coordonnees, forbidden); } if (client.SourceReference is not null) { AssertHasNoPhysicalNames(client.SourceReference, forbidden); } } // ---- Référentiels ---------------------------------------------------------------- [Fact] public void BatikReferenceValue_MapFavorsLegacyIdCodeAndLabel() { var row = new BatikModels.ParametreMetierRow { PAPKID = 60, PACode = "T", PAValeur = "1", PALibelle = "Régie" }; CanonicalMappingResult result = BatikMappers.BatikReferenceValueMapper.Map(row); Assert.Equal("Régie", result.Value.Label); Assert.Equal(60, result.Value.LegacyId); Assert.Equal("T", result.Value.Code); Assert.Null(result.Value.Key); } [Fact] public void OmikaReferenceValue_MapFavorsBusinessKeyAndKeepsLegacyIdAsCompatibility() { var row = new OmikaModels.ParametreMetierRow { PAPKID = 61, PACLE = "ACT", PALibelle = "Actif", PACategorie = "STATUT" }; CanonicalMappingResult result = OmikaMappers.OmikaReferenceValueMapper.Map(row); Assert.Equal("Actif", result.Value.Label); Assert.Equal("ACT", result.Value.Key); Assert.Equal(61, result.Value.LegacyId); Assert.Equal("STATUT", result.Value.Code); } [Fact] public void OmikaReferenceValue_MapByKey_UsesInMemoryDictionary() { var referenceRows = new Dictionary { ["RGE"] = new() { PAPKID = 70, PACLE = "RGE", PALibelle = "Régie", PACategorie = "TYPE" } }; CanonicalMappingResult? resolved = OmikaMappers.OmikaReferenceValueMapper.MapByKey("RGE", referenceRows); Assert.NotNull(resolved); Assert.Equal("Régie", resolved!.Value.Label); Assert.Equal("RGE", resolved.Value.Key); } // ---- Helper --------------------------------------------------------------------- private static void AssertHasNoPhysicalNames(object value, string[] forbidden) { foreach (var property in value.GetType().GetProperties()) { Assert.DoesNotContain(forbidden, f => property.Name.Contains(f, StringComparison.Ordinal)); } } }