From 6380c8c3cfbb562eb103545d674b96ec5f4df1cd Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 15:20:28 +0100 Subject: [PATCH 1/5] added scalar and api defaults --- Directory.Packages.props | 29 +- .../QueryTimingInterceptor.cs | 131 -------- .../CalendarDate.cs | 15 +- .../CalendarYear.cs | 4 +- .../ComparisonDate.cs | 7 +- .../Contract.cs | 67 +++- ...ateReportingAPI.DataTransferObjects.csproj | 1 + .../FileDetails.cs | 13 +- .../FileImportLog.cs | 7 +- .../FileLine.cs | 7 +- .../FileProfileConfiguration.cs | 11 +- .../GetRecentActivityReceiptReportRequest.cs | 7 + .../GetRecentActivityReceiptReportResponse.cs | 15 + .../GroupByOption.cs | 9 +- .../LastSettlement.cs | 8 +- .../Merchant.cs | 78 ++++- .../MerchantDailyPerformanceSummaryRequest.cs | 32 +- .../MerchantDetail.cs | 13 +- .../MerchantDetailSummary.cs | 8 +- .../MerchantKpi.cs | 6 +- .../Operator.cs | 10 +- .../OperatorDetail.cs | 13 +- .../OperatorDetailSummary.cs | 8 +- .../PaginationMetadata.cs | 6 + .../ResponseCode.cs | 6 +- .../SortDirection.cs | 8 +- .../SortField.cs | 9 +- .../TodaysSales.cs | 9 +- .../TodaysSalesByHour.cs | 9 +- .../TodaysSettlement.cs | 12 +- .../TopBottom.cs | 7 +- .../TopBottomMerchantData.cs | 6 +- .../TopBottomOperatorData.cs | 6 +- .../TopBottomProductData.cs | 6 +- .../TransactionDetail.cs | 21 +- .../TransactionDetailReportRequest.cs | 11 +- .../TransactionDetailReportResponse.cs | 28 +- .../TransactionDetailSummary.cs | 7 +- .../TransactionMixSummaryRequest.cs | 18 ++ .../TransactionMixSummaryResponse.cs | 36 +++ .../TransactionResult.cs | 18 +- .../TransactionSearchRequest.cs | 18 +- .../TransactionSummaryByMerchantRequest.cs | 10 +- .../TransactionSummaryByMerchantResponse.cs | 6 +- .../TransactionSummaryByOperatorRequest.cs | 9 +- .../TransactionSummaryByOperatorResponse.cs | 6 +- .../UnsettledFee.cs | 6 +- .../ValueRange.cs | 6 +- .../CalendarEndpointTests.cs | 1 - .../ControllerTestsBase.cs | 24 +- .../CustomWebApplicationFactory.cs | 18 +- ...EstateReportingAPI.IntegrationTests.csproj | 1 - .../TransactionMixSummaryEndpointTests.cs | 5 +- .../TransactionsEndpointTests.cs | 21 +- .../BusinessLogic/DbContextResolverXTests.cs | 7 +- .../EstateReportingAPI.Tests.csproj | 1 - .../Bootstrapper/MediatorRegistry.cs | 6 +- .../Bootstrapper/MiddlewareRegistry.cs | 81 +++-- .../OpenApiExamplesTransformer.cs | 290 ++++++++++++++++++ .../Bootstrapper/RepositoryRegistry.cs | 20 +- .../SharedLibraryCompatibility.cs | 21 ++ .../XmlDocumentationSchemaTransformer.cs | 96 ++++++ EstateReportingAPI/Common/EstateContext.cs | 10 +- .../Endpoints/CalendarEndpoints.cs | 14 +- .../Endpoints/ContractEndpoints.cs | 22 +- .../EndpointDocumentationExtensions.cs | 12 + .../Endpoints/EstateEndpoints.cs | 18 +- .../Endpoints/FileImportLogEndpoints.cs | 29 +- .../Endpoints/MerchantEndpoints.cs | 46 ++- .../Endpoints/OperatorEndpoints.cs | 18 +- .../Endpoints/SettlementEndpoints.cs | 15 +- .../Endpoints/TransactionEndpoints.cs | 50 ++- EstateReportingAPI/EstateReportingAPI.csproj | 9 +- .../Handlers/ContractHandler.cs | 5 +- .../Handlers/FileImportHandler.cs | 11 +- .../Handlers/MerchantHandler.cs | 27 +- .../Handlers/OperatorHandler.cs | 5 +- .../Handlers/SettlementHandler.cs | 3 +- .../Handlers/TransactionHandler.cs | 21 +- EstateReportingAPI/Program.cs | 169 +++++----- EstateReportingAPI/Startup.cs | 94 ------ ...10-21-modern-startup-configuration-plan.md | 95 ++++++ ...-21-modern-startup-configuration-design.md | 60 ++++ 83 files changed, 1572 insertions(+), 536 deletions(-) delete mode 100644 EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs create mode 100644 EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs create mode 100644 EstateReportingAPI/Bootstrapper/SharedLibraryCompatibility.cs create mode 100644 EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs create mode 100644 EstateReportingAPI/Endpoints/EndpointDocumentationExtensions.cs delete mode 100644 EstateReportingAPI/Startup.cs create mode 100644 docs/superpowers/plans/2026-10-21-modern-startup-configuration-plan.md create mode 100644 docs/superpowers/specs/2026-10-21-modern-startup-configuration-design.md diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b40784..00179ef 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,7 @@ true - + @@ -11,49 +11,44 @@ - - + + - - + + - - + + - + - + - - - - - - - + + - + \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs b/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs deleted file mode 100644 index 0e47a0e..0000000 --- a/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Shared.EntityFramework; -using Shared.General; -using Shared.Logger; -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using TransactionProcessor.Database.Contexts; - -namespace EstateReportingAPI.BusinessLogic; - -// Infrastructure component currently scheduled for generalisation and relocation to Shared; exclude until that work is completed. -[ExcludeFromCodeCoverage] -public class QueryTimingInterceptor : DbCommandInterceptor { - - internal void LogIfRequired(DbCommand command, - CommandExecutedEventData eventData) { - - Int32 threshold = ConfigurationReader.GetValueOrDefault("AppSettings", "EFQueryPerformanceThresholdMs", 500); - - if (eventData.Duration.TotalMilliseconds < threshold) - return; - Logger.LogWarning($"PERFORMANCE - EF Query took {eventData.Duration.TotalMilliseconds} ms\n{command.CommandText}\n"); - } - - public override DbDataReader ReaderExecuted(DbCommand command, - CommandExecutedEventData eventData, - DbDataReader result) { - LogIfRequired(command, eventData); - return result; - } - - public override async ValueTask ReaderExecutedAsync(DbCommand command, - CommandExecutedEventData eventData, - DbDataReader result, - CancellationToken cancellationToken) { - LogIfRequired(command, eventData); - - return result; - } -} - - -// Infrastructure component currently scheduled for generalisation and relocation to Shared; exclude until that work is completed. -[ExcludeFromCodeCoverage] -public class DbContextResolverX : IDbContextResolver where TContext : DbContext -{ - private readonly IServiceProvider _rootProvider; - private readonly IConfiguration _config; - private readonly DbCommandInterceptor Interceptor; - - public DbContextResolverX(IServiceProvider rootProvider, - IConfiguration config, - DbCommandInterceptor interceptor) - { - _rootProvider = rootProvider; - _config = config; - this.Interceptor = interceptor; - } - - public ResolvedDbContext Resolve(String connectionStringKey) - { - return this.Resolve(connectionStringKey, String.Empty); - } - - public ResolvedDbContext Resolve(String connectionStringKey, - String databaseNameSuffix) - { - IServiceScope scope = _rootProvider.CreateScope(); - String connectionString = _config.GetConnectionString(connectionStringKey); - if (String.IsNullOrWhiteSpace(connectionString)) - throw new InvalidOperationException($"Connection string for '{connectionStringKey}' not found."); - - // Update the connection string with the identifier if needed - if (!String.IsNullOrWhiteSpace(databaseNameSuffix)) - { - SqlConnectionStringBuilder builder = new(connectionString); - builder.InitialCatalog = $"{builder.InitialCatalog}-{databaseNameSuffix}"; - connectionString = builder.ConnectionString; - - - // Create an isolated service collection and provider - ServiceCollection services = new(); - - SqlServerRetryOptions retryOptions; - try - { - retryOptions = ConfigurationReader.GetSection("AppSettings:SqlServerRetry"); - } - catch (KeyNotFoundException) - { - retryOptions = null; - } - - if (retryOptions != null) - { - services.AddDbContext(options => { - options.UseSharedSqlServer(connectionString, retry => { - retry.AdditionalTransientErrorNumbers = retryOptions.AdditionalTransientErrorNumbers; - retry.MaxRetryCount = retryOptions.MaxRetryCount; - retry.MaxRetryDelay = retryOptions.MaxRetryDelay; - }); - options.AddInterceptors(Interceptor); - }); - } - else - { - services.AddDbContext(options => { - options.UseSqlServer(connectionString, retry => { retry.EnableRetryOnFailure(); }); - options.AddInterceptors(Interceptor); - }); - - } - - - ServiceProvider provider = services.BuildServiceProvider(); - scope = provider.CreateScope(); - // Standard resolution using DI container - } - - return new ResolvedDbContext(scope); - } -} diff --git a/EstateReportingAPI.DataTrasferObjects/CalendarDate.cs b/EstateReportingAPI.DataTrasferObjects/CalendarDate.cs index f012652..b3f668d 100644 --- a/EstateReportingAPI.DataTrasferObjects/CalendarDate.cs +++ b/EstateReportingAPI.DataTrasferObjects/CalendarDate.cs @@ -1,18 +1,31 @@ -namespace EstateReportingAPI.DataTrasferObjects{ +namespace EstateReportingAPI.DataTrasferObjects{ using System; + /// API data model describing calendar date information. + public class CalendarDate { + /// The date value. public DateTime Date { get; set; } + /// The day of week value. public String DayOfWeek{ get; set; } + /// The day of week number value. public Int32 DayOfWeekNumber{ get; set; } + /// The day of week short value. public String DayOfWeekShort{ get; set; } + /// The month name value. public String MonthName{ get; set; } + /// The month name short value. public String MonthNameShort { get; set; } + /// The month number value. public Int32 MonthNumber { get; set; } + /// The week number value. public Int32 WeekNumber { get; set; } + /// The week number string value. public String WeekNumberString { get; set; } + /// The year value. public Int32 Year { get; set; } + /// The year week number value. public String YearWeekNumber { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/CalendarYear.cs b/EstateReportingAPI.DataTrasferObjects/CalendarYear.cs index 694d8a4..9ff295e 100644 --- a/EstateReportingAPI.DataTrasferObjects/CalendarYear.cs +++ b/EstateReportingAPI.DataTrasferObjects/CalendarYear.cs @@ -1,9 +1,11 @@ -using System; +using System; namespace EstateReportingAPI.DataTrasferObjects { + /// API data model describing calendar year information. public class CalendarYear { + /// The year value. public Int32 Year{ get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/ComparisonDate.cs b/EstateReportingAPI.DataTrasferObjects/ComparisonDate.cs index e0de881..0dbdf83 100644 --- a/EstateReportingAPI.DataTrasferObjects/ComparisonDate.cs +++ b/EstateReportingAPI.DataTrasferObjects/ComparisonDate.cs @@ -1,9 +1,14 @@ -namespace EstateReportingAPI.DataTrasferObjects{ +namespace EstateReportingAPI.DataTrasferObjects{ using System; + /// API data model describing comparison date information. + public class ComparisonDate{ + /// The order value value. public Int32 OrderValue{ get; set; } + /// The date value. public DateOnly Date { get; set; } + /// The description value. public String Description { get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/Contract.cs b/EstateReportingAPI.DataTrasferObjects/Contract.cs index 2f0555f..65dd340 100644 --- a/EstateReportingAPI.DataTrasferObjects/Contract.cs +++ b/EstateReportingAPI.DataTrasferObjects/Contract.cs @@ -1,86 +1,151 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing contract information. + public class Contract { #region Properties + /// Unique identifier of the estate. public Guid EstateId { get; set; } + /// The estate reporting id value. public Int32 EstateReportingId { get; set; } + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// The contract reporting id value. public Int32 ContractReportingId { get; set; } + /// The description value. public String Description { get; set; } + /// The operator name value. public String OperatorName { get; set; } + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The operator reporting id value. public Int32 OperatorReportingId { get; set; } + /// Optional product reporting identifiers to include. public List Products { get; set; } #endregion } +/// API data model describing contract product information. + public class ContractProduct { + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// The contract product reporting id value. public Int32 ContractProductReportingId { get; set; } + /// Unique identifier of the product. + public Guid ProductId { get; set; } + /// The product name value. public String ProductName { get; set; } + /// The display text value. public String DisplayText { get; set; } + /// The product type value. public Int32 ProductType { get; set; } + /// The value value. public Decimal? Value { get; set; } + /// The transaction fees value. public List TransactionFees { get; set; } } +/// API data model describing contract product transaction fee information. + public class ContractProductTransactionFee { + /// The transaction fee id value. public Guid TransactionFeeId { get; set; } + /// The contract product transaction fee reporting id value. public Int32 ContractProductTransactionFeeReportingId { get; set; } + /// The description value. + public string? Description { get; set; } + /// The calculation type value. public Int32 CalculationType { get; set; } + /// The fee type value. public Int32 FeeType { get; set; } + /// The value value. public Decimal Value { get; set; } } +/// API data model describing estate information. + + public class Estate { + /// Unique identifier of the estate. public Guid EstateId { get; set; } + /// The estate name value. public string? EstateName { get; set; } + /// The reference value. public string? Reference { get; set; } + /// Optional operator reporting identifiers to include. public List? Operators { get; set; } + /// Optional merchant reporting identifiers to include. public List? Merchants { get; set; } + /// The contracts value. public List? Contracts { get; set; } + /// The users value. public List? Users { get; set; } } +/// API data model describing estate user information. + public class EstateUser { + /// The user id value. public Guid UserId { get; set; } + /// The email address value. public string? EmailAddress { get; set; } + /// The created date time value. public DateTime CreatedDateTime { get; set; } } +/// API data model describing estate operator information. + public class EstateOperator { + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The name value. public string? Name { get; set; } + /// The require custom merchant number value. public bool RequireCustomMerchantNumber { get; set; } + /// The require custom terminal number value. public bool RequireCustomTerminalNumber { get; set; } + /// The created date time value. public DateTime CreatedDateTime { get; set; } } +/// API data model describing estate contract information. + public class EstateContract { + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// The name value. public string? Name { get; set; } + /// The operator name value. public string? OperatorName { get; set; } } +/// API data model describing estate merchant information. + + public class EstateMerchant { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// The name value. public string? Name { get; set; } + /// The reference value. public string? Reference { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/EstateReportingAPI.DataTransferObjects.csproj b/EstateReportingAPI.DataTrasferObjects/EstateReportingAPI.DataTransferObjects.csproj index 248219d..6ad512c 100644 --- a/EstateReportingAPI.DataTrasferObjects/EstateReportingAPI.DataTransferObjects.csproj +++ b/EstateReportingAPI.DataTrasferObjects/EstateReportingAPI.DataTransferObjects.csproj @@ -4,6 +4,7 @@ net10.0 enable None + true diff --git a/EstateReportingAPI.DataTrasferObjects/FileDetails.cs b/EstateReportingAPI.DataTrasferObjects/FileDetails.cs index 551735e..ef7daf4 100644 --- a/EstateReportingAPI.DataTrasferObjects/FileDetails.cs +++ b/EstateReportingAPI.DataTrasferObjects/FileDetails.cs @@ -1,17 +1,28 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing file details information. + public class FileDetails { + /// The file id value. public Guid FileId { get; set; } + /// The file name value. public string FileName { get; set; } + /// The file profile value. public string FileProfile { get; set; } + /// Date and time when the file was uploaded. public DateTimeOffset DateTimeUploaded { get; set; } + /// The user id value. public Guid UserId { get; set; } + /// The uploaded by value. public string UploadedBy { get; set; } + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// The merchant name value. public string MerchantName { get; set; } + /// The file lines value. public List FileLines { get; set; } = new List(); } diff --git a/EstateReportingAPI.DataTrasferObjects/FileImportLog.cs b/EstateReportingAPI.DataTrasferObjects/FileImportLog.cs index dceb309..2f312f7 100644 --- a/EstateReportingAPI.DataTrasferObjects/FileImportLog.cs +++ b/EstateReportingAPI.DataTrasferObjects/FileImportLog.cs @@ -1,11 +1,16 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing file import log information. + public class FileImportLog { + /// The file import log id value. public Guid FileImportLogId { get; set; } + /// Date and time when the import log was recorded. public DateTimeOffset ImportLogDateTime { get; set; } + /// The file details list value. public List FileDetailsList { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/FileLine.cs b/EstateReportingAPI.DataTrasferObjects/FileLine.cs index ec86c6e..0e6a808 100644 --- a/EstateReportingAPI.DataTrasferObjects/FileLine.cs +++ b/EstateReportingAPI.DataTrasferObjects/FileLine.cs @@ -1,10 +1,15 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing file line information. + public sealed class FileLine { + /// One-based line number in the imported file. public int LineNumber { get; set; } + /// Original contents of the file line. public string LineContents { get; set; } + /// Processing status of the file line. public String LineStatus { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/FileProfileConfiguration.cs b/EstateReportingAPI.DataTrasferObjects/FileProfileConfiguration.cs index 7d4ab54..4b6efe4 100644 --- a/EstateReportingAPI.DataTrasferObjects/FileProfileConfiguration.cs +++ b/EstateReportingAPI.DataTrasferObjects/FileProfileConfiguration.cs @@ -1,14 +1,23 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing file profile configuration information. + public class FileProfileConfiguration { + /// The file profile id value. public Guid FileProfileId { get; set; } + /// The name value. public string Name { get; set; } + /// The listening directory value. public string ListeningDirectory { get; set; } + /// The request type value. public string RequestType { get; set; } + /// The operator name value. public string OperatorName { get; set; } + /// The line terminator value. public string LineTerminator { get; set; } + /// The file format handler value. public string FileFormatHandler { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportRequest.cs b/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportRequest.cs index cca821c..e82f72e 100644 --- a/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportRequest.cs @@ -2,11 +2,18 @@ namespace EstateReportingAPI.DataTransferObjects; +/// Request criteria for get recent activity receipt report. + public class GetRecentActivityReceiptReportRequest { + /// Date for which report data is requested or returned. public DateOnly ReportDate { get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public int? MerchantReportingId { get; set; } + /// Optional text used to search matching activity. public string? SearchText { get; set; } + /// One-based page number to return. public int PageNumber { get; set; } = 1; + /// Maximum number of items to include on a page. public int PageSize { get; set; } = 10; } diff --git a/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportResponse.cs b/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportResponse.cs index d295621..3819985 100644 --- a/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportResponse.cs +++ b/EstateReportingAPI.DataTrasferObjects/GetRecentActivityReceiptReportResponse.cs @@ -3,21 +3,36 @@ namespace EstateReportingAPI.DataTransferObjects; +/// Response data for get recent activity receipt report. + public class GetRecentActivityReceiptReportResponse { + /// Date for which report data is requested or returned. public DateOnly ReportDate { get; set; } + /// The items value. public List Items { get; set; } = []; + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } +/// Data item included in an API response: Recent Activity Receipt Item Dto. + public class RecentActivityReceiptItemDto { + /// The reference value. public string? Reference { get; set; } + /// The transaction type value. public string? TransactionType { get; set; } + /// The product value. public string? Product { get; set; } + /// The operator value. public string? Operator { get; set; } + /// The status value. public string? Status { get; set; } + /// The amount value. public decimal Amount { get; set; } + /// Date and time of the transaction. public DateTimeOffset TransactionDateTime { get; set; } + /// The receipt reference value. public string? ReceiptReference { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs b/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs index 871da51..a2f0e11 100644 --- a/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs +++ b/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs @@ -1,7 +1,12 @@ -namespace EstateReportingAPI.DataTransferObjects; +namespace EstateReportingAPI.DataTransferObjects; + +/// API data model describing group by option information. public enum GroupByOption{ + /// Group transactions by operator. Operator, + /// Group transactions by merchant. Merchant, + /// Group transactions by product. Product -} \ No newline at end of file +} diff --git a/EstateReportingAPI.DataTrasferObjects/LastSettlement.cs b/EstateReportingAPI.DataTrasferObjects/LastSettlement.cs index f862962..ce12587 100644 --- a/EstateReportingAPI.DataTrasferObjects/LastSettlement.cs +++ b/EstateReportingAPI.DataTrasferObjects/LastSettlement.cs @@ -1,11 +1,17 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing last settlement information. + public class LastSettlement { + /// The settlement date value. public DateTime SettlementDate { get; set; } + /// The sales value value. public Decimal SalesValue { get; set; } + /// The sales count value. public Int32 SalesCount { get; set; } + /// The fees value value. public Decimal FeesValue { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/Merchant.cs b/EstateReportingAPI.DataTrasferObjects/Merchant.cs index 873a277..7ead333 100644 --- a/EstateReportingAPI.DataTrasferObjects/Merchant.cs +++ b/EstateReportingAPI.DataTrasferObjects/Merchant.cs @@ -1,100 +1,176 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; using System.Collections.Generic; + /// API data model describing merchant information. + public class Merchant{ #region Properties + /// Unique identifier of the merchant. + public Guid MerchantId{ get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public Int32 MerchantReportingId{ get; set; } + /// The name value. public String Name{ get; set; } + /// The reference value. public String Reference { get; set; } + /// The balance value. public Decimal Balance { get; set; } + /// The settlement schedule value. public Int32 SettlementSchedule { get; set; } + /// The created date time value. public DateTime CreatedDateTime { get; set; } + /// The address id value. + public Guid AddressId { get; set; } + /// The address line1 value. public String AddressLine1 { get; set; } + /// The address line2 value. public String AddressLine2 { get; set; } + /// The town value. public String Town { get; set; } + /// The region value. public String Region { get; set; } + /// The post code value. public String PostCode{ get; set; } + /// The country value. public String Country { get; set; } + /// The contact id value. public Guid ContactId { get; set; } + /// The contact name value. public String ContactName { get; set; } + /// The contact email value. public String ContactEmail { get; set; } + /// The contact phone value. public String ContactPhone { get; set; } #endregion } + /// Response data for opening hours. + public class OpeningHoursResponse { + /// The day of week value. public Int32 DayOfWeek { get; set; } + /// The opening value. + public string Opening { get; set; } + /// The closing value. + public string Closing { get; set; } } + /// API data model describing merchant operator information. + public class MerchantOperator { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The operator name value. public String OperatorName { get; set; } + /// The merchant number value. public String MerchantNumber { get; set; } + /// The terminal number value. public String TerminalNumber { get; set; } + /// The is deleted value. public Boolean IsDeleted { get; set; } } + /// API data model describing merchant contract information. + public class MerchantContract { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// The contract name value. public String ContractName { get; set; } + /// The operator name value. public String OperatorName { get; set; } + /// The is deleted value. public Boolean IsDeleted { get; set; } + /// The contract products value. public List ContractProducts { get; set; } } + /// API data model describing merchant contract product information. + public class MerchantContractProduct { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// Unique identifier of the product. public Guid ProductId { get; set; } + /// The product name value. public String ProductName { get; set; } + /// The display text value. public String DisplayText { get; set; } + /// The product type value. public Int32 ProductType { get; set; } + /// The value value. public Decimal? Value { get; set; } } + /// API data model describing merchant device information. + public class MerchantDevice { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// The device id value. public Guid DeviceId { get; set; } + /// The device identifier value. public String DeviceIdentifier { get; set; } + /// The is deleted value. public Boolean IsDeleted { get; set; } } + /// API data model describing merchant opening hour information. + public class MerchantOpeningHour { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// The day of week value. public DayOfWeek DayOfWeek { get; set; } + /// The opening time value. public String OpeningTime { get; set; } + /// The closing time value. public String ClosingTime { get; set; } } + /// Response data for merchant schedule. + public class MerchantScheduleResponse { + /// The year value. public int Year { get; set; } + /// The months value. + public List Months { get; set; } } + /// Response data for merchant schedule month. + public class MerchantScheduleMonthResponse { + /// The month value. public int Month { get; set; } + /// Day numbers within the month on which the merchant is closed. + public List ClosedDays { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/MerchantDailyPerformanceSummaryRequest.cs b/EstateReportingAPI.DataTrasferObjects/MerchantDailyPerformanceSummaryRequest.cs index c5ab6d0..ef9a287 100644 --- a/EstateReportingAPI.DataTrasferObjects/MerchantDailyPerformanceSummaryRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/MerchantDailyPerformanceSummaryRequest.cs @@ -1,45 +1,75 @@ -using System; +using System; using System.Collections.Generic; using System.Text; namespace EstateReportingAPI.DataTransferObjects { + /// Request criteria for merchant daily performance summary. public class MerchantDailyPerformanceSummaryRequest { + /// Numeric reporting identifier used to filter or identify a merchant. public Int32 MerchantReportingId { get; set; } + /// Inclusive start date of the report period. public DateOnly StartDate { get; set; } + /// Inclusive end date of the report period. public DateOnly EndDate { get; set; } } + /// Response data for merchant daily performance summary. + public sealed class MerchantDailyPerformanceSummaryResponse { + /// The metrics value. public List Metrics { get; set; } = []; + /// The drill down transactions value. + public List DrillDownTransactions { get; set; } = []; } + /// API data model describing metric item information. + public sealed class MetricItem { + /// The title value. public string Title { get; set; } + /// The value value. + public Decimal Value { get; set; } + /// The description value. + public string Description { get; set; } + /// The category value. + public int Category { get; set; } + /// The type value. public Int32 Type { get; set; } } + /// API data model describing drill down transaction information. + public sealed class DrillDownTransaction { + /// The reference value. public string Reference { get; set; } + /// The product value. + public string Product { get; set; } + /// The status value. + public string Status { get; set; } + /// The amount value. + public Decimal Amount { get; set; } + /// Date and time of the transaction. + public DateTimeOffset TransactionDateTime { get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/MerchantDetail.cs b/EstateReportingAPI.DataTrasferObjects/MerchantDetail.cs index e1eb527..e430dc1 100644 --- a/EstateReportingAPI.DataTrasferObjects/MerchantDetail.cs +++ b/EstateReportingAPI.DataTrasferObjects/MerchantDetail.cs @@ -1,15 +1,26 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing merchant detail information. + public class MerchantDetail { + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public Int32 MerchantReportingId { get; set; } + /// The merchant name value. public string MerchantName { get; set; } + /// The total count value. public Int32 TotalCount { get; set; } + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// Average transaction value represented by this result. public Decimal AverageValue { get; set; } + /// Number of authorised transactions. public Int32 AuthorisedCount { get; set; } + /// Number of declined transactions. public Int32 DeclinedCount { get; set; } + /// Percentage of transactions that were authorised. public Decimal AuthorisedPercentage { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/MerchantDetailSummary.cs b/EstateReportingAPI.DataTrasferObjects/MerchantDetailSummary.cs index 51b7f92..c61cb7d 100644 --- a/EstateReportingAPI.DataTrasferObjects/MerchantDetailSummary.cs +++ b/EstateReportingAPI.DataTrasferObjects/MerchantDetailSummary.cs @@ -1,10 +1,16 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing merchant detail summary information. + public class MerchantDetailSummary { + /// The total merchants value. public Int32 TotalMerchants { get; set; } + /// The total count value. public Int32 TotalCount { get; set; } + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// Average transaction value represented by this result. public Decimal AverageValue { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/MerchantKpi.cs b/EstateReportingAPI.DataTrasferObjects/MerchantKpi.cs index 767c0ec..6e328ac 100644 --- a/EstateReportingAPI.DataTrasferObjects/MerchantKpi.cs +++ b/EstateReportingAPI.DataTrasferObjects/MerchantKpi.cs @@ -1,11 +1,15 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects { + /// API data model describing merchant kpi information. public class MerchantKpi { + /// The merchants with sale in last hour value. public Int32 MerchantsWithSaleInLastHour{ get; set; } + /// The merchants with no sale today value. public Int32 MerchantsWithNoSaleToday { get; set; } + /// The merchants with no sale in last7 days value. public Int32 MerchantsWithNoSaleInLast7Days { get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/Operator.cs b/EstateReportingAPI.DataTrasferObjects/Operator.cs index 4b92822..643045a 100644 --- a/EstateReportingAPI.DataTrasferObjects/Operator.cs +++ b/EstateReportingAPI.DataTrasferObjects/Operator.cs @@ -1,12 +1,20 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing operator information. + public class Operator{ + /// The estate reporting id value. public Int32 EstateReportingId { get; set; } + /// Unique identifier of the operator. public Guid OperatorId{ get; set; } + /// The name value. public String Name { get; set; } + /// The operator reporting id value. public Int32 OperatorReportingId { get; set; } + /// The require custom merchant number value. public Boolean RequireCustomMerchantNumber { get; set; } + /// The require custom terminal number value. public Boolean RequireCustomTerminalNumber { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/OperatorDetail.cs b/EstateReportingAPI.DataTrasferObjects/OperatorDetail.cs index 2c5b7a9..29c389b 100644 --- a/EstateReportingAPI.DataTrasferObjects/OperatorDetail.cs +++ b/EstateReportingAPI.DataTrasferObjects/OperatorDetail.cs @@ -1,16 +1,27 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing operator detail information. + public class OperatorDetail { + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The operator reporting id value. public Int32 OperatorReportingId { get; set; } + /// The operator name value. public string OperatorName { get; set; } + /// The total count value. public Int32 TotalCount { get; set; } + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// Average transaction value represented by this result. public Decimal AverageValue { get; set; } + /// Number of authorised transactions. public Int32 AuthorisedCount { get; set; } + /// Number of declined transactions. public Int32 DeclinedCount { get; set; } + /// Percentage of transactions that were authorised. public Decimal AuthorisedPercentage { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/OperatorDetailSummary.cs b/EstateReportingAPI.DataTrasferObjects/OperatorDetailSummary.cs index a55be6e..430768d 100644 --- a/EstateReportingAPI.DataTrasferObjects/OperatorDetailSummary.cs +++ b/EstateReportingAPI.DataTrasferObjects/OperatorDetailSummary.cs @@ -1,11 +1,17 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing operator detail summary information. + public class OperatorDetailSummary { + /// The total operators value. public Int32 TotalOperators { get; set; } + /// The total count value. public Int32 TotalCount { get; set; } + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// Average transaction value represented by this result. public Decimal AverageValue { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/PaginationMetadata.cs b/EstateReportingAPI.DataTrasferObjects/PaginationMetadata.cs index 35d4530..e385983 100644 --- a/EstateReportingAPI.DataTrasferObjects/PaginationMetadata.cs +++ b/EstateReportingAPI.DataTrasferObjects/PaginationMetadata.cs @@ -1,9 +1,15 @@ namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing pagination metadata information. + public class PaginationMetadata { + /// One-based page number to return. public int PageNumber { get; set; } + /// Maximum number of items to include on a page. public int PageSize { get; set; } + /// Total number of matching items across all pages. public int TotalItems { get; set; } + /// Total number of pages for the result set. public int TotalPages { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/ResponseCode.cs b/EstateReportingAPI.DataTrasferObjects/ResponseCode.cs index cb79851..c558ad2 100644 --- a/EstateReportingAPI.DataTrasferObjects/ResponseCode.cs +++ b/EstateReportingAPI.DataTrasferObjects/ResponseCode.cs @@ -1,8 +1,12 @@ -namespace EstateReportingAPI.DataTrasferObjects{ +namespace EstateReportingAPI.DataTrasferObjects{ using System; + /// API data model describing response code information. + public class ResponseCode{ + /// The code value. public Int32 Code { get; set; } + /// The description value. public String Description { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/SortDirection.cs b/EstateReportingAPI.DataTrasferObjects/SortDirection.cs index ecea6f3..90d5812 100644 --- a/EstateReportingAPI.DataTrasferObjects/SortDirection.cs +++ b/EstateReportingAPI.DataTrasferObjects/SortDirection.cs @@ -1,7 +1,11 @@ -namespace EstateReportingAPI.DataTransferObjects; +namespace EstateReportingAPI.DataTransferObjects; + +/// API data model describing sort direction information. public enum SortDirection{ + /// Sort from the smallest value to the largest. Ascending, + /// Sort from the largest value to the smallest. Descending, -} \ No newline at end of file +} diff --git a/EstateReportingAPI.DataTrasferObjects/SortField.cs b/EstateReportingAPI.DataTrasferObjects/SortField.cs index 0e89422..0d3a323 100644 --- a/EstateReportingAPI.DataTrasferObjects/SortField.cs +++ b/EstateReportingAPI.DataTrasferObjects/SortField.cs @@ -1,9 +1,14 @@ -namespace EstateReportingAPI.DataTransferObjects; +namespace EstateReportingAPI.DataTransferObjects; + +/// API data model describing sort field information. public enum SortField{ + /// Sort by transaction amount. TransactionAmount = 1, + /// Sort by merchant name. MerchantName = 2, + /// Sort by operator name. OperatorName = 3 -} \ No newline at end of file +} diff --git a/EstateReportingAPI.DataTrasferObjects/TodaysSales.cs b/EstateReportingAPI.DataTrasferObjects/TodaysSales.cs index 0aafa8d..a630a4d 100644 --- a/EstateReportingAPI.DataTrasferObjects/TodaysSales.cs +++ b/EstateReportingAPI.DataTrasferObjects/TodaysSales.cs @@ -1,14 +1,21 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects { + /// API data model describing todays sales information. public class TodaysSales { + /// The todays average sales value value. public Decimal TodaysAverageSalesValue { get; set; } + /// The todays sales value value. public Decimal TodaysSalesValue { get; set; } + /// The todays sales count value. public Int32 TodaysSalesCount { get; set; } + /// The comparison sales value value. public Decimal ComparisonSalesValue { get; set; } + /// The comparison sales count value. public Int32 ComparisonSalesCount { get; set; } + /// The comparison average sales value value. public Decimal ComparisonAverageSalesValue { get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/TodaysSalesByHour.cs b/EstateReportingAPI.DataTrasferObjects/TodaysSalesByHour.cs index 41efb10..73a2b94 100644 --- a/EstateReportingAPI.DataTrasferObjects/TodaysSalesByHour.cs +++ b/EstateReportingAPI.DataTrasferObjects/TodaysSalesByHour.cs @@ -1,11 +1,18 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing todays sales by hour information. + public class TodaysSalesByHour{ + /// The hour value. public Int32 Hour{ get; set; } + /// The todays sales value value. public Decimal TodaysSalesValue { get; set; } + /// The comparison sales value value. public Decimal ComparisonSalesValue { get; set; } + /// The todays sales count value. public Int32 TodaysSalesCount { get; set; } + /// The comparison sales count value. public Int32 ComparisonSalesCount { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TodaysSettlement.cs b/EstateReportingAPI.DataTrasferObjects/TodaysSettlement.cs index a2b87cd..489def2 100644 --- a/EstateReportingAPI.DataTrasferObjects/TodaysSettlement.cs +++ b/EstateReportingAPI.DataTrasferObjects/TodaysSettlement.cs @@ -1,15 +1,25 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing todays settlement information. + public class TodaysSettlement { + /// The todays settlement value value. public Decimal TodaysSettlementValue { get; set; } + /// The todays pending settlement value value. public Decimal TodaysPendingSettlementValue { get; set; } + /// The todays settlement count value. public Int32 TodaysSettlementCount { get; set; } + /// The todays pending settlement count value. public Int32 TodaysPendingSettlementCount { get; set; } + /// The comparison settlement value value. public Decimal ComparisonSettlementValue { get; set; } + /// The comparison pending settlement value value. public Decimal ComparisonPendingSettlementValue { get; set; } + /// The comparison settlement count value. public Int32 ComparisonSettlementCount { get; set; } + /// The comparison pending settlement count value. public Int32 ComparisonPendingSettlementCount { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TopBottom.cs b/EstateReportingAPI.DataTrasferObjects/TopBottom.cs index 01184b1..4d59ab8 100644 --- a/EstateReportingAPI.DataTrasferObjects/TopBottom.cs +++ b/EstateReportingAPI.DataTrasferObjects/TopBottom.cs @@ -1,7 +1,10 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ + /// API data model describing top bottom information. public enum TopBottom { + /// Select the highest ranked values. Top = 0, + /// Select the lowest ranked values. Bottom = 1 } -} \ No newline at end of file +} diff --git a/EstateReportingAPI.DataTrasferObjects/TopBottomMerchantData.cs b/EstateReportingAPI.DataTrasferObjects/TopBottomMerchantData.cs index be3af06..a3cae75 100644 --- a/EstateReportingAPI.DataTrasferObjects/TopBottomMerchantData.cs +++ b/EstateReportingAPI.DataTrasferObjects/TopBottomMerchantData.cs @@ -1,9 +1,13 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing top bottom merchant data information. + public class TopBottomMerchantData { + /// The merchant name value. public String MerchantName { get; set; } + /// The sales value value. public Decimal SalesValue { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TopBottomOperatorData.cs b/EstateReportingAPI.DataTrasferObjects/TopBottomOperatorData.cs index ab20ea2..175ed16 100644 --- a/EstateReportingAPI.DataTrasferObjects/TopBottomOperatorData.cs +++ b/EstateReportingAPI.DataTrasferObjects/TopBottomOperatorData.cs @@ -1,9 +1,13 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing top bottom operator data information. + public class TopBottomOperatorData { + /// The operator name value. public String OperatorName { get; set; } + /// The sales value value. public Decimal SalesValue { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TopBottomProductData.cs b/EstateReportingAPI.DataTrasferObjects/TopBottomProductData.cs index 3a55482..8f0bebe 100644 --- a/EstateReportingAPI.DataTrasferObjects/TopBottomProductData.cs +++ b/EstateReportingAPI.DataTrasferObjects/TopBottomProductData.cs @@ -1,9 +1,13 @@ -namespace EstateReportingAPI.DataTransferObjects{ +namespace EstateReportingAPI.DataTransferObjects{ using System; + /// API data model describing top bottom product data information. + public class TopBottomProductData { + /// The product name value. public String ProductName { get; set; } + /// The sales value value. public Decimal SalesValue { get; set; } } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs index 1ba3c02..35427ec 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs @@ -1,24 +1,43 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing transaction detail information. + public class TransactionDetail { + /// The id value. public Guid Id { get; set; } + /// Date and time of the transaction. public DateTimeOffset DateTime { get; set; } + /// The merchant value. public String Merchant { get; set; } + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public Int32 MerchantReportingId { get; set; } + /// The operator value. public String Operator { get; set; } + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The operator reporting id value. public Int32 OperatorReportingId { get; set; } + /// The product value. public String Product { get; set; } + /// Unique identifier of the product. public Guid ProductId { get; set; } + /// The product reporting id value. public Int32 ProductReportingId { get; set; } + /// The type value. public String Type { get; set; } + /// The status value. public String Status { get; set; } + /// The value value. public Decimal Value { get; set; } + /// The total fees value. public Decimal TotalFees { get; set; } + /// The settlement reference value. public String SettlementReference { get; set; } + /// The transaction number value. public Int32 TransactionNumber { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs index aaedf3b..cda38d3 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs @@ -1,15 +1,24 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Request criteria for transaction detail report. + public class TransactionDetailReportRequest { + /// Optional operator reporting identifiers to include. public List? Operators { get; set; } + /// Optional merchant reporting identifiers to include. public List? Merchants { get; set; } + /// Optional product reporting identifiers to include. public List? Products { get; set; } + /// Inclusive start date of the report period. public DateOnly StartDate { get; set; } + /// Inclusive end date of the report period. public DateOnly EndDate { get; set; } + /// One-based page number to return. public int PageNumber { get; set; } = 1; + /// Maximum number of items to include on a page. public int PageSize { get; set; } = 50; } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs index 4e2c337..79fb3df 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs @@ -1,35 +1,61 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Response data for transaction detail report. + public class TransactionDetailReportResponse { + /// The transactions value. public List Transactions { get; set; } + /// The summary value. public TransactionDetailSummary Summary { get; set; } + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } +/// Response data for product performance. + public class ProductPerformanceResponse { + /// The product details value. public List ProductDetails { get; set; } + /// The summary value. public ProductPerformanceSummary Summary { get; set; } + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } +/// API data model describing product performance detail information. + public class ProductPerformanceDetail { + /// The product name value. public String ProductName { get; set; } + /// Unique identifier of the product. public Guid ProductId { get; set; } + /// The product reporting id value. public Int32 ProductReportingId { get; set; } + /// Unique identifier of the contract. public Guid ContractId { get; set; } + /// The contract reporting id value. public Int32 ContractReportingId { get; set; } + /// Number of transactions represented by this result. public Int32 TransactionCount { get; set; } + /// Total monetary value of the transactions represented by this result. public Decimal TransactionValue { get; set; } + /// The percentage of total value. public Decimal PercentageOfTotal { get; set; } } +/// API data model describing product performance summary information. + public class ProductPerformanceSummary { + /// The total products value. public Int32 TotalProducts { get; set; } + /// The total count value. public Int32 TotalCount { get; set; } + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// The average per product value. public Decimal AveragePerProduct { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs index 4192b15..2e5a577 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs @@ -1,10 +1,15 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing transaction detail summary information. + public class TransactionDetailSummary { + /// Total monetary value represented by this result. public Decimal TotalValue { get; set; } + /// The total fees value. public Decimal TotalFees { get; set; } + /// Number of transactions represented by this result. public Int32 TransactionCount { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryRequest.cs index 1a9bced..ba256a4 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryRequest.cs @@ -3,30 +3,48 @@ namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing transaction mix breakdown information. [JsonConverter(typeof(JsonStringEnumConverter))] public enum TransactionMixBreakdown { + /// Group transactions by product. Product, + /// Group transactions by transaction type. TransactionType, + /// Group transactions by operator. Operator, + /// Group transactions by transaction status. Status } +/// API data model describing transaction mix measure information. [JsonConverter(typeof(JsonStringEnumConverter))] public enum TransactionMixMeasure { + /// Compare groups by transaction count. Count, + /// Compare groups by transaction value. Value } +/// Request criteria for transaction mix summary. + public class TransactionMixSummaryRequest { + /// Numeric reporting identifier used to filter or identify a merchant. public int? MerchantReportingId { get; set; } + /// Inclusive start date of the report period. public DateOnly StartDate { get; set; } + /// Inclusive end date of the report period. public DateOnly EndDate { get; set; } + /// Dimension used to group transactions. public TransactionMixBreakdown Breakdown { get; set; } + /// Metric used to rank or compare transaction groups. public TransactionMixMeasure Measure { get; set; } + /// Maximum number of groups selected before pagination. public int TopN { get; set; } = 5; + /// One-based page number to return. public int PageNumber { get; set; } = 1; + /// Maximum number of items to include on a page. public int PageSize { get; set; } = 50; } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryResponse.cs b/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryResponse.cs index 34a2eea..f8c8116 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryResponse.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionMixSummaryResponse.cs @@ -3,44 +3,80 @@ namespace EstateReportingAPI.DataTransferObjects; +/// Response data for transaction mix summary. + public class TransactionMixSummaryResponse { + /// The from date value. public DateOnly FromDate { get; set; } + /// The to date value. public DateOnly ToDate { get; set; } + /// Dimension used to group transactions. public TransactionMixBreakdown Breakdown { get; set; } + /// Metric used to rank or compare transaction groups. public TransactionMixMeasure Measure { get; set; } + /// The total count value. public int TotalCount { get; set; } + /// Total monetary value represented by this result. public decimal TotalValue { get; set; } + /// The groups value. public List Groups { get; set; } = []; + /// The transactions value. public List Transactions { get; set; } = []; + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } +/// API data model describing transaction mix summary group information. + public class TransactionMixSummaryGroup { + /// The group key value. public string? GroupKey { get; set; } + /// The group name value. public string? GroupName { get; set; } + /// Number of transactions represented by this result. public int TransactionCount { get; set; } + /// Total monetary value of the transactions represented by this result. public decimal TransactionValue { get; set; } } +/// API data model describing transaction mix summary transaction information. + public class TransactionMixSummaryTransaction { + /// The id value. public Guid Id { get; set; } + /// Date and time of the transaction. public DateTimeOffset DateTime { get; set; } + /// The merchant value. public string? Merchant { get; set; } + /// Unique identifier of the merchant. public Guid MerchantId { get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public int MerchantReportingId { get; set; } + /// The operator value. public string? Operator { get; set; } + /// Unique identifier of the operator. public Guid OperatorId { get; set; } + /// The operator reporting id value. public int OperatorReportingId { get; set; } + /// The product value. public string? Product { get; set; } + /// Unique identifier of the product. public Guid ProductId { get; set; } + /// The product reporting id value. public int ProductReportingId { get; set; } + /// The type value. public string? Type { get; set; } + /// The status value. public string? Status { get; set; } + /// The value value. public decimal Value { get; set; } + /// The total fees value. public decimal TotalFees { get; set; } + /// The settlement reference value. public string? SettlementReference { get; set; } + /// The transaction number value. public int TransactionNumber { get; set; } } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs b/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs index 56f19bb..7e9d091 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs @@ -1,21 +1,37 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects{ + /// API data model describing transaction result information. + public class TransactionResult{ + /// The transaction amount value. public Decimal TransactionAmount{ get; set; } + /// The transaction id value. public Guid TransactionId{ get; set; } + /// The transaction reporting id value. public Int32 TransactionReportingId{ get; set; } + /// The is authorised value. public Boolean IsAuthorised{ get; set; } + /// The response code value. public String ResponseCode{ get; set; } + /// The response message value. public String ResponseMessage{ get; set; } + /// Date and time of the transaction. public DateTime TransactionDateTime{ get; set; } + /// The transaction source value. public String TransactionSource{ get; set; } + /// The operator name value. public String OperatorName{ get; set; } + /// The operator reporting id value. public Int32 OperatorReportingId{ get; set; } + /// The product value. public String Product{ get; set; } + /// The product reporting id value. public Int32 ProductReportingId{ get; set; } + /// The merchant name value. public String MerchantName{ get; set; } + /// Numeric reporting identifier used to filter or identify a merchant. public Int32 MerchantReportingId{ get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs index 854fbf3..3bc433a 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs @@ -1,21 +1,37 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Request criteria for transaction search. + public class TransactionSearchRequest{ + /// Optional operator reporting identifiers to include. + public List? Operators{ get; set; } + /// Optional inclusive transaction value range. + public ValueRange? ValueRange{ get; set; } + /// Optional merchant reporting identifiers to include. + public List? Merchants{ get; set; } + /// The query date value. + public DateTime QueryDate{ get; set; } + /// The response code value. + public String? ResponseCode{ get; set; } + /// The auth code value. + public String? AuthCode{ get; set; } + /// The transaction number value. + public String? TransactionNumber{ get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantRequest.cs index 81bf40e..984ec16 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantRequest.cs @@ -1,14 +1,22 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Request criteria for transaction summary by merchant. + public class TransactionSummaryByMerchantRequest { + /// Optional operator reporting identifiers to include. public List? Operators { get; set; } + /// Optional merchant reporting identifiers to include. public List? Merchants { get; set; } + /// Inclusive start date of the report period. public DateOnly StartDate { get; set; } + /// Inclusive end date of the report period. public DateOnly EndDate { get; set; } + /// One-based page number to return. public int PageNumber { get; set; } = 1; + /// Maximum number of items to include on a page. public int PageSize { get; set; } = 50; } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantResponse.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantResponse.cs index 770a5fb..f929ce4 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantResponse.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByMerchantResponse.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Response data for transaction summary by merchant. public class TransactionSummaryByMerchantResponse { + /// Optional merchant reporting identifiers to include. public List Merchants { get; set; } + /// The summary value. public MerchantDetailSummary Summary { get; set; } + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorRequest.cs index 6844567..5714f2a 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorRequest.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorRequest.cs @@ -1,13 +1,20 @@ -using System; +using System; using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Request criteria for transaction summary by operator. public class TransactionSummaryByOperatorRequest { + /// Optional operator reporting identifiers to include. public List? Operators { get; set; } + /// Optional merchant reporting identifiers to include. public List? Merchants { get; set; } + /// Inclusive start date of the report period. public DateOnly StartDate { get; set; } + /// Inclusive end date of the report period. public DateOnly EndDate { get; set; } + /// One-based page number to return. public int PageNumber { get; set; } = 1; + /// Maximum number of items to include on a page. public int PageSize { get; set; } = 50; } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorResponse.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorResponse.cs index bee7450..e430b0c 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorResponse.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSummaryByOperatorResponse.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace EstateReportingAPI.DataTransferObjects; +/// Response data for transaction summary by operator. public class TransactionSummaryByOperatorResponse { + /// Optional operator reporting identifiers to include. public List Operators { get; set; } + /// The summary value. public OperatorDetailSummary Summary { get; set; } + /// The pagination value. public PaginationMetadata Pagination { get; set; } = new(); } diff --git a/EstateReportingAPI.DataTrasferObjects/UnsettledFee.cs b/EstateReportingAPI.DataTrasferObjects/UnsettledFee.cs index 48bf6b9..4c8a796 100644 --- a/EstateReportingAPI.DataTrasferObjects/UnsettledFee.cs +++ b/EstateReportingAPI.DataTrasferObjects/UnsettledFee.cs @@ -1,11 +1,15 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects { + /// API data model describing unsettled fee information. public class UnsettledFee { + /// The dimension name value. public String DimensionName { get; set; } + /// The fees value value. public Decimal FeesValue { get; set; } + /// The fees count value. public Int32 FeesCount { get; set; } } } diff --git a/EstateReportingAPI.DataTrasferObjects/ValueRange.cs b/EstateReportingAPI.DataTrasferObjects/ValueRange.cs index 59d5b83..ead7177 100644 --- a/EstateReportingAPI.DataTrasferObjects/ValueRange.cs +++ b/EstateReportingAPI.DataTrasferObjects/ValueRange.cs @@ -1,8 +1,12 @@ -using System; +using System; namespace EstateReportingAPI.DataTransferObjects; +/// API data model describing value range information. + public class ValueRange{ + /// The start value value. public Decimal StartValue{ get; set; } + /// The end value value. public Decimal EndValue{ get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs b/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs index 566c9e8..fbf8d6d 100644 --- a/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc; using Shouldly; using SimpleResults; -using Swashbuckle.AspNetCore.SwaggerGen; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; diff --git a/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs b/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs index 971d664..d699b0d 100644 --- a/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs +++ b/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using EstateReportingAPI; using Shared.IntegrationTesting.TestContainers; using Shared.Serialisation; using SimpleResults; @@ -69,7 +70,7 @@ public virtual async ValueTask InitializeAsync() String dbConnString = GetLocalConnectionString($"TransactionProcessorReadModel-{this.TestId}"); - this.factory = new CustomWebApplicationFactory(dbConnString); + this.factory = new CustomWebApplicationFactory(dbConnString); this.factory.DefaultEstateId = this.TestId; this.Client = this.factory.CreateClient(); @@ -99,10 +100,21 @@ public virtual async ValueTask DisposeAsync() protected abstract Task SetupStandingData(); protected HttpClient Client; - protected CustomWebApplicationFactory factory; + protected CustomWebApplicationFactory factory; protected Guid TestId; + protected static string Serialize(T value) + { + IStringSerialiser serialiser = new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()); + return serialiser.Serialize(value); + } + + private static T Deserialize(string json) + { + IStringSerialiser serialiser = new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()); + return serialiser.Deserialize(json); + } internal async Task> CreateAndSendHttpRequestMessage(String url, CancellationToken cancellationToken) { HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, url); @@ -114,7 +126,7 @@ public virtual async ValueTask DisposeAsync() String content = await result.Content.ReadAsStringAsync(cancellationToken); content.ShouldNotBeNull(); - return Result.Success(StringSerialiser.Deserialise(content)); + return Result.Success(Deserialize(content)); } internal async Task> CreateAndSendHttpRequestMessage(String url, String payload, CancellationToken cancellationToken) @@ -131,7 +143,7 @@ public virtual async ValueTask DisposeAsync() String content = await result.Content.ReadAsStringAsync(cancellationToken); content.ShouldNotBeNull(); - return Result.Success(StringSerialiser.Deserialise(content)); + return Result.Success(Deserialize(content)); } //public static IContainer DatabaseServerContainer; @@ -201,3 +213,5 @@ public override async Task CreateSubscriptions(CancellationToken cancellationTok return sqlContainer.Item2.GetMappedPublicPort(1433); } } + + diff --git a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs index 1620885..be60817 100644 --- a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs +++ b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using EstateReportingAPI; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using TransactionProcessor.Database.Contexts; @@ -22,15 +23,13 @@ namespace EstateReportingAPI.IntegrationTests; using System.Text.Encodings.Web; using Microsoft.AspNetCore.TestHost; -public class CustomWebApplicationFactory : WebApplicationFactory where TStartup : class +public class CustomWebApplicationFactory : WebApplicationFactory where TEntryPoint : class { private string DatabaseConnectionString; public CustomWebApplicationFactory(string databaseConnectionString) { DatabaseConnectionString = databaseConnectionString; - Environment.SetEnvironmentVariable("InTestMode", "true"); - Environment.SetEnvironmentVariable("AppSettings__DisableAuthorisation", "true"); } public string DefaultUserId { get; set; } = "1"; @@ -38,6 +37,16 @@ public CustomWebApplicationFactory(string databaseConnectionString) protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.UseEnvironment("IntegrationTest"); + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["InTestMode"] = "true", + ["AppSettings:DisableAuthorisation"] = "true" + }); + }); + builder.ConfigureServices(containerBuilder => { var createcontext = new EstateManagementContext(DatabaseConnectionString); @@ -157,3 +166,4 @@ protected override Task HandleAuthenticateAsync() return Task.FromResult(result); } } + diff --git a/EstateReportingAPI.IntegrationTests/EstateReportingAPI.IntegrationTests.csproj b/EstateReportingAPI.IntegrationTests/EstateReportingAPI.IntegrationTests.csproj index 3770a01..fcf6bd5 100644 --- a/EstateReportingAPI.IntegrationTests/EstateReportingAPI.IntegrationTests.csproj +++ b/EstateReportingAPI.IntegrationTests/EstateReportingAPI.IntegrationTests.csproj @@ -11,7 +11,6 @@ - diff --git a/EstateReportingAPI.IntegrationTests/TransactionMixSummaryEndpointTests.cs b/EstateReportingAPI.IntegrationTests/TransactionMixSummaryEndpointTests.cs index 451b3fa..6d87fa7 100644 --- a/EstateReportingAPI.IntegrationTests/TransactionMixSummaryEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/TransactionMixSummaryEndpointTests.cs @@ -109,7 +109,7 @@ public async Task TransactionMixSummary_ProductBreakdown_ReturnsGroupedResults() TopN = 5 }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); var result = await this.CreateAndSendHttpRequestMessage($"{BaseRoute}/transactionmixsummary", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -146,7 +146,7 @@ public async Task TransactionMixSummary_InvalidDateRange_ReturnsBadRequest() TopN = 5 }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); HttpRequestMessage requestMessage = new(HttpMethod.Post, $"{BaseRoute}/transactionmixsummary"); requestMessage.Headers.Add("estateId", this.TestId.ToString()); requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Test"); @@ -160,3 +160,4 @@ public async Task TransactionMixSummary_InvalidDateRange_ReturnsBadRequest() problemDocument.RootElement.GetProperty("detail").GetString().ShouldBe("startDate must be less than or equal to endDate."); } } + diff --git a/EstateReportingAPI.IntegrationTests/TransactionsEndpointTests.cs b/EstateReportingAPI.IntegrationTests/TransactionsEndpointTests.cs index 0c40c80..06f2636 100644 --- a/EstateReportingAPI.IntegrationTests/TransactionsEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/TransactionsEndpointTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -522,7 +522,7 @@ public async Task TransactionsEndpoint_TransactionDetailReport_NoFilters_Transac Products = [] }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionDetailReport", payload, CancellationToken.None); @@ -595,7 +595,7 @@ public async Task TransactionsEndpoint_TransactionDetailReport_MerchantFilter_Tr Products = [] }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionDetailReport", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -670,7 +670,7 @@ public async Task TransactionsEndpoint_TransactionDetailReport_OperatorFilter_Tr Products = [] }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionDetailReport", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -744,7 +744,7 @@ public async Task TransactionsEndpoint_TransactionDetailReport_ProductFilter_Tra Products = productsForFilter.Select(c => c.contractProductReportingId).ToList(), }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); Result result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionDetailReport", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -868,7 +868,7 @@ public async Task TransactionsEndpoint_TransactionSummaryByMerchantReport_NoFilt Products = [] }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); var result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionsummarybymerchantreport", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -1006,7 +1006,7 @@ public async Task TransactionsEndpoint_TransactionSummaryByOperatorReport_NoFilt Products = [] }; - String payload = StringSerialiser.Serialise(request); + String payload = Serialize(request); var result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionsummarybyoperatorreport", payload, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -1311,7 +1311,7 @@ public async Task TransactionsEndpoint_MerchantDailyPerformanceSummary_ReturnsMe var result = await this.CreateAndSendHttpRequestMessage( $"{this.BaseRoute}/merchantdailyperformancesummary", - StringSerialiser.Serialise(request), + Serialize(request), CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -1446,7 +1446,7 @@ await this.helper.BuildTransactionX( var result = await this.CreateAndSendHttpRequestMessage( $"{this.BaseRoute}/merchantdailyperformancesummary", - StringSerialiser.Serialise(request), + Serialize(request), CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -1511,7 +1511,7 @@ await this.helper.BuildTransactionX( var result = await this.CreateAndSendHttpRequestMessage( $"{this.BaseRoute}/merchantdailyperformancesummary", - StringSerialiser.Serialise(request), + Serialize(request), CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -1739,3 +1739,4 @@ await this.helper.BuildTransactionX(reportDate.AddHours(9), otherMerchant.Mercha AssertRecentActivityReceiptItemMatches(items[0], transactions[1], product.productName, contract.operatorName, "receipt-002"); } } + diff --git a/EstateReportingAPI.Tests/BusinessLogic/DbContextResolverXTests.cs b/EstateReportingAPI.Tests/BusinessLogic/DbContextResolverXTests.cs index f6a9fa2..1a56600 100644 --- a/EstateReportingAPI.Tests/BusinessLogic/DbContextResolverXTests.cs +++ b/EstateReportingAPI.Tests/BusinessLogic/DbContextResolverXTests.cs @@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using System.Collections.Generic; using Shared.EntityFramework; -using Shared.General; using Shouldly; using TransactionProcessor.Database.Contexts; using Xunit; @@ -25,11 +25,8 @@ public void Resolve_WhenEstateSuffixIsProvided_UsesTheSuffixedCatalog() }) .Build(); - ConfigurationReader.Initialise(configuration); - using ServiceProvider rootProvider = new ServiceCollection().BuildServiceProvider(); - DbContextResolverX resolver = - new(rootProvider, configuration, new QueryTimingInterceptor()); + DbContextResolver resolver = new(rootProvider, configuration); using ResolvedDbContext resolvedContext = resolver.Resolve("TransactionProcessorReadModel", "estate-123"); diff --git a/EstateReportingAPI.Tests/EstateReportingAPI.Tests.csproj b/EstateReportingAPI.Tests/EstateReportingAPI.Tests.csproj index 9ca8162..4a3a068 100644 --- a/EstateReportingAPI.Tests/EstateReportingAPI.Tests.csproj +++ b/EstateReportingAPI.Tests/EstateReportingAPI.Tests.csproj @@ -20,7 +20,6 @@ - diff --git a/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs b/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs index e8b16c8..5492ae4 100644 --- a/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs @@ -5,15 +5,15 @@ using SimpleResults; using System.Diagnostics.CodeAnalysis; using EstateReportingAPI.BusinessLogic.Queries; -using Shared.General; +using Microsoft.Extensions.Configuration; namespace EstateReportingAPI.Bootstrapper; [ExcludeFromCodeCoverage] public class MediatorRegistry : ServiceRegistry { - public MediatorRegistry() { + public MediatorRegistry(IConfiguration configuration) { this.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(EstateRequestHandler).Assembly)); - this.AddSingleton>(container => (serviceName) => ConfigurationReader.GetBaseServerUri(serviceName).OriginalString); + this.AddSingleton>(_ => serviceName => new Uri(configuration[$"AppSettings:{serviceName}"]!).OriginalString); } } diff --git a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs index ff57258..b71c4ed 100644 --- a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs @@ -1,12 +1,16 @@ using HealthMonitoring.Client; +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Shared.Middleware; +using TransactionProcessing.ApiDefaults; namespace EstateReportingAPI.Bootstrapper{ using EstateReportingAPI.Common; using Lamar; using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenIddict.Validation.AspNetCore; - using Shared.General; using Shared.Serialisation; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -14,16 +18,53 @@ namespace EstateReportingAPI.Bootstrapper{ [ExcludeFromCodeCoverage] public class MiddlewareRegistry : ServiceRegistry{ - public MiddlewareRegistry() + public MiddlewareRegistry(IConfiguration configuration) { - this.ConfigureHealthChecks(); - this.ConfigureAuthentication(); + this.ConfigureHealthChecks(configuration); + this.ConfigureAuthentication(configuration); this.ConfigureControllers(); - this.ConfigureMiddlewareLogging(); + this.ConfigureMiddlewareLogging(configuration); this.ConfigureJsonOptions(); - this.AddHealthMonitoringRegistration(Startup.Configuration); + this.AddHealthMonitoringRegistration(configuration); this.AddScoped(); this.AddScoped(sp => sp.GetRequiredService()); + this.Configure(options => + { + options.Title = "Estate Reporting"; + options.Description = "API providing a variety of reporting facilities consumed by various reporting applications"; + }); + this.AddApiVersioning(options => + { + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + options.DefaultApiVersion = ApiVersions.V1; + options.AssumeDefaultVersionWhenUnspecified = false; + options.ReportApiVersions = true; + }).AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }).AddOpenApi(options => + { + options.Document.AddSchemaTransformer((schema, context, _) => + { + XmlDocumentationSchemaTransformer.Apply(schema, context); + return Task.CompletedTask; + }); + options.Document.AddOperationTransformer((operation, context, _) => + { + OpenApiExamplesTransformer.Apply(operation, context); + return Task.CompletedTask; + }); + options.Document.AddDocumentTransformer((document, context, _) => + { + ApiDefaultsOptions metadata = context.ApplicationServices + .GetRequiredService>() + .Value; + document.Info.Title = metadata.Title; + document.Info.Description = metadata.Description; + return Task.CompletedTask; + }); + }); } @@ -34,8 +75,8 @@ private void ConfigureJsonOptions() }); } - private void ConfigureHealthChecks(){ - var connectionStringSection = Startup.Configuration.GetSection("ConnectionStrings"); + private void ConfigureHealthChecks(IConfiguration configuration){ + var connectionStringSection = configuration.GetSection("ConnectionStrings"); if (connectionStringSection.Exists() == false) { this.AddHealthChecks(); @@ -43,7 +84,7 @@ private void ConfigureHealthChecks(){ else { this.AddHealthChecks().AddSqlServer( - connectionString: ConfigurationReader.GetConnectionString("HealthCheck"), + connectionString: configuration.GetConnectionString("HealthCheck"), healthQuery: "SELECT 1;", name: "Read Model Server", failureStatus: HealthStatus.Degraded, @@ -51,9 +92,9 @@ private void ConfigureHealthChecks(){ } } - private void ConfigureAuthentication(){ - String? inTestMode = Environment.GetEnvironmentVariable("InTestMode"); - if (String.Compare(inTestMode, Boolean.TrueString, StringComparison.InvariantCultureIgnoreCase) != 0){ + private void ConfigureAuthentication(IConfiguration configuration){ + bool inTestMode = configuration.GetValue("InTestMode", false); + if (!inTestMode){ this.AddAuthentication(options => { options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; @@ -62,7 +103,7 @@ private void ConfigureAuthentication(){ .AddValidation(options => { // Same as your Authority - options.SetIssuer(new Uri(ConfigurationReader.GetValue("SecurityConfiguration", "Authority"))); + options.SetIssuer(new Uri(configuration["SecurityConfiguration:Authority"]!)); // Enables discovery and HTTP backchannel support options.UseSystemNetHttp() @@ -77,7 +118,7 @@ private void ConfigureAuthentication(){ options.UseAspNetCore(); // Optionally set expected audience(s): - options.AddAudiences(ConfigurationReader.GetValue("SecurityConfiguration", "ApiName")); + options.AddAudiences(configuration["SecurityConfiguration:ApiName"]!); }); @@ -92,10 +133,10 @@ private void ConfigureControllers() { this.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); } - private void ConfigureMiddlewareLogging(){ - bool logRequests = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogRequests", true); - bool logResponses = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogResponses", true); - LogLevel middlewareLogLevel = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "MiddlewareLogLevel", LogLevel.Warning); + private void ConfigureMiddlewareLogging(IConfiguration configuration){ + bool logRequests = configuration.GetValue("MiddlewareLogging:LogRequests", true); + bool logResponses = configuration.GetValue("MiddlewareLogging:LogResponses", true); + LogLevel middlewareLogLevel = configuration.GetValue("MiddlewareLogging:MiddlewareLogLevel", LogLevel.Warning); RequestResponseMiddlewareLoggingConfig config = new RequestResponseMiddlewareLoggingConfig(middlewareLogLevel, logRequests, logResponses); @@ -121,8 +162,8 @@ public class SerialiserRegistry : ServiceRegistry public SerialiserRegistry() { this.AddSingleton(); - this.AddSingleton>(_ => obj => StringSerialiser.Serialise(obj)); - this.AddSingleton>(_ => (str, type) => StringSerialiser.DeserializeObject(str, type)); + this.AddSingleton>(container => obj => container.GetRequiredService().Serialize(obj)); + this.AddSingleton>(container => (json, type) => container.GetRequiredService().DeserializeObject(json, type)); var serialiserSettings = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); diff --git a/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs new file mode 100644 index 0000000..a6e6269 --- /dev/null +++ b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs @@ -0,0 +1,290 @@ +using System.Collections; +using System.Reflection; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; +using System.Text.Json; + +namespace EstateReportingAPI.Bootstrapper; + +/// +/// Adds representative request and response payloads to the generated OpenAPI document. +/// +internal static class OpenApiExamplesTransformer +{ + public static void Apply(OpenApiOperation operation, OpenApiOperationTransformerContext context) + { + AddParameterExamples(operation, context); + AddRequestBodyExample(operation, context); + AddSuccessResponseExample(operation, context); + + string path = context.Description.RelativePath?.TrimEnd('/') ?? string.Empty; + if (path.EndsWith("/transactiondetailreport", StringComparison.OrdinalIgnoreCase)) + { + SetRequestExample(operation, """ + { + "start_date": "2026-10-01", + "end_date": "2026-10-21", + "operators": [12], + "merchants": [1042], + "products": [7], + "page_number": 1, + "page_size": 50 + } + """); + SetResponseExample(operation, """ + { + "transactions": [ + { + "id": "c6e8c2ba-bb6d-4fe5-aad9-62444fc13a70", + "date_time": "2026-10-21T10:32:15+01:00", + "merchant": "Central Market", + "merchant_id": "ae24b06c-ef12-4c12-a1f7-73112ba18a31", + "merchant_reporting_id": 1042, + "operator": "Example Operator", + "operator_id": "6942e4b8-e2ec-45f7-bd89-d9c75ac27c1f", + "operator_reporting_id": 12, + "product": "Lunch special", + "product_id": "e2b72da1-50dc-49b9-91ae-992a01b98164", + "product_reporting_id": 7, + "type": "Sale", + "status": "Authorised", + "value": 18.5, + "total_fees": 0.42, + "settlement_reference": "SET-20261021-0042", + "transaction_number": 42018 + } + ], + "summary": { + "total_value": 18.5, + "total_fees": 0.42, + "transaction_count": 1 + }, + "pagination": { + "page_number": 1, + "page_size": 50, + "total_items": 1, + "total_pages": 1 + } + } + """); + } + else if (path.EndsWith("/transactionmixsummary", StringComparison.OrdinalIgnoreCase)) + { + SetRequestExample(operation, """ + { + "merchant_reporting_id": 1042, + "start_date": "2026-10-01", + "end_date": "2026-10-21", + "breakdown": "Product", + "measure": "Value", + "top_n": 5, + "page_number": 1, + "page_size": 50 + } + """); + SetResponseExample(operation, """ + { + "from_date": "2026-10-01", + "to_date": "2026-10-21", + "breakdown": "Product", + "measure": "Value", + "total_count": 83, + "total_value": 1274.5, + "groups": [ + { + "group_key": "7", + "group_name": "Lunch special", + "transaction_count": 31, + "transaction_value": 542.5 + } + ], + "transactions": [], + "pagination": { + "page_number": 1, + "page_size": 50, + "total_items": 1, + "total_pages": 1 + } + } + """); + } + } + + private static void AddParameterExamples(OpenApiOperation operation, OpenApiOperationTransformerContext context) + { + if (operation.Parameters is not { } parameters) + { + return; + } + + foreach (OpenApiParameter parameter in parameters) + { + if (parameter.Example is not null) + { + continue; + } + + ApiParameterDescription? description = context.Description.ParameterDescriptions + .FirstOrDefault(item => string.Equals(item.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + Type? parameterType = description?.ModelMetadata?.ModelType; + if (parameterType is not null) + { + parameter.Example = CreateExample(parameterType, parameter.Name); + } + } + } + + private static void AddRequestBodyExample(OpenApiOperation operation, OpenApiOperationTransformerContext context) + { + if (operation.RequestBody is null) + { + return; + } + + Type? bodyType = context.Description.ParameterDescriptions + .FirstOrDefault(parameter => parameter.Source?.Id == "Body")? + .ModelMetadata?.ModelType; + if (bodyType is null || !operation.RequestBody.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + { + return; + } + + mediaType.Example ??= CreateExample(bodyType, "request"); + } + + private static void AddSuccessResponseExample(OpenApiOperation operation, OpenApiOperationTransformerContext context) + { + ApiResponseType? successResponse = context.Description.SupportedResponseTypes + .FirstOrDefault(response => response.StatusCode == StatusCodes.Status200OK && response.Type is not null); + if (successResponse?.Type is null + || !operation.Responses.TryGetValue(StatusCodes.Status200OK.ToString(), out IOpenApiResponse response)) + { + return; + } + + if (response.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + { + mediaType.Example ??= CreateExample(successResponse.Type, "response"); + } + } + + private static JsonNode? CreateExample(Type type, string propertyName, int depth = 0) + { + Type? nullableType = Nullable.GetUnderlyingType(type); + if (nullableType is not null) + { + type = nullableType; + } + + if (type == typeof(string)) + { + string name = propertyName.ToLowerInvariant(); + string value = name switch + { + var n when n.Contains("email") => "alex@example.com", + var n when n.Contains("status") => "Authorised", + var n when n.Contains("type") => "Sale", + var n when n.Contains("reference") => "REF-20261021-0042", + var n when n.Contains("description") => "Example description", + var n when n.Contains("name") || n.Contains("product") || n.Contains("merchant") || n.Contains("operator") => "Example " + propertyName, + var n when n.Contains("address") || n.Contains("town") || n.Contains("region") || n.Contains("country") || n.Contains("postcode") => "Example value", + _ => "Example value" + }; + return JsonValue.Create(value); + } + + if (type == typeof(Guid)) + { + return JsonValue.Create("c6e8c2ba-bb6d-4fe5-aad9-62444fc13a70"); + } + + if (type == typeof(DateOnly)) + { + return JsonValue.Create("2026-10-21"); + } + + if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + { + return JsonValue.Create("2026-10-21T10:32:15+01:00"); + } + + if (type == typeof(TimeOnly) || type == typeof(TimeSpan)) + { + return JsonValue.Create("09:00:00"); + } + + if (type == typeof(bool)) + { + return JsonValue.Create(true); + } + + if (type.IsEnum) + { + string enumValue = propertyName.Contains("measure", StringComparison.OrdinalIgnoreCase) + ? "Value" + : Enum.GetNames(type).FirstOrDefault() ?? "Unknown"; + return JsonValue.Create(enumValue); + } + + if (type == typeof(decimal) || type == typeof(double) || type == typeof(float)) + { + return JsonValue.Create(18.50m); + } + + if (type == typeof(byte) || type == typeof(short) || type == typeof(int) || type == typeof(long) + || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) + { + string name = propertyName.ToLowerInvariant(); + long value = name.Contains("page") && name.Contains("number") ? 1 + : name.Contains("year") ? 2026 + : name.Contains("hour") ? 10 + : name.Contains("count") || name.Contains("total") ? 3 + : 1001; + return JsonValue.Create(value); + } + + if (type.IsArray) + { + return new JsonArray(CreateExample(type.GetElementType()!, propertyName, depth + 1)); + } + + if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType) + { + return new JsonArray(CreateExample(type.GetGenericArguments()[0], propertyName, depth + 1)); + } + + if (depth >= 5 || type.IsAbstract || type.IsInterface) + { + return null; + } + + JsonObject result = new(); + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(property => property.CanRead && property.GetIndexParameters().Length == 0)) + { + string jsonName = JsonNamingPolicy.SnakeCaseLower.ConvertName(property.Name); + result[jsonName] = CreateExample(property.PropertyType, property.Name, depth + 1); + } + + return result; + } + + private static void SetRequestExample(OpenApiOperation operation, string json) + { + if (operation.RequestBody?.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType) == true) + { + mediaType.Example = JsonNode.Parse(json); + } + } + + private static void SetResponseExample(OpenApiOperation operation, string json) + { + if (operation.Responses.TryGetValue("200", out IOpenApiResponse response) + && response.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + { + mediaType.Example = JsonNode.Parse(json); + } + } +} diff --git a/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs b/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs index de064b6..3a02097 100644 --- a/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs @@ -9,13 +9,15 @@ namespace EstateReportingAPI.Bootstrapper; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Shared.EntityFramework; using Shared.General; using System.Diagnostics.CodeAnalysis; [ExcludeFromCodeCoverage] public class RepositoryRegistry : ServiceRegistry{ - public RepositoryRegistry() { + public RepositoryRegistry(IConfiguration configuration, IHostEnvironment environment) { this.AddSingleton(_ => new ReportingDatePolicy(TimeProvider.System)); this.AddSingleton(); this.AddSingleton(); @@ -24,23 +26,17 @@ public RepositoryRegistry() { this.AddSingleton(); this.AddSingleton(); - this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolverX<>)); + this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolver<>)); - if (Startup.WebHostEnvironment.IsEnvironment("IntegrationTest") || Startup.Configuration.GetValue("ServiceOptions:UseInMemoryDatabase") == true) { + if (environment.IsEnvironment("IntegrationTest") || configuration.GetValue("ServiceOptions:UseInMemoryDatabase")) { this.AddDbContext(builder => { builder.UseInMemoryDatabase("TransactionProcessorReadModel"); }); } else { - SqlServerRetryOptions retryOptions; - try { - retryOptions = ConfigurationReader.GetSection("AppSettings:SqlServerRetry"); - } - catch (KeyNotFoundException) { - retryOptions = null; - } + SqlServerRetryOptions? retryOptions = configuration.GetSection("AppSettings:SqlServerRetry").Get(); if (retryOptions != null) { this.AddDbContext(options => { - options.UseSharedSqlServer(ConfigurationReader.GetConnectionString("TransactionProcessorReadModel"), retry => { + options.UseSharedSqlServer(configuration.GetConnectionString("TransactionProcessorReadModel")!, retry => { retry.AdditionalTransientErrorNumbers = retryOptions.AdditionalTransientErrorNumbers; retry.MaxRetryCount = retryOptions.MaxRetryCount; retry.MaxRetryDelay = retryOptions.MaxRetryDelay; @@ -48,7 +44,7 @@ public RepositoryRegistry() { }); } else { - this.AddDbContext(options => { options.UseSqlServer(ConfigurationReader.GetConnectionString("TransactionProcessorReadModel"), retry => { retry.EnableRetryOnFailure(); }); }); + this.AddDbContext(options => { options.UseSqlServer(configuration.GetConnectionString("TransactionProcessorReadModel")!, retry => { retry.EnableRetryOnFailure(); }); }); } } diff --git a/EstateReportingAPI/Bootstrapper/SharedLibraryCompatibility.cs b/EstateReportingAPI/Bootstrapper/SharedLibraryCompatibility.cs new file mode 100644 index 0000000..697e35f --- /dev/null +++ b/EstateReportingAPI/Bootstrapper/SharedLibraryCompatibility.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace EstateReportingAPI.Bootstrapper; + +/// +/// Initializes legacy shared-library static APIs that are still used by shared middleware. +/// Remove this adapter when Shared exposes instance-based configuration and logging APIs. +/// +internal static class SharedLibraryCompatibility +{ + public static void InitializeConfiguration(IConfigurationRoot configuration) + { + Shared.General.ConfigurationReader.Initialise(configuration); + } + + public static void InitializeLogging(ILogger logger) + { + Shared.Logger.Logger.Initialise(logger); + } +} diff --git a/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs b/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs new file mode 100644 index 0000000..8384084 --- /dev/null +++ b/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs @@ -0,0 +1,96 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Xml.Linq; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace EstateReportingAPI.Bootstrapper; + +/// +/// Applies XML summaries to OpenAPI schemas because the API uses the versioned +/// AddOpenApi registration, which does not run the ASP.NET Core XML-doc source generator. +/// +internal static class XmlDocumentationSchemaTransformer +{ + private static readonly ConcurrentDictionary> DocumentationByAssembly = new(); + + public static void Apply(OpenApiSchema schema, OpenApiSchemaTransformerContext context) + { + Type? type = context.JsonTypeInfo?.Type; + if (type is null) + { + return; + } + + MemberInfo? member = context.JsonPropertyInfo?.AttributeProvider as MemberInfo; + Assembly documentationAssembly = member?.DeclaringType?.Assembly ?? type.Assembly; + IReadOnlyDictionary documentation = DocumentationByAssembly.GetOrAdd(documentationAssembly, LoadDocumentation); + string? memberId = member is not null ? GetMemberId(member) : $"T:{GetTypeName(type)}"; + + if (memberId is not null && documentation.TryGetValue(memberId, out string? summary)) + { + schema.Description = summary; + } + } + + private static IReadOnlyDictionary LoadDocumentation(Assembly assembly) + { + string? assemblyPath = assembly.Location; + if (string.IsNullOrEmpty(assemblyPath)) + { + return new Dictionary(); + } + + string documentationPath = Path.ChangeExtension(assemblyPath, ".xml"); + if (!File.Exists(documentationPath)) + { + return new Dictionary(); + } + + try + { + return XDocument.Load(documentationPath) + .Descendants("member") + .Select(element => new + { + Id = (string?)element.Attribute("name"), + Summary = NormalizeSummary(element.Element("summary")) + }) + .Where(member => !string.IsNullOrWhiteSpace(member.Id) && !string.IsNullOrWhiteSpace(member.Summary)) + .ToDictionary(member => member.Id!, member => member.Summary!, StringComparer.Ordinal); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or System.Xml.XmlException or ArgumentException) + { + return new Dictionary(); + } + } + + private static string? GetMemberId(MemberInfo member) + { + return member switch + { + PropertyInfo property => $"P:{GetTypeName(property.DeclaringType!)}.{property.Name}", + FieldInfo field => $"F:{GetTypeName(field.DeclaringType!)}.{field.Name}", + _ => null + }; + } + + private static string GetTypeName(Type type) + { + if (type.IsGenericType) + { + string genericName = type.GetGenericTypeDefinition().FullName!; + int aritySeparator = genericName.IndexOf('`'); + string baseName = aritySeparator < 0 ? genericName : genericName[..aritySeparator]; + return $"{baseName}{{{string.Join(",", type.GetGenericArguments().Select(GetTypeName))}}}"; + } + + return type.FullName!.Replace('+', '.'); + } + + private static string? NormalizeSummary(XElement? summary) + { + string text = string.Join(" ", summary?.Value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) ?? []); + return string.IsNullOrWhiteSpace(text) ? null : text; + } +} diff --git a/EstateReportingAPI/Common/EstateContext.cs b/EstateReportingAPI/Common/EstateContext.cs index c474575..19a173d 100644 --- a/EstateReportingAPI/Common/EstateContext.cs +++ b/EstateReportingAPI/Common/EstateContext.cs @@ -65,9 +65,13 @@ public async Task InvokeAsync( await HandleUnauthenticatedRequestAsync(httpContext, estateContext, headerEstateId); } - private static bool IsAnonymousEndpoint(HttpContext httpContext) - { - return httpContext.GetEndpoint()?.Metadata.GetMetadata() != null; + private static bool IsAnonymousEndpoint(HttpContext httpContext) { + if (httpContext.GetEndpoint()?.Metadata.GetMetadata() != null) + return true; + + PathString path = httpContext.Request.Path; + return path.Equals("/scalar/", StringComparison.OrdinalIgnoreCase) + || path.StartsWithSegments("/openapi", StringComparison.OrdinalIgnoreCase); } private static bool TryGetHeaderEstateId(HttpContext httpContext, out Guid? estateId) diff --git a/EstateReportingAPI/Endpoints/CalendarEndpoints.cs b/EstateReportingAPI/Endpoints/CalendarEndpoints.cs index 3e3fd4f..22a2ff9 100644 --- a/EstateReportingAPI/Endpoints/CalendarEndpoints.cs +++ b/EstateReportingAPI/Endpoints/CalendarEndpoints.cs @@ -1,7 +1,7 @@ -using EstateReportingAPI.DataTrasferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTrasferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -9,18 +9,20 @@ public static class CalendarEndpoints { private const string BaseRoute = "api/calendars"; - public static void MapCalendarEndpoints(this IEndpointRouteBuilder app) { + public static void MapCalendarEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Calendars"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("/comparisondates", CalenderHandler.GetCalendarComparisonDates) - .WithStandardProduces>(); + .WithSummary("List available comparison dates") + .WithDescription("Returns yesterday, the equivalent dates from the previous week and month, and calendar dates available for this estate.") + .WithApiResponse>(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/ContractEndpoints.cs b/EstateReportingAPI/Endpoints/ContractEndpoints.cs index 26210f8..891cf35 100644 --- a/EstateReportingAPI/Endpoints/ContractEndpoints.cs +++ b/EstateReportingAPI/Endpoints/ContractEndpoints.cs @@ -1,7 +1,7 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -9,21 +9,27 @@ public static class ContractEndpoints { private const string BaseRoute = "api/contracts"; - public static void MapContractEndpoints(this IEndpointRouteBuilder app) + public static void MapContractEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Contracts"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("/recent", ContractHandler.GetRecentContracts) - .WithStandardProduces>(); + .WithSummary("List recently created contracts") + .WithDescription("Returns the most recently created contracts for the current estate.") + .WithApiResponse>(); group.MapGet("/", ContractHandler.GetContracts) - .WithStandardProduces>(); + .WithSummary("List contracts") + .WithDescription("Returns contracts and their configured products and transaction fees for the current estate.") + .WithApiResponse>(); group.MapGet("/{contractId}", ContractHandler.GetContract) - .WithStandardProduces>(); + .WithSummary("Get a contract") + .WithDescription("Returns the specified contract, including its products and transaction fees.") + .WithApiResponse(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/EndpointDocumentationExtensions.cs b/EstateReportingAPI/Endpoints/EndpointDocumentationExtensions.cs new file mode 100644 index 0000000..88d1ac0 --- /dev/null +++ b/EstateReportingAPI/Endpoints/EndpointDocumentationExtensions.cs @@ -0,0 +1,12 @@ +using Shared.Extensions; +using Shared.Middleware; + +namespace EstateReportingAPI.Endpoints; + +internal static class EndpointDocumentationExtensions +{ + public static RouteHandlerBuilder WithApiResponse(this RouteHandlerBuilder builder) + { + return builder.WithStandardProduces(); + } +} diff --git a/EstateReportingAPI/Endpoints/EstateEndpoints.cs b/EstateReportingAPI/Endpoints/EstateEndpoints.cs index ec91b24..8291b04 100644 --- a/EstateReportingAPI/Endpoints/EstateEndpoints.cs +++ b/EstateReportingAPI/Endpoints/EstateEndpoints.cs @@ -1,7 +1,7 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -9,19 +9,23 @@ public static class EstateEndpoints { private const string BaseRoute = "api/estates"; - public static void MapEstateEndpoints(this IEndpointRouteBuilder app) + public static void MapEstateEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Estates"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("/", EstateHandler.GetEstate) - .WithStandardProduces(); + .WithSummary("Get the current estate") + .WithDescription("Returns estate details and its associated operators, merchants, contracts, and users.") + .WithApiResponse(); group.MapGet("/operators", EstateHandler.GetOperators) - .WithStandardProduces>(); + .WithSummary("List estate operators") + .WithDescription("Returns operators associated with the current estate.") + .WithApiResponse>(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs b/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs index e43cc89..f9036c3 100644 --- a/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs +++ b/EstateReportingAPI/Endpoints/FileImportLogEndpoints.cs @@ -1,7 +1,7 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -9,19 +9,25 @@ public static class FileImportLogEndpoints { private const string BaseRoute = "api/fileimportlogs"; - public static void MapFileImportLogEndpoints(this IEndpointRouteBuilder app) + public static void MapFileImportLogEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute).WithTags("File Import Logs"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } - group.MapGet("/", FileImportHandler.GetFileImportLogList).WithStandardProduces>(); + group.MapGet("/", FileImportHandler.GetFileImportLogList) + .WithSummary("List file import logs") + .WithDescription("Returns file import logs within the requested date range, optionally filtered by merchant.") + .WithApiResponse>(); - group.MapGet("/{fileimportlogid}", FileImportHandler.GetFileImportLog).WithStandardProduces(); + group.MapGet("/{fileimportlogid}", FileImportHandler.GetFileImportLog) + .WithSummary("Get a file import log") + .WithDescription("Returns one file import log, including its uploaded files and line results. An optional merchant filter can be supplied.") + .WithApiResponse(); } } @@ -29,16 +35,19 @@ public static class FileProfileConfigurationEndpoints { private const string BaseRoute = "api/fileprofiles"; - public static void MapFileProfileConfigurationEndpoints(this IEndpointRouteBuilder app) + public static void MapFileProfileConfigurationEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute).WithTags("File Profile Configurations"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } - group.MapGet("/", FileProfileConfigurationHandler.GetFileProfileConfigurationList).WithStandardProduces>(); + group.MapGet("/", FileProfileConfigurationHandler.GetFileProfileConfigurationList) + .WithSummary("List file profile configurations") + .WithDescription("Returns the file import profile configurations available to the current estate.") + .WithApiResponse>(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/MerchantEndpoints.cs b/EstateReportingAPI/Endpoints/MerchantEndpoints.cs index 58ccb13..99d94bd 100644 --- a/EstateReportingAPI/Endpoints/MerchantEndpoints.cs +++ b/EstateReportingAPI/Endpoints/MerchantEndpoints.cs @@ -1,8 +1,8 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.DataTrasferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -10,33 +10,51 @@ public static class MerchantEndpoints { private const string BaseRoute = "api/merchants"; - public static void MapMerchantEndpoints(this IEndpointRouteBuilder app) + public static void MapMerchantEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Merchants"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("/kpis", MerchantHandler.GetMerchantKpis) - .WithStandardProduces>(); + .WithSummary("Get merchant performance indicators") + .WithDescription("Returns merchant activity counts for the current estate, including recent and absent sales.") + .WithApiResponse(); group.MapGet("/recent", MerchantHandler.GetRecentMerchants) - .WithStandardProduces>(); + .WithSummary("List recently created merchants") + .WithDescription("Returns the most recently created merchants for the current estate.") + .WithApiResponse>(); group.MapGet("/", MerchantHandler.GetMerchants) - .WithStandardProduces>(); + .WithSummary("List merchants") + .WithDescription("Returns merchants belonging to the current estate.") + .WithApiResponse>(); group.MapGet("/{merchantId}", MerchantHandler.GetMerchant) - .WithStandardProduces(); + .WithSummary("Get a merchant") + .WithDescription("Returns details for the specified merchant.") + .WithApiResponse(); group.MapGet("/{merchantId}/operators", MerchantHandler.GetMerchantOperators) - .WithStandardProduces>(); + .WithSummary("List a merchant's operators") + .WithDescription("Returns the operators configured for the specified merchant.") + .WithApiResponse>(); group.MapGet("/{merchantId}/contracts", MerchantHandler.GetMerchantContracts) - .WithStandardProduces>(); + .WithSummary("List a merchant's contracts") + .WithDescription("Returns the contracts and products configured for the specified merchant.") + .WithApiResponse>(); group.MapGet("/{merchantId}/devices", MerchantHandler.GetMerchantDevices) - .WithStandardProduces>(); + .WithSummary("List a merchant's devices") + .WithDescription("Returns the devices configured for the specified merchant.") + .WithApiResponse>(); group.MapGet("/{merchantId}/openinghours", MerchantHandler.GetMerchantOpeningHours) - .WithStandardProduces>(); + .WithSummary("Get a merchant's opening hours") + .WithDescription("Returns the opening and closing times configured for each day for the specified merchant.") + .WithApiResponse>(); group.MapGet("/{merchantId}/schedule/{year}", MerchantHandler.GetMerchantSchedule) - .WithStandardProduces(); + .WithSummary("Get a merchant's schedule") + .WithDescription("Returns the closed days for each month of the specified year for the merchant.") + .WithApiResponse(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/OperatorEndpoints.cs b/EstateReportingAPI/Endpoints/OperatorEndpoints.cs index fb249c3..180087b 100644 --- a/EstateReportingAPI/Endpoints/OperatorEndpoints.cs +++ b/EstateReportingAPI/Endpoints/OperatorEndpoints.cs @@ -1,7 +1,7 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; @@ -9,20 +9,24 @@ public static class OperatorEndpoints { private const string BaseRoute = "api/operators"; - public static void MapOperatorEndpoints(this IEndpointRouteBuilder app) + public static void MapOperatorEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Operators"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("/", OperatorHandler.GetOperators) - .WithStandardProduces>(); + .WithSummary("List operators") + .WithDescription("Returns operators available to the current estate.") + .WithApiResponse>(); group.MapGet("/{operatorId}", OperatorHandler.GetOperator) - .WithStandardProduces(); + .WithSummary("Get an operator") + .WithDescription("Returns details for the specified operator.") + .WithApiResponse(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/SettlementEndpoints.cs b/EstateReportingAPI/Endpoints/SettlementEndpoints.cs index 7860f4c..7f0abd1 100644 --- a/EstateReportingAPI/Endpoints/SettlementEndpoints.cs +++ b/EstateReportingAPI/Endpoints/SettlementEndpoints.cs @@ -1,21 +1,24 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; namespace EstateReportingAPI.Endpoints; public static class SettlementEndpoints { private const string BaseRoute = "api/settlements"; - public static void MapSettlementEndpoints(this IEndpointRouteBuilder app) { + public static void MapSettlementEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute).WithTags("Settlements"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } - group.MapGet("todayssettlements", SettlementHandler.TodaysSettlements).WithStandardProduces(); + group.MapGet("todayssettlements", SettlementHandler.TodaysSettlements) + .WithSummary("Get today's settlements") + .WithDescription("Returns settled and pending settlement totals for today and the configured comparison date.") + .WithApiResponse(); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Endpoints/TransactionEndpoints.cs b/EstateReportingAPI/Endpoints/TransactionEndpoints.cs index 7f5812d..8143a38 100644 --- a/EstateReportingAPI/Endpoints/TransactionEndpoints.cs +++ b/EstateReportingAPI/Endpoints/TransactionEndpoints.cs @@ -1,43 +1,65 @@ -using EstateReportingAPI.DataTransferObjects; +using Microsoft.Extensions.Configuration; +using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.Handlers; using Shared.Extensions; -using Shared.General; +using Shared.Middleware; namespace EstateReportingAPI.Endpoints; public static class TransactionEndpoints { private const string BaseRoute = "api/transactions"; - public static void MapTransactionEndpoints(this IEndpointRouteBuilder app) + public static void MapTransactionEndpoints(this IEndpointRouteBuilder app, IConfiguration configuration) { RouteGroupBuilder group = app.MapGroup(BaseRoute) .WithTags("Transactions"); - Boolean disableAuthorisation = ConfigurationReader.GetValueOrDefault("AppSettings", "DisableAuthorisation", false); + Boolean disableAuthorisation = configuration.GetValue("AppSettings:DisableAuthorisation", false); if (disableAuthorisation == false) { group = group.RequireAuthorization(); } group.MapGet("todayssales", TransactionHandler.TodaysSales) - .WithStandardProduces(); + .WithSummary("Get today's sales totals") + .WithDescription("Returns today's sales count, value, and average alongside totals for the comparison date.") + .WithApiResponse(); group.MapGet("todaysfailedsales", TransactionHandler.TodaysFailedSales) - .WithStandardProduces(); + .WithSummary("Get today's failed sales totals") + .WithDescription("Returns today's failed sales count, value, and average alongside comparison totals.") + .WithApiResponse(); group.MapPost("transactiondetailreport", TransactionHandler.TransactionDetailReport) - .WithStandardProduces(); + .WithSummary("Get a transaction detail report") + .WithDescription("Returns paginated transaction details and aggregate totals for the requested date range and optional operator, merchant, and product filters.") + .WithStandardProduces(); group.MapPost("transactionsummarybymerchantreport", TransactionHandler.TransactionSummaryByMerchantReport) - .WithStandardProduces(); + .WithSummary("Get a transaction summary by merchant") + .WithDescription("Returns per-merchant transaction totals and an overall summary for the requested date range and optional filters.") + .WithApiResponse(); group.MapPost("transactionsummarybyoperatorreport", TransactionHandler.TransactionSummaryByOperatorReport) - .WithStandardProduces(); + .WithSummary("Get a transaction summary by operator") + .WithDescription("Returns per-operator transaction totals and an overall summary for the requested date range and optional filters.") + .WithApiResponse(); group.MapGet("productperformancereport", TransactionHandler.ProductPerformanceReport) - .WithStandardProduces(); + .WithSummary("Get product performance") + .WithDescription("Returns product transaction performance and aggregate totals for the requested date range.") + .WithApiResponse(); group.MapPost("transactionmixsummary", TransactionHandler.TransactionMixSummary) - .WithStandardProduces(); + .WithSummary("Get a transaction mix summary") + .WithDescription("Groups transactions by the requested dimension and measure, with optional merchant filtering, top-N selection, and pagination.") + .WithStandardProduces(); group.MapPost("recentactivityreceiptreport", TransactionHandler.RecentActivityReceiptReport) - .WithStandardProduces(); + .WithSummary("Get recent transaction activity and receipt references") + .WithDescription("Returns paginated transactions for a report date, optionally filtered by merchant and search text.") + .WithApiResponse(); group.MapGet("todayssalesbyhour", TransactionHandler.TodaysSalesByHour) - .WithStandardProduces>(); + .WithSummary("Get today's sales by hour") + .WithDescription("Returns hourly sales counts and values for today alongside the supplied comparison date.") + .WithApiResponse>(); group.MapPost("merchantdailyperformancesummary", TransactionHandler.MerchantDailyPerformanceSummary) - .WithStandardProduces(); + .WithSummary("Get a merchant daily performance summary") + .WithDescription("Returns performance metrics and drill-down transactions for the requested merchant and date range.") + .WithApiResponse(); } } + diff --git a/EstateReportingAPI/EstateReportingAPI.csproj b/EstateReportingAPI/EstateReportingAPI.csproj index fb629f8..5b571ef 100644 --- a/EstateReportingAPI/EstateReportingAPI.csproj +++ b/EstateReportingAPI/EstateReportingAPI.csproj @@ -4,6 +4,7 @@ net10.0 enable enable + true 3ed94abc-423c-49b8-ac5d-edc80e1eaa74 Linux @@ -16,7 +17,6 @@ - @@ -31,14 +31,9 @@ - - - - - - + diff --git a/EstateReportingAPI/Handlers/ContractHandler.cs b/EstateReportingAPI/Handlers/ContractHandler.cs index e062460..6f08c1c 100644 --- a/EstateReportingAPI/Handlers/ContractHandler.cs +++ b/EstateReportingAPI/Handlers/ContractHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; @@ -66,7 +67,7 @@ public static async Task GetContracts(IEstateContext estateContext, } public static async Task GetContract(IEstateContext estateContext, - [FromRoute] Guid contractId, + [FromRoute, Description("Unique identifier of the contract.")] Guid contractId, IMediator mediator, CancellationToken cancellationToken) { @@ -103,4 +104,4 @@ public static async Task GetContract(IEstateContext estateContext, }).ToList() }); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Handlers/FileImportHandler.cs b/EstateReportingAPI/Handlers/FileImportHandler.cs index e0b9e01..44f0e55 100644 --- a/EstateReportingAPI/Handlers/FileImportHandler.cs +++ b/EstateReportingAPI/Handlers/FileImportHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; @@ -33,9 +34,9 @@ public static async Task GetFileProfileConfigurationList(IEstateContext public static class FileImportHandler { public static async Task GetFileImportLogList(IEstateContext estateContext, - [FromQuery] Guid? merchantId, - [FromQuery] DateOnly startDate, - [FromQuery] DateOnly endDate, + [FromQuery, Description("Optional merchant identifier used to filter import logs.")] Guid? merchantId, + [FromQuery, Description("Inclusive first date in the import log period (YYYY-MM-DD).") ] DateOnly startDate, + [FromQuery, Description("Inclusive last date in the import log period (YYYY-MM-DD).") ] DateOnly endDate, BusinessLogic.ReportingDatePolicy datePolicy, IMediator mediator, CancellationToken cancellationToken) @@ -66,8 +67,8 @@ public static async Task GetFileImportLogList(IEstateContext estateCont } public static async Task GetFileImportLog(IEstateContext estateContext, - [FromRoute] Guid fileImportLogId, - [FromQuery] Guid? merchantId, + [FromRoute, Description("Unique identifier of the file import log.")] Guid fileImportLogId, + [FromQuery, Description("Optional merchant identifier used to filter the import log.")] Guid? merchantId, BusinessLogic.ReportingDatePolicy datePolicy, IMediator mediator, CancellationToken cancellationToken) diff --git a/EstateReportingAPI/Handlers/MerchantHandler.cs b/EstateReportingAPI/Handlers/MerchantHandler.cs index a0e3819..847626c 100644 --- a/EstateReportingAPI/Handlers/MerchantHandler.cs +++ b/EstateReportingAPI/Handlers/MerchantHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; @@ -45,11 +46,11 @@ public static async Task GetRecentMerchants(IEstateContext estateContex } public static async Task GetMerchants(IEstateContext estateContext, - [FromQuery] String? name, - [FromQuery] String? reference, - [FromQuery] Int32? settlementSchedule, - [FromQuery] String? region, - [FromQuery] String? postCode, + [FromQuery, Description("Optional merchant name filter.")] String? name, + [FromQuery, Description("Optional merchant reference filter.")] String? reference, + [FromQuery, Description("Optional settlement schedule identifier filter.")] Int32? settlementSchedule, + [FromQuery, Description("Optional region filter.")] String? region, + [FromQuery, Description("Optional postal code filter.")] String? postCode, IMediator mediator, CancellationToken cancellationToken) { // Build the query options @@ -80,7 +81,7 @@ public static async Task GetMerchants(IEstateContext estateContext, } public static async Task GetMerchant(IEstateContext estateContext, - [FromRoute] Guid merchantId, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { // Build the query options @@ -110,7 +111,7 @@ public static async Task GetMerchant(IEstateContext estateContext, } public static async Task GetMerchantOperators(IEstateContext estateContext, - [FromRoute] Guid merchantId, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { MerchantQueries.GetMerchantOperatorsQuery query = new(estateContext.EstateId, merchantId); @@ -129,7 +130,7 @@ public static async Task GetMerchantOperators(IEstateContext estateCont public static async Task GetMerchantContracts(IEstateContext estateContext, - [FromRoute] Guid merchantId, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { MerchantQueries.GetMerchantContractsQuery query = new(estateContext.EstateId, merchantId); @@ -152,7 +153,7 @@ public static async Task GetMerchantContracts(IEstateContext estateCont } public static async Task GetMerchantDevices(IEstateContext estateContext, - [FromRoute] Guid merchantId, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { MerchantQueries.GetMerchantDevicesQuery query = new(estateContext.EstateId, merchantId); @@ -162,7 +163,7 @@ public static async Task GetMerchantDevices(IEstateContext estateContex } public static async Task GetMerchantOpeningHours(IEstateContext estateContext, - [FromRoute] Guid merchantId, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { MerchantQueries.GetMerchantOpeningHoursQuery query = new(estateContext.EstateId, merchantId); @@ -172,8 +173,8 @@ public static async Task GetMerchantOpeningHours(IEstateContext estateC } public static async Task GetMerchantSchedule(IEstateContext estateContext, - [FromRoute] Guid merchantId, - [FromRoute] Int32 year, + [FromRoute, Description("Unique identifier of the merchant.")] Guid merchantId, + [FromRoute, Description("Schedule year (for example, 2026).") ] Int32 year, IMediator mediator, CancellationToken cancellationToken) { MerchantQueries.GetMerchantScheduleQuery query = new(estateContext.EstateId, merchantId, year); @@ -181,4 +182,4 @@ public static async Task GetMerchantSchedule(IEstateContext estateConte return ResponseFactory.FromResult(result, r => new DataTransferObjects.MerchantScheduleResponse() { Year = r.Year, Months = r.Months.Select(m => new DataTransferObjects.MerchantScheduleMonthResponse() { Month = m.Month, ClosedDays = m.ClosedDays }).ToList() }); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Handlers/OperatorHandler.cs b/EstateReportingAPI/Handlers/OperatorHandler.cs index a632ea5..ca3abb2 100644 --- a/EstateReportingAPI/Handlers/OperatorHandler.cs +++ b/EstateReportingAPI/Handlers/OperatorHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; @@ -27,7 +28,7 @@ public static async Task GetOperators(IEstateContext estateContext, } public static async Task GetOperator(IEstateContext estateContext, - [FromRoute] Guid operatorId, + [FromRoute, Description("Unique identifier of the operator.")] Guid operatorId, IMediator mediator, CancellationToken cancellationToken) { @@ -43,4 +44,4 @@ public static async Task GetOperator(IEstateContext estateContext, RequireCustomTerminalNumber = r.RequireCustomTerminalNumber }); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Handlers/SettlementHandler.cs b/EstateReportingAPI/Handlers/SettlementHandler.cs index 6ca9413..1dea593 100644 --- a/EstateReportingAPI/Handlers/SettlementHandler.cs +++ b/EstateReportingAPI/Handlers/SettlementHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.DataTransferObjects; using MediatR; @@ -9,7 +10,7 @@ namespace EstateReportingAPI.Handlers; public static class SettlementHandler { public static async Task TodaysSettlements(IEstateContext estateContext, - [FromQuery] DateOnly comparisonDate, + [FromQuery, Description("Date used to compare today's settlements with the corresponding day (YYYY-MM-DD).") ] DateOnly comparisonDate, IMediator mediator, CancellationToken cancellationToken) { var query = new SettlementQueries.TodaysSettlementQuery(estateContext.EstateId, comparisonDate.ToDateTime(TimeOnly.MinValue)); diff --git a/EstateReportingAPI/Handlers/TransactionHandler.cs b/EstateReportingAPI/Handlers/TransactionHandler.cs index c014581..59b5ff6 100644 --- a/EstateReportingAPI/Handlers/TransactionHandler.cs +++ b/EstateReportingAPI/Handlers/TransactionHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using System.ComponentModel; using EstateReportingAPI.Common; using EstateReportingAPI.DataTransferObjects; using MediatR; @@ -12,9 +13,9 @@ namespace EstateReportingAPI.Handlers; public static class TransactionHandler { public static async Task TodaysSales(IEstateContext estateContext, - [FromQuery] int? merchantReportingId, - [FromQuery] int? operatorReportingId, - [FromQuery] DateOnly comparisonDate, + [FromQuery, Description("Optional merchant reporting identifier used to limit results to one merchant.")] int? merchantReportingId, + [FromQuery, Description("Optional operator reporting identifier used to limit results to one operator.")] int? operatorReportingId, + [FromQuery, Description("Date used to compare today's sales with the corresponding day (YYYY-MM-DD).") ] DateOnly comparisonDate, IMediator mediator, CancellationToken cancellationToken) { @@ -33,8 +34,8 @@ public static async Task TodaysSales(IEstateContext estateContext, } public static async Task TodaysFailedSales(IEstateContext estateContext, - [FromQuery] DateOnly comparisonDate, - [FromQuery] string responseCode, + [FromQuery, Description("Date used to compare failed sales with the corresponding day (YYYY-MM-DD).") ] DateOnly comparisonDate, + [FromQuery, Description("Response code used to filter failed transactions.")] string responseCode, IMediator mediator, CancellationToken cancellationToken) { @@ -162,10 +163,10 @@ TransactionSummaryByMerchantResponse SuccessFactory (Models.TransactionSummaryBy } public static async Task ProductPerformanceReport(IEstateContext estateContext, - [FromQuery] DateOnly startDate, - [FromQuery] DateOnly endDate, - [FromQuery] int? pageNumber, - [FromQuery] int? pageSize, + [FromQuery, Description("Inclusive first date in the report period (YYYY-MM-DD).") ] DateOnly startDate, + [FromQuery, Description("Inclusive last date in the report period (YYYY-MM-DD).") ] DateOnly endDate, + [FromQuery, Description("One-based page number. Defaults to 1.")] int? pageNumber, + [FromQuery, Description("Maximum results per page. Defaults to 50.")] int? pageSize, IMediator mediator, CancellationToken cancellationToken) { int requestedPageNumber = pageNumber ?? 1; @@ -383,7 +384,7 @@ GetRecentActivityReceiptReportResponse SuccessFactory(Models.GetRecentActivityRe } public static async Task TodaysSalesByHour(IEstateContext estateContext, - [FromQuery] DateOnly comparisonDate, + [FromQuery, Description("Date used to compare hourly sales with the corresponding day (YYYY-MM-DD).") ] DateOnly comparisonDate, IMediator mediator, CancellationToken cancellationToken) { diff --git a/EstateReportingAPI/Program.cs b/EstateReportingAPI/Program.cs index 3214cc4..68ad626 100644 --- a/EstateReportingAPI/Program.cs +++ b/EstateReportingAPI/Program.cs @@ -1,47 +1,41 @@ +using EstateReportingAPI.Bootstrapper; +using EstateReportingAPI.Common; +using EstateReportingAPI.Endpoints; +using HealthChecks.UI.Client; +using Lamar; using Lamar.Microsoft.DependencyInjection; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; using NLog; using NLog.Extensions.Logging; using Sentry.Extensibility; -using Shared.General; +using Shared.Extensions; using Shared.Logger; using Shared.Middleware; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using TransactionProcessing.ApiDefaults; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace EstateReportingAPI; [ExcludeFromCodeCoverage] -public class Program{ - +public partial class Program +{ public static void Main(string[] args) { - Program.CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) - { - - - //At this stage, we only need our hosting file for ip and ports - FileInfo fi = new(Assembly.GetExecutingAssembly().Location); - ConfigureLogging(); - IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); - hostBuilder.UseWindowsService(); - hostBuilder.UseLamar(); - hostBuilder.ConfigureLogging(logging => - { - logging.AddConsole(); - logging.AddNLog(); - }); - return ConfigureWebHost(hostBuilder, fi.Directory.FullName); + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + ConfigureHost(builder); + WebApplication app = builder.Build(); + ConfigurePipeline(app); + app.Run(); } - private static void ConfigureLogging() { - String contentRoot = Directory.GetCurrentDirectory(); - String nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); - + private static void ConfigureLogging() + { + string nlogConfigPath = Path.Combine(Directory.GetCurrentDirectory(), "nlog.config"); LogManager.Setup(b => { b.SetupLogFactory(setup => @@ -54,58 +48,91 @@ private static void ConfigureLogging() { }); } - private static IHostBuilder ConfigureWebHost(IHostBuilder hostBuilder, String basePath) { - hostBuilder.ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureAppConfiguration((context, configBuilder) => + private static void ConfigureHost(WebApplicationBuilder builder) + { + builder.Host.UseWindowsService(); + builder.Logging.AddConsole(); + builder.Logging.AddNLog(); + builder.WebHost.UseKestrel(); + + string basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + builder.Configuration.SetBasePath(basePath) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile($"hosting.{builder.Environment.EnvironmentName}.json", optional: true) + .AddJsonFile("/home/txnproc/config/appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"/home/txnproc/config/appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("/home/txnproc/config/appsettings.local.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddInMemoryCollection(new Dictionary { - IWebHostEnvironment env = context.HostingEnvironment; - - configBuilder.SetBasePath(basePath) - .AddJsonFile("hosting.json", optional: true) - .AddJsonFile($"hosting.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("/home/txnproc/config/appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"/home/txnproc/config/appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"/home/txnproc/config/appsettings.local.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables().AddInMemoryCollection(new Dictionary - { - ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" - }); - - // Keep existing static usage (if you must), and initialise the ConfigurationReader now. - Startup.Configuration = configBuilder.Build(); - ConfigurationReader.Initialise(Startup.Configuration); - - ConfigureSentry(env, webBuilder); + ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" }); - webBuilder.UseStartup(); - webBuilder.UseKestrel(); + IConfiguration configuration = builder.Configuration; + SharedLibraryCompatibility.InitializeConfiguration((IConfigurationRoot)builder.Configuration); + if (configuration.GetSection("SentryConfiguration").Exists() && !builder.Environment.IsDevelopment()) + { + builder.WebHost.UseSentry(options => + { + options.Dsn = configuration["SentryConfiguration:Dsn"]; + options.SendDefaultPii = true; + options.MaxRequestBodySize = RequestSize.Always; + options.CaptureBlockingCalls = configuration.GetValue("SentryConfiguration:CaptureBlockingCalls", false); + options.IncludeActivityData = configuration.GetValue("SentryConfiguration:IncludeActivityData", false); + options.Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + }); + } + builder.Host.UseLamar((context, services) => + { + services.IncludeRegistry(new MiddlewareRegistry(context.Configuration)); + services.IncludeRegistry(new RepositoryRegistry(context.Configuration, context.HostingEnvironment)); + services.IncludeRegistry(new MediatorRegistry(context.Configuration)); + services.IncludeRegistry(new SerialiserRegistry()); }); - return hostBuilder; } - private static void ConfigureSentry(IWebHostEnvironment env, - IWebHostBuilder webBuilder) { - // Configure Sentry on the webBuilder using the config snapshot. - IConfigurationSection sentrySection = Startup.Configuration.GetSection("SentryConfiguration"); - if (sentrySection.Exists()) + private static void ConfigurePipeline(WebApplication app) + { + if (app.Environment.IsDevelopment()) { - // Replace the condition below if you intended to only enable Sentry in certain environments. - if (env.IsDevelopment() == false) - { - webBuilder.UseSentry(o => - { - o.Dsn = Startup.Configuration["SentryConfiguration:Dsn"]; - o.SendDefaultPii = true; - o.MaxRequestBodySize = RequestSize.Always; - o.CaptureBlockingCalls = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "CaptureBlockingCalls", false); - o.IncludeActivityData = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "IncludeActivityData", false); - o.Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; - }); - } + app.UseDeveloperExceptionPage(); } + + ILogger logger = app.Services.GetRequiredService().CreateLogger("EstateManagement"); + SharedLibraryCompatibility.InitializeLogging(logger); + ((IConfigurationRoot)app.Configuration).LogConfiguration(message => logger.LogWarning("{ConfigurationMessage}", message)); + + app.UseMiddleware(); + app.AddRequestResponseLogging(); + app.AddExceptionHandler(); + + app.UseRouting(); + app.UseAuthentication(); + app.UseMiddleware(); + app.UseAuthorization(); + + IConfiguration configuration = app.Configuration; + app.MapCalendarEndpoints(configuration); + app.MapEstateEndpoints(configuration); + app.MapOperatorEndpoints(configuration); + app.MapMerchantEndpoints(configuration); + app.MapContractEndpoints(configuration); + app.MapTransactionEndpoints(configuration); + app.MapSettlementEndpoints(configuration); + app.MapFileImportLogEndpoints(configuration); + app.MapFileProfileConfigurationEndpoints(configuration); + app.MapHealthChecks("health", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse + }).AllowAnonymous(); + app.MapHealthChecks("healthui", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }).AllowAnonymous(); + app.UseApiDefaults(); } } diff --git a/EstateReportingAPI/Startup.cs b/EstateReportingAPI/Startup.cs deleted file mode 100644 index ff9ff10..0000000 --- a/EstateReportingAPI/Startup.cs +++ /dev/null @@ -1,94 +0,0 @@ -using EstateReportingAPI.Bootstrapper; -using EstateReportingAPI.Endpoints; -using HealthChecks.UI.Client; -using Lamar; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Shared.Extensions; -using Shared.General; -using Shared.Logger; -using System.Diagnostics.CodeAnalysis; - -namespace EstateReportingAPI -{ - using EstateReportingAPI.Common; - using Shared.Middleware; - using Shared.Serialisation; - - [ExcludeFromCodeCoverage] - public class Startup - { - public static IConfigurationRoot Configuration { get; set; } - public static IWebHostEnvironment WebHostEnvironment { get; set; } - public static IServiceProvider ServiceProvider { get; set; } - public static Container Container; - - public Startup(IWebHostEnvironment webHostEnvironment) - { - WebHostEnvironment = webHostEnvironment; - } - - public void ConfigureContainer(ServiceRegistry services) - { - ConfigurationReader.Initialise(Configuration); - - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - - Container = new Container(services); - - var serialiser = Container.GetRequiredService(); - StringSerialiser.Initialise(serialiser); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - IHostApplicationLifetime lifetime, IHost host) - { - if (env.IsDevelopment()) { - app.UseDeveloperExceptionPage(); - } - - Microsoft.Extensions.Logging.ILogger logger = loggerFactory.CreateLogger("EstateManagement"); - - Logger.Initialise(logger); - Configuration.LogConfiguration(Logger.LogWarning); - - ConfigurationReader.Initialise(Configuration); - app.UseMiddleware(); - app.AddRequestResponseLogging(); - app.AddExceptionHandler(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseMiddleware(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapCalendarEndpoints(); - endpoints.MapEstateEndpoints(); - endpoints.MapOperatorEndpoints(); - endpoints.MapMerchantEndpoints(); - endpoints.MapContractEndpoints(); - endpoints.MapTransactionEndpoints(); - endpoints.MapSettlementEndpoints(); - endpoints.MapFileImportLogEndpoints(); - endpoints.MapFileProfileConfigurationEndpoints(); - - endpoints.MapHealthChecks("health", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse - }).AllowAnonymous(); - endpoints.MapHealthChecks("healthui", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }).AllowAnonymous(); - }); - - } - } -} diff --git a/docs/superpowers/plans/2026-10-21-modern-startup-configuration-plan.md b/docs/superpowers/plans/2026-10-21-modern-startup-configuration-plan.md new file mode 100644 index 0000000..13d2b4c --- /dev/null +++ b/docs/superpowers/plans/2026-10-21-modern-startup-configuration-plan.md @@ -0,0 +1,95 @@ +# Modern Startup Configuration Refactor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. + +**Goal:** Move EstateReportingAPI to minimal hosting and eliminate API-owned static startup/configuration state while preserving deployed behavior. + +**Architecture:** `Program.cs` becomes the composition root. Private composition methods and the existing Lamar registries handle host setup, middleware, and endpoint mapping; configuration is injected through `IConfiguration` or options. Lamar remains the container. Integration tests configure each `WebApplicationFactory` instance instead of changing process-wide environment variables. + +**Tech Stack:** .NET 10, ASP.NET Core minimal hosting, Lamar, NLog, Sentry, OpenIddict, EF Core. + +**Spec:** `docs/superpowers/specs/2026-10-21-modern-startup-configuration-design.md` + +## Global Constraints + +- Preserve public routes, response behavior, configuration key names, and external config file locations. +- Preserve the effective configuration provider precedence and reload behavior. +- Preserve existing middleware order, service selection, authentication, logging, health checks, Windows Service, and Sentry behavior. +- Do not add or run tests unless requested by the user. +- Do not write to `.git`; repository metadata is read-only in this workspace. + +## Review Focus + +- Configuration provider precedence for appsettings, external files, environment variables, command-line arguments, and the in-memory version value. +- DI lifetime/availability for configuration needed by endpoints, business logic, serializers, and Lamar registrations. +- Auth and database behavior under IntegrationTest and test factory overrides. +- Middleware ordering and endpoint mappings. +- Static API dependencies from shared packages that may constrain complete static-state removal. + +--- + +### Task 1: Minimal host and configuration + +**Files:** +- Modify: `EstateReportingAPI/Program.cs` +- Delete: `EstateReportingAPI/Startup.cs` + +**Interfaces:** +- Produces public partial `Program` for `WebApplicationFactory`. +- Produces one `WebApplication` configured with all preserved configuration sources and host integrations. + +- [x] Create the `WebApplicationBuilder`, explicitly add the existing configuration files in their existing order and base paths, and preserve environment-variable and in-memory version precedence. +- [x] Configure NLog, Windows Service, Lamar, logging, Kestrel, and Sentry on the builder. +- [x] Add middleware and endpoint wiring through focused extension methods introduced in later tasks. +- [x] Remove the `Startup` class and its static fields. + +### Task 2: DI registration and configuration consumers + +**Files:** +- Modify: `EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs` +- Modify: `EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs` +- Modify: `EstateReportingAPI/Bootstrapper/MediatorRegistry.cs` +- Modify: `EstateReportingAPI/Endpoints/*.cs` +- Modify: `EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs`\n- Modify: `EstateReportingAPI.Tests/BusinessLogic/DbContextResolverXTests.cs` +- Add focused options/registration files under `EstateReportingAPI/Bootstrapper/` as needed. + +**Interfaces:** +- Consumes `IConfiguration` and `IWebHostEnvironment` from the host. +- Removes production `ConfigurationReader` calls and global `StringSerialiser` initialization from API-controlled code. + +- [x] Replace static configuration/environment reads in registries with explicit composition-time configuration or DI-resolved options. +- [x] Replace endpoint authorization static reads with injected `IConfiguration`/options as endpoint mapping parameters. +- [x] Replace business logic static reads with injected options/configuration dependencies. +- [x] Keep database, health-check, authentication, serialization, mediator, and middleware registrations behaviorally equivalent. + +### Task 3: Middleware and endpoint composition + +**Files:** +- Modify: `EstateReportingAPI/Program.cs` +- Modify: `EstateReportingAPI/Program.cs` + +- [x] Move current middleware and endpoint mapping order into application extensions, retaining all existing endpoint and health-check routes. +- [x] Register logger and serializer dependencies through DI, removing static initializers where API code owns the call site. +- [x] Ensure no `Startup`, `ConfigurationReader`, or mutable startup state remains in production API paths. + +### Task 4: Integration test host + +**Files:** +- Modify: `EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs` +- Modify: `EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs` + +- [x] Retarget factories to `Program`. +- [x] Configure integration environment and test flags per factory, without process-wide environment mutation. +- [x] Preserve SQL database replacement and test authentication setup. + +### Task 5: Review and compile + +**Files:** +- Review all changed production and integration-test files against the spec. + +- [x] Search for stale `Startup`/`ConfigurationReader`/global initializer usage. +- [x] Review service lifetimes, configuration precedence, middleware order, and test overrides. +- [x] Do not run tests or builds unless the user explicitly requests verification. + + + diff --git a/docs/superpowers/specs/2026-10-21-modern-startup-configuration-design.md b/docs/superpowers/specs/2026-10-21-modern-startup-configuration-design.md new file mode 100644 index 0000000..09c3254 --- /dev/null +++ b/docs/superpowers/specs/2026-10-21-modern-startup-configuration-design.md @@ -0,0 +1,60 @@ +# Modern Startup Configuration Refactor Design + +## Goal + +Move EstateReportingAPI to ASP.NET Core minimal hosting and remove static startup/configuration state, while preserving its deployed behavior and integration-test customization. + +## Current State + +The application targets .NET 10 but builds its host through `Host.CreateDefaultBuilder`, configures the web host in `Program.ConfigureWebHost`, then delegates services and middleware to `Startup`. Configuration is manually layered in the web-host callback and copied into `Startup.Configuration`; `Startup.WebHostEnvironment`, `Startup.Container`, and other static initializers provide global state. `ConfigurationReader` is used across registries, endpoints, and business logic. Lamar registries currently register authentication, database access, health checks, middleware options, serialization, repositories, and MediatR. Integration tests use `WebApplicationFactory` and process-wide environment variables. + +## Proposed Architecture + +`Program.cs` will use `WebApplication.CreateBuilder(args)` as the composition root. A small set of focused registration extensions will configure host integrations (Windows Service, Lamar, logging, NLog, Sentry), application services, and the HTTP pipeline. The existing configuration source order and base paths will be represented explicitly on the new builder so precedence, optional files, reload behavior, and the in-memory service version stay compatible. + +Configuration will flow through `IConfiguration`, typed options where appropriate, and injected services. Startup decisions that depend on the environment (such as in-memory database selection and Sentry activation) will be made at composition time from builder environment/configuration rather than static `Startup` fields. `ConfigurationReader` calls in this API will be replaced with injected configuration/options or small configuration-bound services; no API-owned static mutable configuration, environment, service provider, or container will remain. Serialization will be registered and consumed through DI. The current Shared package requires a narrowly scoped compatibility initializer: `TenantMiddleware` reads `ConfigurationReader`, and the same middleware logs through the shared static `Logger`. Keep those shared-library initializations isolated in one adapter until Shared exposes instance-based APIs. + +Lamar remains the DI container and existing registries may be split or retained as registration modules where they can receive configuration and environment explicitly. Endpoint mappings, middleware ordering, authentication setup, health-check behavior, JSON serialization behavior, NLog call-site filtering, Windows Service support, and Sentry behavior remain functionally equivalent. + +Integration tests will use `WebApplicationFactory` with per-factory configuration and service overrides. Test-specific environment/configuration will not be set as process-global mutable state. Database override behavior and the test authentication scheme will remain available. + +## Configuration Contract + +Preserve the effective application configuration inputs and ordering from `Program.cs`: + +1. Host defaults and command-line arguments supported by `WebApplication.CreateBuilder(args)`. +2. `hosting.json` and `hosting.{Environment}.json` rooted at the executing assembly directory. +3. `/home/txnproc/config/appsettings.json` and its environment-specific companion. +4. `appsettings.json` and `appsettings.{Environment}.json` rooted at the executing assembly directory. +5. `/home/txnproc/config/appsettings.local.json`. +6. Environment variables. +7. In-memory `HealthMonitoring:Service:Version` derived from the entry assembly, with the current fallback. + +The implementation must deliberately reconcile default-builder providers with this established ordering so that existing deployment overrides continue to win as they do today. JSON reload-on-change settings must be retained for the same files. The special absolute Linux paths remain optional. + +## HTTP and Service Behavior to Preserve + +- Windows Service hosting, Kestrel, console logging, NLog, and existing NLog configuration behavior. +- Sentry registration only when its section exists and the environment is not Development, including current options. +- Lamar registrations for middleware, repository, mediator, and serialization services. +- Authentication and authorization behavior, including `InTestMode` and `DisableAuthorisation` behavior unless a safer per-host equivalent is required to eliminate process-wide state. +- Database selection for `IntegrationTest` and `ServiceOptions:UseInMemoryDatabase`, SQL Server retry settings, and health-check configuration. +- JSON options and serializer initialization behavior. +- Middleware ordering and all currently mapped endpoints and health endpoints. + +## Out of Scope + +- Endpoint, response, route, DTO, database schema, or business-rule changes. +- Changing external configuration key names or deployment file locations. +- Removing Lamar, NLog, Sentry, OpenIddict, or shared packages. +- Broad redesign of the `Shared` package. If a shared static API blocks complete removal of a static bridge, isolate that compatibility boundary and record it for separate follow-up. + +## Acceptance Criteria + +1. `Program.cs` builds and runs the app using `WebApplication`; `Startup.cs` and `UseStartup` are removed. +2. API-owned static startup state is removed; direct static configuration use is removed except for the documented Shared compatibility adapter. +3. Configuration precedence, file paths, reload behavior, and environment-based decisions match the contract above. +4. The full middleware and endpoint pipeline remains behaviorally equivalent. +5. Integration tests target `Program` and apply configuration/service overrides per factory without mutating process-wide environment variables. +6. The project compiles and existing verification appropriate to the repository passes. + From 3d8e97e9f873d62773d99242d388226d261b9542 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 15:31:48 +0100 Subject: [PATCH 2/5] Use valid page size in OpenAPI examples --- EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs index a6e6269..079decc 100644 --- a/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs +++ b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs @@ -237,7 +237,8 @@ var n when n.Contains("address") || n.Contains("town") || n.Contains("region") | || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) { string name = propertyName.ToLowerInvariant(); - long value = name.Contains("page") && name.Contains("number") ? 1 + long value = name.Contains("page") && name.Contains("size") ? 50 + : name.Contains("page") && name.Contains("number") ? 1 : name.Contains("year") ? 2026 : name.Contains("hour") ? 10 : name.Contains("count") || name.Contains("total") ? 3 @@ -288,3 +289,4 @@ private static void SetResponseExample(OpenApiOperation operation, string json) } } } + From d83e6c84db0904ceda7934ad4efddb444e102a6a Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 15:37:30 +0100 Subject: [PATCH 3/5] Use application configuration for service registration --- EstateReportingAPI/Program.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EstateReportingAPI/Program.cs b/EstateReportingAPI/Program.cs index 68ad626..1eac801 100644 --- a/EstateReportingAPI/Program.cs +++ b/EstateReportingAPI/Program.cs @@ -86,9 +86,9 @@ private static void ConfigureHost(WebApplicationBuilder builder) } builder.Host.UseLamar((context, services) => { - services.IncludeRegistry(new MiddlewareRegistry(context.Configuration)); - services.IncludeRegistry(new RepositoryRegistry(context.Configuration, context.HostingEnvironment)); - services.IncludeRegistry(new MediatorRegistry(context.Configuration)); + services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(new RepositoryRegistry(configuration, context.HostingEnvironment)); + services.IncludeRegistry(new MediatorRegistry(configuration)); services.IncludeRegistry(new SerialiserRegistry()); }); } @@ -136,3 +136,4 @@ private static void ConfigurePipeline(WebApplication app) app.UseApiDefaults(); } } + From 1facf1d7b8a473215cceb870e0674d2346f94e40 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 15:41:29 +0100 Subject: [PATCH 4/5] Skip external authentication setup in integration tests --- .../Bootstrapper/MiddlewareRegistry.cs | 373 +++++++++--------- EstateReportingAPI/Program.cs | 2 +- 2 files changed, 188 insertions(+), 187 deletions(-) diff --git a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs index b71c4ed..d03410a 100644 --- a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs @@ -1,186 +1,187 @@ -using HealthMonitoring.Client; -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using Shared.Middleware; -using TransactionProcessing.ApiDefaults; - -namespace EstateReportingAPI.Bootstrapper{ - using EstateReportingAPI.Common; - using Lamar; - using Microsoft.Extensions.Diagnostics.HealthChecks; - using OpenIddict.Validation.AspNetCore; - using Shared.Serialisation; - using System.Diagnostics.CodeAnalysis; - using System.Reflection; - using System.Text.Json; - - [ExcludeFromCodeCoverage] - public class MiddlewareRegistry : ServiceRegistry{ - public MiddlewareRegistry(IConfiguration configuration) - { - this.ConfigureHealthChecks(configuration); - this.ConfigureAuthentication(configuration); - this.ConfigureControllers(); - this.ConfigureMiddlewareLogging(configuration); - this.ConfigureJsonOptions(); - this.AddHealthMonitoringRegistration(configuration); - this.AddScoped(); - this.AddScoped(sp => sp.GetRequiredService()); - this.Configure(options => - { - options.Title = "Estate Reporting"; - options.Description = "API providing a variety of reporting facilities consumed by various reporting applications"; - }); - this.AddApiVersioning(options => - { - options.ApiVersionReader = new UrlSegmentApiVersionReader(); - options.DefaultApiVersion = ApiVersions.V1; - options.AssumeDefaultVersionWhenUnspecified = false; - options.ReportApiVersions = true; - }).AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }).AddOpenApi(options => - { - options.Document.AddSchemaTransformer((schema, context, _) => - { - XmlDocumentationSchemaTransformer.Apply(schema, context); - return Task.CompletedTask; - }); - options.Document.AddOperationTransformer((operation, context, _) => - { - OpenApiExamplesTransformer.Apply(operation, context); - return Task.CompletedTask; - }); - options.Document.AddDocumentTransformer((document, context, _) => - { - ApiDefaultsOptions metadata = context.ApplicationServices - .GetRequiredService>() - .Value; - document.Info.Title = metadata.Title; - document.Info.Description = metadata.Description; - return Task.CompletedTask; - }); - }); - - } - - private void ConfigureJsonOptions() - { - this.ConfigureHttpJsonOptions(options => { - JsonSerializerConfiguration.ConfigureMinimalApi(options.SerializerOptions); - }); - } - - private void ConfigureHealthChecks(IConfiguration configuration){ - var connectionStringSection = configuration.GetSection("ConnectionStrings"); - if (connectionStringSection.Exists() == false) - { - this.AddHealthChecks(); - } - else - { - this.AddHealthChecks().AddSqlServer( - connectionString: configuration.GetConnectionString("HealthCheck"), - healthQuery: "SELECT 1;", - name: "Read Model Server", - failureStatus: HealthStatus.Degraded, - tags: new[] { "db", "sql", "sqlserver" }); - } - } - - private void ConfigureAuthentication(IConfiguration configuration){ - bool inTestMode = configuration.GetValue("InTestMode", false); - if (!inTestMode){ - this.AddAuthentication(options => - { - options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; - }); - this.AddOpenIddict() - .AddValidation(options => - { - // Same as your Authority - options.SetIssuer(new Uri(configuration["SecurityConfiguration:Authority"]!)); - - // Enables discovery and HTTP backchannel support - options.UseSystemNetHttp() - .ConfigureHttpClientHandler(handler => - { - // DEV ONLY: bypass all certificate errors - handler.ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - }); - - // Register the ASP.NET Core integration - options.UseAspNetCore(); - - // Optionally set expected audience(s): - options.AddAudiences(configuration["SecurityConfiguration:ApiName"]!); - - }); - - this.AddAuthorization(); - } - } - - private void ConfigureControllers() { - this.AddControllers(); - - Assembly assembly = this.GetType().GetTypeInfo().Assembly; - this.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); - } - - private void ConfigureMiddlewareLogging(IConfiguration configuration){ - bool logRequests = configuration.GetValue("MiddlewareLogging:LogRequests", true); - bool logResponses = configuration.GetValue("MiddlewareLogging:LogResponses", true); - LogLevel middlewareLogLevel = configuration.GetValue("MiddlewareLogging:MiddlewareLogLevel", LogLevel.Warning); - - RequestResponseMiddlewareLoggingConfig config = - new RequestResponseMiddlewareLoggingConfig(middlewareLogLevel, logRequests, logResponses); - - this.AddSingleton(config); - } - - private HttpClientHandler ApiEndpointHttpHandler(IServiceProvider serviceProvider){ - return new HttpClientHandler{ - ServerCertificateCustomValidationCallback = (message, - cert, - chain, - errors) => { - return true; - } - }; - } - } - - [ExcludeFromCodeCoverage] - public class SerialiserRegistry : ServiceRegistry - { - public SerialiserRegistry() - { - this.AddSingleton(); - this.AddSingleton>(container => obj => container.GetRequiredService().Serialize(obj)); - this.AddSingleton>(container => (json, type) => container.GetRequiredService().DeserializeObject(json, type)); - - var serialiserSettings = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); - - this.AddSingleton(serialiserSettings); - } - } - - public static class JsonSerializerConfiguration - { - public static void ConfigureMinimalApi(JsonSerializerOptions serializerOptions) - { - var defaultOptions = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); - serializerOptions.PropertyNamingPolicy = defaultOptions.PropertyNamingPolicy; - serializerOptions.DictionaryKeyPolicy = defaultOptions.DictionaryKeyPolicy; - serializerOptions.ReferenceHandler = defaultOptions.ReferenceHandler; - serializerOptions.WriteIndented = defaultOptions.WriteIndented; - serializerOptions.Converters.Add(new DateTimeSpaceConverter()); - } - } -} +using HealthMonitoring.Client; +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Shared.Middleware; +using TransactionProcessing.ApiDefaults; + +namespace EstateReportingAPI.Bootstrapper{ + using EstateReportingAPI.Common; + using Lamar; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using OpenIddict.Validation.AspNetCore; + using Shared.Serialisation; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using System.Text.Json; + + [ExcludeFromCodeCoverage] + public class MiddlewareRegistry : ServiceRegistry{ + public MiddlewareRegistry(IConfiguration configuration, IHostEnvironment environment) + { + this.ConfigureHealthChecks(configuration); + this.ConfigureAuthentication(configuration, environment); + this.ConfigureControllers(); + this.ConfigureMiddlewareLogging(configuration); + this.ConfigureJsonOptions(); + this.AddHealthMonitoringRegistration(configuration); + this.AddScoped(); + this.AddScoped(sp => sp.GetRequiredService()); + this.Configure(options => + { + options.Title = "Estate Reporting"; + options.Description = "API providing a variety of reporting facilities consumed by various reporting applications"; + }); + this.AddApiVersioning(options => + { + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + options.DefaultApiVersion = ApiVersions.V1; + options.AssumeDefaultVersionWhenUnspecified = false; + options.ReportApiVersions = true; + }).AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }).AddOpenApi(options => + { + options.Document.AddSchemaTransformer((schema, context, _) => + { + XmlDocumentationSchemaTransformer.Apply(schema, context); + return Task.CompletedTask; + }); + options.Document.AddOperationTransformer((operation, context, _) => + { + OpenApiExamplesTransformer.Apply(operation, context); + return Task.CompletedTask; + }); + options.Document.AddDocumentTransformer((document, context, _) => + { + ApiDefaultsOptions metadata = context.ApplicationServices + .GetRequiredService>() + .Value; + document.Info.Title = metadata.Title; + document.Info.Description = metadata.Description; + return Task.CompletedTask; + }); + }); + + } + + private void ConfigureJsonOptions() + { + this.ConfigureHttpJsonOptions(options => { + JsonSerializerConfiguration.ConfigureMinimalApi(options.SerializerOptions); + }); + } + + private void ConfigureHealthChecks(IConfiguration configuration){ + var connectionStringSection = configuration.GetSection("ConnectionStrings"); + if (connectionStringSection.Exists() == false) + { + this.AddHealthChecks(); + } + else + { + this.AddHealthChecks().AddSqlServer( + connectionString: configuration.GetConnectionString("HealthCheck"), + healthQuery: "SELECT 1;", + name: "Read Model Server", + failureStatus: HealthStatus.Degraded, + tags: new[] { "db", "sql", "sqlserver" }); + } + } + + private void ConfigureAuthentication(IConfiguration configuration, IHostEnvironment environment){ + bool inTestMode = environment.IsEnvironment("IntegrationTest") || configuration.GetValue("InTestMode", false); + if (!inTestMode){ + this.AddAuthentication(options => + { + options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + }); + this.AddOpenIddict() + .AddValidation(options => + { + // Same as your Authority + options.SetIssuer(new Uri(configuration["SecurityConfiguration:Authority"]!)); + + // Enables discovery and HTTP backchannel support + options.UseSystemNetHttp() + .ConfigureHttpClientHandler(handler => + { + // DEV ONLY: bypass all certificate errors + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + }); + + // Register the ASP.NET Core integration + options.UseAspNetCore(); + + // Optionally set expected audience(s): + options.AddAudiences(configuration["SecurityConfiguration:ApiName"]!); + + }); + + this.AddAuthorization(); + } + } + + private void ConfigureControllers() { + this.AddControllers(); + + Assembly assembly = this.GetType().GetTypeInfo().Assembly; + this.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); + } + + private void ConfigureMiddlewareLogging(IConfiguration configuration){ + bool logRequests = configuration.GetValue("MiddlewareLogging:LogRequests", true); + bool logResponses = configuration.GetValue("MiddlewareLogging:LogResponses", true); + LogLevel middlewareLogLevel = configuration.GetValue("MiddlewareLogging:MiddlewareLogLevel", LogLevel.Warning); + + RequestResponseMiddlewareLoggingConfig config = + new RequestResponseMiddlewareLoggingConfig(middlewareLogLevel, logRequests, logResponses); + + this.AddSingleton(config); + } + + private HttpClientHandler ApiEndpointHttpHandler(IServiceProvider serviceProvider){ + return new HttpClientHandler{ + ServerCertificateCustomValidationCallback = (message, + cert, + chain, + errors) => { + return true; + } + }; + } + } + + [ExcludeFromCodeCoverage] + public class SerialiserRegistry : ServiceRegistry + { + public SerialiserRegistry() + { + this.AddSingleton(); + this.AddSingleton>(container => obj => container.GetRequiredService().Serialize(obj)); + this.AddSingleton>(container => (json, type) => container.GetRequiredService().DeserializeObject(json, type)); + + var serialiserSettings = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); + + this.AddSingleton(serialiserSettings); + } + } + + public static class JsonSerializerConfiguration + { + public static void ConfigureMinimalApi(JsonSerializerOptions serializerOptions) + { + var defaultOptions = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); + serializerOptions.PropertyNamingPolicy = defaultOptions.PropertyNamingPolicy; + serializerOptions.DictionaryKeyPolicy = defaultOptions.DictionaryKeyPolicy; + serializerOptions.ReferenceHandler = defaultOptions.ReferenceHandler; + serializerOptions.WriteIndented = defaultOptions.WriteIndented; + serializerOptions.Converters.Add(new DateTimeSpaceConverter()); + } + } +} + diff --git a/EstateReportingAPI/Program.cs b/EstateReportingAPI/Program.cs index 1eac801..518a072 100644 --- a/EstateReportingAPI/Program.cs +++ b/EstateReportingAPI/Program.cs @@ -86,7 +86,7 @@ private static void ConfigureHost(WebApplicationBuilder builder) } builder.Host.UseLamar((context, services) => { - services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(new MiddlewareRegistry(configuration, context.HostingEnvironment)); services.IncludeRegistry(new RepositoryRegistry(configuration, context.HostingEnvironment)); services.IncludeRegistry(new MediatorRegistry(configuration)); services.IncludeRegistry(new SerialiserRegistry()); From eee01882d060e4a109f48e736fc03de1d111a96a Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 15:55:25 +0100 Subject: [PATCH 5/5] Address Codacy feedback on OpenAPI documentation --- .../OpenApiExamplesTransformer.cs | 164 ++++++++++++------ .../XmlDocumentationSchemaTransformer.cs | 15 +- EstateReportingAPI/Program.cs | 4 + 3 files changed, 129 insertions(+), 54 deletions(-) diff --git a/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs index 079decc..5d048ae 100644 --- a/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs +++ b/EstateReportingAPI/Bootstrapper/OpenApiExamplesTransformer.cs @@ -13,6 +13,8 @@ namespace EstateReportingAPI.Bootstrapper; /// internal static class OpenApiExamplesTransformer { + private const string JsonMediaType = "application/json"; + public static void Apply(OpenApiOperation operation, OpenApiOperationTransformerContext context) { AddParameterExamples(operation, context); @@ -146,7 +148,7 @@ private static void AddRequestBodyExample(OpenApiOperation operation, OpenApiOpe Type? bodyType = context.Description.ParameterDescriptions .FirstOrDefault(parameter => parameter.Source?.Id == "Body")? .ModelMetadata?.ModelType; - if (bodyType is null || !operation.RequestBody.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + if (bodyType is null || !operation.RequestBody.Content.TryGetValue(JsonMediaType, out OpenApiMediaType? mediaType)) { return; } @@ -164,7 +166,7 @@ private static void AddSuccessResponseExample(OpenApiOperation operation, OpenAp return; } - if (response.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + if (response.Content.TryGetValue(JsonMediaType, out OpenApiMediaType? mediaType)) { mediaType.Example ??= CreateExample(successResponse.Type, "response"); } @@ -172,52 +174,97 @@ private static void AddSuccessResponseExample(OpenApiOperation operation, OpenAp private static JsonNode? CreateExample(Type type, string propertyName, int depth = 0) { - Type? nullableType = Nullable.GetUnderlyingType(type); - if (nullableType is not null) + type = Nullable.GetUnderlyingType(type) ?? type; + + if (TryCreateStringExample(type, propertyName, out JsonNode? example) + || TryCreateDateTimeExample(type, out example) + || TryCreateOtherScalarExample(type, propertyName, out example)) { - type = nullableType; + return example; } - if (type == typeof(string)) + if (type.IsArray) { - string name = propertyName.ToLowerInvariant(); - string value = name switch - { - var n when n.Contains("email") => "alex@example.com", - var n when n.Contains("status") => "Authorised", - var n when n.Contains("type") => "Sale", - var n when n.Contains("reference") => "REF-20261021-0042", - var n when n.Contains("description") => "Example description", - var n when n.Contains("name") || n.Contains("product") || n.Contains("merchant") || n.Contains("operator") => "Example " + propertyName, - var n when n.Contains("address") || n.Contains("town") || n.Contains("region") || n.Contains("country") || n.Contains("postcode") => "Example value", - _ => "Example value" - }; - return JsonValue.Create(value); + return new JsonArray(CreateExample(type.GetElementType()!, propertyName, depth + 1)); + } + + if (typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType) + { + return new JsonArray(CreateExample(type.GetGenericArguments()[0], propertyName, depth + 1)); + } + + if (depth >= 5 || type.IsAbstract || type.IsInterface) + { + return null; } + return CreateObjectExample(type, depth); + } + + private static bool TryCreateStringExample(Type type, string propertyName, out JsonNode? example) + { + if (type != typeof(string)) + { + example = null; + return false; + } + + string name = propertyName.ToLowerInvariant(); + string value = name switch + { + var n when n.Contains("email") => "alex@example.com", + var n when n.Contains("status") => "Authorised", + var n when n.Contains("type") => "Sale", + var n when n.Contains("reference") => "REF-20261021-0042", + var n when n.Contains("description") => "Example description", + var n when HasNameToken(n) => "Example " + propertyName, + _ => "Example value" + }; + example = JsonValue.Create(value); + return true; + } + + private static bool HasNameToken(string name) + { + return name.Contains("name") || name.Contains("product") || name.Contains("merchant") || name.Contains("operator"); + } + + private static bool TryCreateDateTimeExample(Type type, out JsonNode? example) + { if (type == typeof(Guid)) { - return JsonValue.Create("c6e8c2ba-bb6d-4fe5-aad9-62444fc13a70"); + example = JsonValue.Create("c6e8c2ba-bb6d-4fe5-aad9-62444fc13a70"); + return true; } if (type == typeof(DateOnly)) { - return JsonValue.Create("2026-10-21"); + example = JsonValue.Create("2026-10-21"); + return true; } if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) { - return JsonValue.Create("2026-10-21T10:32:15+01:00"); + example = JsonValue.Create("2026-10-21T10:32:15+01:00"); + return true; } if (type == typeof(TimeOnly) || type == typeof(TimeSpan)) { - return JsonValue.Create("09:00:00"); + example = JsonValue.Create("09:00:00"); + return true; } + example = null; + return false; + } + + private static bool TryCreateOtherScalarExample(Type type, string propertyName, out JsonNode? example) + { if (type == typeof(bool)) { - return JsonValue.Create(true); + example = JsonValue.Create(true); + return true; } if (type.IsEnum) @@ -225,45 +272,53 @@ var n when n.Contains("address") || n.Contains("town") || n.Contains("region") | string enumValue = propertyName.Contains("measure", StringComparison.OrdinalIgnoreCase) ? "Value" : Enum.GetNames(type).FirstOrDefault() ?? "Unknown"; - return JsonValue.Create(enumValue); + example = JsonValue.Create(enumValue); + return true; } - if (type == typeof(decimal) || type == typeof(double) || type == typeof(float)) + if (IsDecimalType(type)) { - return JsonValue.Create(18.50m); + example = JsonValue.Create(18.50m); + return true; } - if (type == typeof(byte) || type == typeof(short) || type == typeof(int) || type == typeof(long) - || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) + if (IsIntegerType(type)) { - string name = propertyName.ToLowerInvariant(); - long value = name.Contains("page") && name.Contains("size") ? 50 - : name.Contains("page") && name.Contains("number") ? 1 - : name.Contains("year") ? 2026 - : name.Contains("hour") ? 10 - : name.Contains("count") || name.Contains("total") ? 3 - : 1001; - return JsonValue.Create(value); + example = JsonValue.Create(CreateIntegerExample(propertyName)); + return true; } - if (type.IsArray) - { - return new JsonArray(CreateExample(type.GetElementType()!, propertyName, depth + 1)); - } + example = null; + return false; + } - if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType) - { - return new JsonArray(CreateExample(type.GetGenericArguments()[0], propertyName, depth + 1)); - } + private static bool IsDecimalType(Type type) + { + return type == typeof(decimal) || type == typeof(double) || type == typeof(float); + } - if (depth >= 5 || type.IsAbstract || type.IsInterface) - { - return null; - } + private static bool IsIntegerType(Type type) + { + return type == typeof(byte) || type == typeof(short) || type == typeof(int) || type == typeof(long) + || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong); + } + private static long CreateIntegerExample(string propertyName) + { + string name = propertyName.ToLowerInvariant(); + return name.Contains("page") && name.Contains("size") ? 50 + : name.Contains("page") && name.Contains("number") ? 1 + : name.Contains("year") ? 2026 + : name.Contains("hour") ? 10 + : name.Contains("count") || name.Contains("total") ? 3 + : 1001; + } + + private static JsonObject CreateObjectExample(Type type, int depth) + { JsonObject result = new(); foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(property => property.CanRead && property.GetIndexParameters().Length == 0)) + .Where(IsExampleProperty)) { string jsonName = JsonNamingPolicy.SnakeCaseLower.ConvertName(property.Name); result[jsonName] = CreateExample(property.PropertyType, property.Name, depth + 1); @@ -272,9 +327,14 @@ var n when n.Contains("address") || n.Contains("town") || n.Contains("region") | return result; } + private static bool IsExampleProperty(PropertyInfo property) + { + return property.CanRead && property.GetIndexParameters().Length == 0; + } + private static void SetRequestExample(OpenApiOperation operation, string json) { - if (operation.RequestBody?.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType) == true) + if (operation.RequestBody?.Content.TryGetValue(JsonMediaType, out OpenApiMediaType? mediaType) == true) { mediaType.Example = JsonNode.Parse(json); } @@ -283,7 +343,7 @@ private static void SetRequestExample(OpenApiOperation operation, string json) private static void SetResponseExample(OpenApiOperation operation, string json) { if (operation.Responses.TryGetValue("200", out IOpenApiResponse response) - && response.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + && response.Content.TryGetValue(JsonMediaType, out OpenApiMediaType? mediaType)) { mediaType.Example = JsonNode.Parse(json); } diff --git a/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs b/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs index 8384084..bd60838 100644 --- a/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs +++ b/EstateReportingAPI/Bootstrapper/XmlDocumentationSchemaTransformer.cs @@ -23,9 +23,9 @@ public static void Apply(OpenApiSchema schema, OpenApiSchemaTransformerContext c } MemberInfo? member = context.JsonPropertyInfo?.AttributeProvider as MemberInfo; - Assembly documentationAssembly = member?.DeclaringType?.Assembly ?? type.Assembly; + Assembly documentationAssembly = GetDocumentationAssembly(member, type); IReadOnlyDictionary documentation = DocumentationByAssembly.GetOrAdd(documentationAssembly, LoadDocumentation); - string? memberId = member is not null ? GetMemberId(member) : $"T:{GetTypeName(type)}"; + string? memberId = GetDocumentationMemberId(member, type); if (memberId is not null && documentation.TryGetValue(memberId, out string? summary)) { @@ -33,6 +33,16 @@ public static void Apply(OpenApiSchema schema, OpenApiSchemaTransformerContext c } } + private static Assembly GetDocumentationAssembly(MemberInfo? member, Type type) + { + return member?.DeclaringType?.Assembly ?? type.Assembly; + } + + private static string? GetDocumentationMemberId(MemberInfo? member, Type type) + { + return member is not null ? GetMemberId(member) : $"T:{GetTypeName(type)}"; + } + private static IReadOnlyDictionary LoadDocumentation(Assembly assembly) { string? assemblyPath = assembly.Location; @@ -94,3 +104,4 @@ private static string GetTypeName(Type type) return string.IsNullOrWhiteSpace(text) ? null : text; } } + diff --git a/EstateReportingAPI/Program.cs b/EstateReportingAPI/Program.cs index 518a072..9557691 100644 --- a/EstateReportingAPI/Program.cs +++ b/EstateReportingAPI/Program.cs @@ -22,6 +22,10 @@ namespace EstateReportingAPI; [ExcludeFromCodeCoverage] public partial class Program { + protected Program() + { + } + public static void Main(string[] args) { ConfigureLogging();