diff --git a/src/ProjectOrigin.WalletSystem.IntegrationTests/ApiTests/ApiTests.open_api_specification_not_changed.verified.txt b/src/ProjectOrigin.WalletSystem.IntegrationTests/ApiTests/ApiTests.open_api_specification_not_changed.verified.txt index 969fbbcb..7dde4053 100644 --- a/src/ProjectOrigin.WalletSystem.IntegrationTests/ApiTests/ApiTests.open_api_specification_not_changed.verified.txt +++ b/src/ProjectOrigin.WalletSystem.IntegrationTests/ApiTests/ApiTests.open_api_specification_not_changed.verified.txt @@ -13,7 +13,7 @@ "summary": "Gets all certificates in the wallet that are available for use.", "parameters": [ { - "name": "start", + "name": "Start", "in": "query", "description": "The start of the time range in Unix time in seconds.", "schema": { @@ -22,7 +22,7 @@ } }, { - "name": "end", + "name": "End", "in": "query", "description": "The end of the time range in Unix time in seconds.", "schema": { @@ -31,7 +31,7 @@ } }, { - "name": "type", + "name": "Type", "in": "query", "description": "Filter the type of certificates to return.", "schema": { @@ -39,7 +39,7 @@ } }, { - "name": "limit", + "name": "Limit", "in": "query", "description": "The number of items to return.", "schema": { @@ -48,7 +48,7 @@ } }, { - "name": "skip", + "name": "Skip", "in": "query", "description": "The number of items to skip.", "schema": { @@ -83,15 +83,15 @@ "summary": "Returns aggregates certificates that are available to use, based on the specified time zone and time range.", "parameters": [ { - "name": "timeAggregate", + "name": "TimeAggregate", "in": "query", - "description": "The size of each bucket in the aggregation", + "description": "The size of each bucket in the aggregation.", "schema": { "$ref": "#/components/schemas/TimeAggregate" } }, { - "name": "timeZone", + "name": "TimeZone", "in": "query", "description": "The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones.", "schema": { @@ -99,7 +99,7 @@ } }, { - "name": "start", + "name": "Start", "in": "query", "description": "The start of the time range in Unix time in seconds.", "schema": { @@ -108,7 +108,7 @@ } }, { - "name": "end", + "name": "End", "in": "query", "description": "The end of the time range in Unix time in seconds.", "schema": { @@ -117,7 +117,7 @@ } }, { - "name": "type", + "name": "Type", "in": "query", "description": "Filter the type of certificates to return.", "schema": { @@ -125,7 +125,7 @@ } }, { - "name": "limit", + "name": "Limit", "in": "query", "description": "The number of items to return.", "schema": { @@ -134,7 +134,7 @@ } }, { - "name": "skip", + "name": "Skip", "in": "query", "description": "The number of items to skip.", "schema": { @@ -172,7 +172,7 @@ "summary": "Gets all claims in the wallet", "parameters": [ { - "name": "start", + "name": "Start", "in": "query", "description": "The start of the time range in Unix time in seconds.", "schema": { @@ -181,7 +181,7 @@ } }, { - "name": "end", + "name": "End", "in": "query", "description": "The end of the time range in Unix time in seconds.", "schema": { @@ -190,7 +190,7 @@ } }, { - "name": "limit", + "name": "Limit", "in": "query", "description": "The number of items to return.", "schema": { @@ -199,7 +199,7 @@ } }, { - "name": "skip", + "name": "Skip", "in": "query", "description": "The number of items to skip.", "schema": { @@ -275,7 +275,7 @@ "summary": "Returns a list of aggregates claims for the authenticated user based on the specified time zone and time range.", "parameters": [ { - "name": "timeAggregate", + "name": "TimeAggregate", "in": "query", "description": "The size of each bucket in the aggregation", "schema": { @@ -283,7 +283,7 @@ } }, { - "name": "timeZone", + "name": "TimeZone", "in": "query", "description": "The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones.", "schema": { @@ -291,7 +291,7 @@ } }, { - "name": "start", + "name": "Start", "in": "query", "description": "The start of the time range in Unix time in seconds.", "schema": { @@ -300,7 +300,7 @@ } }, { - "name": "end", + "name": "End", "in": "query", "description": "The end of the time range in Unix time in seconds.", "schema": { @@ -309,7 +309,7 @@ } }, { - "name": "limit", + "name": "Limit", "in": "query", "description": "The number of items to return.", "schema": { @@ -318,7 +318,7 @@ } }, { - "name": "skip", + "name": "Skip", "in": "query", "description": "The number of items to skip.", "schema": { diff --git a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/CertificatesControllerTests.cs b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/CertificatesControllerTests.cs index 06baa14c..356688c4 100644 --- a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/CertificatesControllerTests.cs +++ b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/CertificatesControllerTests.cs @@ -34,12 +34,7 @@ public async Task Verify_Unauthorized() var controller = new CertificatesController(); // Act - var result = await controller.GetCertificates( - _unitOfWork, - null, - null, - null, - null); + var result = await controller.GetCertificates(_unitOfWork, new GetCertificatesQueryParameters()); // Assert result.Result.Should().BeOfType(); @@ -88,12 +83,14 @@ public async Task Test_AggregateCertificates(string timezone, long[] values, Cer // Act var result = await controller.AggregateCertificates( _unitOfWork, - TimeAggregate.Day, - timezone, - queryStartDate.ToUnixTimeSeconds(), - queryEndDate.ToUnixTimeSeconds(), - type, - null); + new AggregateCertificatesQueryParameters + { + TimeAggregate = TimeAggregate.Day, + TimeZone = timezone, + Start = queryStartDate.ToUnixTimeSeconds(), + End = queryEndDate.ToUnixTimeSeconds(), + Type = type + }); // Assert result.Value.Should().NotBeNull(); @@ -116,12 +113,11 @@ public async Task AggregateCertificates_Invalid_TimeZone() // Act var result = await controller.AggregateCertificates( _unitOfWork, - TimeAggregate.Day, - "invalid-time-zone", - null, - null, - null, - null); + new AggregateCertificatesQueryParameters + { + TimeAggregate = TimeAggregate.Day, + TimeZone = "invalid-time-zone", + }); // Assert result.Result.Should().BeOfType(); diff --git a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/ClaimsControllerTests.cs b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/ClaimsControllerTests.cs index 28edff29..68a7a7a9 100644 --- a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/ClaimsControllerTests.cs +++ b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/ClaimsControllerTests.cs @@ -40,9 +40,7 @@ public async Task Verify_Unauthorized() // Act var result = await controller.GetClaims( _unitOfWork, - null, - null, - null); + new GetClaimsQueryParameters()); // Assert result.Result.Should().BeOfType(); @@ -93,11 +91,13 @@ public async Task Test_AggregateClaims(string timezone, long[] values) // Act var result = await controller.AggregateClaims( _unitOfWork, - TimeAggregate.Day, - timezone, - queryStartDate.ToUnixTimeSeconds(), - queryEndDate.ToUnixTimeSeconds(), - null); + new AggregateClaimsQueryParameters + { + TimeAggregate = TimeAggregate.Day, + TimeZone = timezone, + Start = queryStartDate.ToUnixTimeSeconds(), + End = queryEndDate.ToUnixTimeSeconds() + }); // Assert result.Value.Should().NotBeNull(); @@ -120,11 +120,11 @@ public async Task AggregateClaims_Invalid_TimeZone() // Act var result = await controller.AggregateClaims( _unitOfWork, - TimeAggregate.Day, - "invalid-time-zone", - null, - null, - null); + new AggregateClaimsQueryParameters + { + TimeAggregate = TimeAggregate.Day, + TimeZone = "invalid-time-zone" + }); // Assert result.Result.Should().BeOfType(); diff --git a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/WalletControllerTests.cs b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/WalletControllerTests.cs index 918b7a13..9d4345e6 100644 --- a/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/WalletControllerTests.cs +++ b/src/ProjectOrigin.WalletSystem.IntegrationTests/REST/WalletControllerTests.cs @@ -139,7 +139,7 @@ public async Task Verify_Valid() } [Fact] - public async Task Verify_OnlySingleAllowedPerSubject() + public async Task Verify_MultipleWalletsAllowedPerSubject() { // Arrange var subject = _fixture.Create(); @@ -154,7 +154,8 @@ public async Task Verify_OnlySingleAllowedPerSubject() _options, new CreateWalletRequest()); - firstCreateResult.Result.Should().BeOfType(); + var firstResponse = firstCreateResult.Result.Should().BeOfType() + .Which.Value.Should().BeOfType().Which; // Act var secondCreateResult = await controller.CreateWallet( @@ -164,8 +165,9 @@ public async Task Verify_OnlySingleAllowedPerSubject() new CreateWalletRequest()); // Assert - secondCreateResult.Result.Should().BeOfType() - .Which.Value.Should().Be("Wallet already exists."); + var secondResponse = secondCreateResult.Result.Should().BeOfType() + .Which.Value.Should().BeOfType().Which; + firstResponse.WalletId.Should().NotBe(secondResponse.WalletId); } [Fact] diff --git a/src/ProjectOrigin.WalletSystem.IntegrationTests/Repositories/CertificateRepositoryTests.cs b/src/ProjectOrigin.WalletSystem.IntegrationTests/Repositories/CertificateRepositoryTests.cs index 275d4f06..b7f18cdc 100644 --- a/src/ProjectOrigin.WalletSystem.IntegrationTests/Repositories/CertificateRepositoryTests.cs +++ b/src/ProjectOrigin.WalletSystem.IntegrationTests/Repositories/CertificateRepositoryTests.cs @@ -417,22 +417,8 @@ public async Task GetAvailableSlice() { var registry = _fixture.Create(); var wallet1 = await CreateWallet(_fixture.Create()); - var endpointPosition = 1; - var endpoint = await CreateWalletEndpoint(wallet1); - var certificate = await CreateCertificate(registry); - var slice = new WalletSlice - { - Id = Guid.NewGuid(), - WalletEndpointId = endpoint.Id, - WalletEndpointPosition = endpointPosition, - RegistryName = registry, - CertificateId = certificate.Id, - Quantity = _fixture.Create(), - RandomR = _fixture.Create(), - State = WalletSliceState.Available - }; - await _certRepository.InsertWalletSlice(slice); + var slice = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet1), 1); var sliceDb = await _certRepository.GetOwnersAvailableSlices(registry, slice.CertificateId, wallet1.Owner); @@ -440,6 +426,25 @@ public async Task GetAvailableSlice() sliceDb.Should().ContainEquivalentOf(slice); } + [Fact] + public async Task QueryAvailableCertificates_MultipleWallets() + { + var registry = _fixture.Create(); + var subject = _fixture.Create(); + var wallet1 = await CreateWallet(subject); + var wallet2 = await CreateWallet(subject); + var slice1 = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet1), 1); + var slice2 = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet2), 1); + + var result = await _certRepository.QueryAvailableCertificates(new CertificatesFilter + { + Owner = subject + }); + + result.Count.Should().Be(2); + result.Items.Sum(x => x.Quantity).Should().Be(slice1.Quantity + slice2.Quantity); + } + [Fact] public async Task GetAvailableSlice_WhenSliceStateOtherThanAvailable_ExpectNull() { @@ -513,25 +518,12 @@ public async Task ReserveSlice() { // Arrange var registry = _fixture.Create(); - var certificate = await CreateCertificate(registry); var owner = _fixture.Create(); var wallet = await CreateWallet(owner); - var endpoint = await CreateWalletEndpoint(wallet); - var slice = new WalletSlice - { - Id = Guid.NewGuid(), - WalletEndpointId = endpoint.Id, - WalletEndpointPosition = 1, - RegistryName = registry, - CertificateId = certificate.Id, - Quantity = 150, - RandomR = _fixture.Create(), - State = WalletSliceState.Available - }; - await _certRepository.InsertWalletSlice(slice); + var slice = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet), 1, quantity: 150); // Act - var reservedSlices = await _certRepository.ReserveQuantity(owner, certificate.RegistryName, certificate.Id, 100); + var reservedSlices = await _certRepository.ReserveQuantity(owner, slice.RegistryName, slice.CertificateId, 100); // Assert reservedSlices.Should().ContainEquivalentOf(slice); @@ -557,25 +549,13 @@ public async Task ReserveSlice_LessThan_ThrowsException() { // Arrange var registry = _fixture.Create(); - var certificate = await CreateCertificate(registry); var owner = _fixture.Create(); var wallet = await CreateWallet(owner); var endpoint = await CreateWalletEndpoint(wallet); - var slice = new WalletSlice - { - Id = Guid.NewGuid(), - WalletEndpointId = endpoint.Id, - WalletEndpointPosition = 1, - RegistryName = registry, - CertificateId = certificate.Id, - Quantity = 150, - RandomR = _fixture.Create(), - State = WalletSliceState.Available - }; - await _certRepository.InsertWalletSlice(slice); + var slice = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet), 1, quantity: 150); // Act - var act = () => _certRepository.ReserveQuantity(owner, certificate.RegistryName, certificate.Id, 200); + var act = () => _certRepository.ReserveQuantity(owner, slice.RegistryName, slice.CertificateId, 200); // Assert await act.Should().ThrowAsync().WithMessage("Owner has less to reserve than available"); @@ -586,22 +566,10 @@ public async Task ReserveSlice_ToBe_ThrowsException() { // Arrange var registry = _fixture.Create(); - var certificate = await CreateCertificate(registry); var owner = _fixture.Create(); var wallet = await CreateWallet(owner); - var endpoint = await CreateWalletEndpoint(wallet); - var slice = new WalletSlice - { - Id = Guid.NewGuid(), - WalletEndpointId = endpoint.Id, - WalletEndpointPosition = 1, - RegistryName = registry, - CertificateId = certificate.Id, - Quantity = 150, - RandomR = _fixture.Create(), - State = WalletSliceState.Available - }; - await _certRepository.InsertWalletSlice(slice); + + var slice = await CreateAndInsertCertificateWithSlice(registry, await CreateWalletEndpoint(wallet), 1, quantity: 150); await _certRepository.InsertWalletSlice(slice with { Id = Guid.NewGuid(), @@ -611,7 +579,7 @@ await _certRepository.InsertWalletSlice(slice with }); // Act - var act = () => _certRepository.ReserveQuantity(owner, certificate.RegistryName, certificate.Id, 200); + var act = () => _certRepository.ReserveQuantity(owner, slice.RegistryName, slice.CertificateId, 200); // Assert await act.Should().ThrowAsync().WithMessage("Owner has enough quantity, but it is not yet available to reserve"); @@ -748,4 +716,30 @@ private async Task CreateCertificatesAndSlices(Wallet wallet, int numberOfCertif await _certRepository.InsertWalletSlice(slice); } } + + private async Task CreateAndInsertCertificateWithSlice( + string registry, + WalletEndpoint endpoint, + int endpointPosition, + int? quantity = null) + { + quantity = quantity ?? _fixture.Create(); + + var certificate = await CreateCertificate(registry); + var slice = new WalletSlice + { + Id = Guid.NewGuid(), + WalletEndpointId = endpoint.Id, + WalletEndpointPosition = endpointPosition, + RegistryName = registry, + CertificateId = certificate.Id, + Quantity = quantity.Value, + RandomR = _fixture.Create(), + State = WalletSliceState.Available + }; + + await _certRepository.InsertWalletSlice(slice); + + return slice; + } } diff --git a/src/ProjectOrigin.WalletSystem.Server/Database/Postgres/Scripts/v2/v2-0005.sql b/src/ProjectOrigin.WalletSystem.Server/Database/Postgres/Scripts/v2/v2-0005.sql new file mode 100644 index 00000000..523b5abd --- /dev/null +++ b/src/ProjectOrigin.WalletSystem.Server/Database/Postgres/Scripts/v2/v2-0005.sql @@ -0,0 +1,2 @@ +-- Modify the wallets table to remove the unique constraint on the owner column +ALTER TABLE wallets DROP CONSTRAINT wallets_owner_key1; diff --git a/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/CertificatesController.cs b/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/CertificatesController.cs index 19969fb2..b53e7ebf 100644 --- a/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/CertificatesController.cs +++ b/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/CertificatesController.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -18,12 +18,6 @@ public class CertificatesController : ControllerBase /// /// Gets all certificates in the wallet that are available for use. /// - /// - /// The start of the time range in Unix time in seconds. - /// The end of the time range in Unix time in seconds. - /// Filter the type of certificates to return. - /// The number of items to skip. - /// The number of items to return. /// Returns the aggregated claims. /// If the user is not authenticated. [HttpGet] @@ -33,22 +27,18 @@ public class CertificatesController : ControllerBase [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] public async Task>> GetCertificates( [FromServices] IUnitOfWork unitOfWork, - [FromQuery] long? start, - [FromQuery] long? end, - [FromQuery] CertificateType? type, - [FromQuery] int? limit, - [FromQuery] int skip = 0) + [FromQuery] GetCertificatesQueryParameters param) { if (!User.TryGetSubject(out var subject)) return Unauthorized(); var certificates = await unitOfWork.CertificateRepository.QueryAvailableCertificates(new CertificatesFilter { Owner = subject, - Start = start != null ? DateTimeOffset.FromUnixTimeSeconds(start.Value) : null, - End = end != null ? DateTimeOffset.FromUnixTimeSeconds(end.Value) : null, - Type = type != null ? (GranularCertificateType)type.Value : null, - Skip = skip, - Limit = limit ?? int.MaxValue + Start = param.Start != null ? DateTimeOffset.FromUnixTimeSeconds(param.Start.Value) : null, + End = param.End != null ? DateTimeOffset.FromUnixTimeSeconds(param.End.Value) : null, + Type = param.Type != null ? (GranularCertificateType)param.Type.Value : null, + Skip = param.Skip, + Limit = param.Limit ?? int.MaxValue }); return certificates.ToResultList(c => c.MapToV1()); @@ -57,14 +47,6 @@ public async Task>> GetCertificates /// /// Returns aggregates certificates that are available to use, based on the specified time zone and time range. /// - /// - /// The size of each bucket in the aggregation - /// The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. - /// The start of the time range in Unix time in seconds. - /// The end of the time range in Unix time in seconds. - /// Filter the type of certificates to return. - /// The number of items to skip. - /// The number of items to return. /// Returns the aggregated claims. /// If the time zone is invalid. /// If the user is not authenticated. @@ -76,26 +58,22 @@ public async Task>> GetCertificates [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] public async Task>> AggregateCertificates( [FromServices] IUnitOfWork unitOfWork, - [FromQuery] TimeAggregate timeAggregate, - [FromQuery] string timeZone, - [FromQuery] long? start, - [FromQuery] long? end, - [FromQuery] CertificateType? type, - [FromQuery] int? limit, - [FromQuery] int skip = 0) + [FromQuery] AggregateCertificatesQueryParameters parameters) { if (!User.TryGetSubject(out var subject)) return Unauthorized(); - if (!timeZone.TryParseTimeZone(out var timeZoneInfo)) return BadRequest("Invalid time zone"); + if (!parameters.TimeZone.TryParseTimeZone(out var timeZoneInfo)) return BadRequest("Invalid time zone"); var certificates = await unitOfWork.CertificateRepository.QueryAggregatedAvailableCertificates(new CertificatesFilter { Owner = subject, - Start = start != null ? DateTimeOffset.FromUnixTimeSeconds(start.Value) : null, - End = end != null ? DateTimeOffset.FromUnixTimeSeconds(end.Value) : null, - Type = type != null ? (GranularCertificateType)type.Value : null, - Skip = skip, - Limit = limit ?? int.MaxValue - }, (Models.TimeAggregate)timeAggregate, timeZone); + Start = parameters.Start != null ? DateTimeOffset.FromUnixTimeSeconds(parameters.Start.Value) : null, + End = parameters.End != null ? DateTimeOffset.FromUnixTimeSeconds(parameters.End.Value) : null, + Type = parameters.Type != null ? (GranularCertificateType)parameters.Type.Value : null, + Skip = parameters.Skip, + Limit = parameters.Limit ?? int.MaxValue + }, + (Models.TimeAggregate)parameters.TimeAggregate, + parameters.TimeZone); return certificates.ToResultList(c => new AggregatedCertificates { @@ -108,6 +86,73 @@ public async Task>> AggregateCer } #region Records +public record GetCertificatesQueryParameters +{ + /// + /// The start of the time range in Unix time in seconds. + /// + public long? Start { get; init; } + + /// + /// The end of the time range in Unix time in seconds. + /// + public long? End { get; init; } + + /// + /// Filter the type of certificates to return. + /// + public CertificateType? Type { get; init; } + + /// + /// The number of items to return. + /// + public int? Limit { get; init; } + + /// + /// The number of items to skip. + /// + [DefaultValue(0)] + public int Skip { get; init; } +} + +public record AggregateCertificatesQueryParameters +{ + /// + /// The size of each bucket in the aggregation. + /// + public required TimeAggregate TimeAggregate { get; init; } + + /// + /// The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. + /// + public required string TimeZone { get; init; } + + /// + ///The start of the time range in Unix time in seconds. + /// + public long? Start { get; init; } + + /// + /// The end of the time range in Unix time in seconds. + /// + public long? End { get; init; } + + /// + /// Filter the type of certificates to return. + /// + public CertificateType? Type { get; init; } + + /// + /// The number of items to return. + /// + public int? Limit { get; init; } + + /// + /// The number of items to skip. + /// + [DefaultValue(0)] + public int Skip { get; init; } +} /// /// A certificate that is available to use in the wallet. diff --git a/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/ClaimsController.cs b/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/ClaimsController.cs index c356da44..65742067 100644 --- a/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/ClaimsController.cs +++ b/src/ProjectOrigin.WalletSystem.Server/Services/REST/v1/ClaimsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using MassTransit; @@ -20,11 +21,6 @@ public class ClaimsController : ControllerBase /// /// Gets all claims in the wallet /// - /// - /// The start of the time range in Unix time in seconds. - /// The end of the time range in Unix time in seconds. - /// The number of items to skip. - /// The number of items to return. /// Returns all the indiviual claims. /// If the user is not authenticated. [HttpGet] @@ -34,20 +30,17 @@ public class ClaimsController : ControllerBase [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] public async Task>> GetClaims( [FromServices] IUnitOfWork unitOfWork, - [FromQuery] long? start, - [FromQuery] long? end, - [FromQuery] int? limit, - [FromQuery] int skip = 0) + [FromQuery] GetClaimsQueryParameters param) { if (!User.TryGetSubject(out var subject)) return Unauthorized(); var claims = await unitOfWork.ClaimRepository.QueryClaims(new ClaimFilter { Owner = subject, - Start = start != null ? DateTimeOffset.FromUnixTimeSeconds(start.Value) : null, - End = end != null ? DateTimeOffset.FromUnixTimeSeconds(end.Value) : null, - Skip = skip, - Limit = limit ?? int.MaxValue, + Start = param.Start != null ? DateTimeOffset.FromUnixTimeSeconds(param.Start.Value) : null, + End = param.End != null ? DateTimeOffset.FromUnixTimeSeconds(param.End.Value) : null, + Skip = param.Skip, + Limit = param.Limit ?? int.MaxValue, }); return claims.ToResultList(c => c.MapToV1()); @@ -56,13 +49,6 @@ public async Task>> GetClaims( /// /// Returns a list of aggregates claims for the authenticated user based on the specified time zone and time range. /// - /// - /// The size of each bucket in the aggregation - /// The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. - /// The start of the time range in Unix time in seconds. - /// The end of the time range in Unix time in seconds. - /// The number of items to skip. - /// The number of items to return. /// Returns the aggregated claims. /// If the time zone is invalid. /// If the user is not authenticated. @@ -74,24 +60,19 @@ public async Task>> GetClaims( [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] public async Task>> AggregateClaims( [FromServices] IUnitOfWork unitOfWork, - [FromQuery] TimeAggregate timeAggregate, - [FromQuery] string timeZone, - [FromQuery] long? start, - [FromQuery] long? end, - [FromQuery] int? limit, - [FromQuery] int skip = 0) + [FromQuery] AggregateClaimsQueryParameters param) { if (!User.TryGetSubject(out var subject)) return Unauthorized(); - if (!timeZone.TryParseTimeZone(out var timeZoneInfo)) return BadRequest("Invalid time zone"); + if (!param.TimeZone.TryParseTimeZone(out var timeZoneInfo)) return BadRequest("Invalid time zone"); var result = await unitOfWork.ClaimRepository.QueryAggregatedClaims(new ClaimFilter { Owner = subject, - Start = start != null ? DateTimeOffset.FromUnixTimeSeconds(start.Value) : null, - End = end != null ? DateTimeOffset.FromUnixTimeSeconds(end.Value) : null, - Skip = skip, - Limit = limit ?? int.MaxValue - }, (Models.TimeAggregate)timeAggregate, timeZone); + Start = param.Start != null ? DateTimeOffset.FromUnixTimeSeconds(param.Start.Value) : null, + End = param.End != null ? DateTimeOffset.FromUnixTimeSeconds(param.End.Value) : null, + Skip = param.Skip, + Limit = param.Limit ?? int.MaxValue + }, (Models.TimeAggregate)param.TimeAggregate, param.TimeZone); return result.ToResultList(c => new AggregatedClaims() { @@ -142,6 +123,64 @@ [FromBody] ClaimRequest request #region Records +public record GetClaimsQueryParameters +{ + /// + /// The start of the time range in Unix time in seconds. + /// + public long? Start { get; init; } + + /// + /// The end of the time range in Unix time in seconds. + /// + public long? End { get; init; } + + /// + /// The number of items to return. + /// + public int? Limit { get; init; } + + /// + /// The number of items to skip. + /// + [DefaultValue(0)] + public int Skip { get; init; } +} + +public record AggregateClaimsQueryParameters +{ + /// + /// The size of each bucket in the aggregation + /// + public required TimeAggregate TimeAggregate { get; init; } + + /// + /// The time zone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. + /// + public required string TimeZone { get; init; } + + /// + /// The start of the time range in Unix time in seconds. + /// + public long? Start { get; init; } + + /// + /// The end of the time range in Unix time in seconds. + /// + public long? End { get; init; } + + /// + /// The number of items to return. + /// + public int? Limit { get; init; } + + /// + /// The number of items to skip. + /// + [DefaultValue(0)] + public int Skip { get; init; } +} + /// /// A claim record representing a claim of a production and consumption certificate. ///