Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,306 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Organisation;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using BatikModels = Omika.Data.HfSql.Models.Batik;
using BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Batik → canonique des collaborateurs (table Collaborateur).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class BatikCollaborateurMapperTests
{
// ---- 1. Collaborateur Batik minimal -------------------------------------------
[Fact]
public void CollaborateurBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.CollaborateurRow { COPKID = 1 };
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Civilite);
Assert.Null(result.Value.Statut);
Assert.Null(result.Value.Coordonnees);
Assert.False(result.Value.Actif);
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. Collaborateur Batik complet -------------------------------------------
[Fact]
public void CollaborateurBatik_Full_MapsAllCanonicalProperties()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 42,
COIDCivilite = 2,
CONom = "Dupont",
COPrenom = "Jean",
COTrigramme = "JD",
COTelFixe = "0472000000",
COTelMobile = "0600000000",
COEmail = "jean.dupont@ville-lyon.fr",
COIDStatut = 3,
COActif = true,
COIDImport = "IMP-B-42"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "C", PAValeur = "1", PALibelle = "M." },
[3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "Actif" }
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row, referenceRows);
Collaborateur collaborateur = result.Value;
Assert.Equal("M.", collaborateur.Civilite!.Label);
Assert.Equal(2, collaborateur.Civilite!.LegacyId);
Assert.Equal("Actif", collaborateur.Statut!.Label);
Assert.Equal(3, collaborateur.Statut!.LegacyId);
Assert.Equal("Dupont", collaborateur.Nom);
Assert.Equal("Jean", collaborateur.Prenom);
Assert.Equal("JD", collaborateur.Code);
Assert.Equal("jean.dupont@ville-lyon.fr", collaborateur.Coordonnees!.Email);
Assert.Equal("0472000000", collaborateur.Coordonnees!.Phone);
Assert.Equal("0600000000", collaborateur.Coordonnees!.Mobile);
Assert.True(collaborateur.Actif);
Assert.Equal(42, collaborateur.SourceReference!.NumericId);
Assert.Equal(DatabaseSchemaVersion.Batik, collaborateur.SourceReference!.Source);
}
// ---- 3. Coordonnées nulles ----------------------------------------------------
[Fact]
public void CollaborateurBatik_EmptyCoordinates_ProducesNullCoordonnees()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 5,
CONom = "Martin",
COEmail = " ",
COTelFixe = "",
COTelMobile = null
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.Prenom);
Assert.Equal("Martin", result.Value.Nom);
}
[Fact]
public void CollaborateurBatik_PartialCoordinates_KeepsOnlyProvidedValues()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 6,
COEmail = "m@exemple.fr",
COTelFixe = null,
COTelMobile = "0600000000"
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.Coordonnees);
Assert.Equal("m@exemple.fr", result.Value.Coordonnees!.Email);
Assert.Null(result.Value.Coordonnees!.Phone);
Assert.Equal("0600000000", result.Value.Coordonnees!.Mobile);
}
// ---- 4. Conservation de l'identifiant source ----------------------------------
[Fact]
public void CollaborateurBatik_SourceIdentifier_IsPreservedWithoutFabricatedGuid()
{
var row = new BatikModels.CollaborateurRow { COPKID = 1234, COIDImport = "IMP-B" };
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Batik, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
Assert.Equal("1234", result.Value.SourceReference!.PublicIdentifier);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
}
// ---- 5. Aucune exception sur les chaînes vides ----------------------------------
[Fact]
public void CollaborateurBatik_EmptyOrNullStrings_NeverThrow()
{
var row = new BatikModels.CollaborateurRow
{
CONom = "",
COPrenom = " ",
COEmail = "",
COTelFixe = null,
COTelMobile = " ",
COTrigramme = "",
COIDImport = ""
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Coordonnees);
Assert.Empty(result.Warnings);
}
[Fact]
public void CollaborateurBatik_DefaultRow_NeverThrows()
{
var row = new BatikModels.CollaborateurRow();
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.Actif);
Assert.False(result.Value.EstArchive);
}
// ---- 6. Absence de noms physiques HFSQL dans le domaine canonique --------------
[Fact]
public void CollaborateurBatik_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 40,
COIDCivilite = 2,
CONom = "Dupont",
COPrenom = "Jean",
COTelFixe = "01",
COTelMobile = "06",
COEmail = "j@exemple.fr",
COActif = true,
COIDImport = "IMP-40"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "C", PAValeur = "1", PALibelle = "M." }
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row, referenceRows);
Collaborateur collaborateur = result.Value;
string[] forbidden =
[
"CO_", "CL_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(collaborateur, forbidden);
if (collaborateur.Coordonnees is not null)
{
AssertHasNoPhysicalNames(collaborateur.Coordonnees, forbidden);
}
if (collaborateur.SourceReference is not null)
{
AssertHasNoPhysicalNames(collaborateur.SourceReference, forbidden);
}
}
// ---- Référentiels non résolus --------------------------------------------------
[Fact]
public void CollaborateurBatik_UnresolvedCiviliteAndStatut_KeepsLegacyIdAndWarns()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 7,
COIDCivilite = 77,
COIDStatut = 88
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal(77, result.Value.Civilite!.LegacyId);
Assert.Equal(string.Empty, result.Value.Civilite!.Label);
Assert.NotNull(result.Value.Statut);
Assert.Equal(88, result.Value.Statut!.LegacyId);
Assert.Contains(result.Warnings, w => w.Contains("CO_IDCivilite", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
Assert.Contains(result.Warnings, w => w.Contains("CO_IDStatut", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void CollaborateurBatik_ZeroCiviliteAndStatut_ProducesNoReference()
{
var row = new BatikModels.CollaborateurRow { COPKID = 8, COIDCivilite = 0, COIDStatut = 0 };
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.Null(result.Value.Civilite);
Assert.Null(result.Value.Statut);
Assert.Empty(result.Warnings);
}
// ---- Rubriques sans équivalent --------------------------------------------------
[Fact]
public void CollaborateurBatik_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new BatikModels.CollaborateurRow
{
COPKID = 9,
COFonction = "Développeur",
COPonderation = 1.5f,
COIDImport = "IMP-9",
COAuPlanning = true,
COAccesBO = true,
COAccesMobile = true,
CreationDate = new DateTime(2024, 1, 1),
ModifDate = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Collaborateur> result = BatikMappers.BatikCollaborateurMapper.Map(row);
Assert.Equal("Développeur", result.Value.Fonction);
Assert.Contains(result.IgnoredFields, f => f == "CO_Ponderation");
Assert.Contains(result.IgnoredFields, f => f == "CO_IDImport");
Assert.Contains(result.IgnoredFields, f => f == "CO_AuPlanning");
Assert.Contains(result.IgnoredFields, f => f == "CO_AccesBO");
Assert.Contains(result.IgnoredFields, f => f == "CO_AccesMobile");
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "ModifDate");
Assert.Null(result.Value.SocieteId);
}
// ---- 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));
}
}
}
@@ -0,0 +1,332 @@
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 BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Batik → canonique des contacts (table Personne).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class BatikContactMapperTests
{
// ---- 1. Contact Batik minimal -------------------------------------------------
[Fact]
public void ContactBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.PersonneRow { PEPKID = 1 };
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Civilite);
Assert.Empty(result.Value.Roles);
Assert.Null(result.Value.Coordonnees);
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. Contact Batik complet -------------------------------------------------
[Fact]
public void ContactBatik_Full_MapsAllCanonicalProperties()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 42,
PEIDCivilite = 2,
PENom = "Dupont",
PEPrenom = "Jean",
PEIDRole = 3,
PETelFixe = "0472000000",
PETelMobile = "0600000000",
PEEmail = "jean.dupont@ville-lyon.fr",
PEFax = "0472000001",
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "Contact principal",
PEActif = true,
PEUsageInterne = false,
PEInfoSupp = "Disponible en journée",
Archive = true,
PEIDImport = "IMP-B-42"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "C", PAValeur = "1", PALibelle = "M." },
[3] = new() { PAPKID = 3, PACode = "R", PAValeur = "1", PALibelle = "Contact principal" }
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row, referenceRows);
Contact contact = result.Value;
Assert.Equal("M.", contact.Civilite!.Label);
Assert.Equal(2, contact.Civilite!.LegacyId);
Assert.Equal("Dupont", contact.Nom);
Assert.Equal("Jean", contact.Prenom);
Assert.Equal("Jean Dupont", contact.NomAffiche);
Assert.Single(contact.Roles);
Assert.Equal("Contact principal", contact.Roles[0].Label);
Assert.Equal(3, contact.Roles[0].LegacyId);
Assert.Equal("jean.dupont@ville-lyon.fr", contact.Coordonnees!.Email);
Assert.Equal("0472000000", contact.Coordonnees!.Phone);
Assert.Equal("0600000000", contact.Coordonnees!.Mobile);
Assert.Equal("0472000001", contact.Coordonnees!.Fax);
Assert.True(contact.Actif);
Assert.True(contact.EstArchive);
Assert.Equal("Disponible en journée", contact.InformationsSupplementaires);
Assert.Equal(42, contact.SourceReference!.NumericId);
Assert.Equal("IMP-B-42", contact.SourceReference!.ImportId);
Assert.Equal(DatabaseSchemaVersion.Batik, contact.SourceReference!.Source);
}
// ---- 3. Coordonnées nulles ----------------------------------------------------
[Fact]
public void ContactBatik_EmptyCoordinates_ProducesNullCoordonnees()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 5,
PENom = "Martin",
PEEmail = " ",
PETelFixe = "",
PETelMobile = null,
PEFax = "\t"
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.Prenom);
Assert.Equal("Martin", result.Value.Nom);
}
[Fact]
public void ContactBatik_PartialCoordinates_KeepsOnlyProvidedValues()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 6,
PEEmail = "m@exemple.fr",
PETelFixe = null,
PETelMobile = "0600000000",
PEFax = ""
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.NotNull(result.Value.Coordonnees);
Assert.Equal("m@exemple.fr", result.Value.Coordonnees!.Email);
Assert.Null(result.Value.Coordonnees!.Phone);
Assert.Equal("0600000000", result.Value.Coordonnees!.Mobile);
Assert.Null(result.Value.Coordonnees!.Fax);
}
// ---- 4. Conservation de l'identifiant source ----------------------------------
[Fact]
public void ContactBatik_SourceIdentifier_IsPreservedWithoutFabricatedGuid()
{
var row = new BatikModels.PersonneRow { PEPKID = 1234, PEIDImport = "IMP-B" };
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Batik, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
Assert.Equal("IMP-B", result.Value.SourceReference!.ImportId);
Assert.Equal("1234", result.Value.SourceReference!.PublicIdentifier);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
}
// ---- 5. Aucune exception sur les chaînes vides ----------------------------------
[Fact]
public void ContactBatik_EmptyOrNullStrings_NeverThrow()
{
var row = new BatikModels.PersonneRow
{
PENom = "",
PEPrenom = " ",
PEEmail = "",
PETelFixe = null,
PETelMobile = " ",
PEFax = "",
PENote = "",
PEInfoSupp = " ",
PEIDImport = ""
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.InformationsSupplementaires);
Assert.Empty(result.Warnings);
}
[Fact]
public void ContactBatik_DefaultRow_NeverThrows()
{
var row = new BatikModels.PersonneRow();
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.Actif);
Assert.False(result.Value.EstArchive);
}
// ---- 6. Absence de noms physiques HFSQL dans le domaine canonique --------------
[Fact]
public void ContactBatik_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 40,
PEIDCivilite = 2,
PENom = "Dupont",
PEPrenom = "Jean",
PEIDRole = 3,
PEEmail = "j@exemple.fr",
PETelFixe = "01",
PETelMobile = "06",
PEFax = "02",
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "note",
PEActif = true,
PEUsageInterne = true,
PEInfoSupp = "info",
Archive = false,
PEIDImport = "IMP-40"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "C", PAValeur = "1", PALibelle = "M." },
[3] = new() { PAPKID = 3, PACode = "R", PAValeur = "1", PALibelle = "Rôle" }
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row, referenceRows);
Contact contact = result.Value;
string[] forbidden =
[
"PE_", "CL_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(contact, forbidden);
if (contact.Coordonnees is not null)
{
AssertHasNoPhysicalNames(contact.Coordonnees, forbidden);
}
if (contact.SourceReference is not null)
{
AssertHasNoPhysicalNames(contact.SourceReference, forbidden);
}
}
// ---- Référentiels non résolus --------------------------------------------------
[Fact]
public void ContactBatik_UnresolvedCiviliteAndRole_KeepsLegacyIdAndWarns()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 7,
PEIDCivilite = 77,
PEIDRole = 88
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal(77, result.Value.Civilite!.LegacyId);
Assert.Equal(string.Empty, result.Value.Civilite!.Label);
Assert.Single(result.Value.Roles);
Assert.Equal(88, result.Value.Roles[0].LegacyId);
Assert.Contains(result.Warnings, w => w.Contains("PE_IDCivilite", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
Assert.Contains(result.Warnings, w => w.Contains("PE_IDRole", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void ContactBatik_ZeroCiviliteAndRole_ProducesNoReference()
{
var row = new BatikModels.PersonneRow { PEPKID = 8, PEIDCivilite = 0, PEIDRole = 0 };
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.Null(result.Value.Civilite);
Assert.Empty(result.Value.Roles);
Assert.Empty(result.Warnings);
}
// ---- Rubriques sans équivalent --------------------------------------------------
[Fact]
public void ContactBatik_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new BatikModels.PersonneRow
{
PEPKID = 9,
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "note",
PEUsageInterne = true,
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1),
DateArchivage = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Contact> result = BatikMappers.BatikContactMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "PE_IDClient");
Assert.Contains(result.IgnoredFields, f => f == "PE_IDAdressePrincipale");
Assert.Contains(result.IgnoredFields, f => f == "PE_Note");
Assert.Contains(result.IgnoredFields, f => f == "PE_UsageInterne");
Assert.Contains(result.IgnoredFields, f => f == "ModifDate");
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
Assert.Null(result.Value.ClientId);
Assert.Empty(result.Value.SiteIds);
}
// ---- 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));
}
}
}
@@ -0,0 +1,181 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Interventions;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using BatikModels = Omika.Data.HfSql.Models.Batik;
using BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Batik → canonique des dossiers (table Dossier).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class BatikDossierMapperTests
{
// ---- 1. Dossier Batik minimal ------------------------------------------------
[Fact]
public void DossierBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.DossierRow { DOPKID = 1 };
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.ReferenceMetier);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.Type);
Assert.Null(result.Value.Statut);
Assert.False(result.Value.EstArchive);
Assert.Empty(result.Warnings);
}
// ---- 2. Conservation de l'identifiant source (cause du count=0 snapshot) -----
[Fact]
public void DossierBatik_SourceReference_IsPreservedWithNumericId()
{
// Cause réelle du micro-lot 19 : le mapper ne remplissait pas SourceReference,
// donc LoadDossiersAsync skippait tous les dossiers (NumericId null) → count=0.
var row = new BatikModels.DossierRow { DOPKID = 1234 };
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Batik, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
Assert.Equal("1234", result.Value.SourceReference!.PublicIdentifier);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
}
// ---- 3. Dossier Batik complet ------------------------------------------------
[Fact]
public void DossierBatik_Full_MapsAllCanonicalProperties()
{
var row = new BatikModels.DossierRow
{
DOPKID = 42,
DOIDTypeDossier = 2,
DOIDStatut = 3,
DOTitre = "Dossier chantier",
DONumDossier = "DOS-2024-001",
DOIDClient = 7,
Archive = false,
DODateDebut = new DateOnly(2024, 1, 15)
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Chantier" },
[3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "Ouvert" }
};
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row, referenceRows);
Dossier dossier = result.Value;
Assert.Equal("DOS-2024-001", dossier.ReferenceMetier);
Assert.Equal("Dossier chantier", dossier.Titre);
Assert.Equal("Chantier", dossier.Type!.Label);
Assert.Equal(2, dossier.Type!.LegacyId);
Assert.Equal("Ouvert", dossier.Statut!.Label);
Assert.Equal(3, dossier.Statut!.LegacyId);
Assert.Equal(new DateTime(2024, 1, 15), dossier.DateOuverture);
Assert.False(dossier.EstArchive);
Assert.Equal(42, dossier.SourceReference!.NumericId);
Assert.Equal(DatabaseSchemaVersion.Batik, dossier.SourceReference!.Source);
// La référence physique du client lié est conservée en interne.
Assert.NotNull(dossier.ClientSourceReference);
Assert.Equal(7, dossier.ClientSourceReference!.NumericId);
}
// ---- 4. Aucune exception sur les chaînes vides --------------------------------
[Fact]
public void DossierBatik_EmptyOrNullStrings_NeverThrow()
{
var row = new BatikModels.DossierRow
{
DOPKID = 5,
DOTitre = "",
DONumDossier = " "
};
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.ReferenceMetier);
Assert.Empty(result.Warnings);
}
[Fact]
public void DossierBatik_DefaultRow_NeverThrows()
{
var row = new BatikModels.DossierRow();
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row);
Assert.NotNull(result.Value);
// SourceReference est toujours rempli (porte DO_PKID) ; avec une ligne par
// défaut DO_PKID = 0, NumericId vaut 0 (jamais null en production car la
// clé primaire est auto-générée et strictement positive).
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(0, result.Value.SourceReference!.NumericId);
Assert.False(result.Value.EstArchive);
}
// ---- 5. Absence de noms physiques HFSQL dans le domaine canonique --------------
[Fact]
public void DossierBatik_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new BatikModels.DossierRow
{
DOPKID = 40,
DOIDTypeDossier = 2,
DOIDStatut = 3,
DOTitre = "Dossier",
DONumDossier = "DOS-40",
DOIDClient = 7,
Archive = false
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Chantier" },
[3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "Ouvert" }
};
CanonicalMappingResult<Dossier> result = BatikMappers.BatikDossierMapper.Map(row, referenceRows);
Dossier dossier = result.Value;
string[] forbidden =
[
"DO_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(dossier, forbidden);
if (dossier.SourceReference is not null)
{
AssertHasNoPhysicalNames(dossier.SourceReference, forbidden);
}
}
// ---- 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));
}
}
}
@@ -0,0 +1,281 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Interventions;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using BatikModels = Omika.Data.HfSql.Models.Batik;
using BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Batik → canonique des interventions.
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class BatikInterventionMapperTests
{
// ---- 1. Intervention Batik minimale -------------------------------------------
[Fact]
public void InterventionBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.InterventionRow { INPKID = 1 };
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.Reference);
Assert.Null(result.Value.Type);
Assert.Null(result.Value.Etat);
Assert.False(result.Value.EstArchive);
Assert.Empty(result.Warnings);
}
// ---- 2. Intervention Batik complète -------------------------------------------
[Fact]
public void InterventionBatik_Full_MapsAllCanonicalProperties()
{
var row = new BatikModels.InterventionRow
{
INPKID = 42,
INTitre = "Réparation chaudière",
INRefInterne = "INT-042",
INDescription = "Remplacement de la chaudière",
INIDTypeIntervention = 2,
INIDEtatIntervention = 3,
INDateIntervention = new DateOnly(2026, 3, 15),
Archive = true,
INIDImport = "IMP-B-42"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Maintenance" },
[3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "En cours" }
};
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row, referenceRows);
Intervention intervention = result.Value;
Assert.Equal("Réparation chaudière", intervention.Titre);
Assert.Equal("INT-042", intervention.Reference);
Assert.Equal("Remplacement de la chaudière", intervention.Description);
Assert.Equal("Maintenance", intervention.Type!.Label);
Assert.Equal(2, intervention.Type!.LegacyId);
Assert.Equal("En cours", intervention.Etat!.Label);
Assert.Equal(3, intervention.Etat!.LegacyId);
Assert.Equal(new DateTime(2026, 3, 15), intervention.DatePrevueDebut);
Assert.True(intervention.EstArchive);
}
// ---- 4. Type et état ----------------------------------------------------------
[Fact]
public void InterventionBatik_Type_IsResolvedFromLegacyId()
{
var row = new BatikModels.InterventionRow { INPKID = 5, INIDTypeIntervention = 7 };
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[7] = new() { PAPKID = 7, PACode = "T", PAValeur = "2", PALibelle = "Urgence" }
};
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row, referenceRows);
Assert.NotNull(result.Value.Type);
Assert.Equal("Urgence", result.Value.Type!.Label);
Assert.Equal(7, result.Value.Type!.LegacyId);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionBatik_UnresolvedType_KeepsLegacyIdAndWarns()
{
var row = new BatikModels.InterventionRow { INPKID = 6, INIDTypeIntervention = 77 };
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.NotNull(result.Value.Type);
Assert.Equal(77, result.Value.Type!.LegacyId);
Assert.Equal(string.Empty, result.Value.Type!.Label);
Assert.Contains(result.Warnings, w => w.Contains("IN_IDTypeIntervention", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void InterventionBatik_ZeroType_ProducesNoReference()
{
var row = new BatikModels.InterventionRow { INPKID = 8, INIDTypeIntervention = 0 };
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.Null(result.Value.Type);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionBatik_UnresolvedEtat_KeepsLegacyIdAndWarns()
{
var row = new BatikModels.InterventionRow { INPKID = 9, INIDEtatIntervention = 88 };
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.NotNull(result.Value.Etat);
Assert.Equal(88, result.Value.Etat!.LegacyId);
Assert.Contains(result.Warnings, w => w.Contains("IN_IDEtatIntervention", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
// ---- 5. Champs ignorés --------------------------------------------------------
[Fact]
public void InterventionBatik_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new BatikModels.InterventionRow
{
INPKID = 15,
INIDClient = 1,
INIDLieu = 2,
INIDDossier = 3,
INIDSuiviPar = 4,
INIDDemandeur = 5,
INIDSociete = 6,
INAttestationTVAPresente = true,
INNoteInterne = "note",
INRefExterne = "EXT-1",
INDureePrevue = 2.5f,
INNumDevis = "DEV-1",
INDateDevis = new DateOnly(2026, 1, 1),
INNumFacture = "FAC-1",
INDateFacture = new DateOnly(2026, 2, 1),
INNumIntervention = "N-1",
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1),
DateArchivage = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "IN_IDClient");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDLieu");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDDossier");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDSuiviPar");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDDemandeur");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDSociete");
Assert.Contains(result.IgnoredFields, f => f == "IN_AttestationTVAPresente");
Assert.Contains(result.IgnoredFields, f => f == "IN_NoteInterne");
Assert.Contains(result.IgnoredFields, f => f == "IN_DureePrevue");
Assert.Contains(result.IgnoredFields, f => f == "IN_DateDevis");
Assert.Contains(result.IgnoredFields, f => f == "IN_DateFacture");
Assert.Equal(new DateTime(2024, 1, 1), result.Value.DateModification);
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
// Champs désormais dotés d'un équivalent canonique : ils ne sont plus
// ignorés, ils sont mappés dans l'entité Intervention.
Assert.Equal("EXT-1", result.Value.ReferenceExterne);
Assert.Equal("DEV-1", result.Value.NumeroDevis);
Assert.Equal("N-1", result.Value.NumeroIntervention);
Assert.Equal("FAC-1", result.Value.NumeroFacture);
}
// ---- 6. Aucune exception sur les chaînes vides --------------------------------
[Fact]
public void InterventionBatik_EmptyOrNullStrings_NeverThrow()
{
var row = new BatikModels.InterventionRow
{
INTitre = "",
INRefInterne = " ",
INDescription = "",
INNoteInterne = " ",
INRefExterne = "",
INIDImport = " "
};
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.Reference);
Assert.Null(result.Value.Description);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionBatik_DefaultRow_NeverThrows()
{
var row = new BatikModels.InterventionRow();
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row);
Assert.NotNull(result.Value);
Assert.False(result.Value.EstArchive);
}
// ---- 7. Absence de noms physiques HFSQL dans le domaine canonique -------------
[Fact]
public void InterventionBatik_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new BatikModels.InterventionRow
{
INPKID = 40,
INTitre = "Titre",
INRefInterne = "INT-040",
INDescription = "desc",
INIDTypeIntervention = 2,
INIDEtatIntervention = 3,
INDateIntervention = new DateOnly(2026, 1, 1),
Archive = false,
INIDImport = "IMP-40",
INIDClient = 1,
INIDLieu = 2,
INIDDossier = 3,
INIDSuiviPar = 4,
INIDDemandeur = 5,
INIDSociete = 6,
INAttestationTVAPresente = true,
INNoteInterne = "note",
INRefExterne = "EXT",
INDureePrevue = 1f,
INNumDevis = "DEV",
INNumFacture = "FAC",
INNumIntervention = "N"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Maintenance" },
[3] = new() { PAPKID = 3, PACode = "S", PAValeur = "1", PALibelle = "En cours" }
};
CanonicalMappingResult<Intervention> result = BatikMappers.BatikInterventionMapper.Map(row, referenceRows);
Intervention intervention = result.Value;
string[] forbidden =
[
"IN_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(intervention, forbidden);
if (intervention.SourceReference is not null)
{
AssertHasNoPhysicalNames(intervention.SourceReference, forbidden);
}
}
// ---- 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));
}
}
}
@@ -0,0 +1,89 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Planning;
using BatikModels = Omika.Data.HfSql.Models.Batik;
using BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapping Batik des valeurs de référentiel d'annulation d'un
/// rendez-vous. Aucune base réelle : uniquement des objets en mémoire.
///
/// En Batik, l'origine et le motif d'annulation sont stockés sous forme
/// d'identifiants numériques (<c>PL_IDOrigineAnnul</c> / <c>PL_IDMotifAnnul</c> →
/// <c>PA_PKID</c>). Ils doivent être résolus dans le référentiel ParametreMetier,
/// sans quoi la fiche du rendez-vous n'affiche aucun libellé.
/// </summary>
public class BatikRendezVousMapperTests
{
[Fact]
public void Map_AnnulationAvecReferentiel_LibellesOrigineEtMotifResolusParPkId()
{
var row = new BatikModels.PlanningRDVRow
{
PLPKID = 42,
PLDateDebut = new DateTime(2026, 8, 19, 9, 0, 0),
PLDateFin = new DateTime(2026, 8, 19, 10, 0, 0),
PLIDOrigineAnnul = 2000UL,
PLIDMotifAnnul = 3000UL
};
var references = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2000L] = new BatikModels.ParametreMetierRow
{
PAPKID = 2000L,
PACode = "34",
PALibelle = "Externe : Client"
},
[3000L] = new BatikModels.ParametreMetierRow
{
PAPKID = 3000L,
PACode = "35",
PALibelle = "Absent"
}
};
CanonicalMappingResult<RendezVous> mapped = BatikMappers.BatikRendezVousMapper.Map(row, references);
Assert.NotNull(mapped.Value.Annulation);
Assert.Equal(2000L, mapped.Value.Annulation!.Origine!.LegacyId);
Assert.Equal("Externe : Client", mapped.Value.Annulation.Origine!.Label);
Assert.Equal(3000L, mapped.Value.Annulation.Motif!.LegacyId);
Assert.Equal("Absent", mapped.Value.Annulation.Motif!.Label);
}
[Fact]
public void Map_AnnulationSansReferentiel_ConserveLIdentifiantSansInventerDeLibelle()
{
var row = new BatikModels.PlanningRDVRow
{
PLPKID = 43,
PLDateDebut = new DateTime(2026, 8, 19, 9, 0, 0),
PLDateFin = new DateTime(2026, 8, 19, 10, 0, 0),
PLIDMotifAnnul = 3000UL
};
CanonicalMappingResult<RendezVous> mapped = BatikMappers.BatikRendezVousMapper.Map(row, null);
Assert.NotNull(mapped.Value.Annulation);
Assert.Equal(3000L, mapped.Value.Annulation!.Motif!.LegacyId);
Assert.True(string.IsNullOrEmpty(mapped.Value.Annulation.Motif!.Label));
Assert.Null(mapped.Value.Annulation.Origine);
}
[Fact]
public void Map_SansAnnulation_AucuneAnnulation()
{
var row = new BatikModels.PlanningRDVRow
{
PLPKID = 44,
PLDateDebut = new DateTime(2026, 8, 19, 9, 0, 0),
PLDateFin = new DateTime(2026, 8, 19, 10, 0, 0)
};
CanonicalMappingResult<RendezVous> mapped = BatikMappers.BatikRendezVousMapper.Map(row, null);
Assert.Null(mapped.Value.Annulation);
}
}
@@ -0,0 +1,355 @@
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 BatikMappers = Omika.Data.Mapping.Batik;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Batik → canonique des sites (table Lieu).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class BatikSiteMapperTests
{
// ---- 1. Site Batik minimal -------------------------------------------------
[Fact]
public void SiteBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.LieuRow { LIPKID = 1 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Batik, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Code);
Assert.Null(result.Value.Type);
Assert.Null(result.Value.Adresse);
Assert.Null(result.Value.InformationsComplementaires);
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. Site Batik complet -------------------------------------------------
[Fact]
public void SiteBatik_Full_MapsAllCanonicalProperties()
{
var row = new BatikModels.LieuRow
{
LIPKID = 42,
LICodeLieu = "S-001",
LIIDType = 2,
LINote = "Site principal",
Archive = true,
LIIDImport = "IMP-B-42"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Immeuble" }
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row, referenceRows);
Site site = result.Value;
Assert.Equal("S-001", site.Code);
Assert.Equal("Immeuble", site.Type!.Label);
Assert.Equal(2, site.Type!.LegacyId);
Assert.Equal("T", site.Type!.Code);
Assert.Equal("Site principal", site.InformationsComplementaires);
Assert.True(site.EstArchive);
Assert.Equal(42, site.SourceReference!.NumericId);
Assert.Equal("IMP-B-42", site.SourceReference!.ImportId);
Assert.Equal(DatabaseSchemaVersion.Batik, site.SourceReference!.Source);
}
// ---- 3. Conservation de l'identifiant source ----------------------------------
[Fact]
public void SiteBatik_SourceIdentifier_IsPreservedWithoutFabricatedGuid()
{
var row = new BatikModels.LieuRow { LIPKID = 1234, LIIDImport = "IMP-B" };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Batik, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
Assert.Equal("IMP-B", result.Value.SourceReference!.ImportId);
Assert.Equal("1234", result.Value.SourceReference!.PublicIdentifier);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
}
// ---- 4. Type de site ----------------------------------------------------------
[Fact]
public void SiteBatik_Type_IsResolvedFromLegacyId()
{
var row = new BatikModels.LieuRow { LIPKID = 5, LIIDType = 7 };
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[7] = new() { PAPKID = 7, PACode = "T", PAValeur = "2", PALibelle = "Maison Indiv." }
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row, referenceRows);
Assert.NotNull(result.Value.Type);
Assert.Equal("Maison Indiv.", result.Value.Type!.Label);
Assert.Equal(7, result.Value.Type!.LegacyId);
Assert.Empty(result.Warnings);
}
[Fact]
public void SiteBatik_UnresolvedType_KeepsLegacyIdAndWarns()
{
var row = new BatikModels.LieuRow { LIPKID = 6, LIIDType = 77 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.NotNull(result.Value.Type);
Assert.Equal(77, result.Value.Type!.LegacyId);
Assert.Equal(string.Empty, result.Value.Type!.Label);
Assert.Contains(result.Warnings, w => w.Contains("LI_IDType", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void SiteBatik_ZeroType_ProducesNoReference()
{
var row = new BatikModels.LieuRow { LIPKID = 8, LIIDType = 0 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Null(result.Value.Type);
Assert.Empty(result.Warnings);
}
// ---- 5. Informations d'accès --------------------------------------------
[Fact]
public void SiteBatik_AccessInformation_CodeAccesBadgeInterphone_Mapped()
{
var row = new BatikModels.LieuRow
{
LIPKID = 9,
LICodeAcces = "Code 1234",
LIBadge = true,
LIInterphone = true
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Equal("Code 1234", result.Value.CodeAcces);
Assert.True(result.Value.Badge);
Assert.True(result.Value.Interphone);
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_CodeAcces");
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_Badge");
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_Interphone");
}
// ---- 6. Blocage sans équivalent métier ----------------------------------------
[Fact]
public void SiteBatik_Blocage_IsReportedAsIgnored()
{
var row = new BatikModels.LieuRow
{
LIPKID = 10,
LIBlocage = true,
LIRaisonBlocage = "Travaux en cours"
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "LI_Blocage");
Assert.Contains(result.IgnoredFields, f => f == "LI_RaisonBlocage");
}
// ---- 7. Adresse déjà chargée en mémoire ---------------------------------------
[Fact]
public void SiteBatik_Adresse_IsMappedFromInMemoryDictionary()
{
var row = new BatikModels.LieuRow { LIPKID = 11, LIIDAdresse = 20 };
var adresses = new Dictionary<long, BatikModels.AdresseRow>
{
[20] = new() { ADPKID = 20, ADEtiquette = "Siège", ADLigne1 = "1 place de la Comédie", ADVille = "Lyon", ADCodePostal = "69001" }
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row, adresses: adresses);
Assert.NotNull(result.Value.Adresse);
Assert.Equal("1 place de la Comédie", result.Value.Adresse!.Line1);
Assert.Equal("69001", result.Value.Adresse!.PostalCode);
Assert.Equal("Lyon", result.Value.Adresse!.City);
}
[Fact]
public void SiteBatik_NoAdresse_WhenNothingProvided()
{
var row = new BatikModels.LieuRow { LIPKID = 12, LIIDAdresse = 0 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Null(result.Value.Adresse);
Assert.Empty(result.Warnings);
}
[Fact]
public void SiteBatik_UnresolvedAdresse_WarnsAndProducesNull()
{
var row = new BatikModels.LieuRow { LIPKID = 13, LIIDAdresse = 99 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Null(result.Value.Adresse);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
// ---- 8. Relations différées ----------------------------------------------------
[Fact]
public void SiteBatik_Relations_AreLeftForLaterAssembly()
{
var row = new BatikModels.LieuRow { LIPKID = 14, LIIDClientPrincipal = 30 };
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Null(result.Value.ClientId);
Assert.Empty(result.Value.ContactIds);
Assert.Contains(result.IgnoredFields, f => f == "LI_IDClientPrincipal");
}
// ---- 9. Aucune exception sur les chaînes vides ---------------------------------
[Fact]
public void SiteBatik_EmptyOrNullStrings_NeverThrow()
{
var row = new BatikModels.LieuRow
{
LICodeLieu = "",
LINote = " ",
LIIDImport = "",
LICodeAcces = " ",
LIRaisonBlocage = "",
LIRepertoireDocs = " "
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Code);
Assert.Null(result.Value.InformationsComplementaires);
Assert.Null(result.Value.SourceReference!.ImportId);
Assert.Empty(result.Warnings);
}
[Fact]
public void SiteBatik_DefaultRow_NeverThrows()
{
var row = new BatikModels.LieuRow();
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.EstArchive);
}
// ---- 10. Champs ignorés et warnings attendus -----------------------------------
[Fact]
public void SiteBatik_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new BatikModels.LieuRow
{
LIPKID = 15,
LIRepertoireDocs = "docs/site",
DateArchivage = new DateTime(2024, 6, 1),
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1)
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "LI_RepertoireDocs");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
Assert.Equal(new DateTime(2024, 1, 1), result.Value.DateModification);
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
}
// ---- 11. Absence de noms physiques HFSQL dans le domaine canonique -------------
[Fact]
public void SiteBatik_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new BatikModels.LieuRow
{
LIPKID = 40,
LICodeLieu = "S-040",
LIIDType = 2,
LINote = "note",
Archive = false,
LIIDImport = "IMP-40",
LIIDAdresse = 20,
LIIDClientPrincipal = 30,
LICodeAcces = "code",
LIBadge = true,
LIInterphone = true,
LIBlocage = true,
LIRaisonBlocage = "raison",
LIRepertoireDocs = "docs"
};
var referenceRows = new Dictionary<long, BatikModels.ParametreMetierRow>
{
[2] = new() { PAPKID = 2, PACode = "T", PAValeur = "1", PALibelle = "Immeuble" }
};
var adresses = new Dictionary<long, BatikModels.AdresseRow>
{
[20] = new() { ADPKID = 20, ADLigne1 = "1 rue de la Paix", ADVille = "Lyon", ADCodePostal = "69001" }
};
CanonicalMappingResult<Site> result = BatikMappers.BatikSiteMapper.Map(row, referenceRows, adresses);
Site site = result.Value;
string[] forbidden =
[
"LI_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(site, forbidden);
if (site.Adresse is not null)
{
AssertHasNoPhysicalNames(site.Adresse, forbidden);
}
if (site.SourceReference is not null)
{
AssertHasNoPhysicalNames(site.SourceReference, forbidden);
}
}
// ---- 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));
}
}
}
@@ -0,0 +1,416 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Clients;
using Omika.Data.Mapping.Common;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires de l'assembleur d'agrégat <see cref="ClientAggregateAssembler"/>.
/// Aucune base réelle, aucun HFSQL : uniquement des objets canoniques en mémoire.
/// </summary>
public class ClientAggregateAssemblerTests
{
// ---- 1. Client seul avec collections vides -------------------------------------
[Fact]
public void Assemble_ClientAlone_KeepsCollectionsEmpty()
{
var client = new Client { Nom = "Ville de Lyon" };
Client result = ClientAggregateAssembler.Assemble(client);
Assert.Same(client, result);
Assert.Empty(result.Adresses);
Assert.Empty(result.Contacts);
Assert.Empty(result.Sites);
Assert.Null(result.AdressePrincipale);
Assert.Null(result.ContactPrincipal);
}
// ---- 2. Assemblage d'une adresse ------------------------------------------------
[Fact]
public void Assemble_SingleAdresse_IsAddedToCollection()
{
var client = new Client();
var adresse = new Adresse { Etiquette = "Mairie" };
ClientAggregateAssembler.Assemble(client, adresses: [adresse]);
Assert.Single(client.Adresses);
Assert.Same(adresse, client.Adresses[0]);
}
// ---- 3. Plusieurs adresses ------------------------------------------------------
[Fact]
public void Assemble_MultipleAdresses_AreAllAdded()
{
var client = new Client();
var a1 = new Adresse { Etiquette = "Mairie" };
var a2 = new Adresse { Etiquette = "Agence" };
ClientAggregateAssembler.Assemble(client, adresses: [a1, a2]);
Assert.Equal(2, client.Adresses.Count);
Assert.Contains(a1, client.Adresses);
Assert.Contains(a2, client.Adresses);
}
// ---- 4. Assemblage d'un contact -------------------------------------------------
[Fact]
public void Assemble_SingleContact_IsAddedToCollection()
{
var client = new Client();
var contact = new Contact { Nom = "Dupont" };
ClientAggregateAssembler.Assemble(client, contacts: [contact]);
Assert.Single(client.Contacts);
Assert.Same(contact, client.Contacts[0]);
}
// ---- 5. Plusieurs contacts ------------------------------------------------------
[Fact]
public void Assemble_MultipleContacts_AreAllAdded()
{
var client = new Client();
var c1 = new Contact { Nom = "Dupont" };
var c2 = new Contact { Nom = "Martin" };
ClientAggregateAssembler.Assemble(client, contacts: [c1, c2]);
Assert.Equal(2, client.Contacts.Count);
Assert.Contains(c1, client.Contacts);
Assert.Contains(c2, client.Contacts);
}
// ---- 6. Assemblage d'un site ----------------------------------------------------
[Fact]
public void Assemble_SingleSite_IsAddedToCollection()
{
var client = new Client();
var site = new Site { Nom = "Hôtel de ville" };
ClientAggregateAssembler.Assemble(client, sites: [site]);
Assert.Single(client.Sites);
Assert.Same(site, client.Sites[0]);
}
// ---- 7. Plusieurs sites ---------------------------------------------------------
[Fact]
public void Assemble_MultipleSites_AreAllAdded()
{
var client = new Client();
var s1 = new Site { Nom = "Hôtel de ville" };
var s2 = new Site { Nom = "Agence" };
ClientAggregateAssembler.Assemble(client, sites: [s1, s2]);
Assert.Equal(2, client.Sites.Count);
Assert.Contains(s1, client.Sites);
Assert.Contains(s2, client.Sites);
}
// ---- 8. Toutes les collections ensemble -----------------------------------------
[Fact]
public void Assemble_AllCollections_AreAssembled()
{
var client = new Client();
var adresse = new Adresse { Etiquette = "Mairie" };
var contact = new Contact { Nom = "Dupont" };
var site = new Site { Nom = "Hôtel de ville" };
ClientAggregateAssembler.Assemble(client, [adresse], [contact], [site]);
Assert.Single(client.Adresses);
Assert.Single(client.Contacts);
Assert.Single(client.Sites);
}
// ---- 9. Détection de l'adresse principale ---------------------------------------
[Fact]
public void Assemble_AdressePrincipale_AlreadyMapped_IsNotDuplicatedInCollection()
{
var principale = new Adresse
{
Etiquette = "Mairie",
EstPrincipale = true,
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
var client = new Client { AdressePrincipale = principale };
var secondaire = new Adresse { Etiquette = "Agence" };
// La collection contient l'adresse principale (même identité) et une secondaire.
ClientAggregateAssembler.Assemble(client, adresses: [principale, secondaire]);
Assert.Same(principale, client.AdressePrincipale);
Assert.Single(client.Adresses);
Assert.Same(secondaire, client.Adresses[0]);
}
[Fact]
public void Assemble_AdressePrincipale_NotMapped_IsDetectedFromEstPrincipale()
{
var client = new Client();
var principale = new Adresse { Etiquette = "Mairie", EstPrincipale = true };
var secondaire = new Adresse { Etiquette = "Agence", EstPrincipale = false };
ClientAggregateAssembler.Assemble(client, adresses: [secondaire, principale]);
Assert.Same(principale, client.AdressePrincipale);
Assert.Equal(2, client.Adresses.Count);
}
[Fact]
public void Assemble_NoAdressePrincipale_WhenNoneIsMarked()
{
var client = new Client();
var adresse = new Adresse { Etiquette = "Agence", EstPrincipale = false };
ClientAggregateAssembler.Assemble(client, adresses: [adresse]);
Assert.Null(client.AdressePrincipale);
Assert.Single(client.Adresses);
}
// ---- 10. Contact principal ------------------------------------------------------
[Fact]
public void Assemble_ContactPrincipal_IsNotInvented()
{
var client = new Client();
var contact = new Contact { Nom = "Dupont" };
ClientAggregateAssembler.Assemble(client, contacts: [contact]);
Assert.Single(client.Contacts);
Assert.Null(client.ContactPrincipal);
}
// ---- 11. Site principal ---------------------------------------------------------
[Fact]
public void Assemble_SitePrincipal_IsNotInvented()
{
var client = new Client();
var site = new Site { Nom = "Hôtel de ville" };
ClientAggregateAssembler.Assemble(client, sites: [site]);
Assert.Single(client.Sites);
// Aucune propriété SitePrincipal n'existe dans Client : la notion reste non assemblée.
Assert.DoesNotContain(typeof(Client).GetProperties(), p => p.Name == "SitePrincipal");
}
// ---- 12. Doublon exact d'adresse éliminé ----------------------------------------
[Fact]
public void Assemble_DuplicateAdresse_IsEliminated()
{
var client = new Client();
var adresse = new Adresse
{
Etiquette = "Mairie",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
ClientAggregateAssembler.Assemble(client, adresses: [adresse, adresse]);
Assert.Single(client.Adresses);
}
// ---- 13. Doublon exact de contact éliminé ---------------------------------------
[Fact]
public void Assemble_DuplicateContact_IsEliminated()
{
var client = new Client();
var contact = new Contact
{
Nom = "Dupont",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
ClientAggregateAssembler.Assemble(client, contacts: [contact, contact]);
Assert.Single(client.Contacts);
}
// ---- 14. Doublon exact de site éliminé ------------------------------------------
[Fact]
public void Assemble_DuplicateSite_IsEliminated()
{
var client = new Client();
var site = new Site
{
Nom = "Hôtel de ville",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
ClientAggregateAssembler.Assemble(client, sites: [site, site]);
Assert.Single(client.Sites);
}
// ---- 15. Deux homonymes restent distincts ---------------------------------------
[Fact]
public void Assemble_Homonyms_RemainDistinct()
{
var client = new Client();
var c1 = new Contact
{
Nom = "Dupont",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 1)
};
var c2 = new Contact
{
Nom = "Dupont",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 2)
};
ClientAggregateAssembler.Assemble(client, contacts: [c1, c2]);
Assert.Equal(2, client.Contacts.Count);
}
// ---- 16. Même NumericId mais sources différentes restent distinctes -------------
[Fact]
public void Assemble_SameNumericId_DifferentSources_RemainDistinct()
{
var client = new Client();
var batik = new Adresse
{
Etiquette = "Mairie",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Batik, NumericId: 10)
};
var omika = new Adresse
{
Etiquette = "Mairie",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
ClientAggregateAssembler.Assemble(client, adresses: [batik, omika]);
Assert.Equal(2, client.Adresses.Count);
}
// ---- 17. GUID identique reconnu comme même entité -------------------------------
[Fact]
public void Assemble_SameCanonicalGuid_IsRecognizedAsSameEntity()
{
var client = new Client();
var guid = Guid.NewGuid();
var a1 = new Adresse { Id = guid, Etiquette = "Mairie" };
var a2 = new Adresse { Id = guid, Etiquette = "Mairie" };
ClientAggregateAssembler.Assemble(client, adresses: [a1, a2]);
Assert.Single(client.Adresses);
}
// ---- 18. Collections nulles ou vides sans exception -----------------------------
[Fact]
public void Assemble_NullCollections_DoNotThrow()
{
var client = new Client { Nom = "Ville" };
Client result = ClientAggregateAssembler.Assemble(client, null, null, null);
Assert.Same(client, result);
Assert.Empty(result.Adresses);
Assert.Empty(result.Contacts);
Assert.Empty(result.Sites);
}
[Fact]
public void Assemble_EmptyCollections_DoNotThrow()
{
var client = new Client { Nom = "Ville" };
ClientAggregateAssembler.Assemble(client, [], [], []);
Assert.Empty(client.Adresses);
Assert.Empty(client.Contacts);
Assert.Empty(client.Sites);
}
[Fact]
public void Assemble_NullEntries_AreIgnored()
{
var client = new Client();
Adresse? adresse = null;
Contact? contact = null;
Site? site = null;
// Les éléments null sont ignorés par l'assembleur.
ClientAggregateAssembler.Assemble(client, new[] { adresse! }, new[] { contact! }, new[] { site! });
Assert.Empty(client.Adresses);
Assert.Empty(client.Contacts);
Assert.Empty(client.Sites);
}
// ---- 19. Assemblage déterministe ------------------------------------------------
[Fact]
public void Assemble_IsDeterministic_AndIdempotent()
{
var client = new Client();
var adresse = new Adresse
{
Etiquette = "Mairie",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 10)
};
var contact = new Contact
{
Nom = "Dupont",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 20)
};
var site = new Site
{
Nom = "Hôtel de ville",
SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 30)
};
// Premier assemblage.
ClientAggregateAssembler.Assemble(client, [adresse], [contact], [site]);
Assert.Single(client.Adresses);
Assert.Single(client.Contacts);
Assert.Single(client.Sites);
// Second assemblage avec les mêmes objets : aucun doublon créé.
ClientAggregateAssembler.Assemble(client, [adresse], [contact], [site]);
Assert.Single(client.Adresses);
Assert.Single(client.Contacts);
Assert.Single(client.Sites);
}
// ---- 20. Aucun type physique HFSQL dans la signature publique -------------------
[Fact]
public void Assemble_PublicSignature_DoesNotExposePhysicalTypes()
{
var method = typeof(ClientAggregateAssembler).GetMethod(nameof(ClientAggregateAssembler.Assemble))!;
foreach (var parameter in method.GetParameters())
{
Assert.DoesNotContain("HfSql", parameter.ParameterType.FullName, StringComparison.Ordinal);
Assert.DoesNotContain("Row", parameter.ParameterType.Name, StringComparison.Ordinal);
}
Assert.Equal(typeof(Client), method.ReturnType);
}
}
@@ -0,0 +1,51 @@
using Omika.Core.Compatibility;
using Omika.Core.Contracts.Documents;
using Omika.Core.Domain.Entities.Documents;
using Omika.Core.Mapping;
using Xunit;
namespace Omika.Tests.Mapping;
public class ModeleDocumentContractMapperTests
{
[Fact]
public void ModeleMailContractMapper_UsesSourceReferenceAsPublicId()
{
var modele = new ModeleMail
{
Id = Guid.Empty,
Code = "REL1",
Nom = "Relance",
Sujet = "Bonjour [[NomClient]]",
Corps = "Corps",
EnvoiMobile = true
};
modele.SourceReference = LegacyEntityReference.FromLegacyId(42);
ModeleMailDto dto = ModeleMailContractMapper.ToDto(modele);
Assert.Equal("42", dto.Id);
Assert.Equal("REL1", dto.Code);
Assert.Equal("Bonjour [[NomClient]]", dto.Sujet);
Assert.True(dto.EnvoiMobile);
}
[Fact]
public void ModeleRapportContractMapper_UsesSourceReferenceAsPublicId()
{
var modele = new ModeleRapport
{
Id = Guid.Empty,
Nom = "Compte rendu",
Modele = "Contenu [[Site]]",
EnvoiMobile = true
};
modele.SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 7);
ModeleRapportDto dto = ModeleRapportContractMapper.ToDto(modele);
Assert.Equal("7", dto.Id);
Assert.Equal("Compte rendu", dto.Nom);
Assert.True(dto.EnvoiMobile);
}
}
@@ -0,0 +1,324 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Organisation;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using OmikaModels = Omika.Data.HfSql.Models.Omika;
using OmikaMappers = Omika.Data.Mapping.Omika;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Omika → canonique des collaborateurs (table Collaborateur).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class OmikaCollaborateurMapperTests
{
[Fact]
public void CollaborateurOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new OmikaModels.CollaborateurRow { COPKID = 1 };
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Civilite);
Assert.Null(result.Value.Statut);
Assert.Null(result.Value.Coordonnees);
Assert.False(result.Value.Actif);
Assert.False(result.Value.EstArchive);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(1, result.Value.SourceReference!.NumericId);
Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Empty(result.Warnings);
}
[Fact]
public void CollaborateurOmika_Full_MapsAllCanonicalProperties()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 42,
COCleCivilite = "M",
CONom = "Dupont",
COPrenom = "Jean",
COTrigramme = "JD",
COTelFixe = "0472000000",
COTelMobile = "0600000000",
COEmail = "jean.dupont@ville-lyon.fr",
COCleStatut = "ACTIF",
COActif = true,
GUID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["M"] = new() { PACLE = "M", PALibelle = "M.", PAPKID = 2 },
["ACTIF"] = new() { PACLE = "ACTIF", PALibelle = "Actif", PAPKID = 3 }
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row, referenceRows);
Collaborateur collaborateur = result.Value;
Assert.Equal("M.", collaborateur.Civilite!.Label);
Assert.Equal("M", collaborateur.Civilite!.Key);
Assert.Equal("Actif", collaborateur.Statut!.Label);
Assert.Equal("ACTIF", collaborateur.Statut!.Key);
Assert.Equal("Dupont", collaborateur.Nom);
Assert.Equal("Jean", collaborateur.Prenom);
Assert.Equal("JD", collaborateur.Code);
Assert.Equal("jean.dupont@ville-lyon.fr", collaborateur.Coordonnees!.Email);
Assert.Equal("0472000000", collaborateur.Coordonnees!.Phone);
Assert.Equal("0600000000", collaborateur.Coordonnees!.Mobile);
Assert.True(collaborateur.Actif);
Assert.Equal(42, collaborateur.SourceReference!.NumericId);
Assert.Equal(DatabaseSchemaVersion.Omika, collaborateur.SourceReference!.Source);
Assert.Equal(Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"), collaborateur.SourceReference!.EntityGuid);
}
[Fact]
public void CollaborateurOmika_EmptyCoordinates_ProducesNullCoordonnees()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 5,
CONom = "Martin",
COEmail = " ",
COTelFixe = "",
COTelMobile = null
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.Prenom);
Assert.Equal("Martin", result.Value.Nom);
}
[Fact]
public void CollaborateurOmika_PartialCoordinates_KeepsOnlyProvidedValues()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 6,
COEmail = "m@exemple.fr",
COTelFixe = null,
COTelMobile = "0600000000"
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.Coordonnees);
Assert.Equal("m@exemple.fr", result.Value.Coordonnees!.Email);
Assert.Null(result.Value.Coordonnees!.Phone);
Assert.Equal("0600000000", result.Value.Coordonnees!.Mobile);
}
[Fact]
public void CollaborateurOmika_InvalidGuid_ProducesNullEntityGuidAndWarns()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 7,
GUID = "not-a-guid"
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Equal("not-a-guid", result.Value.SourceReference!.PhysicalGuid);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.InvalidPhysicalGuid, StringComparison.Ordinal));
}
[Fact]
public void CollaborateurOmika_NoGuid_ProducesNullEntityGuid()
{
var row = new OmikaModels.CollaborateurRow { COPKID = 8 };
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
Assert.Empty(result.Warnings);
}
[Fact]
public void CollaborateurOmika_EmptyOrNullStrings_NeverThrow()
{
var row = new OmikaModels.CollaborateurRow
{
CONom = "",
COPrenom = " ",
COEmail = "",
COTelFixe = null,
COTelMobile = " ",
COTrigramme = "",
COCleCivilite = "",
COCleStatut = null
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.Civilite);
Assert.Null(result.Value.Statut);
}
[Fact]
public void CollaborateurOmika_DefaultRow_NeverThrows()
{
var row = new OmikaModels.CollaborateurRow();
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.Actif);
Assert.False(result.Value.EstArchive);
}
[Fact]
public void CollaborateurOmika_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 40,
COCleCivilite = "M",
CONom = "Dupont",
COPrenom = "Jean",
COTelFixe = "01",
COTelMobile = "06",
COEmail = "j@exemple.fr",
COActif = true,
GUID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["M"] = new() { PACLE = "M", PALibelle = "M.", PAPKID = 2 }
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row, referenceRows);
Collaborateur collaborateur = result.Value;
string[] forbidden =
[
"CO_", "CL_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(collaborateur, forbidden);
if (collaborateur.Coordonnees is not null)
{
AssertHasNoPhysicalNames(collaborateur.Coordonnees, forbidden);
}
if (collaborateur.SourceReference is not null)
{
AssertHasNoPhysicalNames(collaborateur.SourceReference, forbidden);
}
}
[Fact]
public void CollaborateurOmika_UnresolvedCle_KeepsKeyAndWarns()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 9,
COCleCivilite = "INCONNU",
COCleStatut = "AUTRE"
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal("INCONNU", result.Value.Civilite!.Key);
Assert.Equal(string.Empty, result.Value.Civilite!.Label);
Assert.NotNull(result.Value.Statut);
Assert.Equal("AUTRE", result.Value.Statut!.Key);
Assert.Contains(result.Warnings, w => w.Contains("CO_CleCivilite", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
Assert.Contains(result.Warnings, w => w.Contains("CO_CleStatut", StringComparison.Ordinal)
&& w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void CollaborateurOmika_NoCleButLegacyId_FallsBackToLegacyIdAndWarns()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 10,
COIDCivilite = 77,
COIDStatut = 88
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal(77, result.Value.Civilite!.LegacyId);
Assert.NotNull(result.Value.Statut);
Assert.Equal(88, result.Value.Statut!.LegacyId);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal));
}
[Fact]
public void CollaborateurOmika_NoCleAndZeroLegacyId_ProducesNoReference()
{
var row = new OmikaModels.CollaborateurRow { COPKID = 11 };
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Null(result.Value.Civilite);
Assert.Null(result.Value.Statut);
}
[Fact]
public void CollaborateurOmika_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new OmikaModels.CollaborateurRow
{
COPKID = 12,
COFonction = "Développeur",
COPonderation = 1.5f,
COIDImport = "IMP-12",
COAuPlanning = true,
COAccesBO = true,
COAccesMobile = true,
CreationDate = new DateTime(2024, 1, 1),
ModifDate = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Collaborateur> result = OmikaMappers.OmikaCollaborateurMapper.Map(row);
Assert.Equal("Développeur", result.Value.Fonction);
Assert.Contains(result.IgnoredFields, f => f == "CO_Ponderation");
Assert.Contains(result.IgnoredFields, f => f == "CO_IDImport");
Assert.Contains(result.IgnoredFields, f => f == "CO_AuPlanning");
Assert.Contains(result.IgnoredFields, f => f == "CO_AccesBO");
Assert.Contains(result.IgnoredFields, f => f == "CO_AccesMobile");
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "ModifDate");
Assert.Null(result.Value.SocieteId);
}
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));
}
}
}
@@ -0,0 +1,467 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Clients;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using OmikaModels = Omika.Data.HfSql.Models.Omika;
using OmikaMappers = Omika.Data.Mapping.Omika;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Omika → canonique des contacts (table Personne).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class OmikaContactMapperTests
{
// ---- 1. Contact Omika minimal --------------------------------------------------
[Fact]
public void ContactOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new OmikaModels.PersonneRow { PEPKID = 1 };
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Civilite);
Assert.Empty(result.Value.Roles);
Assert.Null(result.Value.Coordonnees);
Assert.False(result.Value.EstArchive);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source);
Assert.Equal(1, result.Value.SourceReference!.NumericId);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Empty(result.Warnings);
}
// ---- 2. Contact Omika complet --------------------------------------------------
[Fact]
public void ContactOmika_Full_MapsAllCanonicalProperties()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 42,
GUID = "550e8400-e29b-41d4-a716-446655440000",
PECleCivilite = "MR",
PEIDCivilite = 2,
PENom = "Dupont",
PEPrenom = "Jean",
PECleRole = "R_CP",
PEIDRole = 3,
PETelFixe = "0472000000",
PETelMobile = "0600000000",
PEEmail = "jean.dupont@ville-lyon.fr",
PEFax = "0472000001",
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "Contact principal",
PEActif = true,
PEUsageInterne = false,
PEInfoSupp = "Disponible en journée",
Archive = true,
PEIDImport = "IMP-O-42"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" },
["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Contact principal", PACategorie = "ROLE" }
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows);
Contact contact = result.Value;
Assert.Equal("M.", contact.Civilite!.Label);
Assert.Equal("MR", contact.Civilite!.Key);
Assert.Equal(2, contact.Civilite!.LegacyId);
Assert.Equal("Dupont", contact.Nom);
Assert.Equal("Jean", contact.Prenom);
Assert.Equal("Jean Dupont", contact.NomAffiche);
Assert.Single(contact.Roles);
Assert.Equal("Contact principal", contact.Roles[0].Label);
Assert.Equal("R_CP", contact.Roles[0].Key);
Assert.Equal(3, contact.Roles[0].LegacyId);
Assert.Equal("jean.dupont@ville-lyon.fr", contact.Coordonnees!.Email);
Assert.Equal("0472000000", contact.Coordonnees!.Phone);
Assert.Equal("0600000000", contact.Coordonnees!.Mobile);
Assert.Equal("0472000001", contact.Coordonnees!.Fax);
Assert.True(contact.Actif);
Assert.True(contact.EstArchive);
Assert.Equal("Disponible en journée", contact.InformationsSupplementaires);
Assert.Equal(42, contact.SourceReference!.NumericId);
Assert.Equal("IMP-O-42", contact.SourceReference!.ImportId);
Assert.Equal(DatabaseSchemaVersion.Omika, contact.SourceReference!.Source);
Assert.NotNull(contact.SourceReference!.EntityGuid);
}
// ---- 3. GUID valide avec tirets -------------------------------------------------
[Fact]
public void ContactOmika_ValidGuidWithDashes_IsKeptInEntityGuid()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 7,
GUID = "550e8400-e29b-41d4-a716-446655440000",
PENom = "Dupont"
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
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.Empty(result.Warnings);
}
// ---- 4. GUID valide sans tirets -------------------------------------------------
[Fact]
public void ContactOmika_ValidGuidWithoutDashes_IsParsedByTryParse()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 8,
GUID = "550e8400e29b41d4a716446655440000"
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.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);
}
// ---- 5. GUID invalide -----------------------------------------------------------
[Fact]
public void ContactOmika_InvalidGuid_ProducesNullGuidAndWarningWithoutException()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 9,
GUID = "not-a-guid",
PENom = "Société inconnue"
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.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);
}
// ---- 6. Coordonnées entièrement nulles -------------------------------------------
[Fact]
public void ContactOmika_EmptyCoordinates_ProducesNullCoordonnees()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 5,
PENom = "Martin",
PEEmail = " ",
PETelFixe = "",
PETelMobile = null,
PEFax = "\t"
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Null(result.Value.Coordonnees);
Assert.Equal("Martin", result.Value.Nom);
}
// ---- 7. Coordonnées partielles ---------------------------------------------------
[Fact]
public void ContactOmika_PartialCoordinates_KeepsOnlyProvidedValues()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 6,
PEEmail = "m@exemple.fr",
PETelFixe = null,
PETelMobile = "0600000000",
PEFax = ""
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value.Coordonnees);
Assert.Equal("m@exemple.fr", result.Value.Coordonnees!.Email);
Assert.Null(result.Value.Coordonnees!.Phone);
Assert.Equal("0600000000", result.Value.Coordonnees!.Mobile);
Assert.Null(result.Value.Coordonnees!.Fax);
}
// ---- 8. Priorité de la clé métier sur l'ancien identifiant ------------------------
[Fact]
public void ContactOmika_BusinessKeyIsPreferredAndLegacyIdIsKeptAsCompatibility()
{
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["MR"] = new() { PAPKID = 50, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" }
};
var row = new OmikaModels.PersonneRow
{
PEPKID = 13,
PECleCivilite = "MR",
PEIDCivilite = 99 // zombie : ancien identifiant
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows);
Assert.Equal("MR", result.Value.Civilite!.Key);
Assert.Equal("M.", result.Value.Civilite!.Label);
Assert.Equal(99, result.Value.Civilite!.LegacyId);
Assert.Empty(result.Warnings);
}
// ---- 9. Conservation de l'identifiant numérique historique -------------------------
[Fact]
public void ContactOmika_NumericIdentifier_IsPreserved()
{
var row = new OmikaModels.PersonneRow { PEPKID = 1234 };
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
}
// ---- 10. Conservation de l'identifiant d'import -------------------------------------
[Fact]
public void ContactOmika_ImportId_IsPreserved()
{
var row = new OmikaModels.PersonneRow { PEPKID = 5678, PEIDImport = "IMP-O" };
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Equal("IMP-O", result.Value.SourceReference!.ImportId);
Assert.Equal(5678, result.Value.SourceReference!.NumericId);
}
// ---- 11. Absence de GUID fabriqué -----------------------------------------------------
[Fact]
public void ContactOmika_WithoutGuid_ProducesNoFabricatedGuid()
{
var row = new OmikaModels.PersonneRow { PEPKID = 11 };
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
Assert.Empty(result.Warnings);
}
// ---- 12. Repli sur un champ zombie avec avertissement --------------------------------
[Fact]
public void ContactOmika_MissingBusinessKey_ExposesZombieLegacyIdWithWarning()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 14,
PECleCivilite = null, // rubrique active absente
PEIDCivilite = 77 // ancien identifiant (zombie)
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal(77, result.Value.Civilite!.LegacyId);
Assert.Null(result.Value.Civilite!.Key);
Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal));
}
// ---- 13. Référentiel non résolu ---------------------------------------------------------
[Fact]
public void ContactOmika_UnresolvedBusinessKey_KeepsKeyAndLegacyIdAndWarns()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 15,
PECleCivilite = "MR",
PEIDCivilite = 77
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value.Civilite);
Assert.Equal("MR", result.Value.Civilite!.Key);
Assert.Equal(77, result.Value.Civilite!.LegacyId);
Assert.Equal(string.Empty, result.Value.Civilite!.Label);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void ContactOmika_ZeroCiviliteAndRole_ProducesNoReference()
{
var row = new OmikaModels.PersonneRow { PEPKID = 16, PEIDCivilite = 0, PEIDRole = 0 };
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Null(result.Value.Civilite);
Assert.Empty(result.Value.Roles);
Assert.Empty(result.Warnings);
}
// ---- 14. Chaînes vides sans exception -----------------------------------------------------
[Fact]
public void ContactOmika_EmptyOrNullStrings_NeverThrow()
{
var row = new OmikaModels.PersonneRow
{
PENom = "",
PEPrenom = " ",
PEEmail = "",
PETelFixe = null,
PETelMobile = " ",
PEFax = "",
PENote = "",
PEInfoSupp = " ",
PEIDImport = "",
GUID = null
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Nom);
Assert.Null(result.Value.Prenom);
Assert.Null(result.Value.Coordonnees);
Assert.Null(result.Value.InformationsSupplementaires);
Assert.Empty(result.Warnings);
}
[Fact]
public void ContactOmika_DefaultRow_NeverThrows()
{
var row = new OmikaModels.PersonneRow();
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.Actif);
Assert.False(result.Value.EstArchive);
}
// ---- 15. Champs sans équivalent dans IgnoredFields ----------------------------------------
[Fact]
public void ContactOmika_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 9,
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "note",
PEUsageInterne = true,
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1),
DateArchivage = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "PE_IDClient");
Assert.Contains(result.IgnoredFields, f => f == "PE_IDAdressePrincipale");
Assert.Contains(result.IgnoredFields, f => f == "PE_Note");
Assert.Contains(result.IgnoredFields, f => f == "PE_UsageInterne");
Assert.Contains(result.IgnoredFields, f => f == "ModifDate");
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
Assert.Null(result.Value.ClientId);
Assert.Empty(result.Value.SiteIds);
}
// ---- 16. Absence de noms physiques HFSQL dans le domaine canonique -------------------------
[Fact]
public void ContactOmika_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new OmikaModels.PersonneRow
{
PEPKID = 40,
GUID = "550e8400-e29b-41d4-a716-446655440000",
PECleCivilite = "MR",
PEIDCivilite = 2,
PENom = "Dupont",
PEPrenom = "Jean",
PECleRole = "R_CP",
PEIDRole = 3,
PEEmail = "j@exemple.fr",
PETelFixe = "01",
PETelMobile = "06",
PEFax = "02",
PEIDClient = 10,
PEIDAdressePrincipale = 20,
PENote = "note",
PEActif = true,
PEUsageInterne = true,
PEInfoSupp = "info",
Archive = false,
PEIDImport = "IMP-40"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["MR"] = new() { PAPKID = 2, PACLE = "MR", PALibelle = "M.", PACategorie = "CIV" },
["R_CP"] = new() { PAPKID = 3, PACLE = "R_CP", PALibelle = "Rôle", PACategorie = "ROLE" }
};
CanonicalMappingResult<Contact> result = OmikaMappers.OmikaContactMapper.Map(row, referenceRows);
Contact contact = result.Value;
string[] forbidden =
[
"PE_", "CL_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(contact, forbidden);
if (contact.Coordonnees is not null)
{
AssertHasNoPhysicalNames(contact.Coordonnees, forbidden);
}
if (contact.SourceReference is not null)
{
AssertHasNoPhysicalNames(contact.SourceReference, forbidden);
}
}
// ---- 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));
}
}
}
@@ -0,0 +1,301 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Interventions;
using Omika.Data.Mapping.Common;
using OmikaModels = Omika.Data.HfSql.Models.Omika;
using OmikaMappers = Omika.Data.Mapping.Omika;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Omika → canonique des interventions.
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
///
/// Le mapper ne renseigne pas <c>SourceReference</c> sur l'entité canonique
/// (la référence source est construite en interne mais non affectée) ; ces
/// tests se concentrent donc sur les propriétés canoniques réellement mappées.
/// </summary>
public class OmikaInterventionMapperTests
{
// ---- 1. Intervention Omika minimale -------------------------------------------
[Fact]
public void InterventionOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new OmikaModels.InterventionRow { INPKID = 1 };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.Reference);
Assert.Null(result.Value.Type);
Assert.Null(result.Value.Etat);
Assert.False(result.Value.EstArchive);
Assert.Empty(result.Warnings);
}
// ---- 2. Intervention Omika complète -------------------------------------------
[Fact]
public void InterventionOmika_Full_MapsAllCanonicalProperties()
{
var row = new OmikaModels.InterventionRow
{
INPKID = 42,
INTitre = "Réparation chaudière",
INRefInterne = "INT-042",
INDescription = "Remplacement de la chaudière",
INCleType = "TYP-MAINT",
INCleEtat = "STA-ENCOURS",
INDateIntervention = new DateOnly(2026, 3, 15),
Archive = true,
INIDImport = "IMP-O-42",
GUID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["TYP-MAINT"] = new() { PAPKID = 2, PACLE = "TYP-MAINT", PALibelle = "Maintenance" },
["STA-ENCOURS"] = new() { PAPKID = 3, PACLE = "STA-ENCOURS", PALibelle = "En cours" }
};
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row, referenceRows);
Intervention intervention = result.Value;
Assert.Equal("Réparation chaudière", intervention.Titre);
Assert.Equal("INT-042", intervention.Reference);
Assert.Equal("Remplacement de la chaudière", intervention.Description);
Assert.Equal("Maintenance", intervention.Type!.Label);
Assert.Equal("TYP-MAINT", intervention.Type!.Key);
Assert.Equal("En cours", intervention.Etat!.Label);
Assert.Equal("STA-ENCOURS", intervention.Etat!.Key);
Assert.Equal(new DateTime(2026, 3, 15), intervention.DatePrevueDebut);
Assert.True(intervention.EstArchive);
}
// ---- 3. GUID ------------------------------------------------------------------
[Fact]
public void InterventionOmika_InvalidGuid_ProducesWarning()
{
var row = new OmikaModels.InterventionRow { INPKID = 42, GUID = "not-a-valid-guid" };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.InvalidPhysicalGuid, StringComparison.Ordinal));
}
[Fact]
public void InterventionOmika_EmptyGuid_NoWarning()
{
var row = new OmikaModels.InterventionRow { INPKID = 42, GUID = "" };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.Empty(result.Warnings);
}
// ---- 4. Type et état (clés métier) --------------------------------------------
[Fact]
public void InterventionOmika_Type_IsResolvedFromCleType()
{
var row = new OmikaModels.InterventionRow { INPKID = 5, INCleType = "TYP-URG" };
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["TYP-URG"] = new() { PAPKID = 7, PACLE = "TYP-URG", PALibelle = "Urgence" }
};
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row, referenceRows);
Assert.NotNull(result.Value.Type);
Assert.Equal("Urgence", result.Value.Type!.Label);
Assert.Equal("TYP-URG", result.Value.Type!.Key);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionOmika_UnresolvedType_KeepsKeyAndWarns()
{
var row = new OmikaModels.InterventionRow { INPKID = 6, INCleType = "TYP-INCONNU" };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.NotNull(result.Value.Type);
Assert.Equal("TYP-INCONNU", result.Value.Type!.Key);
Assert.Equal(string.Empty, result.Value.Type!.Label);
Assert.Contains(result.Warnings, w => w.Contains("IN_CleType", StringComparison.Ordinal));
}
[Fact]
public void InterventionOmika_NoTypeKey_ProducesNoReference()
{
var row = new OmikaModels.InterventionRow { INPKID = 8 };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.Null(result.Value.Type);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionOmika_UnresolvedEtat_KeepsKeyAndWarns()
{
var row = new OmikaModels.InterventionRow { INPKID = 9, INCleEtat = "STA-INCONNU" };
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.NotNull(result.Value.Etat);
Assert.Equal("STA-INCONNU", result.Value.Etat!.Key);
Assert.Contains(result.Warnings, w => w.Contains("IN_CleEtat", StringComparison.Ordinal));
}
// ---- 5. Champs ignorés --------------------------------------------------------
[Fact]
public void InterventionOmika_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new OmikaModels.InterventionRow
{
INPKID = 15,
INIDClient = 1,
INIDLieu = 2,
INIDDossier = 3,
INIDSuiviPar = 4,
INIDDemandeur = 5,
INIDSociete = 6,
INAttestationTVAPresente = true,
INNoteInterne = "note",
INRefExterne = "EXT-1",
INDureePrevue = 2.5f,
INNumDevis = "DEV-1",
INDateDevis = new DateOnly(2026, 1, 1),
INNumFacture = "FAC-1",
INDateFacture = new DateOnly(2026, 2, 1),
INNumIntervention = "N-1",
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1),
DateArchivage = new DateTime(2024, 6, 1)
};
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.Contains(result.IgnoredFields, f => f == "IN_IDClient");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDLieu");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDDossier");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDSuiviPar");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDDemandeur");
Assert.Contains(result.IgnoredFields, f => f == "IN_IDSociete");
Assert.Contains(result.IgnoredFields, f => f == "IN_AttestationTVAPresente");
Assert.Contains(result.IgnoredFields, f => f == "IN_NoteInterne");
Assert.Contains(result.IgnoredFields, f => f == "IN_DureePrevue");
Assert.Contains(result.IgnoredFields, f => f == "IN_DateDevis");
Assert.Contains(result.IgnoredFields, f => f == "IN_DateFacture");
Assert.Equal(new DateTime(2024, 1, 1), result.Value.DateModification);
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
// Champs désormais dotés d'un équivalent canonique : ils ne sont plus
// ignorés, ils sont mappés dans l'entité Intervention.
Assert.Equal("EXT-1", result.Value.ReferenceExterne);
Assert.Equal("DEV-1", result.Value.NumeroDevis);
Assert.Equal("N-1", result.Value.NumeroIntervention);
Assert.Equal("FAC-1", result.Value.NumeroFacture);
}
// ---- 6. Aucune exception sur les chaînes vides --------------------------------
[Fact]
public void InterventionOmika_EmptyOrNullStrings_NeverThrow()
{
var row = new OmikaModels.InterventionRow
{
INTitre = "",
INRefInterne = " ",
INDescription = "",
INNoteInterne = " ",
INRefExterne = "",
INIDImport = " "
};
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Titre);
Assert.Null(result.Value.Reference);
Assert.Null(result.Value.Description);
Assert.Empty(result.Warnings);
}
[Fact]
public void InterventionOmika_DefaultRow_NeverThrows()
{
var row = new OmikaModels.InterventionRow();
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row);
Assert.NotNull(result.Value);
Assert.False(result.Value.EstArchive);
}
// ---- 7. Absence de noms physiques HFSQL dans le domaine canonique -------------
[Fact]
public void InterventionOmika_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new OmikaModels.InterventionRow
{
INPKID = 40,
INTitre = "Titre",
INRefInterne = "INT-040",
INDescription = "desc",
INCleType = "TYP-MAINT",
INCleEtat = "STA-ENCOURS",
INDateIntervention = new DateOnly(2026, 1, 1),
Archive = false,
INIDImport = "IMP-40",
INIDClient = 1,
INIDLieu = 2,
INIDDossier = 3,
INIDSuiviPar = 4,
INIDDemandeur = 5,
INIDSociete = 6,
INAttestationTVAPresente = true,
INNoteInterne = "note",
INRefExterne = "EXT",
INDureePrevue = 1f,
INNumDevis = "DEV",
INNumFacture = "FAC",
INNumIntervention = "N",
GUID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["TYP-MAINT"] = new() { PAPKID = 2, PACLE = "TYP-MAINT", PALibelle = "Maintenance" },
["STA-ENCOURS"] = new() { PAPKID = 3, PACLE = "STA-ENCOURS", PALibelle = "En cours" }
};
CanonicalMappingResult<Intervention> result = OmikaMappers.OmikaInterventionMapper.Map(row, referenceRows);
Intervention intervention = result.Value;
string[] forbidden =
[
"IN_", "PA_", "PKID", "HfSql", "HFSQL"
];
AssertHasNoPhysicalNames(intervention, forbidden);
}
// ---- 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));
}
}
}
@@ -0,0 +1,258 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Planning;
using Omika.Core.Domain.ValueObjects;
using OmikaModels = Omika.Data.HfSql.Models.Omika;
using OmikaMappers = Omika.Data.Mapping.Omika;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapping Omika de l'affectation Collaborateur d'un
/// rendez-vous (table RDV_Collaborateur). Aucune base réelle : uniquement des
/// objets en mémoire.
/// </summary>
public class OmikaRendezVousMapperTests
{
[Fact]
public void MapCollaborateur_DateFinAnterieureADateDeb_NeLevePasEtPeriodeNull()
{
var row = new OmikaModels.RDVCollaborateurRow
{
RVCPKID = 1,
RVCGUIDColb = "0f8fad5b-d9cb-469f-a165-70867728950e",
RVCDateDeb = new DateTime(2026, 8, 19, 10, 0, 0),
RVCDateFin = new DateTime(2026, 8, 19, 9, 0, 0), // antérieure → anomalie historique
RVCResponsable = true,
RVCTempsValide = true
};
// Aucune DomainValidationException ne doit remonter.
AffectationCollaborateur affectation = OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row);
Assert.NotNull(affectation);
Assert.Null(affectation.PeriodeParticuliere);
// L'affectation elle-même est conservée (identifiant + flags).
Assert.NotNull(affectation.CollaborateurId);
Assert.True(affectation.Responsable);
Assert.True(affectation.TempsValide);
}
[Fact]
public void MapCollaborateur_PlageValide_PeriodeMappee()
{
var debut = new DateTime(2026, 8, 19, 9, 0, 0);
var fin = new DateTime(2026, 8, 19, 10, 0, 0);
var row = new OmikaModels.RDVCollaborateurRow
{
RVCDateDeb = debut,
RVCDateFin = fin
};
AffectationCollaborateur affectation = OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row);
Assert.NotNull(affectation.PeriodeParticuliere);
Assert.Equal(debut, affectation.PeriodeParticuliere!.Start);
Assert.Equal(fin, affectation.PeriodeParticuliere.End);
}
[Fact]
public void MapCollaborateur_DateEgale_PeriodeMappee()
{
var instant = new DateTime(2026, 8, 19, 9, 0, 0);
var row = new OmikaModels.RDVCollaborateurRow
{
RVCDateDeb = instant,
RVCDateFin = instant
};
AffectationCollaborateur affectation = OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row);
Assert.NotNull(affectation.PeriodeParticuliere);
Assert.Equal(instant, affectation.PeriodeParticuliere!.Start);
Assert.Equal(instant, affectation.PeriodeParticuliere.End);
}
[Fact]
public void MapCollaborateur_DatesParDefaut_PeriodeNull()
{
var row = new OmikaModels.RDVCollaborateurRow
{
RVCDateDeb = default,
RVCDateFin = default
};
AffectationCollaborateur affectation = OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row);
Assert.Null(affectation.PeriodeParticuliere);
}
[Fact]
public void MapCollaborateur_PkNumeriqueFourni_SourceReferencePorteNumericIdEtEntityGuid()
{
Guid guid = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e");
var row = new OmikaModels.RDVCollaborateurRow
{
RVCGUIDColb = guid.ToString("D")
};
AffectationCollaborateur affectation =
OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row, collaborateurNumericId: 42);
Assert.NotNull(affectation.SourceReference);
Assert.Equal(42, affectation.SourceReference!.NumericId);
Assert.Equal(guid, affectation.SourceReference.EntityGuid);
}
[Fact]
public void MapCollaborateur_SansPkNumerique_SourceReferenceSansNumericId()
{
Guid guid = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e");
var row = new OmikaModels.RDVCollaborateurRow
{
RVCGUIDColb = guid.ToString("D")
};
AffectationCollaborateur affectation =
OmikaMappers.OmikaRendezVousMapper.MapCollaborateur(row);
Assert.NotNull(affectation.SourceReference);
Assert.Null(affectation.SourceReference!.NumericId);
Assert.Equal(guid, affectation.SourceReference.EntityGuid);
}
[Fact]
public void Map_RVIdInterPresent_InterventionSourceReferenceAvecGUID()
{
var interventionGuid = Guid.Parse("11111111-1111-1111-1111-111111111111");
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 1,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19),
RVIdInter = 105
};
var interventionGuids = new Dictionary<long, Guid> { [105] = interventionGuid };
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, null, interventionGuids);
Assert.NotNull(mapped.Value.InterventionSourceReference);
Assert.Equal(105, mapped.Value.InterventionSourceReference.NumericId);
Assert.Equal(interventionGuid, mapped.Value.InterventionSourceReference.EntityGuid);
}
[Fact]
public void Map_RVIdInterAbsent_InterventionSourceReferenceNull()
{
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 1,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19),
RVIdInter = 0
};
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, null, null);
Assert.Null(mapped.Value.InterventionSourceReference);
}
[Fact]
public void Map_RVIdInterOrphelin_InterventionSourceReferenceSansGUID()
{
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 1,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19),
RVIdInter = 999
};
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, null, new Dictionary<long, Guid>());
Assert.NotNull(mapped.Value.InterventionSourceReference);
Assert.Equal(999, mapped.Value.InterventionSourceReference.NumericId);
Assert.Null(mapped.Value.InterventionSourceReference.EntityGuid);
}
[Fact]
public void Map_AnnulationAvecReferentiel_LibellesOrigineEtMotifResolus()
{
// La fiche du rendez-vous affiche le motif et l'origine d'annulation : sans
// résolution dans le référentiel, elle n'afficherait que le CODE du
// paramètre (« O.ANU.RDV.E.CLI ») au lieu de son libellé.
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 7,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19),
RVCleOrigineAnnu = "O.ANU.RDV.E.CLI",
RVCleMotifAnnu = "M.ANU.RDV.ABS"
};
var references = new Dictionary<string, OmikaModels.ParametreMetierRow>(StringComparer.OrdinalIgnoreCase)
{
["O.ANU.RDV.E.CLI"] = new OmikaModels.ParametreMetierRow
{
PAPKID = 10,
PACLE = "O.ANU.RDV.E.CLI",
PACategorie = "O.ANU.RDV",
PALibelle = "Externe : Client"
},
["M.ANU.RDV.ABS"] = new OmikaModels.ParametreMetierRow
{
PAPKID = 11,
PACLE = "M.ANU.RDV.ABS",
PACategorie = "M.ANU.RDV",
PALibelle = "Absent"
}
};
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, references, null);
Assert.NotNull(mapped.Value.Annulation);
Assert.Equal("O.ANU.RDV.E.CLI", mapped.Value.Annulation!.Origine!.Key);
Assert.Equal("Externe : Client", mapped.Value.Annulation.Origine!.Label);
Assert.Equal("M.ANU.RDV.ABS", mapped.Value.Annulation.Motif!.Key);
Assert.Equal("Absent", mapped.Value.Annulation.Motif!.Label);
}
[Fact]
public void Map_AnnulationSansReferentiel_ConserveLaCleSansInventerDeLibelle()
{
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 8,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19),
RVCleMotifAnnu = "M.ANU.RDV.ABS"
};
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, null, null);
Assert.NotNull(mapped.Value.Annulation);
Assert.Equal("M.ANU.RDV.ABS", mapped.Value.Annulation!.Motif!.Key);
Assert.True(string.IsNullOrEmpty(mapped.Value.Annulation.Motif!.Label));
Assert.Null(mapped.Value.Annulation.Origine);
}
[Fact]
public void Map_SansAnnulation_AucuneAnnulation()
{
var row = new OmikaModels.RDVPlanningRow
{
RVPKID = 9,
GUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
RVDate = new DateOnly(2026, 8, 19)
};
CanonicalMappingResult<RendezVous> mapped =
OmikaMappers.OmikaRendezVousMapper.Map(row, null, null);
Assert.Null(mapped.Value.Annulation);
}
}
@@ -0,0 +1,456 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Clients;
using Omika.Core.Domain.ValueObjects;
using Omika.Data.Mapping.Common;
using OmikaModels = Omika.Data.HfSql.Models.Omika;
using OmikaMappers = Omika.Data.Mapping.Omika;
namespace Omika.Tests.Mapping;
/// <summary>
/// Tests unitaires du mapper Omika → canonique des sites (table Lieu).
/// Aucune base réelle, aucun HFSQL : uniquement des objets en mémoire.
/// </summary>
public class OmikaSiteMapperTests
{
// ---- 1. Site Omika minimal --------------------------------------------------
[Fact]
public void SiteOmika_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new OmikaModels.LieuRow { LIPKID = 1 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Equal(DatabaseSchemaVersion.Omika, result.SourceVersion);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Code);
Assert.Null(result.Value.Type);
Assert.Null(result.Value.Adresse);
Assert.Null(result.Value.InformationsComplementaires);
Assert.False(result.Value.EstArchive);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source);
Assert.Equal(1, result.Value.SourceReference!.NumericId);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Empty(result.Warnings);
}
// ---- 2. Site Omika complet --------------------------------------------------
[Fact]
public void SiteOmika_Full_MapsAllCanonicalProperties()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 42,
GUID = "550e8400-e29b-41d4-a716-446655440000",
LICodeLieu = "S-001",
LICleType = "IMM",
LIIDType = 2,
LINote = "Site principal",
Archive = true,
LIIDImport = "IMP-O-42"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["IMM"] = new() { PAPKID = 2, PACLE = "IMM", PALibelle = "Immeuble", PACategorie = "TYPE" }
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row, referenceRows);
Site site = result.Value;
Assert.Equal("S-001", site.Code);
Assert.Equal("Immeuble", site.Type!.Label);
Assert.Equal("IMM", site.Type!.Key);
Assert.Equal(2, site.Type!.LegacyId);
Assert.Equal("Site principal", site.InformationsComplementaires);
Assert.True(site.EstArchive);
Assert.Equal(42, site.SourceReference!.NumericId);
Assert.Equal("IMP-O-42", site.SourceReference!.ImportId);
Assert.Equal(DatabaseSchemaVersion.Omika, site.SourceReference!.Source);
Assert.NotNull(site.SourceReference!.EntityGuid);
}
// ---- 3. GUID valide avec tirets ----------------------------------------------
[Fact]
public void SiteOmika_ValidGuidWithDashes_IsKeptInEntityGuid()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 7,
GUID = "550e8400-e29b-41d4-a716-446655440000",
LICodeLieu = "S-007"
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
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.Empty(result.Warnings);
}
// ---- 4. GUID valide sans tirets ----------------------------------------------
[Fact]
public void SiteOmika_ValidGuidWithoutDashes_IsParsedByTryParse()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 8,
GUID = "550e8400e29b41d4a716446655440000"
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.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);
}
// ---- 5. GUID invalide ---------------------------------------------------------
[Fact]
public void SiteOmika_InvalidGuid_ProducesNullGuidAndWarningWithoutException()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 9,
GUID = "not-a-guid",
LICodeLieu = "S-009"
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.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("S-009", result.Value.Code);
}
// ---- 6. Absence de GUID fabriqué ----------------------------------------------
[Fact]
public void SiteOmika_WithoutGuid_ProducesNoFabricatedGuid()
{
var row = new OmikaModels.LieuRow { LIPKID = 11 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Null(result.Value.SourceReference!.EntityGuid);
Assert.Null(result.Value.SourceReference!.PhysicalGuid);
Assert.Empty(result.Warnings);
}
// ---- 7. Conservation de l'identifiant numérique historique ---------------------
[Fact]
public void SiteOmika_NumericIdentifier_IsPreserved()
{
var row = new OmikaModels.LieuRow { LIPKID = 1234 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.NotNull(result.Value.SourceReference);
Assert.Equal(DatabaseSchemaVersion.Omika, result.Value.SourceReference!.Source);
Assert.Equal(1234, result.Value.SourceReference!.NumericId);
}
// ---- 8. Conservation de l'identifiant d'import ---------------------------------
[Fact]
public void SiteOmika_ImportId_IsPreserved()
{
var row = new OmikaModels.LieuRow { LIPKID = 5678, LIIDImport = "IMP-O" };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Equal("IMP-O", result.Value.SourceReference!.ImportId);
Assert.Equal(5678, result.Value.SourceReference!.NumericId);
}
// ---- 9. Priorité de la clé métier sur l'ancien identifiant ---------------------
[Fact]
public void SiteOmika_BusinessKeyIsPreferredAndLegacyIdIsKeptAsCompatibility()
{
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["IMM"] = new() { PAPKID = 50, PACLE = "IMM", PALibelle = "Immeuble", PACategorie = "TYPE" }
};
var row = new OmikaModels.LieuRow
{
LIPKID = 13,
LICleType = "IMM",
LIIDType = 99 // zombie : ancien identifiant
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row, referenceRows);
Assert.Equal("IMM", result.Value.Type!.Key);
Assert.Equal("Immeuble", result.Value.Type!.Label);
Assert.Equal(99, result.Value.Type!.LegacyId);
Assert.Empty(result.Warnings);
}
// ---- 10. Repli sur un champ zombie avec avertissement ---------------------------
[Fact]
public void SiteOmika_MissingBusinessKey_ExposesZombieLegacyIdWithWarning()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 14,
LICleType = null, // rubrique active absente
LIIDType = 77 // ancien identifiant (zombie)
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.NotNull(result.Value.Type);
Assert.Equal(77, result.Value.Type!.LegacyId);
Assert.Null(result.Value.Type!.Key);
Assert.Contains(result.Warnings, w => w.StartsWith(MappingWarningCodes.ZombieFieldUsedAsFallback, StringComparison.Ordinal));
}
// ---- 11. Référentiel non résolu -------------------------------------------------
[Fact]
public void SiteOmika_UnresolvedBusinessKey_KeepsKeyAndLegacyIdAndWarns()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 15,
LICleType = "IMM",
LIIDType = 77
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.NotNull(result.Value.Type);
Assert.Equal("IMM", result.Value.Type!.Key);
Assert.Equal(77, result.Value.Type!.LegacyId);
Assert.Equal(string.Empty, result.Value.Type!.Label);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
[Fact]
public void SiteOmika_ZeroType_ProducesNoReference()
{
var row = new OmikaModels.LieuRow { LIPKID = 16, LIIDType = 0 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Null(result.Value.Type);
Assert.Empty(result.Warnings);
}
// ---- 12. Adresse déjà chargée en mémoire ---------------------------------------
[Fact]
public void SiteOmika_Adresse_IsMappedFromInMemoryDictionary()
{
var row = new OmikaModels.LieuRow { LIPKID = 17, LIIDAdresse = 20 };
var adresses = new Dictionary<long, OmikaModels.AdresseRow>
{
[20] = new() { ADPKID = 20, ADAdresse = "1 place de la Comédie", ADVille = "Lyon", ADCodePostal = "69001" }
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row, adresses: adresses);
Assert.NotNull(result.Value.Adresse);
Assert.Equal("1 place de la Comédie", result.Value.Adresse!.Line1);
Assert.Equal("69001", result.Value.Adresse!.PostalCode);
Assert.Equal("Lyon", result.Value.Adresse!.City);
}
[Fact]
public void SiteOmika_NoAdresse_WhenNothingProvided()
{
var row = new OmikaModels.LieuRow { LIPKID = 18, LIIDAdresse = 0 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Null(result.Value.Adresse);
Assert.Empty(result.Warnings);
}
[Fact]
public void SiteOmika_UnresolvedAdresse_WarnsAndProducesNull()
{
var row = new OmikaModels.LieuRow { LIPKID = 19, LIIDAdresse = 99 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Null(result.Value.Adresse);
Assert.Contains(result.Warnings, w => w.Contains(MappingWarningCodes.UnresolvedReference, StringComparison.Ordinal));
}
// ---- 13. Relations différées ----------------------------------------------------
[Fact]
public void SiteOmika_Relations_AreLeftForLaterAssembly()
{
var row = new OmikaModels.LieuRow { LIPKID = 20, LIIDClientPrincipal = 30 };
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Null(result.Value.ClientId);
Assert.Empty(result.Value.ContactIds);
Assert.Contains(result.IgnoredFields, f => f == "LI_IDClientPrincipal");
}
// ---- 14. Chaînes vides sans exception -------------------------------------------
[Fact]
public void SiteOmika_EmptyOrNullStrings_NeverThrow()
{
var row = new OmikaModels.LieuRow
{
LICodeLieu = "",
LINote = " ",
LIIDImport = "",
LICodeAcces = " ",
LIRaisonBlocage = "",
LIRepertoireDocs = " ",
GUID = null
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.Code);
Assert.Null(result.Value.InformationsComplementaires);
Assert.Null(result.Value.SourceReference!.ImportId);
Assert.Empty(result.Warnings);
}
[Fact]
public void SiteOmika_DefaultRow_NeverThrows()
{
var row = new OmikaModels.LieuRow();
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.NotNull(result.Value);
Assert.Null(result.Value.SourceReference!.NumericId);
Assert.False(result.Value.EstArchive);
}
// ---- 15. Champs sans équivalent dans IgnoredFields --------------------------------
[Fact]
public void SiteOmika_FieldsWithoutCanonicalEquivalent_AreReportedAsIgnored()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 21,
LICodeAcces = "Code 1234",
LIBadge = true,
LIInterphone = true,
LIBlocage = true,
LIRaisonBlocage = "Travaux",
LIRepertoireDocs = "docs/site",
LIIDClientPrincipal = 30,
DateArchivage = new DateTime(2024, 6, 1),
ModifDate = new DateTime(2024, 1, 1),
CreationDate = new DateTime(2023, 1, 1),
CreationUser = 5,
ModifUser = 6
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row);
Assert.Equal("Code 1234", result.Value.CodeAcces);
Assert.True(result.Value.Badge);
Assert.True(result.Value.Interphone);
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_CodeAcces");
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_Badge");
Assert.DoesNotContain(result.IgnoredFields, f => f == "LI_Interphone");
Assert.Contains(result.IgnoredFields, f => f == "LI_Blocage");
Assert.Contains(result.IgnoredFields, f => f == "LI_RaisonBlocage");
Assert.Contains(result.IgnoredFields, f => f == "LI_RepertoireDocs");
Assert.Contains(result.IgnoredFields, f => f == "LI_IDClientPrincipal");
Assert.Contains(result.IgnoredFields, f => f == "DateArchivage");
Assert.Equal(new DateTime(2024, 1, 1), result.Value.DateModification);
Assert.Contains(result.IgnoredFields, f => f == "CreationDate");
Assert.Contains(result.IgnoredFields, f => f == "CreationUser");
Assert.Contains(result.IgnoredFields, f => f == "ModifUser");
}
// ---- 16. Absence de noms physiques HFSQL dans le domaine canonique ----------------
[Fact]
public void SiteOmika_CanonicalDomain_DoesNotExposeHfSqlNames()
{
var row = new OmikaModels.LieuRow
{
LIPKID = 40,
GUID = "550e8400-e29b-41d4-a716-446655440000",
LICodeLieu = "S-040",
LICleType = "IMM",
LIIDType = 2,
LINote = "note",
Archive = false,
LIIDImport = "IMP-40",
LIIDAdresse = 20,
LIIDClientPrincipal = 30,
LICodeAcces = "code",
LIBadge = true,
LIInterphone = true,
LIBlocage = true,
LIRaisonBlocage = "raison",
LIRepertoireDocs = "docs"
};
var referenceRows = new Dictionary<string, OmikaModels.ParametreMetierRow>
{
["IMM"] = new() { PAPKID = 2, PACLE = "IMM", PALibelle = "Immeuble", PACategorie = "TYPE" }
};
var adresses = new Dictionary<long, OmikaModels.AdresseRow>
{
[20] = new() { ADPKID = 20, ADAdresse = "1 rue de la Paix", ADVille = "Lyon", ADCodePostal = "69001" }
};
CanonicalMappingResult<Site> result = OmikaMappers.OmikaSiteMapper.Map(row, referenceRows, adresses);
Site site = result.Value;
string[] forbidden =
[
"LI_", "AD_", "PA_", "PKID", "HfSql", "HFSQL", "GUID"
];
AssertHasNoPhysicalNames(site, forbidden);
if (site.Adresse is not null)
{
AssertHasNoPhysicalNames(site.Adresse, forbidden);
}
if (site.SourceReference is not null)
{
AssertHasNoPhysicalNames(site.SourceReference, forbidden);
}
}
// ---- 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));
}
}
}
@@ -0,0 +1,495 @@
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;
/// <summary>
/// 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.
/// </summary>
public class PhysicalToCanonicalMappingTests
{
// ---- 1. Client Batik minimal -------------------------------------------------
[Fact]
public void ClientBatik_Minimal_MapsWithoutExceptionAndKeepsNoFabricatedValue()
{
var row = new BatikModels.ClientRow { CLPKID = 1 };
CanonicalMappingResult<Client> 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<long, BatikModels.ParametreMetierRow>
{
[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<long, BatikModels.AdresseRow>
{
[10] = new() { ADPKID = 10, ADEtiquette = "Mairie", ADLigne1 = "1 place de la Comédie", ADVille = "Lyon", ADCodePostal = "69001" }
};
CanonicalMappingResult<Client> 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<Adresse> 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<Adresse> 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<Client> 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<Client> 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<Client> 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<Adresse> 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<Adresse> 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<string, OmikaModels.ParametreMetierRow>
{
["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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<ReferenceValue> 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<ReferenceValue> 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<Client> 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<ReferenceValue> 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<ReferenceValue> 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<string, OmikaModels.ParametreMetierRow>
{
["RGE"] = new() { PAPKID = 70, PACLE = "RGE", PALibelle = "Régie", PACategorie = "TYPE" }
};
CanonicalMappingResult<ReferenceValue>? 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));
}
}
}