From 3083e9b001c238f4c17ea099131dc95ea6831ee5 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Tue, 20 Oct 2026 11:31:43 +0800 Subject: [PATCH 1/5] write license amendments to the usage report --- src/Directory.Packages.props | 2 +- .../LicensedEndpointDetails.cs | 14 ++++----- .../InMemoryLicensingDataStore.cs | 6 +++- .../ILicensingDataStore.cs | 3 +- .../MonitoringService_Tests.cs | 3 +- .../ThroughputCollector.cs | 30 ++++++++++++++++++- .../Implementation/LicensingDataStore.cs | 13 ++++---- .../Throughput/LicensingDataStore.cs | 18 +++++++---- .../Throughput/LicenseMetadataTests.cs | 18 +++++++---- .../Licensing/LicenseController.cs | 2 +- 10 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d5cab607dd..fdffbbd225 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -71,7 +71,7 @@ - + diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs index 7ecd1b3490..11af5e4ee3 100644 --- a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs +++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs @@ -31,16 +31,16 @@ public class Product public record QueueIdentity { - public string? NameHash { get; set; } + public required string NameHash { get; set; } public string? ScopeHash { get; set; } public string? OriginalLocation { get; set; } } public record LicensedEndpoint { - public string? ClientId { get; set; } - public string? Name { get; set; } - public int? Classification { get; set; } + public required string ClientId { get; set; } + public required string Name { get; set; } + public required int Classification { get; set; } public string? EndpointSize { get; set; } public QueueIdentity[] Queues { get; set; } = []; @@ -48,9 +48,9 @@ public record LicensedEndpoint public class LicensedEndpointAmendment { - public string? ClientId { get; set; } - public string? Name { get; set; } - public int? Classification { get; set; } + public required string ClientId { get; set; } + public required string Name { get; set; } + public required int Classification { get; set; } public QueueIdentity[] AmendedQueues { get; set; } = []; } } diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 8ed4679bbe..7f56b96780 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -176,7 +176,8 @@ public Task SaveReportMasks(List reportMasks, CancellationToken cancella return Task.CompletedTask; } - public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) => Task.FromResult(endpointDetails?.Amend(endpointDetailsAmendment)); + public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) => + Task.FromResult(amendWithUserChanges ? endpointDetails?.Amend(endpointDetailsAmendment) : endpointDetails); public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) { @@ -185,6 +186,9 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella return Task.CompletedTask; } + public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => + Task.FromResult(endpointDetailsAmendment); + public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) { result.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues]; diff --git a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs index dab80b5fc4..d254277f54 100644 --- a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs @@ -39,7 +39,8 @@ Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSo Task> GetReportMasks(CancellationToken cancellationToken = default); Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default); - Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default); + Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default); Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default); + Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default); Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs index 1b4104f5be..4233e405c3 100644 --- a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs @@ -258,12 +258,13 @@ public Task> GetReportMasks(CancellationToken cancellationToken = d public Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default) => throw new NotSupportedException(); - public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) => + public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 945640ea10..733a7d4cff 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -158,6 +158,8 @@ public async Task GenerateThroughputReport(string spVersion, DateT var auditServiceMetadata = await dataStore.GetAuditServiceMetadata(cancellationToken); var brokerMetaData = await dataStore.GetBrokerMetadata(cancellationToken); + var endpointMetadata = await dataStore.GetLicensedEndpointDetails(false, cancellationToken); + var endpointMetadataAmendments = await dataStore.GetLicensedEndpointDetailsAmendments(cancellationToken); var report = new Report.Report { EndTime = new DateTimeOffset((DateTime)reportEndDate, TimeSpan.Zero), @@ -172,7 +174,33 @@ public async Task GenerateThroughputReport(string spVersion, DateT Queues = [.. queueThroughputs], TotalQueues = queueThroughputs.Count, // A copy, because the report adds its own keys. The data store can return a shared instance. - EnvironmentInformation = new EnvironmentInformation { AuditServicesData = new AuditServicesData(auditServiceMetadata.Versions, auditServiceMetadata.Transports), EnvironmentData = new Dictionary(brokerMetaData.Data) } + EnvironmentInformation = new EnvironmentInformation { AuditServicesData = new AuditServicesData(auditServiceMetadata.Versions, auditServiceMetadata.Transports), EnvironmentData = new Dictionary(brokerMetaData.Data) }, + CustomerLicenseAmendments = (endpointMetadata, endpointMetadataAmendments) switch + { + (null, null) => null, + (_, null) => null, + (null, _) => null, + (LicensedEndpointDetails baseDetails, LicensedEndpointDetailsAmendment amendments) => new LicenseAmendments + { + ExcludedQueues = [.. amendments.ExcludedQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })], + InfrastructureQueues = [.. amendments.InfrastructureQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })], + RemovedEndpointNames = [.. baseDetails.Endpoints.Where(endpoint => endpointMetadataAmendments.RemovedEndpointIds.Contains(endpoint.ClientId)).Select(endpoint => endpoint.Name)], + Endpoints = [.. amendments.Endpoints.Select(amended => { + var previousEndpoint = baseDetails.Endpoints.FirstOrDefault(endpoint => endpoint.ClientId == amended.ClientId); + Contracts.QueueIdentity[] notThisEndpointQueues = [.. amendments.UnassignedQueues, .. amendments.ExcludedQueues, .. amendments.InfrastructureQueues, .. amendments.Endpoints.Where(endpoint => endpoint.ClientId != amended.ClientId).SelectMany(endpoint => endpoint.AmendedQueues)]; + return new Report.LicensedEndpointAmendment + { + PreviousName = previousEndpoint?.Name, + PreviousNameHash = previousEndpoint is not null ? OneWayHasher.CalculateOneWayHash(previousEndpoint.Name) : null, + CurrentName = masker.Mask(amended.Name), + CurrentNameHash = OneWayHasher.CalculateOneWayHash(amended.Name), + IsSendOnly = amended.Classification == 1, + AmendedQueues = [.. amended.AmendedQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })], + RemovedQueues = previousEndpoint is not null ? [.. previousEndpoint.Queues.Where(qId => notThisEndpointQueues.Contains(qId)).Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })] : [] + }; + })] + }, + } }; var auditThroughput = queueThroughputs.SelectMany(w => w.DailyThroughputFromAudit); diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index f6fd359362..55e2c966ec 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -308,16 +308,16 @@ public Task> GetReportMasks(CancellationToken cancellationToken = d public Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default) => ExecuteWithDbContext((context, token) => context.StoreSetting(SettingKeys.ReportMasks, reportMasks, token), cancellationToken); - public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) => + public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) => ExecuteWithDbContext(async (context, token) => { var baseDetails = await context.GetSetting(SettingKeys.LicensedEndpointDetails, token); - if (baseDetails is null) + if (baseDetails is not null && amendWithUserChanges) { - return null; + var amendments = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token); + return baseDetails.Amend(amendments); } - var amendments = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token); - return baseDetails.Amend(amendments); + return baseDetails; }, cancellationToken); public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) => @@ -327,6 +327,9 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella await context.StoreSetting(SettingKeys.LicensedEndpointDetailsAmendment, new LicensedEndpointDetailsAmendment(), token); }, cancellationToken); + public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => + ExecuteWithDbContext((context, token) => context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token), cancellationToken); + public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) => ExecuteWithDbContext(async (context, token) => { diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index dfecbbe661..ed5d6ca38e 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -338,18 +338,18 @@ public async Task SaveReportMasks(List reportMasks, CancellationToken ca await session.SaveChangesAsync(cancellationToken); } - public async Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) + public async Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) { var store = await storeProvider.GetDocumentStore(cancellationToken); using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); var baseDetails = await session.LoadAsync(LicencedEndpointDetailsDocumentId, cancellationToken); - if (baseDetails is null) + if (baseDetails is not null && amendWithUserChanges) { - return null; + var amendments = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); + return baseDetails.Amend(amendments); } - var amendments = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); - return baseDetails.Amend(amendments); + return baseDetails; } public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) @@ -362,6 +362,14 @@ public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Ca await session.SaveChangesAsync(cancellationToken); } + public async Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) + { + var store = await storeProvider.GetDocumentStore(cancellationToken); + using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); + + return await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); + } + public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) { var store = await storeProvider.GetDocumentStore(cancellationToken); diff --git a/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs index 756e66c250..2a4d5ee859 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs @@ -92,12 +92,14 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails new LicensedEndpoint { ClientId = "endpoint-to-keep", - Name = "Endpoint to keep" + Name = "Endpoint to keep", + Classification = 0 }, new LicensedEndpoint { ClientId = "endpoint-to-remove", - Name = "Endpoint to remove" + Name = "Endpoint to remove", + Classification = 0 } ] }); @@ -130,12 +132,14 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails new LicensedEndpoint { ClientId = "first-endpoint", - Name = "First endpoint" + Name = "First endpoint", + Classification = 0 }, new LicensedEndpoint { ClientId = "second-endpoint", - Name = "Second endpoint" + Name = "Second endpoint", + Classification = 0 } ] }); @@ -170,7 +174,8 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails new LicensedEndpoint { ClientId = "endpoint-id", - Name = "Original name" + Name = "Original name", + Classification = 0 } ] }); @@ -188,7 +193,8 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails new LicensedEndpoint { ClientId = "endpoint-id", - Name = "Replacement name" + Name = "Replacement name", + Classification = 0 } ] }); diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index fd2ff4b142..438f353eb2 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -62,7 +62,7 @@ public async Task> License(bool refresh, string client return (LicensedEndpointDetails?)null; } - var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken); + var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken: cancellationToken); if (licenseDetails is null) { return (LicensedEndpointDetails?)null; From e996d876cddabd96d518f767da7db9dfc33b95f4 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 21 Oct 2026 08:03:55 +0800 Subject: [PATCH 2/5] enable removal of license detail amendments --- .../InMemoryLicensingDataStore.cs | 4 ++-- .../ILicensingDataStore.cs | 2 +- .../Implementation/LicensingDataStore.cs | 8 ++++++- .../Throughput/LicensingDataStore.cs | 22 +++++++++++++------ .../Licensing/LicenseController.cs | 6 +++++ 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 7f56b96780..0cb9a921c9 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -189,9 +189,9 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => Task.FromResult(endpointDetailsAmendment); - public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) + public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) { - result.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues]; + result?.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues]; endpointDetailsAmendment = result; return Task.CompletedTask; } diff --git a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs index d254277f54..36b73de7c2 100644 --- a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs @@ -42,5 +42,5 @@ Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSo Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default); Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default); Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default); - Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default); + Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index 55e2c966ec..a5c2f0801f 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -330,12 +330,18 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => ExecuteWithDbContext((context, token) => context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token), cancellationToken); - public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) => + public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) => ExecuteWithDbContext(async (context, token) => { var existing = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token); if (existing is not null) { + if (result is null) + { + context.Remove(existing); + await context.SaveChangesAsync(cancellationToken); + return; + } // these do not round-trip result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; } diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index ed5d6ca38e..9b547a1062 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -370,20 +370,28 @@ public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Ca return await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); } - public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) + public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) { var store = await storeProvider.GetDocumentStore(cancellationToken); using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); - var existing = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); - if (existing is not null) + if (result is null) { - // these do not round-trip - result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; - session.Advanced.Evict(existing); + session.Delete(LicencedEndpointDetailsAmendmentDocumentId); + } + else + { + var existing = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); + if (existing is not null) + { + // these do not round-trip + result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; + session.Advanced.Evict(existing); + } + + await session.StoreAsync(result, LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); } - await session.StoreAsync(result, LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); await session.SaveChangesAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 438f353eb2..a9c8b90d66 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -108,5 +108,11 @@ public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationTo [Route("license/amendDetails")] public async Task AmendLicenseDetails([FromBody] LicensedEndpointDetailsAmendment amendment, CancellationToken cancellationToken = default) => await dataStore.SaveLicensedEndpointDetailsAmendment(amendment, cancellationToken); + + [Authorize(Policy = Permissions.ErrorThroughputManage)] + [HttpPost] + [Route("license/removeAmendments")] + public async Task RemoveLicenseDetailsAmendments(CancellationToken cancellationToken = default) => + await dataStore.SaveLicensedEndpointDetailsAmendment(null, cancellationToken); } } \ No newline at end of file From 5b6975d522a433b949d405b58da5be5a1801ccbb Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 21 Oct 2026 12:24:35 +0800 Subject: [PATCH 3/5] update package --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fdffbbd225..1817277376 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -71,7 +71,7 @@ - + From eccaea00797f19b6f4ccf3426ec5e3aebdac8aa8 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 21 Oct 2026 12:24:50 +0800 Subject: [PATCH 4/5] fix deserialisation issue --- .../LicensedEndpointDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs index 11af5e4ee3..d61bd4abcf 100644 --- a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs +++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs @@ -38,7 +38,7 @@ public record QueueIdentity public record LicensedEndpoint { - public required string ClientId { get; set; } + public string ClientId { get; set; } = Guid.NewGuid().ToString(); public required string Name { get; set; } public required int Classification { get; set; } public string? EndpointSize { get; set; } From 9c3dc4a49665deadceb94ec3414ed694e4ef8050 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 21 Oct 2026 12:32:09 +0800 Subject: [PATCH 5/5] update apiroute approval test --- .../ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index d9f20d7486..edb8c3ec55 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -44,6 +44,7 @@ GET /license => ServiceControl.Licensing.LicenseController:License(Boolean refre POST /license/amendDetails => ServiceControl.Licensing.LicenseController:AmendLicenseDetails(LicensedEndpointDetailsAmendment amendment, CancellationToken cancellationToken) GET /license/details => ServiceControl.Licensing.LicenseController:LicenseDetails(CancellationToken cancellationToken) POST /license/detailsUpload => ServiceControl.Licensing.LicenseController:UploadLicenseDetails(IFormFile file, CancellationToken cancellationToken) +POST /license/removeAmendments => ServiceControl.Licensing.LicenseController:RemoveLicenseDetailsAmendments(CancellationToken cancellationToken) GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, CancellationToken cancellationToken) GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId, CancellationToken cancellationToken) GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q, CancellationToken cancellationToken)