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,69 @@
using Omika.Core.Compatibility;
using Omika.Core.Domain.Entities.Clients;
using Omika.Core.Domain.ValueObjects;
namespace Omika.Tests.Domain;
/// <summary>
/// Tests de l'entité métier canonique <see cref="Client"/> : indépendance vis-à
/// vis du stockage HFSQL et portage des collections sans table de liaison.
/// </summary>
public class ClientDomainTests
{
[Fact]
public void Client_DoesNotExposePhysicalColumnNames()
{
var client = new Client();
string[] forbidden = ["CL_PKID", "CL_IDType", "CL_CleType", "CL_IDStatut", "CL_CleStatut"];
foreach (var property in client.GetType().GetProperties())
{
Assert.DoesNotContain(forbidden, f => property.Name.Contains(f, StringComparison.Ordinal));
}
}
[Fact]
public void Client_CanCarryTypeStatutAndFormeJuridiqueAsReferenceValues()
{
var client = new Client
{
Nom = "Ville de Lyon",
Type = new ReferenceValue("Collectivité") { Key = "COLLECTIVITE" },
Statut = new ReferenceValue("Actif") { LegacyId = 5 },
FormeJuridique = new ReferenceValue("Commune")
};
Assert.Equal("Collectivité", client.Type!.Label);
Assert.Equal("Actif", client.Statut!.Label);
Assert.Equal("Commune", client.FormeJuridique!.Label);
}
[Fact]
public void Client_CanCarrySourceReferenceWithoutHfSqlName()
{
var client = new Client
{
SourceReference = LegacyEntityReference.FromLegacyId(12_345)
};
Assert.Equal(DatabaseSchemaVersion.Batik, client.SourceReference!.Source);
Assert.Equal(12_345, client.SourceReference.NumericId);
}
[Fact]
public void Client_CollectionsAreControlledByAddMethods()
{
var client = new Client();
client.AddAdresse(new Adresse { Etiquette = "Mairie" });
client.AddContact(new Contact { Nom = "Dupont" });
client.AddSite(new Site { Nom = "Hôtel de ville" });
client.AddReferenceExterne(new ExternalReference("Batigest", ExternalId: "X-1"));
Assert.Single(client.Adresses);
Assert.Single(client.Contacts);
Assert.Single(client.Sites);
Assert.Single(client.ReferencesExternes);
}
}
@@ -0,0 +1,57 @@
using Omika.Core.Domain.Exceptions;
using Omika.Core.Domain.ValueObjects;
namespace Omika.Tests.Domain;
/// <summary>
/// Tests de l'objet de valeur <see cref="DateTimeRange"/>.
/// </summary>
public class DateTimeRangeTests
{
[Fact]
public void DateTimeRange_RejectsEndBeforeStart()
{
var start = new DateTime(2026, 1, 10, 9, 0, 0);
var end = new DateTime(2026, 1, 10, 8, 0, 0);
Assert.Throws<DomainValidationException>(() => new DateTimeRange(start, end));
}
[Fact]
public void DateTimeRange_AcceptsEqualDates()
{
var instant = new DateTime(2026, 1, 10, 9, 0, 0);
var range = new DateTimeRange(instant, instant);
Assert.Equal(TimeSpan.Zero, range.Duration);
}
[Fact]
public void DateTimeRange_ComputesDurationAndContains()
{
var range = new DateTimeRange(
new DateTime(2026, 1, 10, 9, 0, 0),
new DateTime(2026, 1, 10, 11, 30, 0));
Assert.Equal(TimeSpan.FromMinutes(150), range.Duration);
Assert.True(range.Contains(new DateTime(2026, 1, 10, 10, 0, 0)));
Assert.False(range.Contains(new DateTime(2026, 1, 10, 12, 0, 0)));
}
[Fact]
public void DateTimeRange_DetectsOverlap()
{
var a = new DateTimeRange(
new DateTime(2026, 1, 10, 9, 0, 0),
new DateTime(2026, 1, 10, 11, 0, 0));
var b = new DateTimeRange(
new DateTime(2026, 1, 10, 10, 0, 0),
new DateTime(2026, 1, 10, 12, 0, 0));
var c = new DateTimeRange(
new DateTime(2026, 1, 10, 13, 0, 0),
new DateTime(2026, 1, 10, 14, 0, 0));
Assert.True(a.Overlaps(b));
Assert.False(a.Overlaps(c));
}
}
@@ -0,0 +1,54 @@
using Omika.Core.Domain.Entities.Interventions;
using Omika.Core.Domain.ValueObjects;
namespace Omika.Tests.Domain;
/// <summary>
/// Tests de l'entité métier canonique <see cref="Intervention"/>.
/// </summary>
public class InterventionDomainTests
{
[Fact]
public void Intervention_UsesReferenceValuesForTypeEtatPriorite()
{
var intervention = new Intervention
{
Type = new ReferenceValue("Maintenance") { Key = "MAINT" },
Etat = new ReferenceValue("En cours"),
Priorite = new ReferenceValue("Haute") { LegacyId = 3 }
};
Assert.Equal("MAINT", intervention.Type!.Key);
Assert.Equal("En cours", intervention.Etat!.Label);
Assert.Equal(3, intervention.Priorite!.LegacyId);
}
[Fact]
public void Intervention_CanCarryCollaborateursActionsAndComptesRendus()
{
var intervention = new Intervention();
intervention.AddCollaborateur(Guid.NewGuid());
intervention.AddContact(Guid.NewGuid());
intervention.AddAction(new ActionIntervention { Description = "Préparer le plan", Terminee = false });
intervention.AddCompteRendu(new CompteRendu { Contenu = "Site inspecté" });
Assert.Single(intervention.CollaborateurIds);
Assert.Single(intervention.ContactIds);
Assert.Single(intervention.Actions);
Assert.Single(intervention.ComptesRendus);
}
[Fact]
public void Intervention_DoesNotExposeZombieFields()
{
var intervention = new Intervention();
string[] forbidden = ["Pkid", "IdStatut", "CleStatut", "CleType", "RDP_"];
foreach (var property in intervention.GetType().GetProperties())
{
Assert.DoesNotContain(forbidden, f => property.Name.Contains(f, StringComparison.Ordinal));
}
}
}
@@ -0,0 +1,181 @@
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Planning;
using Omika.Core.Mapping;
using Omika.Core.Domain.Entities.Planning;
using Omika.Core.Domain.ValueObjects;
namespace Omika.Tests.Domain;
/// <summary>
/// Scénario de compatibilité des rendez-vous : le même
/// <see cref="RendezVousDetailsDto"/> doit pouvoir représenter un rendez-vous de
/// l'ancien schéma (Batik) et du nouveau schéma (Omika), avec ou sans contacts,
/// avec plusieurs collaborateurs, et avec des informations absentes selon la
/// version.
/// </summary>
public class RendezVousCrossSchemaTests
{
private static RendezVous BuildOldSchemaRendezVous()
{
var rendezVous = new RendezVous
{
Titre = "RDV Batik",
Description = null, // absent dans l'ancien modèle
Periode = new DateTimeRange(new DateTime(2026, 2, 10, 9, 0, 0), new DateTime(2026, 2, 10, 10, 0, 0)),
TypeActivite = new ReferenceValue("Visite") { LegacyId = 3 },
Statut = new ReferenceValue("Planifié") { LegacyId = 1 },
Couleur = "#FF0000",
EstTouteLaJournee = false,
EstArchive = false
};
rendezVous.AddCollaborateur(new AffectationCollaborateur
{
CollaborateurId = Guid.NewGuid(),
NomAffiche = "Dupont Jean",
Responsable = true,
Statut = new ReferenceValue("Affecté") { LegacyId = 2 },
TempsValide = false
});
rendezVous.AddCollaborateur(new AffectationCollaborateur
{
CollaborateurId = Guid.NewGuid(),
NomAffiche = "Martin Claire",
Responsable = false,
TempsValide = false
});
rendezVous.AddContact(new ParticipantRendezVous
{
ContactId = Guid.NewGuid(),
NomAffiche = "Durand Paul",
Email = "paul@exemple.fr",
Telephone = "0600000000",
PresenceConfirmee = false
});
return rendezVous;
}
private static RendezVous BuildNewSchemaRendezVous()
{
var rendezVous = new RendezVous
{
Titre = "RDV Omika",
Description = "Visite de contrôle annuelle",
Periode = new DateTimeRange(new DateTime(2026, 2, 12, 14, 0, 0), new DateTime(2026, 2, 12, 16, 0, 0)),
ClientId = Guid.NewGuid(),
SiteId = Guid.NewGuid(),
DossierId = Guid.NewGuid(),
InterventionId = Guid.NewGuid(),
TypeActivite = new ReferenceValue("Contrôle") { Key = "CONTROLE" },
Statut = new ReferenceValue("Confirmé") { Key = "CONFIRME" },
Couleur = "#00FF00",
EstTouteLaJournee = false,
EstArchive = false
};
rendezVous.AddCollaborateur(new AffectationCollaborateur
{
CollaborateurId = Guid.NewGuid(),
NomAffiche = "Dupont Jean",
Responsable = true,
Statut = new ReferenceValue("Validé") { Key = "VALIDE" },
TempsValide = true,
PeriodeParticuliere = new DateTimeRange(new DateTime(2026, 2, 12, 14, 30, 0), new DateTime(2026, 2, 12, 15, 30, 0))
});
// Pas de contact : le nouveau schéma gère RDV_Contact séparément.
return rendezVous;
}
[Fact]
public void SameDto_RepresentsOldAndNewSchema()
{
RendezVousDetailsDto oldDto = RendezVousContractMapper.ToDetails(BuildOldSchemaRendezVous());
RendezVousDetailsDto newDto = RendezVousContractMapper.ToDetails(BuildNewSchemaRendezVous());
// Le type du DTO est strictement le même pour les deux versions.
Assert.Equal(oldDto.GetType(), newDto.GetType());
// Les informations partagées sont portées de la même façon.
Assert.Equal("RDV Batik", oldDto.Titre);
Assert.Equal("RDV Omika", newDto.Titre);
Assert.Equal(new DateTime(2026, 2, 10, 9, 0, 0), oldDto.Debut);
Assert.Equal(new DateTime(2026, 2, 12, 16, 0, 0), newDto.Fin);
}
[Fact]
public void NewSchema_CanCarryLinkedEntities()
{
RendezVousDetailsDto newDto = RendezVousContractMapper.ToDetails(BuildNewSchemaRendezVous());
Assert.NotNull(newDto.ClientId);
Assert.NotNull(newDto.SiteId);
Assert.NotNull(newDto.DossierId);
Assert.NotNull(newDto.InterventionId);
}
[Fact]
public void OldSchema_AbsentInformationIsNull()
{
RendezVousDetailsDto oldDto = RendezVousContractMapper.ToDetails(BuildOldSchemaRendezVous());
// La description et les liens métier n'existaient pas dans l'ancien modèle.
Assert.Null(oldDto.Description);
Assert.Null(oldDto.ClientId);
Assert.Null(oldDto.DossierId);
Assert.Null(oldDto.InterventionId);
}
[Fact]
public void OldSchema_CarriesLegacyReferenceValues()
{
RendezVousDetailsDto oldDto = RendezVousContractMapper.ToDetails(BuildOldSchemaRendezVous());
Assert.NotNull(oldDto.TypeActivite);
Assert.Equal(3, oldDto.TypeActivite.LegacyId);
Assert.Equal(1, oldDto.Statut!.LegacyId);
// La nouvelle clé métier n'existe pas dans l'ancien schéma.
Assert.Null(oldDto.TypeActivite.Key);
}
[Fact]
public void NewSchema_CarriesBusinessKeys()
{
RendezVousDetailsDto newDto = RendezVousContractMapper.ToDetails(BuildNewSchemaRendezVous());
Assert.NotNull(newDto.TypeActivite);
Assert.Equal("CONTROLE", newDto.TypeActivite.Key);
Assert.Null(newDto.TypeActivite.LegacyId);
}
[Fact]
public void RendezVous_WithMultipleCollaborateurs_MapsAllOfThem()
{
RendezVousDetailsDto oldDto = RendezVousContractMapper.ToDetails(BuildOldSchemaRendezVous());
Assert.Equal(2, oldDto.Collaborateurs.Count);
Assert.True(oldDto.Collaborateurs[0].Responsable);
Assert.False(oldDto.Collaborateurs[1].Responsable);
}
[Fact]
public void RendezVous_WithoutContacts_IsSupported()
{
RendezVousDetailsDto newDto = RendezVousContractMapper.ToDetails(BuildNewSchemaRendezVous());
Assert.Empty(newDto.Contacts);
}
[Fact]
public void RendezVous_WithSingleCollaborateurCarriesTempsValide()
{
RendezVousDetailsDto newDto = RendezVousContractMapper.ToDetails(BuildNewSchemaRendezVous());
Assert.Single(newDto.Collaborateurs);
Assert.True(newDto.Collaborateurs[0].TempsValide);
Assert.NotNull(newDto.Collaborateurs[0].PeriodeDebut);
Assert.NotNull(newDto.Collaborateurs[0].PeriodeFin);
}
}
@@ -0,0 +1,69 @@
using Omika.Core.Domain.Entities.Planning;
using Omika.Core.Domain.ValueObjects;
namespace Omika.Tests.Domain;
/// <summary>
/// Tests de l'agrégat métier canonique <see cref="RendezVous"/>, unique pour
/// l'ancien et le nouveau schéma de base.
/// </summary>
public class RendezVousDomainTests
{
[Fact]
public void RendezVous_IsIndependentOfPhysicalTableNames()
{
var rendezVous = new RendezVous();
string[] forbidden = ["Planning_RDV", "RDV_Planning", "RDP_GUID", "PL_PKID"];
Assert.DoesNotContain(forbidden, name => rendezVous.GetType().Name.Contains(name, StringComparison.Ordinal));
foreach (var property in rendezVous.GetType().GetProperties())
{
Assert.DoesNotContain(forbidden, name => property.Name.Contains(name, StringComparison.Ordinal));
}
}
[Fact]
public void RendezVous_HoldsPeriodeAsValueObject()
{
var rendezVous = new RendezVous
{
Titre = "Visite",
Periode = new DateTimeRange(new DateTime(2026, 2, 1, 9, 0, 0), new DateTime(2026, 2, 1, 11, 0, 0))
};
Assert.Equal(new DateTime(2026, 2, 1, 9, 0, 0), rendezVous.Periode.Start);
Assert.Equal(new DateTime(2026, 2, 1, 11, 0, 0), rendezVous.Periode.End);
}
[Fact]
public void RendezVous_CanCarryCollaborateursAndContacts()
{
var rendezVous = new RendezVous();
rendezVous.AddCollaborateur(new AffectationCollaborateur { CollaborateurId = Guid.NewGuid(), Responsable = true });
rendezVous.AddCollaborateur(new AffectationCollaborateur { CollaborateurId = Guid.NewGuid() });
rendezVous.AddContact(new ParticipantRendezVous { ContactId = Guid.NewGuid(), PresenceConfirmee = true });
Assert.Equal(2, rendezVous.Collaborateurs.Count);
Assert.Single(rendezVous.Contacts);
Assert.True(rendezVous.Contacts[0].PresenceConfirmee);
}
[Fact]
public void RendezVous_CanCarryAnnulation()
{
var rendezVous = new RendezVous
{
Annulation = new AnnulationRendezVous
{
DateAnnulation = new DateTime(2026, 2, 1, 8, 0, 0),
Commentaire = "Reporté"
}
};
Assert.NotNull(rendezVous.Annulation);
Assert.Equal("Reporté", rendezVous.Annulation.Commentaire);
}
}
@@ -0,0 +1,73 @@
using Omika.Core.Services;
using Xunit;
namespace Omika.Tests.Domain;
public class RubriqueSubstitutionEngineTests
{
[Fact]
public void Substitute_ReplacesTokensCaseInsensitively()
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["NomClient"] = "ACME",
["DateIntervention"] = "2026-08-06"
};
string result = RubriqueSubstitutionEngine.Substitute(
"Bonjour $NOMCLIENT$, intervention du $DateIntervention$.",
values);
Assert.Equal("Bonjour ACME, intervention du 2026-08-06.", result);
}
[Fact]
public void Substitute_LeavesUnknownTokensUntouched()
{
var values = new Dictionary<string, string> { ["NomClient"] = "ACME" };
string result = RubriqueSubstitutionEngine.Substitute(
"Bonjour $NomClient$ — $Inconnu$",
values);
Assert.Equal("Bonjour ACME — $Inconnu$", result);
}
[Fact]
public void Substitute_NullOrEmptyTemplate_ReturnsEmpty()
{
Assert.Equal(string.Empty, RubriqueSubstitutionEngine.Substitute(null, new Dictionary<string, string>()));
Assert.Equal(string.Empty, RubriqueSubstitutionEngine.Substitute(string.Empty, new Dictionary<string, string>()));
}
[Fact]
public void Substitute_UnclosedTokenIsLeftUntouched()
{
string result = RubriqueSubstitutionEngine.Substitute(
"Bonjour $NomClient",
new Dictionary<string, string> { ["NomClient"] = "ACME" });
Assert.Equal("Bonjour $NomClient", result);
}
[Fact]
public void ExtractTokens_ReturnsOrderedUniqueTokens()
{
IReadOnlyList<string> tokens = RubriqueSubstitutionEngine.ExtractTokens(
"$A$ puis $B$ et encore $a$");
Assert.Equal(["A", "B"], tokens);
}
[Fact]
public void FindUnknownTokens_ReturnsTokensMissingFromValues()
{
var values = new Dictionary<string, string> { ["A"] = "1" };
IReadOnlyList<string> unknown = RubriqueSubstitutionEngine.FindUnknownTokens(
"$A$ $B$ $A$",
values);
Assert.Equal(["B"], unknown);
}
}