using Omika.Core.Compatibility; using Omika.Core.Contracts.Clients; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Documents; using Omika.Core.Contracts.Interventions; using Omika.Core.Contracts.Mobile.Interventions; using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Contracts.Organisation; using Omika.Core.Contracts.Planning; using Omika.Core.Contracts.Sites; using Omika.Core.Interfaces; using Omika.Core.Interfaces.Mobile; using Omika.Core.Services.Mobile; using Omika.Core.Domain; namespace Omika.Tests.Mobile.Local; /// /// Tests du service d'enrichissement mobile (MobileInterventionSearchService) : /// recherche par numéro ciblée, identité canonique, lien rendez-vous/site/client, /// sans full scan. /// public class MobileInterventionSearchServiceTests { private static readonly Guid SiteGuid = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"); private static readonly Guid ClientGuid = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"); [Fact] public async Task Search_ForceFiltreNonArchive() { var interventions = new FakeInterventionService(); var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); Assert.False(interventions.LastRequest?.EstArchive); } [Fact] public async Task Search_NumeroNumerique_ResteReferencePartielle() { var interventions = new FakeInterventionService(); var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); await service.SearchAsync(new MobileInterventionSearchRequest { Reference = "105" }, null, new Dictionary()); // Le champ N° d'intervention cible IN_RefInterne (recherche partielle) : // « 105 » est transmis tel quel à Reference, jamais à NumIntervention. Assert.Equal("105", interventions.LastRequest?.Reference); Assert.Null(interventions.LastRequest?.NumIntervention); } [Fact] public async Task Search_Numero105EtTous_ReferencePasseEtIdsNull() { var interventions = new FakeInterventionService(); var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); await service.SearchAsync(new MobileInterventionSearchRequest { Reference = "105", ClientId = "", SiteId = " ", CollaborateurId = Guid.Empty.ToString(), AvecRendezVous = null, RendezVousDate = null }, null, new Dictionary()); Assert.Equal("105", interventions.LastRequest?.Reference); Assert.Null(interventions.LastRequest?.NumIntervention); Assert.Null(interventions.LastRequest?.ClientId); Assert.Null(interventions.LastRequest?.SiteId); Assert.Null(interventions.LastRequest?.CollaborateurId); Assert.False(interventions.LastRequest?.EstArchive); } [Fact] public async Task Search_NumeroNonNumerique_ResteReferenceTexte() { var interventions = new FakeInterventionService(); var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); await service.SearchAsync(new MobileInterventionSearchRequest { Reference = "D000105-1" }, null, new Dictionary()); Assert.Null(interventions.LastRequest?.NumIntervention); Assert.Equal("D000105-1", interventions.LastRequest?.Reference); } [Fact] public async Task Search_CibleRendezVousParInterventionId() { var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", Reference = "105", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService(); var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); // Une SEULE requête bulk : Ids porte les PK numériques (jamais un // InterventionId par intervention, ni un N+1). var request = Assert.Single(rdv.Requests); Assert.Equal("105", Assert.Single(request.Ids!)); Assert.Null(request.InterventionId); } [Fact] public async Task Search_DetailPorteIdentiteCanonique() { // L'identité interne du DTO source est aléatoire (EntityBase.Id) ; le // résultat mobile doit porter le GUID public canonique résolu (GUID // physique Omika), afin que la fiche retrouve le snapshot et les // rendez-vous/contacts liés par identité. Guid canonical = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0000"); var intervention = new InterventionDetailsDto { Id = Guid.NewGuid().ToString(), Reference = "105", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, EntityGuid: canonical) }; var interventions = new FakeInterventionService { Results = [intervention] }; var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(canonical.ToString("D"), item.Detail.Id); } [Fact] public async Task Search_ZeroIntervention_NeChargeRien() { var interventions = new FakeInterventionService { Results = [] }; var rdv = new FakeRendezVousService(); var sites = new FakeSiteService(); var clients = new FakeClientService(); var service = NewService(interventions, rdv, sites, clients); await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); Assert.Empty(rdv.Requests); Assert.Empty(sites.RequestedIds); Assert.Empty(clients.RequestedIds); } [Fact] public async Task Search_ResoutSiteClientEtAdresse() { var contactGuid = Guid.Parse("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", // Les noms client/site, l'adresse et les contacts sont désormais // dénormalisés par le repository via une jointure SQL unique : le // service mobile les lit directement, sans appel site/client. SiteNom = "Site A", SiteAdresse = "12 rue Exemple, 75000 Paris", ClientNom = "Client A", Contacts = [ new ContactDto { Id = contactGuid.ToString(), Nom = "Dupont", Prenom = "Jean", NomAffiche = "Jean Dupont" } ] }; var interventions = new FakeInterventionService { Results = [intervention] }; var service = NewService(interventions, new FakeRendezVousService(), new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal("Site A", item.SiteNom); Assert.Equal("12 rue Exemple, 75000 Paris", item.SiteAddressDisplay); Assert.Equal("Client A", item.ClientNom); Assert.Equal("Jean Dupont", item.ContactNom); } [Fact] public async Task Search_RendezVousSansDate_ExcluDuCompteur() { var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = new DateTime(2026, 8, 19, 10, 0, 0), InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }, new RendezVousDetailsDto { Debut = default } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); // 1 RDV daté + 1 RDV sans date → seul le daté compte (éligibilité mobile). var item = Assert.Single(results); Assert.Equal(1, item.NbRendezVous); } [Fact] public async Task Search_InterventionSansRdv_NbRendezVousZero() { var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(0, item.NbRendezVous); } [Fact] public async Task Search_InterventionAvecUnRdvDate_NbRendezVousUn() { var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = new DateTime(2026, 8, 19, 10, 0, 0), InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(1, item.NbRendezVous); } [Fact] public async Task Search_InterventionAvecPlusieursRdv_NbRendezVousCorrect() { var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = new DateTime(2026, 8, 19, 10, 0, 0), InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }, new RendezVousDetailsDto { Debut = new DateTime(2026, 8, 20, 11, 0, 0), InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(2, item.NbRendezVous); } [Fact] public async Task Search_RdvDateUniquementSurAffectation_CompteDansLeCompteur() { // Omika : RV_Date (racine) vide → Debut = default, mais l'affectation // porte la date réelle (RVC_DateDeb / RVC_DateFin). Le RDV doit compter. var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = default, InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105), Collaborateurs = [ new AffectationCollaborateurDto { CollaborateurId = Guid.NewGuid().ToString(), PeriodeDebut = new DateTime(2026, 8, 19, 10, 0, 0), PeriodeFin = new DateTime(2026, 8, 19, 11, 30, 0) } ] } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(1, item.NbRendezVous); } [Fact] public async Task Search_RdvGroupeMultipleAffectations_CompteCommeUnSeulRdv() { // Un RDV groupe Omika = un noyau RDV_Planning + plusieurs affectations. // Le compteur doit compter 1 (le noyau), jamais le nombre d'affectations. var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = default, InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105), Collaborateurs = [ new AffectationCollaborateurDto { CollaborateurId = Guid.NewGuid().ToString(), PeriodeDebut = new DateTime(2026, 8, 19, 10, 0, 0) }, new AffectationCollaborateurDto { CollaborateurId = Guid.NewGuid().ToString(), PeriodeDebut = new DateTime(2026, 8, 19, 10, 0, 0) } ] } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(1, item.NbRendezVous); } [Fact] public async Task GetRendezVousAsync_RdvDateSurAffectation_RestitueLeRdvRabattu() { // Omika : RV_Date (racine) vide → Debut = default. Le détail de la fiche // intervention doit rabattre la date de l'affectation (RVC_DateDeb) AVANT // l'éligibilité, exactement comme le compteur NbRendezVous. Sans cela, un // rendez-vous réellement daté est écarté (liste vide) alors que la liste // des interventions affiche un compteur non nul. var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { GetByIdResult = intervention }; DateTime debutAffectation = new DateTime(2026, 8, 19, 10, 0, 0); var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Id = Guid.NewGuid().ToString(), Debut = default, SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 200), InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105), Collaborateurs = [ new AffectationCollaborateurDto { CollaborateurId = Guid.NewGuid().ToString(), PeriodeDebut = debutAffectation, PeriodeFin = debutAffectation.AddHours(1.5) } ] } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); IReadOnlyList result = await service.GetRendezVousAsync("intervention-dto-id"); RendezVousDetailsDto item = Assert.Single(result); Assert.Equal(debutAffectation, item.Debut); } [Fact] public async Task GetRendezVousAsync_TrieDuPlusRecentAuPlusAncien() { // Règle produit : la liste des rendez-vous d'une intervention est triée // par date décroissante (le plus récent d'abord), comme Batik. var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { GetByIdResult = intervention }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Id = Guid.NewGuid().ToString(), Debut = new DateTime(2026, 8, 19, 10, 0, 0) }, new RendezVousDetailsDto { Id = Guid.NewGuid().ToString(), Debut = new DateTime(2026, 8, 21, 10, 0, 0) }, new RendezVousDetailsDto { Id = Guid.NewGuid().ToString(), Debut = new DateTime(2026, 8, 20, 10, 0, 0) } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); IReadOnlyList result = await service.GetRendezVousAsync("intervention-dto-id"); Assert.Equal(3, result.Count); Assert.Equal(new DateTime(2026, 8, 21, 10, 0, 0), result[0].Debut); Assert.Equal(new DateTime(2026, 8, 20, 10, 0, 0), result[1].Debut); Assert.Equal(new DateTime(2026, 8, 19, 10, 0, 0), result[2].Debut); } [Fact] public async Task Search_RdvSansAucuneDate_ExcluDuCompteur() { // Un RDV sans date sur la racine ET sans affectation datée est // « à attribuer » : il ne doit pas compter. var intervention = new InterventionDetailsDto { Id = "intervention-dto-id", SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) }; var interventions = new FakeInterventionService { Results = [intervention] }; var rdv = new FakeRendezVousService { Results = [ new RendezVousDetailsDto { Debut = default, InterventionSourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: 105) } ] }; var service = NewService(interventions, rdv, new FakeSiteService(), new FakeClientService()); var results = await service.SearchAsync(new MobileInterventionSearchRequest(), null, new Dictionary()); var item = Assert.Single(results); Assert.Equal(0, item.NbRendezVous); } private static MobileInterventionSearchService NewService( IInterventionService interventions, IRendezVousService rdv, ISiteService sites, IClientService clients) => new(interventions, rdv, sites, clients, new FakeCollaborateurService(), new FakeIdentityResolver()); private sealed class FakeCollaborateurService : ICollaborateurService { public Task> SearchAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task CreateAsync(CreateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetSocieteIdsAsync(long collaborateurId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetAllowedCollaborateurIdsAsync(long collaborateurId, bool includeSocietePrincipale, bool includeSocietes, bool includeEquipe, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetRendezVousAsync(string collaborateurId, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetRendezVousForCollaborateursAsync(IReadOnlyList collaborateurIds, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } private sealed class FakeIdentityResolver : IMobileEntityIdentityResolver { public Guid CreateCanonicalIdentity(string entityType, PhysicalEntityIdentity physical) => physical.Guid ?? Guid.NewGuid(); public PhysicalEntityIdentity? ResolvePhysicalIdentity(string entityType, Guid publicGuid) => new PhysicalEntityIdentity { Guid = publicGuid }; public bool IsCanonical(string entityType, Guid publicGuid) => publicGuid != Guid.Empty; } private sealed class FakeInterventionService : IInterventionService { public List Results { get; set; } = []; public InterventionDetailsDto? GetByIdResult { get; set; } public SearchInterventionsRequest? LastRequest { get; set; } public Task> SearchAsync(SearchInterventionsRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchInterventionsRequest request, CancellationToken cancellationToken = default) { LastRequest = request; return Task.FromResult(new PagedResult(Results, 1, Results.Count, Results.Count, 1)); } public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) => Task.FromResult(GetByIdResult); public Task CreateAsync(CreateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateInterventionRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetNotesAsync(string interventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetNotesAdministrativesAsync(string interventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task AddNoteAsync(string interventionId, string contenu, AuditActor? auteur, CancellationToken cancellationToken = default) => Task.FromResult(new NoteInterneDto()); public Task> GetEmailsAsync(string interventionId, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task AddEmailAsync(string interventionId, CreateEmailInterventionRequest request, AuditActor? auteur, CancellationToken cancellationToken = default) => Task.FromResult(new EmailInterventionDto()); } private sealed class FakeRendezVousService : IRendezVousService { public List Requests { get; } = []; public List Results { get; set; } = []; public Task> SearchAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default) { Requests.Add(request); return Task.FromResult(new PagedResult(Results, 1, Results.Count, Results.Count, 1)); } public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task CreateAsync(CreateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task CancelAsync(string id, AnnulationRendezVousDto annulation, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task SetTempsValideAsync(string rendezVousId, string collaborateurId, bool tempsValide, CancellationToken cancellationToken = default, AuditActor? auteur = null) => Task.FromResult(true); } private sealed class FakeSiteService : ISiteService { public Dictionary ById { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List RequestedIds { get; } = []; public Task> SearchAsync(SearchSitesRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchSitesRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) { RequestedIds.Add(id); ById.TryGetValue(id, out SiteDetailsDto? site); return Task.FromResult(site); } public Task> GetContactsAsync(string id, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetRendezVousAsync(string id, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task> GetClientsAsync(string id, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task CreateAsync(CreateSiteRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateSiteRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } private sealed class FakeClientService : IClientService { public Dictionary ById { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List RequestedIds { get; } = []; public Task> SearchAsync(SearchClientsRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchClientsRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) { RequestedIds.Add(id); ById.TryGetValue(id, out ClientDetailsDto? client); return Task.FromResult(client); } public Task CreateAsync(CreateClientRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateClientRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } }