From 9c25972ac4c410053fe1db0f9a2f652dcd121600 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 15 Jul 2026 14:49:37 +0800 Subject: [PATCH 1/8] update from max to average monthly throughput and fix calculation --- .../EndpointThroughputSummary.cs | 3 +- ...ughputCollector_ThroughputSummary_Tests.cs | 4 +-- .../ThroughputCollector.cs | 3 +- .../ThroughputDataExtensions.cs | 32 +++++++++++-------- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs b/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs index 7860ae8eec..f42271d3c1 100644 --- a/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs +++ b/src/Particular.LicensingComponent.Contracts/EndpointThroughputSummary.cs @@ -4,11 +4,12 @@ public class EndpointThroughputSummary { public string Name { get; set; } + public string NameHash { get; set; } public bool IsKnownEndpoint { get; set; } public string UserIndicator { get; set; } public long MaxDailyThroughput { get; set; } public MonthlyThroughput[] MonthlyThroughput { get; set; } - public long MaxMonthlyThroughput { get; set; } + public long AverageMonthlyThroughput { get; set; } } public class UpdateUserIndicator diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index 9f7f44b0a9..ec25af981f 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -171,8 +171,8 @@ await DataStore.CreateBuilder() using (Assert.EnterMultipleScope()) { - Assert.That(summary.First(w => w.Name == "Endpoint1").MaxMonthlyThroughput, Is.EqualTo(250), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); - Assert.That(summary.First(w => w.Name == "Endpoint2").MaxMonthlyThroughput, Is.EqualTo(165), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); + Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(250), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); + Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(165), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); } } diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 925cc203c0..71f4012eee 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -70,11 +70,12 @@ public async Task> GetThroughputSummary(Cancella var endpointSummary = new EndpointThroughputSummary { Name = endpointData.Name, + NameHash = OneWayHasher.CalculateOneWayHash(endpointData.Name), UserIndicator = endpointData.UserIndicator ?? (endpointData.IsKnownEndpoint ? Contracts.UserIndicator.NServiceBusEndpoint.ToString() : string.Empty), IsKnownEndpoint = endpointData.IsKnownEndpoint, MaxDailyThroughput = endpointData.ThroughputData.MaxDailyThroughput(), MonthlyThroughput = endpointData.ThroughputData.MonthlyThroughput(), - MaxMonthlyThroughput = endpointData.ThroughputData.MaxMonthlyThroughput() + AverageMonthlyThroughput = endpointData.ThroughputData.AverageMonthlyThroughput() }; endpointSummaries.Add(endpointSummary); diff --git a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs index 6d54edb6c9..ce19589692 100644 --- a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs +++ b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs @@ -25,23 +25,29 @@ public static long MaxDailyThroughput(this List throughputs) public static MonthlyThroughput[] MonthlyThroughput(this List throughputs) => [.. throughputs .SelectMany(data => data) - .GroupBy(kvp => $"{kvp.Key:yyyy-MM}") - .Select(group => new MonthlyThroughput(group.Key, group.Sum(kvp => kvp.Value)))]; - - public static long MaxMonthlyThroughput(this List throughputs) + // Older SQL Reports could return a negative value for daily throughput. These are not valid. See https://github.com/Particular/ServiceControl/pull/5404 + .Where(x => x.Value >= 0) + .GroupBy(x => x.Key, x => x.Value) + .ToLookup(x => x.Key, x => x.Max()) + .GroupBy(kvp => $"{kvp.Key:yyyy-MM}", x => x.Sum()) + .Select(group => new MonthlyThroughput(group.Key, group.Sum()))]; + + public static long AverageMonthlyThroughput(this List throughputs) { - var monthlySums = throughputs - .SelectMany(data => data) - .GroupBy(kvp => $"{kvp.Key.Year}-{kvp.Key.Month}") - .Select(group => group.Sum(kvp => kvp.Value)) - .ToArray(); - - if (monthlySums.Any()) + if (!throughputs.Any(x => x.Any())) { - return monthlySums.Max(); + return 0; } - return 0; + // keep this in sync with the internal licensing calculation + var maxDailyThroughput = throughputs + .SelectMany(x => x) + .Where(x => x.Value >= 0) + .GroupBy(x => x.Key, x => x.Value) + .ToLookup(x => x.Key, x => x.Max()) + .ToDictionary(x => x.Key, x => x.Sum()); + + return (long)Math.Truncate(maxDailyThroughput.Sum(x => x.Value) / (decimal)maxDailyThroughput.Count * 365 / 12); } public static bool HasDataFromSource(this IDictionary> throughputPerQueue, ThroughputSource source) => From 69360983ce8b5a397848d0837905ca4c1357bed2 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 15 Jul 2026 14:50:13 +0800 Subject: [PATCH 2/8] create endpoint for supplying endpoint details metadata --- .../Licensing/LicenseController.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 9dc7abe437..72ad6bbfaf 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Licensing { + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; @@ -44,6 +45,16 @@ public async Task> License(bool refresh, string client return licenseInfo; } + [Authorize(Policy = Permissions.ErrorLicensingView)] + [HttpGet] + [Route("license/details")] + public async Task> LicenseDetails() + { + var fileContents = await System.IO.File.ReadAllTextAsync(@"C:\Projects\ServicePulse\src\Frontend\src\views\throughputreport\licenseDetails\sample.json"); + var result = JsonSerializer.Deserialize(fileContents, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return result; + } + public class LicenseInfo { public bool TrialLicense { get; set; } @@ -68,5 +79,34 @@ public class LicenseInfo public string LicenseExtensionUrl { get; set; } } + + public class LicensedEndpointDetails + { + public LicensedEndpoint[] Endpoints { get; set; } + public QueueIdentity[] InfrastructureQueues { get; set; } + public QueueIdentity[] ExcludedQueues { get; set; } + public string ServiceEndDate { get; set; } + public Product[] Products { get; set; } + } + + public class Product + { + public string ProductCode { get; set; } + public int? MonthlyThroughput { get; set; } + } + + public class QueueIdentity + { + public string NameHash { get; set; } + public string Scope { get; set; } + } + + public class LicensedEndpoint + { + public string Name { get; set; } + public int Classification { get; set; } + public string EndpointSize { get; set; } + public QueueIdentity[] Queues { get; set; } + } } } \ No newline at end of file From a2e0d8347f7d138f2ed90bc907254d0230a867cb Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Thu, 16 Jul 2026 14:41:51 +0800 Subject: [PATCH 3/8] create endpoint for uploading metadata file and handle persistence of metadata --- .../LicensedEndpointDetails.cs | 31 +++++++ .../InMemoryLicensingDataStore.cs | 4 + .../ILicensingDataStore.cs | 3 + .../Throughput/LicensingDataStore.cs | 18 ++++ .../Licensing/LicenseController.cs | 88 +++++++------------ .../Licensing/LicenseControllerTypes.cs | 29 ++++++ 6 files changed, 115 insertions(+), 58 deletions(-) create mode 100644 src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs create mode 100644 src/ServiceControl/Licensing/LicenseControllerTypes.cs diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs new file mode 100644 index 0000000000..c435dc48b4 --- /dev/null +++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs @@ -0,0 +1,31 @@ +namespace Particular.LicensingComponent.Contracts +{ + public class LicensedEndpointDetails + { + public LicensedEndpoint[] Endpoints { get; set; } = []; + public QueueIdentity[] InfrastructureQueues { get; set; } = []; + public QueueIdentity[] ExcludedQueues { get; set; } = []; + public string? ServiceEndDate { get; set; } + public Product[] Products { get; set; } = []; + } + + public class Product + { + public string? ProductCode { get; set; } + public int? MonthlyThroughput { get; set; } + } + + public class QueueIdentity + { + public string? NameHash { get; set; } + public string? Scope { get; set; } + } + + public class LicensedEndpoint + { + public string? Name { get; set; } + public int? Classification { get; set; } + public string? EndpointSize { get; set; } + public QueueIdentity[] Queues { get; set; } = []; + } +} diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 816dd605b2..57399aaf37 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -170,6 +170,10 @@ public Task SaveReportMasks(List reportMasks, CancellationToken cancella return Task.CompletedTask; } + public Task GetLicensedEndpointDetails(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) => throw new NotImplementedException(); + class EndpointCollection : KeyedCollection { protected override EndpointIdentifier GetKeyForItem(Endpoint item) => item.Id; diff --git a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs index 9a1b340aa3..3a73a50891 100644 --- a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs @@ -36,4 +36,7 @@ Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSo Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken); Task> GetReportMasks(CancellationToken cancellationToken); Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken); + + Task GetLicensedEndpointDetails(CancellationToken cancellationToken); + Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 287ffb5a56..26b666cdcb 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -23,6 +23,7 @@ class LicensingDataStore( const string AuditServiceMetadataDocumentId = "AuditServiceMetadata"; const string BrokerMetadataDocumentId = "BrokerMetadata"; const string ReportMasksDocumentId = "ReportMasks"; + const string LicencedEndpointDetailsDocumentId = "LicensedEndpointDetails"; static readonly AuditServiceMetadata DefaultAuditServiceMetadata = new([], []); static readonly BrokerMetadata DefaultBrokerMetadata = new(null, []); @@ -316,4 +317,21 @@ public async Task SaveReportMasks(List reportMasks, CancellationToken ca await session.StoreAsync(new ReportConfigurationDocument { MaskedStrings = reportMasks }, ReportMasksDocumentId, cancellationToken); await session.SaveChangesAsync(cancellationToken); } + + public async Task GetLicensedEndpointDetails(CancellationToken cancellationToken) + { + var store = await storeProvider.GetDocumentStore(cancellationToken); + using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); + + return await session.LoadAsync(LicencedEndpointDetailsDocumentId, cancellationToken); + } + + public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) + { + var store = await storeProvider.GetDocumentStore(cancellationToken); + using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); + + await session.StoreAsync(result, LicencedEndpointDetailsDocumentId, 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 72ad6bbfaf..4c147ccf5b 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -1,19 +1,26 @@ -namespace ServiceControl.Licensing +#nullable enable +namespace ServiceControl.Licensing { + using System; + using System.IO; + using System.IO.Compression; + using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Infrastructure.Auth; using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Monitoring.HeartbeatMonitoring; + using Particular.LicensingComponent.Contracts; + using Particular.LicensingComponent.Persistence; using Particular.ServiceControl.Licensing; using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.LicenseManagement; [ApiController] [Route("api")] - public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : ControllerBase + public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus, ILicensingDataStore dataStore) : ControllerBase { [Authorize(Policy = Permissions.ErrorLicensingView)] [HttpGet] @@ -48,65 +55,30 @@ public async Task> License(bool refresh, string client [Authorize(Policy = Permissions.ErrorLicensingView)] [HttpGet] [Route("license/details")] - public async Task> LicenseDetails() + public async Task> LicenseDetails(CancellationToken cancellationToken) { - var fileContents = await System.IO.File.ReadAllTextAsync(@"C:\Projects\ServicePulse\src\Frontend\src\views\throughputreport\licenseDetails\sample.json"); - var result = JsonSerializer.Deserialize(fileContents, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - return result; + return await dataStore.GetLicensedEndpointDetails(cancellationToken); } - public class LicenseInfo + [Authorize(Policy = Permissions.ErrorLicensingManage)] + [HttpPost] + [Route("license/detailsUpload")] + public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationToken cancellationToken) { - public bool TrialLicense { get; set; } - - public string Edition { get; set; } - - public string RegisteredTo { get; set; } - - public string UpgradeProtectionExpiration { get; set; } - - public string ExpirationDate { get; set; } - - public string Status { get; set; } - - public LicensedProduct[] Products { get; set; } - - public string LicenseType { get; set; } - - public string InstanceName { get; set; } - - public string LicenseStatus { get; set; } - - public string LicenseExtensionUrl { get; set; } - } - - public class LicensedEndpointDetails - { - public LicensedEndpoint[] Endpoints { get; set; } - public QueueIdentity[] InfrastructureQueues { get; set; } - public QueueIdentity[] ExcludedQueues { get; set; } - public string ServiceEndDate { get; set; } - public Product[] Products { get; set; } - } - - public class Product - { - public string ProductCode { get; set; } - public int? MonthlyThroughput { get; set; } - } - - public class QueueIdentity - { - public string NameHash { get; set; } - public string Scope { get; set; } - } - - public class LicensedEndpoint - { - public string Name { get; set; } - public int Classification { get; set; } - public string EndpointSize { get; set; } - public QueueIdentity[] Queues { get; set; } + //perform date and license id checks + using var fileStream = file.OpenReadStream(); + using var fileStreamReader = new StreamReader(fileStream); + var compressed = await fileStreamReader.ReadToEndAsync(cancellationToken); + var compressedData = Convert.FromBase64String(compressed); + using var memoryStream = new MemoryStream(compressedData); + using var brotliStream = new BrotliStream(memoryStream, CompressionMode.Decompress); + using var reader = new StreamReader(brotliStream, Encoding.UTF8); + + var fileContents = reader.ReadToEnd(); + var result = JsonSerializer.Deserialize(fileContents, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? throw new InvalidDataException("File contents cannot be deserialized"); + //persist + await dataStore.SaveLicensedEndpointDetails(result, cancellationToken); } } } \ No newline at end of file diff --git a/src/ServiceControl/Licensing/LicenseControllerTypes.cs b/src/ServiceControl/Licensing/LicenseControllerTypes.cs new file mode 100644 index 0000000000..5a4a7f9d79 --- /dev/null +++ b/src/ServiceControl/Licensing/LicenseControllerTypes.cs @@ -0,0 +1,29 @@ +namespace ServiceControl.Licensing +{ + using ServiceControl.LicenseManagement; + + public class LicenseInfo + { + public bool TrialLicense { get; set; } + + public string Edition { get; set; } + + public string RegisteredTo { get; set; } + + public string UpgradeProtectionExpiration { get; set; } + + public string ExpirationDate { get; set; } + + public string Status { get; set; } + + public LicensedProduct[] Products { get; set; } + + public string LicenseType { get; set; } + + public string InstanceName { get; set; } + + public string LicenseStatus { get; set; } + + public string LicenseExtensionUrl { get; set; } + } +} From cf51d11bf1c637bb58a17b6d428754fa63efb141 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Fri, 17 Jul 2026 15:09:25 +0800 Subject: [PATCH 4/8] validate endpoint metadata id and licenseid match --- .../LicensedEndpointDetails.cs | 2 ++ src/ServiceControl.LicenseManagement/LicenseDetails.cs | 2 ++ src/ServiceControl/Licensing/LicenseController.cs | 9 ++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs index c435dc48b4..7d518fc60e 100644 --- a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs +++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs @@ -2,11 +2,13 @@ { public class LicensedEndpointDetails { + public string? LicenseId { get; set; } public LicensedEndpoint[] Endpoints { get; set; } = []; public QueueIdentity[] InfrastructureQueues { get; set; } = []; public QueueIdentity[] ExcludedQueues { get; set; } = []; public string? ServiceEndDate { get; set; } public Product[] Products { get; set; } = []; + public bool ValidId { get; set; } } public class Product diff --git a/src/ServiceControl.LicenseManagement/LicenseDetails.cs b/src/ServiceControl.LicenseManagement/LicenseDetails.cs index eab2cf5015..e3899a9295 100644 --- a/src/ServiceControl.LicenseManagement/LicenseDetails.cs +++ b/src/ServiceControl.LicenseManagement/LicenseDetails.cs @@ -7,6 +7,7 @@ public class LicenseDetails { + public string Id { get; private init; } public DateTime? ExpirationDate { get; private init; } public DateTime? UpgradeProtectionExpiration { get; private init; } @@ -56,6 +57,7 @@ internal static LicenseDetails FromLicense(License license) var details = new LicenseDetails { + Id = license.LicenseId, UpgradeProtectionExpiration = license.UpgradeProtectionExpiration, //If expiration date is greater that 50 years treat is as no expiration date ExpirationDate = license.ExpirationDate.HasValue diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 4c147ccf5b..561ad09178 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -57,7 +57,14 @@ public async Task> License(bool refresh, string client [Route("license/details")] public async Task> LicenseDetails(CancellationToken cancellationToken) { - return await dataStore.GetLicensedEndpointDetails(cancellationToken); + var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken); + if (licenseDetails is null) + { + return (LicensedEndpointDetails?)null; + } + + licenseDetails.ValidId = licenseDetails.LicenseId == activeLicense.Details.Id; + return licenseDetails; } [Authorize(Policy = Permissions.ErrorLicensingManage)] From d8ff4606e49c921df969ac880f46a6138c6b9bc0 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Tue, 21 Jul 2026 14:22:54 +0800 Subject: [PATCH 5/8] implement in-memory version and placeholder for EF version --- .../InMemoryLicensingDataStore.cs | 9 +++++++-- .../Implementation/LicensingDataStore.cs | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 57399aaf37..46639e6305 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -15,6 +15,7 @@ public class InMemoryLicensingDataStore : ILicensingDataStore BrokerMetadata brokerMetadata = new(null, []); AuditServiceMetadata auditServiceMetadata = new([], []); List reportMasks = []; + LicensedEndpointDetails? endpointDetails = null; public Task> GetAllEndpoints(bool includePlatformEndpoints, CancellationToken cancellationToken) { @@ -170,9 +171,13 @@ public Task SaveReportMasks(List reportMasks, CancellationToken cancella return Task.CompletedTask; } - public Task GetLicensedEndpointDetails(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetLicensedEndpointDetails(CancellationToken cancellationToken) => Task.FromResult(endpointDetails); - public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) + { + endpointDetails = result; + return Task.CompletedTask; + } class EndpointCollection : KeyedCollection { diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index 03a0c1e6fe..acf8fe9102 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -9,6 +9,7 @@ class LicensingDataStore : ILicensingDataStore public Task GetEndpoint(EndpointIdentifier id, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> GetEndpoints(IList endpointIds, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task>> GetEndpointThroughputByQueueName(IList queueNames, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task GetLicensedEndpointDetails(CancellationToken cancellationToken) => throw new NotImplementedException(); public Task> GetReportMasks(CancellationToken cancellationToken) => throw new NotImplementedException(); public Task IsThereThroughputForLastXDays(int days, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task IsThereThroughputForLastXDaysForSource(int days, ThroughputSource throughputSource, bool includeToday, CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -16,6 +17,7 @@ class LicensingDataStore : ILicensingDataStore public Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveBrokerMetadata(BrokerMetadata brokerMetadata, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveEndpoint(Particular.LicensingComponent.Contracts.Endpoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task UpdateUserIndicatorOnEndpoints(List userIndicatorUpdates, CancellationToken cancellationToken) => throw new NotImplementedException(); } \ No newline at end of file From 2c061ba8d6c73fba3c47be16c6a36ab46877945f Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 22 Jul 2026 10:29:18 +0800 Subject: [PATCH 6/8] update api approval file --- .../ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index 5a0176d2f5..8dbb69897d 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -42,6 +42,8 @@ GET /eventlogitems => ServiceControl.EventLog.EventLogApiController:Items(Paging GET /heartbeats/stats => ServiceControl.Monitoring.EndpointsMonitoringController:HeartbeatStats() GET /instance-info => ServiceControl.Infrastructure.WebApi.RootController:Config() GET /license => ServiceControl.Licensing.LicenseController:License(Boolean refresh, String clientName, CancellationToken cancellationToken) +GET /license/details => ServiceControl.Licensing.LicenseController:LicenseDetails(CancellationToken cancellationToken) +POST /license/detailsUpload => ServiceControl.Licensing.LicenseController:UploadLicenseDetails(IFormFile file, CancellationToken cancellationToken) GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages) GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId) GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q) From d17b304f720212a6a8eaaae7e69db722a7d00f57 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 22 Jul 2026 11:03:11 +0800 Subject: [PATCH 7/8] fix average monthly throughput test --- .../ThroughputCollector_ThroughputSummary_Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index ec25af981f..71b7819a01 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -171,8 +171,8 @@ await DataStore.CreateBuilder() using (Assert.EnterMultipleScope()) { - Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(250), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); - Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(165), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); + Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(2694), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); + Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(2585), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); } } From db1e4a00bdd1941994da2fd59fd868bd08fdd2f2 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Wed, 22 Jul 2026 11:03:42 +0800 Subject: [PATCH 8/8] test error text --- .../ThroughputCollector_ThroughputSummary_Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index 71b7819a01..b3007fe41f 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -171,8 +171,8 @@ await DataStore.CreateBuilder() using (Assert.EnterMultipleScope()) { - Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(2694), $"Incorrect MaxDailyThroughput recorded for Endpoint1"); - Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(2585), $"Incorrect MaxDailyThroughput recorded for Endpoint2"); + Assert.That(summary.First(w => w.Name == "Endpoint1").AverageMonthlyThroughput, Is.EqualTo(2694), $"Incorrect AverageMonthlyThroughput recorded for Endpoint1"); + Assert.That(summary.First(w => w.Name == "Endpoint2").AverageMonthlyThroughput, Is.EqualTo(2585), $"Incorrect AverageMonthlyThroughput recorded for Endpoint2"); } }