From fb1f7fa47ca3e700d994448b5e9b6398a3c4f5b0 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 19:22:12 +0100 Subject: [PATCH 1/5] added api defaults and scalar --- Directory.Packages.props | 56 ++- README.md | 2 + .../Mediator/MediatorTests.cs | 15 +- .../CreateFloatForContractProductRequest.cs | 28 +- .../DataTransferObject.cs | 36 +- .../GetVoucherResponse.cs | 18 +- .../IssueVoucherRequest.cs | 13 +- .../IssueVoucherResponse.cs | 10 +- .../LogonTransactionRequest.cs | 17 +- .../LogonTransactionResponse.cs | 8 +- .../MerchantBalanceChangedEntryResponse.cs | 14 +- .../MerchantBalanceResponse.cs | 12 +- .../MetadataContants.cs | 7 +- .../OperatorTotalRequest.cs | 18 +- .../ProcessSettlementResponse.cs | 2 +- .../ReconciliationRequest.cs | 20 +- .../ReconciliationResponse.cs | 8 +- .../RedeemVoucherRequest.cs | 17 +- .../RedeemVoucherResponse.cs | 11 +- .../Contract/AddProductToContractRequest.cs | 19 +- ...ansactionFeeForProductToContractRequest.cs | 19 +- .../Contract/CreateContractRequest.cs | 12 +- .../Requests/Estate/AssignOperatorRequest.cs | 10 +- .../Requests/Estate/CreateEstateRequest.cs | 14 +- .../Estate/CreateEstateUserRequest.cs | 21 +- .../Merchant/AddMerchantContractRequest.cs | 11 +- .../Merchant/AddMerchantDeviceRequest.cs | 9 +- .../Requests/Merchant/Address.cs | 32 +- .../Merchant/AssignOperatorRequest.cs | 16 +- .../Requests/Merchant/Contact.cs | 17 +- .../Merchant/CreateMerchantRequest.cs | 25 +- .../Merchant/CreateMerchantUserRequest.cs | 21 +- .../GenerateMerchantStatementRequest.cs | 11 +- .../Merchant/MakeMerchantDepositRequest.cs | 16 +- .../Merchant/MakeMerchantWithdrawalRequest.cs | 17 +- .../Merchant/MerchantDepositSource.cs | 9 +- .../Merchant/SetSettlementScheduleRequest.cs | 5 +- .../Merchant/SwapMerchantDeviceRequest.cs | 11 +- .../Merchant/UpdateMerchantRequest.cs | 57 ++- .../CreateMerchantScheduleRequest.cs | 29 +- .../Operator/CreateOperatorRequest.cs | 19 +- .../Operator/UpdateOperatorRequest.cs | 17 +- .../Contract/AddProductToContractResponse.cs | 10 +- ...nsactionFeeForProductToContractResponse.cs | 10 +- .../Responses/Contract/CalculationType.cs | 13 +- .../Responses/Contract/ContactResponse.cs | 10 +- .../Responses/Contract/ContractProduct.cs | 15 +- .../Contract/ContractProductTransactionFee.cs | 14 +- .../Responses/Contract/ContractResponse.cs | 14 +- .../Contract/CreateContractResponse.cs | 9 +- .../Responses/Contract/FeeType.cs | 13 +- .../Responses/Contract/ProductType.cs | 12 +- .../Responses/Estate/CreateEstateResponse.cs | 7 +- .../Estate/CreateEstateUserResponse.cs | 9 +- .../Estate/EstateOperatorResponse.cs | 11 +- .../Responses/Estate/EstateResponse.cs | 13 +- .../Responses/Estate/SecurityUserResponse.cs | 8 +- .../Responses/File/FileDetailsResponse.cs | 11 +- .../Responses/File/FileLineDetailsResponse.cs | 10 +- .../Responses/File/TransactionResponse.cs | 11 +- .../Merchant/AddMerchantDeviceResponse.cs | 7 +- .../Responses/Merchant/AddressResponse.cs | 15 +- .../Merchant/AssignOperatorResponse.cs | 10 +- .../Merchant/CreateMerchantResponse.cs | 9 +- .../Merchant/CreateMerchantUserResponse.cs | 9 +- .../GenerateMerchantStatementResponse.cs | 9 +- .../Merchant/MakeMerchantDepositResponse.cs | 7 +- .../MakeMerchantWithdrawalResponse.cs | 9 +- .../Merchant/MerchantContractResponse.cs | 9 +- .../Merchant/MerchantOperatorResponse.cs | 11 +- .../Responses/Merchant/MerchantResponse.cs | 20 +- .../Merchant/MerchantScheduleResponse.cs | 14 +- .../Responses/Merchant/SettlementSchedule.cs | 10 +- .../Merchant/SwapMerchantDeviceResponse.cs | 9 +- .../Operator/CreateOperatorResponse.cs | 8 +- .../Responses/Operator/OperatorResponse.cs | 10 +- .../Settlement/SettlementFeeResponse.cs | 17 +- .../Settlement/SettlementResponse.cs | 14 +- .../SaleTransactionRequest.cs | 32 +- .../SaleTransactionResponse.cs | 9 +- .../SettlementResponse.cs | 14 +- ...actionProcessor.DataTransferObjects.csproj | 3 +- .../TransactionProcessorSteps.cs | 5 +- .../Common/DockerHelper.cs | 7 +- .../Common/GenericSteps.cs | 2 +- .../Common/TransactionProcessorWebFactory.cs | 8 +- .../DomainEventControllerTests.cs | 3 +- .../TransactionControllerTests.cs | 6 +- .../General/AutoLogonWorkerServiceTests.cs | 55 +-- .../General/BootstrapperTests.cs | 46 +- .../DomainEventHandlerRegistry.cs | 19 +- .../Bootstrapper/MiddlewareRegistry.cs | 49 +-- .../Bootstrapper/OperatorRegistry.cs | 23 +- .../Bootstrapper/RepositoryRegistry.cs | 6 +- .../Controllers/DomainEventController.cs | 48 ++- .../Endpoints/ContractEndpoints.cs | 26 +- .../Endpoints/EstateEndpoints.cs | 22 +- .../Endpoints/FloatEndpoints.cs | 8 +- .../Endpoints/MerchantEndpoints.cs | 100 +++-- .../Endpoints/OperatorEndpoints.cs | 18 +- .../Endpoints/SettlementEndpoints.cs | 35 +- .../Endpoints/TransactionEndpoints.cs | 16 +- .../Endpoints/VoucherEndpoints.cs | 13 +- TransactionProcessor/Extensions.cs | 43 +- .../Handlers/MerchantHandlers.cs | 7 +- .../Handlers/SettlementHandlers.cs | 11 +- .../Handlers/TransactionHandlers.cs | 3 + .../Handlers/VoucherHandlers.cs | 6 +- .../OpenApi/DtoDocumentationTransformer.cs | 399 ++++++++++++++++++ TransactionProcessor/Program.cs | 270 +++++++----- TransactionProcessor/Startup.cs | 197 --------- .../TransactionProcessor.csproj | 19 +- TransactionProcessor/TransactionProcessor.xml | 61 +-- docs/api-reference.md | 210 +++++++++ .../plans/2026-10-21-modern-startup.md | 75 ++++ .../specs/2026-10-21-modern-startup-design.md | 30 ++ openapi-runtime.json | 0 117 files changed, 2222 insertions(+), 808 deletions(-) create mode 100644 TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs delete mode 100644 TransactionProcessor/Startup.cs create mode 100644 docs/api-reference.md create mode 100644 docs/superpowers/plans/2026-10-21-modern-startup.md create mode 100644 docs/superpowers/specs/2026-10-21-modern-startup-design.md create mode 100644 openapi-runtime.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 5da70172..ecd38517 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,24 +8,25 @@ - - + + - - - + + + - - + + + @@ -44,7 +45,7 @@ - + @@ -53,7 +54,7 @@ - + @@ -62,33 +63,28 @@ - - + + - - - - - - - + + + + + + + - - - - - - - + - - - + + + - + + @@ -107,4 +103,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index a4c2497e..81d2a0e5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Transaction Processor +The interactive Scalar API reference is available at `/scalar` and its versioned OpenAPI documents are available under `/openapi/` (for example, `/openapi/v1.json`). The [endpoint and DTO inventory](docs/api-reference.md) is also available as Markdown. + ## Build Status [![Last Nightly Build Status Badge](https://github.com/TransactionProcessing/TransactionProcessor/workflows/Nightly%20Build/badge.svg)](https://github.com/TransactionProcessing/TransactionProcessor/workflows/Nightly%20Build/badge.svg) diff --git a/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs b/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs index 26b0e070..7018c197 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs @@ -122,14 +122,14 @@ public async Task Mediator_Send_RequestHandled() hostingEnvironment.ApplicationName.Getter().Returns("Test Application"); ServiceRegistry services = new ServiceRegistry(); - Startup s = new Startup(hostingEnvironment.Instance()); - Startup.Configuration = this.SetupMemoryConfiguration(); + IConfigurationRoot configuration = this.SetupMemoryConfiguration(); - this.AddTestRegistrations(services, hostingEnvironment.Instance()); - s.ConfigureContainer(services); + this.AddTestRegistrations(services, hostingEnvironment.Instance(), configuration); + Program.ConfigureServices(services, configuration, hostingEnvironment.Instance()); + Container container = new(services); List errors = new List(); - IMediator mediator = Startup.Container.GetService(); + IMediator mediator = container.GetService(); foreach (IBaseRequest baseRequest in this.Requests) { try @@ -161,7 +161,8 @@ private IConfigurationRoot SetupMemoryConfiguration() } private void AddTestRegistrations(ServiceRegistry services, - IWebHostEnvironment hostingEnvironment) + IWebHostEnvironment hostingEnvironment, + IConfiguration configuration) { services.AddLogging(); DiagnosticListener diagnosticSource = new DiagnosticListener(hostingEnvironment.ApplicationName); @@ -169,7 +170,7 @@ private void AddTestRegistrations(ServiceRegistry services, services.AddSingleton(diagnosticSource); services.AddSingleton(hostingEnvironment); services.AddSingleton(hostingEnvironment); - services.AddSingleton(Startup.Configuration); + services.AddSingleton(configuration); services.OverrideServices(s => { s.AddSingleton(); diff --git a/TransactionProcessor.DataTransferObjects/CreateFloatForContractProductRequest.cs b/TransactionProcessor.DataTransferObjects/CreateFloatForContractProductRequest.cs index 01e326d2..f731e677 100644 --- a/TransactionProcessor.DataTransferObjects/CreateFloatForContractProductRequest.cs +++ b/TransactionProcessor.DataTransferObjects/CreateFloatForContractProductRequest.cs @@ -1,20 +1,46 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects{ + /// + /// Float identifier and creation timestamp. + /// + /// {"floatId":"00000000-0000-0000-0000-000000000166","createDateTime":"2026-01-15T10:30:00Z"} public class CreateFloatRequest{ + /// + /// Identifier of the float. + /// public Guid FloatId { get; set; } + /// + /// Date and time the float was created. + /// public DateTime CreateDateTime{ get; set; } } + /// + /// Float credit amount, cost price, and purchase timestamp. + /// + /// {"floatId":"00000000-0000-0000-0000-000000000166","purchaseDateTime":"2026-01-15T10:30:00Z","creditAmount":42.5,"costPrice":42.5} public class RecordFloatCreditPurchaseRequest{ + /// + /// Identifier of the float. + /// public Guid FloatId { get; set; } + /// + /// Date and time of the credit purchase. + /// public DateTime PurchaseDateTime { get; set; } + /// + /// Amount of credit added to the float. + /// public Decimal CreditAmount { get; set; } + /// + /// Cost of purchasing the float credit. + /// public Decimal CostPrice { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/DataTransferObject.cs b/TransactionProcessor.DataTransferObjects/DataTransferObject.cs index 11fe1bd6..3940cea9 100644 --- a/TransactionProcessor.DataTransferObjects/DataTransferObject.cs +++ b/TransactionProcessor.DataTransferObjects/DataTransferObject.cs @@ -1,35 +1,67 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + /// + /// Shared estate, merchant, transaction timestamp, and transaction type fields. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","transactionDateTime":"2026-01-15T10:30:00Z","transactionType":"example"} [ExcludeFromCodeCoverage] public class TransactionRequest { #region Properties + /// + /// Estate that owns or scopes this operation. + /// public Guid EstateId { get; set; } + /// + /// Merchant associated with the transaction. + /// public Guid MerchantId { get; set; } + /// + /// Date and time when the transaction occurred at the terminal. + /// public DateTime TransactionDateTime { get; set; } + /// + /// Transaction type reported by the terminal. + /// public String TransactionType { get; set; } #endregion } + /// + /// Shared estate, merchant, response status, transaction identifier, and transaction type fields. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionId":"00000000-0000-0000-0000-000000000057","transactionType":"example"} public class TransactionResponse { + /// + /// Estate that owns or scopes this operation. + /// public Guid EstateId { get; set; } + /// + /// Merchant associated with the transaction. + /// public Guid MerchantId { get; set; } + /// The response code. public String ResponseCode { get; set; } + /// The response message. public String ResponseMessage { get; set; } + /// The transaction id. public Guid TransactionId { get; set; } + /// + /// Transaction type reported by the terminal. + /// public String TransactionType { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/GetVoucherResponse.cs b/TransactionProcessor.DataTransferObjects/GetVoucherResponse.cs index 5ec30052..2e829f94 100644 --- a/TransactionProcessor.DataTransferObjects/GetVoucherResponse.cs +++ b/TransactionProcessor.DataTransferObjects/GetVoucherResponse.cs @@ -1,29 +1,45 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects { + /// + /// Voucher identity, value, balance, lifecycle status, dates, and associated transaction. + /// + /// {"voucherId":"00000000-0000-0000-0000-000000000141","expiryDate":"2026-01-15T10:30:00Z","value":42.5,"balance":42.5,"voucherCode":"VOUCHER-2026-001","isGenerated":false,"isIssued":false,"isRedeemed":false,"issuedDateTime":"2026-01-15T10:30:00Z","generatedDateTime":"2026-01-15T10:30:00Z","redeemedDateTime":"2026-01-15T10:30:00Z","transactionId":"00000000-0000-0000-0000-000000000057"} public class GetVoucherResponse { + /// The voucher id. public Guid VoucherId { get; set; } + /// The expiry date. public DateTime ExpiryDate { get; set; } + /// The value. public Decimal Value { get; set; } + /// The balance. public Decimal Balance { get; set; } + /// The voucher code. public String VoucherCode { get; set; } + /// The is generated. public Boolean IsGenerated { get; set; } + /// The is issued. public Boolean IsIssued { get; set; } + /// The is redeemed. public Boolean IsRedeemed { get; set; } + /// The issued date time. public DateTime IssuedDateTime { get; set; } + /// The generated date time. public DateTime GeneratedDateTime { get; set; } + /// The redeemed date time. public DateTime RedeemedDateTime { get; set; } + /// The transaction id. public Guid TransactionId { get; set; } } diff --git a/TransactionProcessor.DataTransferObjects/IssueVoucherRequest.cs b/TransactionProcessor.DataTransferObjects/IssueVoucherRequest.cs index ddd2872f..31584a35 100644 --- a/TransactionProcessor.DataTransferObjects/IssueVoucherRequest.cs +++ b/TransactionProcessor.DataTransferObjects/IssueVoucherRequest.cs @@ -1,21 +1,30 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; + /// Data transfer object for IssueVoucherRequest. + /// {"operatorIdentifier":"EXAMPLE-001","estateId":"00000000-0000-0000-0000-000000000023","transactionId":"00000000-0000-0000-0000-000000000057","value":42.5,"recipientEmail":"customer@example.com","recipientMobile":"+15550100","issuedDateTime":"2026-01-15T10:30:00Z"} public class IssueVoucherRequest { + /// The operator identifier. public String OperatorIdentifier { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The transaction id. public Guid TransactionId { get; set; } + /// The value. public Decimal Value { get; set; } + /// The recipient email. public String RecipientEmail { get; set; } + /// The recipient mobile. public String RecipientMobile { get; set; } + /// The issued date time. public DateTime? IssuedDateTime { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/IssueVoucherResponse.cs b/TransactionProcessor.DataTransferObjects/IssueVoucherResponse.cs index 92fe8f5f..52db38a5 100644 --- a/TransactionProcessor.DataTransferObjects/IssueVoucherResponse.cs +++ b/TransactionProcessor.DataTransferObjects/IssueVoucherResponse.cs @@ -1,15 +1,21 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; + /// Data transfer object for IssueVoucherResponse. + /// {"voucherId":"00000000-0000-0000-0000-000000000141","expiryDate":"2026-01-15T10:30:00Z","voucherCode":"VOUCHER-2026-001","message":"Example message"} public class IssueVoucherResponse { + /// The voucher id. public Guid VoucherId { get; set; } + /// The expiry date. public DateTime ExpiryDate { get; set; } + /// The voucher code. public String VoucherCode { get; set; } + /// The message. public String Message { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs b/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs index 4e31f4a0..1e78f0df 100644 --- a/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs +++ b/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs @@ -1,19 +1,32 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// + /// Terminal logon message fields, including the shared transaction identity and timestamp. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","transactionDateTime":"2026-01-15T10:30:00Z","transactionType":"example","deviceIdentifier":"EXAMPLE-001","transactionNumber":"EXAMPLE-001"} [ExcludeFromCodeCoverage] public class LogonTransactionRequest : TransactionRequest { #region Properties + /// + /// Identifier assigned to the terminal device submitting the logon. + /// public String DeviceIdentifier { get; set; } + /// + /// Reference number assigned to the transaction by the terminal. + /// public String TransactionNumber { get; set; } + /// + /// Transaction type reported by the terminal. + /// public String TransactionType { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs b/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs index 9c67e331..2ab0246d 100644 --- a/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs +++ b/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs @@ -1,11 +1,15 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// + /// Processor response to a terminal logon transaction. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionId":"00000000-0000-0000-0000-000000000057","transactionType":"example"} [ExcludeFromCodeCoverage] public class LogonTransactionResponse : TransactionResponse { } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/MerchantBalanceChangedEntryResponse.cs b/TransactionProcessor.DataTransferObjects/MerchantBalanceChangedEntryResponse.cs index 6758780b..073a3869 100644 --- a/TransactionProcessor.DataTransferObjects/MerchantBalanceChangedEntryResponse.cs +++ b/TransactionProcessor.DataTransferObjects/MerchantBalanceChangedEntryResponse.cs @@ -1,25 +1,37 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects { using System.Diagnostics.CodeAnalysis; + /// + /// One merchant balance change with its event, timestamp, amount, reference, and resulting balance. + /// + /// {"originalEventId":"00000000-0000-0000-0000-000000000234","estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","changeAmount":42.5,"dateTime":"2026-01-15T10:30:00Z","reference":"REF-2026-001","debitOrCredit":"example","balance":42.5} [ExcludeFromCodeCoverage] public class MerchantBalanceChangedEntryResponse { + /// The original event id. public Guid OriginalEventId { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The change amount. public Decimal ChangeAmount { get; set; } + /// The date time. public DateTime DateTime { get; set; } + /// The reference. public String Reference { get; set; } + /// The debit or credit. public String DebitOrCredit { get; set; } + /// The balance. public Decimal Balance { get; set; } } diff --git a/TransactionProcessor.DataTransferObjects/MerchantBalanceResponse.cs b/TransactionProcessor.DataTransferObjects/MerchantBalanceResponse.cs index cd913ec3..ea336ab2 100644 --- a/TransactionProcessor.DataTransferObjects/MerchantBalanceResponse.cs +++ b/TransactionProcessor.DataTransferObjects/MerchantBalanceResponse.cs @@ -1,17 +1,25 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// + /// Stored and available balance values for a merchant. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","availableBalance":42.5,"balance":42.5} [ExcludeFromCodeCoverage] public class MerchantBalanceResponse { + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The available balance. public Decimal AvailableBalance { get; set; } + /// The balance. public Decimal Balance { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/MetadataContants.cs b/TransactionProcessor.DataTransferObjects/MetadataContants.cs index 7cec284c..97d2e936 100644 --- a/TransactionProcessor.DataTransferObjects/MetadataContants.cs +++ b/TransactionProcessor.DataTransferObjects/MetadataContants.cs @@ -1,11 +1,12 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; /// - /// + /// Metadata key names used by transaction processing. /// + /// {} [ExcludeFromCodeCoverage] public static class MetadataContants { @@ -23,4 +24,4 @@ public static class MetadataContants #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs b/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs index e002304d..958b36ef 100644 --- a/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs +++ b/TransactionProcessor.DataTransferObjects/OperatorTotalRequest.cs @@ -1,21 +1,35 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// Data transfer object for OperatorTotalRequest. + /// {"contractId":"00000000-0000-0000-0000-000000000239","operatorId":"00000000-0000-0000-0000-000000000253","transactionCount":1,"transactionValue":42.5} [ExcludeFromCodeCoverage] public class OperatorTotalRequest { #region Properties + /// + /// Contract associated with the product or transaction. + /// public Guid ContractId { get; set; } + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } + /// + /// Number of transactions included in the reconciliation. + /// public Int32 TransactionCount { get; set; } + /// + /// Total value of transactions included in the reconciliation. + /// public Decimal TransactionValue { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs b/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs index 2437b7b5..a9d41371 100644 --- a/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs +++ b/TransactionProcessor.DataTransferObjects/ProcessSettlementResponse.cs @@ -1,4 +1,4 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { internal class ProcessSettlementResponse { diff --git a/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs b/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs index f460cd1f..c9c6225f 100644 --- a/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs +++ b/TransactionProcessor.DataTransferObjects/ReconciliationRequest.cs @@ -1,22 +1,38 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + /// + /// Terminal transaction totals and optional per-operator/per-contract breakdown. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","transactionDateTime":"2026-01-15T10:30:00Z","transactionType":"example","deviceIdentifier":"EXAMPLE-001","operatorTotals":[{"contractId":"00000000-0000-0000-0000-000000000239","operatorId":"00000000-0000-0000-0000-000000000253","transactionCount":1,"transactionValue":42.5}],"transactionCount":1,"transactionValue":42.5} [ExcludeFromCodeCoverage] public class ReconciliationRequest : TransactionRequest { #region Properties + /// + /// Identifier assigned to the terminal device submitting the reconciliation. + /// public String DeviceIdentifier { get; set; } + /// + /// Optional transaction totals grouped by operator and contract. + /// public List OperatorTotals { get; set; } + /// + /// Number of transactions included in the reconciliation. + /// public Int32 TransactionCount { get; set; } + /// + /// Total value of transactions included in the reconciliation. + /// public Decimal TransactionValue { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs b/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs index 783b7c71..f77e1adb 100644 --- a/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs +++ b/TransactionProcessor.DataTransferObjects/ReconciliationResponse.cs @@ -1,11 +1,15 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// + /// Processor response to a reconciliation transaction. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionId":"00000000-0000-0000-0000-000000000057","transactionType":"example"} [ExcludeFromCodeCoverage] public class ReconciliationResponse : TransactionResponse { } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/RedeemVoucherRequest.cs b/TransactionProcessor.DataTransferObjects/RedeemVoucherRequest.cs index 18956468..fe298fa0 100644 --- a/TransactionProcessor.DataTransferObjects/RedeemVoucherRequest.cs +++ b/TransactionProcessor.DataTransferObjects/RedeemVoucherRequest.cs @@ -1,13 +1,26 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; + /// + /// Voucher code and estate to redeem, with an optional redemption timestamp. + /// + /// {"voucherCode":"VOUCHER-2026-001","estateId":"00000000-0000-0000-0000-000000000023","redeemedDateTime":"2026-01-15T10:30:00Z"} public class RedeemVoucherRequest { + /// + /// Code printed on or assigned to the voucher. + /// public String VoucherCode { get; set; } + /// + /// Estate that owns or scopes this operation. + /// public Guid EstateId { get; set; } + /// + /// Optional date and time when the voucher was redeemed; omitted values use the current local time. + /// public DateTime? RedeemedDateTime { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/RedeemVoucherResponse.cs b/TransactionProcessor.DataTransferObjects/RedeemVoucherResponse.cs index d2953bbc..c53a55db 100644 --- a/TransactionProcessor.DataTransferObjects/RedeemVoucherResponse.cs +++ b/TransactionProcessor.DataTransferObjects/RedeemVoucherResponse.cs @@ -1,12 +1,19 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; + /// + /// Voucher code, expiry date, and remaining balance after redemption. + /// + /// {"expiryDate":"2026-01-15T10:30:00Z","voucherCode":"VOUCHER-2026-001","remainingBalance":42.5} public class RedeemVoucherResponse { + /// The expiry date. public DateTime ExpiryDate { get; set; } + /// The voucher code. public String VoucherCode { get; set; } + /// The remaining balance. public Decimal RemainingBalance { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Contract/AddProductToContractRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Contract/AddProductToContractRequest.cs index 63257d47..ecb37f2a 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Contract/AddProductToContractRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Contract/AddProductToContractRequest.cs @@ -1,23 +1,36 @@ -using System; +using System; using TransactionProcessor.DataTransferObjects.Responses.Contract; namespace TransactionProcessor.DataTransferObjects.Requests.Contract { /// - /// + /// Product name, display details, type, and optional value to add to a contract. /// + /// {"displayText":"Example description","productName":"Example Name","value":42.5,"productType":0} public class AddProductToContractRequest { #region Properties + /// + /// Text shown to a user for this product. + /// public String DisplayText { get; set; } + /// + /// Name of the product. + /// public String ProductName { get; set; } + /// + /// Monetary value associated with the product or fee. + /// public Decimal? Value { get; set; } + /// + /// Category of product being added. + /// public ProductType ProductType { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Contract/AddTransactionFeeForProductToContractRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Contract/AddTransactionFeeForProductToContractRequest.cs index 9d81f020..f252ae22 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Contract/AddTransactionFeeForProductToContractRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Contract/AddTransactionFeeForProductToContractRequest.cs @@ -1,23 +1,36 @@ -using System; +using System; using TransactionProcessor.DataTransferObjects.Responses.Contract; namespace TransactionProcessor.DataTransferObjects.Requests.Contract { /// - /// + /// Fee calculation, category, description, and value to add to a contract product. /// + /// {"calculationType":0,"description":"Example description","feeType":0,"value":42.5} public class AddTransactionFeeForProductToContractRequest { #region Properties + /// + /// Method used to calculate the fee value. + /// public CalculationType CalculationType { get; set; } + /// + /// Human-readable description of the contract or fee. + /// public String Description { get; set; } + /// + /// Party to which the fee applies. + /// public FeeType FeeType { get; set; } + /// + /// Monetary value associated with the product or fee. + /// public Decimal Value { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Contract/CreateContractRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Contract/CreateContractRequest.cs index 1ba75888..38201b96 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Contract/CreateContractRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Contract/CreateContractRequest.cs @@ -1,13 +1,23 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Contract { + /// + /// Information needed to create a contract and associate it with an estate operator. + /// + /// {"operatorId":"00000000-0000-0000-0000-000000000253","description":"Example description"} [ExcludeFromCodeCoverage] public class CreateContractRequest { + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } + /// + /// Human-readable description of the contract or fee. + /// public String Description { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Estate/AssignOperatorRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Estate/AssignOperatorRequest.cs index ba6b6271..d5fb63d5 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Estate/AssignOperatorRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Estate/AssignOperatorRequest.cs @@ -1,19 +1,23 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Estate { /// - /// + /// Operator identifier to associate with an estate. /// + /// {"operatorId":"00000000-0000-0000-0000-000000000253"} [ExcludeFromCodeCoverage] public class AssignOperatorRequest { #region Properties + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs index cadce77a..49a6fc2e 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateRequest.cs @@ -1,12 +1,22 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace TransactionProcessor.DataTransferObjects.Requests.Estate { + /// + /// Estate identifier and display name used to create an estate. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","estateName":"Example Name"} public class CreateEstateRequest { + /// + /// Estate that owns or scopes this operation. + /// public Guid EstateId { get; set; } + /// + /// Display name for the estate. + /// public String EstateName { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateUserRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateUserRequest.cs index 22a89b9d..d93f9b71 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateUserRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Estate/CreateEstateUserRequest.cs @@ -1,20 +1,39 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Estate { + /// + /// Login credentials and personal name fields for a new estate user. + /// + /// {"emailAddress":"customer@example.com","password":"example-password","givenName":"Example Name","middleName":"Example Name","familyName":"Example Name"} [ExcludeFromCodeCoverage] public class CreateEstateUserRequest { + /// + /// Email address for the user or contact. + /// public String EmailAddress { get; set; } + /// + /// Password for the user account. + /// public String Password { get; set; } + /// + /// Given name of the user. + /// public String GivenName { get; set; } + /// + /// Middle name of the user, if applicable. + /// public String MiddleName { get; set; } + /// + /// Family name of the user. + /// public String FamilyName { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantContractRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantContractRequest.cs index efe53ff6..3d698055 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantContractRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantContractRequest.cs @@ -1,10 +1,17 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant{ + /// + /// Contract identifier to associate with a merchant. + /// + /// {"contractId":"00000000-0000-0000-0000-000000000239"} [ExcludeFromCodeCoverage] public class AddMerchantContractRequest { + /// + /// Contract associated with the product or transaction. + /// public Guid ContractId { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantDeviceRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantDeviceRequest.cs index 1afb31ac..13588a0e 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantDeviceRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AddMerchantDeviceRequest.cs @@ -1,11 +1,18 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Device identifier to register for a merchant. + /// + /// {"deviceIdentifier":"EXAMPLE-001"} [ExcludeFromCodeCoverage] public class AddMerchantDeviceRequest { + /// + /// Identifier of the merchant device to register. + /// public String DeviceIdentifier { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/Address.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/Address.cs index 69620c68..55621e41 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/Address.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/Address.cs @@ -1,27 +1,55 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Postal address fields used when adding or updating a merchant address. + /// + /// {"addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"} public class Address { #region Properties + /// + /// First line of the postal address. + /// public String AddressLine1 { get; set; } + /// + /// Second line of the postal address. + /// public String AddressLine2 { get; set; } + /// + /// Third line of the postal address. + /// public String AddressLine3 { get; set; } + /// + /// Fourth line of the postal address. + /// public String AddressLine4 { get; set; } + /// + /// Country for the postal address. + /// public String Country { get; set; } + /// + /// Postal or ZIP code for the address. + /// public String PostalCode { get; set; } + /// + /// State, county, or region for the address. + /// public String Region { get; set; } + /// + /// Town or city for the address. + /// public String Town { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AssignOperatorRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AssignOperatorRequest.cs index b86aade4..9ac48985 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/AssignOperatorRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/AssignOperatorRequest.cs @@ -1,22 +1,32 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { /// - /// + /// Operator identifier and merchant or terminal identifiers to assign to a merchant. /// + /// {"merchantNumber":"EXAMPLE-001","operatorId":"00000000-0000-0000-0000-000000000253","terminalNumber":"EXAMPLE-001"} [ExcludeFromCodeCoverage] public class AssignOperatorRequest { #region Properties + /// + /// Merchant number used when this operator is assigned. + /// public String MerchantNumber { get; set; } + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } + /// + /// Terminal number used when this operator is assigned. + /// public String TerminalNumber { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/Contact.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/Contact.cs index f876a88f..21ee746f 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/Contact.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/Contact.cs @@ -1,18 +1,31 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Contact name, email address, and phone number for a merchant contact. + /// + /// {"contactName":"Example Name","emailAddress":"customer@example.com","phoneNumber":"+15550100"} public class Contact { #region Properties + /// + /// Name of the contact person. + /// public String ContactName { get; set; } + /// + /// Email address for the user or contact. + /// public String EmailAddress { get; set; } + /// + /// Telephone number for the contact person. + /// public String PhoneNumber { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantRequest.cs index e40bda8c..0c445a04 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantRequest.cs @@ -1,28 +1,47 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { /// - /// + /// Merchant identity, contact, address, and settlement schedule used at creation. /// + /// {"address":{"addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"},"contact":{"contactName":"Example Name","emailAddress":"customer@example.com","phoneNumber":"+15550100"},"name":"Example Name","settlementSchedule":0,"merchantId":"00000000-0000-0000-0000-000000000227","createdDateTime":"2026-01-15T10:30:00Z"} public class CreateMerchantRequest { #region Properties + /// + /// Postal address for the merchant. + /// public Address Address { get; set; } + /// + /// Primary contact for the merchant. + /// public Contact Contact { get; set; } + /// + /// Display name for the estate, merchant, or operator. + /// public String Name { get; set; } + /// + /// Settlement schedule assigned to the merchant. + /// public SettlementSchedule SettlementSchedule { get; set; } + /// + /// Optional caller-supplied merchant identifier; when omitted the service creates one. + /// public Guid? MerchantId { get; set; } + /// + /// Optional merchant creation timestamp. + /// public DateTime? CreatedDateTime { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantUserRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantUserRequest.cs index 400e70ff..3bd722e8 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantUserRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/CreateMerchantUserRequest.cs @@ -1,20 +1,39 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Login credentials and personal name fields for a new merchant user. + /// + /// {"emailAddress":"customer@example.com","password":"example-password","givenName":"Example Name","middleName":"Example Name","familyName":"Example Name"} [ExcludeFromCodeCoverage] public class CreateMerchantUserRequest { + /// + /// Email address for the user or contact. + /// public String EmailAddress { get; set; } + /// + /// Password for the user account. + /// public String Password { get; set; } + /// + /// Given name of the user. + /// public String GivenName { get; set; } + /// + /// Middle name of the user, if applicable. + /// public String MiddleName { get; set; } + /// + /// Family name of the user. + /// public String FamilyName { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/GenerateMerchantStatementRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/GenerateMerchantStatementRequest.cs index eac8147d..b923c14d 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/GenerateMerchantStatementRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/GenerateMerchantStatementRequest.cs @@ -1,13 +1,20 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Statement date for the merchant statement to generate. + /// + /// {"merchantStatementDate":"2026-01-15T10:30:00Z"} public class GenerateMerchantStatementRequest { #region Properties + /// + /// Statement date for the statement to generate. + /// public DateTime MerchantStatementDate { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantDepositRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantDepositRequest.cs index 13285d11..a4284e43 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantDepositRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantDepositRequest.cs @@ -1,22 +1,32 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { /// - /// + /// Deposit amount, effective date and time, and reference. /// + /// {"amount":42.5,"depositDateTime":"2026-01-15T10:30:00Z","reference":"REF-2026-001"} [ExcludeFromCodeCoverage] public class MakeMerchantDepositRequest { #region Properties + /// + /// Monetary amount of the deposit or withdrawal. + /// public Decimal Amount { get; set; } + /// + /// Date and time the deposit was made. + /// public DateTime DepositDateTime { get; set; } + /// + /// External or user-supplied reference for the deposit or withdrawal. + /// public String Reference { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantWithdrawalRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantWithdrawalRequest.cs index b1d57e9c..f23b3de6 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantWithdrawalRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MakeMerchantWithdrawalRequest.cs @@ -1,19 +1,32 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Withdrawal amount, effective date and time, and reference. + /// + /// {"amount":42.5,"withdrawalDateTime":"2026-01-15T10:30:00Z","reference":"REF-2026-001"} [ExcludeFromCodeCoverage] public class MakeMerchantWithdrawalRequest { #region Properties + /// + /// Monetary amount of the deposit or withdrawal. + /// public Decimal Amount { get; set; } + /// + /// Date and time the withdrawal was made. + /// public DateTime WithdrawalDateTime { get; set; } + /// + /// External or user-supplied reference for the deposit or withdrawal. + /// public String Reference { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MerchantDepositSource.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MerchantDepositSource.cs index ef3d1476..1ee27dc4 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/MerchantDepositSource.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/MerchantDepositSource.cs @@ -1,9 +1,14 @@ -namespace TransactionProcessor.DataTransferObjects.Requests.Merchant +namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// Sources from which merchant deposits can be recorded. + /// 0 public enum MerchantDepositSource { + /// The source has not been specified. NotSet, + /// A deposit entered manually. Manual, + /// A deposit created by an automated process. Automatic } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/SetSettlementScheduleRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/SetSettlementScheduleRequest.cs index bc724c3f..e254c56f 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/SetSettlementScheduleRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/SetSettlementScheduleRequest.cs @@ -1,11 +1,14 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// Data transfer object for SetSettlementScheduleRequest. + /// {"settlementSchedule":0} [ExcludeFromCodeCoverage] public class SetSettlementScheduleRequest { + /// The settlement schedule. public SettlementSchedule SettlementSchedule { get; set; } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/SwapMerchantDeviceRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/SwapMerchantDeviceRequest.cs index 599d1526..c1c2d47f 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/SwapMerchantDeviceRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/SwapMerchantDeviceRequest.cs @@ -1,9 +1,16 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Replacement device identifier for a merchant device. + /// + /// {"newDeviceIdentifier":"EXAMPLE-001"} public class SwapMerchantDeviceRequest { + /// + /// Replacement device identifier. + /// public String NewDeviceIdentifier { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Merchant/UpdateMerchantRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Merchant/UpdateMerchantRequest.cs index bffc1136..14d032ba 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Merchant/UpdateMerchantRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Merchant/UpdateMerchantRequest.cs @@ -1,38 +1,91 @@ -using System; +using System; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace TransactionProcessor.DataTransferObjects.Requests.Merchant { + /// + /// Opening and closing times by day of week for a merchant. + /// + /// {"name":"Example Name","settlementSchedule":0} public class UpdateMerchantRequest { #region Properties + /// + /// Display name for the estate, merchant, or operator. + /// public String Name { get; set; } + /// + /// Settlement schedule assigned to the merchant. + /// public SettlementSchedule SettlementSchedule { get; set; } #endregion } + /// + /// Opening and closing times for each day of the merchant week. + /// + /// {"sunday":{"opening":"example","closing":"example"},"monday":{"opening":"example","closing":"example"},"tuesday":{"opening":"example","closing":"example"},"wednesday":{"opening":"example","closing":"example"},"thursday":{"opening":"example","closing":"example"},"friday":{"opening":"example","closing":"example"},"saturday":{"opening":"example","closing":"example"}} public class MerchantOpeningRequest { + /// + /// Opening and closing times for Sunday. + /// public OpeningHours Sunday { get; set; } + /// + /// Opening and closing times for Monday. + /// public OpeningHours Monday { get; set; } + /// + /// Opening and closing times for Tuesday. + /// public OpeningHours Tuesday { get; set; } + /// + /// Opening and closing times for Wednesday. + /// public OpeningHours Wednesday { get; set; } + /// + /// Opening and closing times for Thursday. + /// public OpeningHours Thursday { get; set; } + /// + /// Opening and closing times for Friday. + /// public OpeningHours Friday { get; set; } + /// + /// Opening and closing times for Saturday. + /// public OpeningHours Saturday { get; set; } } + /// + /// Opening and closing time for one day. + /// + /// {"opening":"example","closing":"example"} public class OpeningHours { + /// + /// Local opening time for the day, represented as a string. + /// public string Opening { get; set; } + /// + /// Local closing time for the day, represented as a string. + /// public string Closing { get; set; } } + /// Data transfer object for OpeningHoursResponse. + /// {"opening":"example","closing":"example"} public class OpeningHoursResponse { + /// + /// Local opening time for the day, represented as a string. + /// public string Opening { get; set; } + /// + /// Local closing time for the day, represented as a string. + /// public string Closing { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/MerchantSchedule/CreateMerchantScheduleRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/MerchantSchedule/CreateMerchantScheduleRequest.cs index f17ab8a1..9fb51674 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/MerchantSchedule/CreateMerchantScheduleRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/MerchantSchedule/CreateMerchantScheduleRequest.cs @@ -1,28 +1,55 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.MerchantSchedule { + /// + /// Year and monthly closure-day definitions for a new merchant schedule. + /// + /// {"year":1,"months":[{"month":1,"closedDays":[]}]} [ExcludeFromCodeCoverage] public class CreateMerchantScheduleRequest { + /// + /// Calendar year for the schedule. + /// public Int32 Year { get; set; } + /// + /// Month entries that define closure days. + /// public List Months { get; set; } = []; } + /// + /// Monthly closure-day definitions to apply to an existing merchant schedule. + /// + /// {"months":[{"month":1,"closedDays":[]}]} [ExcludeFromCodeCoverage] public class UpdateMerchantScheduleRequest { + /// + /// Month entries that define closure days. + /// public List Months { get; set; } = []; } + /// + /// One schedule month and the day numbers on which the merchant is closed. + /// + /// {"month":1,"closedDays":[]} [ExcludeFromCodeCoverage] public class MerchantScheduleMonthRequest { + /// + /// Calendar month number. + /// public Int32 Month { get; set; } + /// + /// Day numbers in the month when the merchant is closed. + /// public List ClosedDays { get; set; } = []; } } diff --git a/TransactionProcessor.DataTransferObjects/Requests/Operator/CreateOperatorRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Operator/CreateOperatorRequest.cs index aca1e7bb..305ca84e 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Operator/CreateOperatorRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Operator/CreateOperatorRequest.cs @@ -1,25 +1,38 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Operator { /// - /// + /// Operator identity and configuration used to create an estate operator. /// + /// {"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","requireCustomMerchantNumber":false,"requireCustomTerminalNumber":false} [ExcludeFromCodeCoverage] public class CreateOperatorRequest { #region Properties + /// + /// Display name for the estate, merchant, or operator. + /// public String Name { get; set; } + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } + /// + /// Whether a custom merchant number is required for this operator. + /// public Boolean? RequireCustomMerchantNumber { get; set; } + /// + /// Whether a custom terminal number is required for this operator. + /// public Boolean? RequireCustomTerminalNumber { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Requests/Operator/UpdateOperatorRequest.cs b/TransactionProcessor.DataTransferObjects/Requests/Operator/UpdateOperatorRequest.cs index 876f74e7..8999f09a 100644 --- a/TransactionProcessor.DataTransferObjects/Requests/Operator/UpdateOperatorRequest.cs +++ b/TransactionProcessor.DataTransferObjects/Requests/Operator/UpdateOperatorRequest.cs @@ -1,19 +1,32 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Requests.Operator{ + /// + /// Operator name and optional merchant or terminal number requirements to update. + /// + /// {"name":"Example Name","requireCustomMerchantNumber":false,"requireCustomTerminalNumber":false} [ExcludeFromCodeCoverage] public class UpdateOperatorRequest { #region Properties + /// + /// Display name for the estate, merchant, or operator. + /// public String Name { get; set; } + /// + /// Whether a custom merchant number is required for this operator. + /// public Boolean? RequireCustomMerchantNumber { get; set; } + /// + /// Whether a custom terminal number is required for this operator. + /// public Boolean? RequireCustomTerminalNumber { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/AddProductToContractResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/AddProductToContractResponse.cs index 806f1f18..e5f68223 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/AddProductToContractResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/AddProductToContractResponse.cs @@ -1,22 +1,26 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { /// - /// + /// Identifiers of the contract, estate, and newly added product. /// + /// {"contractId":"00000000-0000-0000-0000-000000000239","estateId":"00000000-0000-0000-0000-000000000023","productId":"00000000-0000-0000-0000-000000000146"} [ExcludeFromCodeCoverage] public class AddProductToContractResponse { #region Properties + /// The contract id. public Guid ContractId { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The product id. public Guid ProductId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/AddTransactionFeeForProductToContractResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/AddTransactionFeeForProductToContractResponse.cs index 7bc8e5b1..21831446 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/AddTransactionFeeForProductToContractResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/AddTransactionFeeForProductToContractResponse.cs @@ -1,21 +1,27 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// Data transfer object for AddTransactionFeeForProductToContractResponse. + /// {"contractId":"00000000-0000-0000-0000-000000000239","estateId":"00000000-0000-0000-0000-000000000023","productId":"00000000-0000-0000-0000-000000000146","transactionFeeId":"00000000-0000-0000-0000-000000000074"} [ExcludeFromCodeCoverage] public class AddTransactionFeeForProductToContractResponse { #region Properties + /// The contract id. public Guid ContractId { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The product id. public Guid ProductId { get; set; } + /// The transaction fee id. public Guid TransactionFeeId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/CalculationType.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/CalculationType.cs index 5ed95303..3ea9cf21 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/CalculationType.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/CalculationType.cs @@ -1,18 +1,17 @@ -namespace TransactionProcessor.DataTransferObjects.Responses.Contract +namespace TransactionProcessor.DataTransferObjects.Responses.Contract { - /// - /// - /// + /// Methods used to calculate a transaction fee. + /// 0 public enum CalculationType { /// - /// The fixed + /// A fixed fee amount. /// Fixed, /// - /// The percentage + /// A fee calculated as a percentage of the transaction value. /// Percentage } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContactResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContactResponse.cs index 718fafa2..e69fdc71 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContactResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContactResponse.cs @@ -1,20 +1,26 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// Data transfer object for ContactResponse. + /// {"contactEmailAddress":"customer@example.com","contactId":"00000000-0000-0000-0000-000000000125","contactName":"Example Name","contactPhoneNumber":"+15550100"} [ExcludeFromCodeCoverage] public class ContactResponse { #region Properties + /// The contact email address. public String ContactEmailAddress { get; set; } + /// The contact id. public Guid ContactId { get; set; } + /// The contact name. public String ContactName { get; set; } + /// The contact phone number. public String ContactPhoneNumber { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProduct.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProduct.cs index 9b166470..3b10e644 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProduct.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProduct.cs @@ -1,22 +1,33 @@ -using System; +using System; using System.Collections.Generic; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// + /// Product details and transaction fees included in a contract. + /// + /// {"displayText":"Example description","name":"Example Name","productId":"00000000-0000-0000-0000-000000000146","productReportingId":1,"transactionFees":[{"calculationType":0,"feeType":0,"description":"Example description","transactionFeeId":"00000000-0000-0000-0000-000000000074","transactionFeeReportingId":1,"value":42.5}],"value":42.5,"productType":0} public class ContractProduct { + /// The display text. public String DisplayText { get; set; } + /// The name. public String Name { get; set; } + /// The product id. public Guid ProductId { get; set; } + /// The product reporting id. public Int32 ProductReportingId { get; set; } + /// The transaction fees. public List TransactionFees { get; set; } + /// The value. public Decimal? Value { get; set; } + /// The product type. public ProductType ProductType { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProductTransactionFee.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProductTransactionFee.cs index fe80ee42..aea85007 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProductTransactionFee.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractProductTransactionFee.cs @@ -1,23 +1,33 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// + /// Transaction fee calculation details included in a contract product. + /// + /// {"calculationType":0,"feeType":0,"description":"Example description","transactionFeeId":"00000000-0000-0000-0000-000000000074","transactionFeeReportingId":1,"value":42.5} public class ContractProductTransactionFee { #region Properties + /// The calculation type. public CalculationType CalculationType { get; set; } + /// The fee type. public FeeType FeeType { get; set; } + /// The description. public String Description { get; set; } + /// The transaction fee id. public Guid TransactionFeeId { get; set; } + /// The transaction fee reporting id. public Int32 TransactionFeeReportingId { get; set; } + /// The value. public Decimal Value { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractResponse.cs index 72066986..360a6ac2 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/ContractResponse.cs @@ -1,23 +1,35 @@ -using System; +using System; using System.Collections.Generic; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// + /// Contract identity and details, including products and their transaction fees. + /// + /// {"contractId":"00000000-0000-0000-0000-000000000239","contractReportingId":1,"description":"Example description","estateId":"00000000-0000-0000-0000-000000000023","estateReportingId":1,"operatorId":"00000000-0000-0000-0000-000000000253","operatorName":"Example Name","products":[{"displayText":"Example description","name":"Example Name","productId":"00000000-0000-0000-0000-000000000146","productReportingId":1,"transactionFees":[{"calculationType":0,"feeType":0,"description":"Example description","transactionFeeId":"00000000-0000-0000-0000-000000000074","transactionFeeReportingId":1,"value":42.5}],"value":42.5,"productType":0}]} public class ContractResponse { + /// The contract id. public Guid ContractId { get; set; } + /// The contract reporting id. public Int32 ContractReportingId { get; set; } + /// The description. public String Description { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The estate reporting id. public Int32 EstateReportingId { get; set; } + /// The operator id. public Guid OperatorId { get; set; } + /// The operator name. public String OperatorName { get; set; } + /// The products. public List Products { get; set; } } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/CreateContractResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/CreateContractResponse.cs index b55a3260..e4610bc0 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/CreateContractResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/CreateContractResponse.cs @@ -1,16 +1,21 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// Data transfer object for CreateContractResponse. + /// {"contractId":"00000000-0000-0000-0000-000000000239","estateId":"00000000-0000-0000-0000-000000000023","operatorId":"00000000-0000-0000-0000-000000000253"} public class CreateContractResponse { #region Properties + /// The contract id. public Guid ContractId { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The operator id. public Guid OperatorId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/FeeType.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/FeeType.cs index 3ec0e665..1b2e1a93 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/FeeType.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/FeeType.cs @@ -1,18 +1,17 @@ -namespace TransactionProcessor.DataTransferObjects.Responses.Contract +namespace TransactionProcessor.DataTransferObjects.Responses.Contract { - /// - /// - /// + /// Parties that can receive a transaction fee. + /// 0 public enum FeeType { /// - /// The merchant + /// The fee is payable to the merchant. /// Merchant, /// - /// The service provider + /// The fee is payable to the service provider. /// ServiceProvider } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Contract/ProductType.cs b/TransactionProcessor.DataTransferObjects/Responses/Contract/ProductType.cs index d0d6b9bc..967532c5 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Contract/ProductType.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Contract/ProductType.cs @@ -1,14 +1,22 @@ -namespace TransactionProcessor.DataTransferObjects.Responses.Contract +namespace TransactionProcessor.DataTransferObjects.Responses.Contract { + /// Product categories that can be assigned to a contract. + /// 0 public enum ProductType { + /// No product category has been selected. NotSet = 0, + /// Mobile top-up product. MobileTopup, + /// Voucher product. Voucher, + /// Bill payment product. BillPayment, + + /// Agency banking product. AgencyBanking } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateResponse.cs index 9161e4de..c4cf8d35 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateResponse.cs @@ -1,11 +1,14 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Estate { + /// Data transfer object for CreateEstateResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023"} [ExcludeFromCodeCoverage] public class CreateEstateResponse { + /// The estate id. public Guid EstateId { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateUserResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateUserResponse.cs index 84d7e17a..6f3f366d 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateUserResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Estate/CreateEstateUserResponse.cs @@ -1,16 +1,19 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Estate { /// - /// + /// Identifiers of the estate and newly created user. /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","userId":"00000000-0000-0000-0000-000000000079"} [ExcludeFromCodeCoverage] public class CreateEstateUserResponse { + /// The estate id. public Guid EstateId { get; set; } + /// The user id. public Guid UserId { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateOperatorResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateOperatorResponse.cs index 88696050..9793cceb 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateOperatorResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateOperatorResponse.cs @@ -1,23 +1,30 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Estate { + /// Data transfer object for EstateOperatorResponse. + /// {"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","requireCustomMerchantNumber":false,"requireCustomTerminalNumber":false,"isDeleted":false} [ExcludeFromCodeCoverage] public class EstateOperatorResponse { #region Properties + /// The name. public String Name { get; set; } + /// The operator id. public Guid OperatorId { get; set; } + /// The require custom merchant number. public Boolean RequireCustomMerchantNumber { get; set; } + /// The require custom terminal number. public Boolean RequireCustomTerminalNumber { get; set; } + /// The is deleted. public Boolean IsDeleted { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateResponse.cs index ce93d2bd..a68997c7 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Estate/EstateResponse.cs @@ -1,29 +1,36 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Estate { /// - /// + /// Estate details with its operators and security users. /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","estateReportingId":1,"estateName":"Example Name","estateReference":"REF-2026-001","operators":[{"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","requireCustomMerchantNumber":false,"requireCustomTerminalNumber":false,"isDeleted":false}],"securityUsers":[{"emailAddress":"customer@example.com","securityUserId":"00000000-0000-0000-0000-000000000170"}]} [ExcludeFromCodeCoverage] public class EstateResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The estate reporting id. public Int32 EstateReportingId { get; set; } + /// The estate name. public String EstateName { get; set; } + /// The estate reference. public String EstateReference { get; set; } + /// The operators. public List Operators { get; set; } + /// The security users. public List SecurityUsers { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Estate/SecurityUserResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Estate/SecurityUserResponse.cs index 7efafb98..df196f67 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Estate/SecurityUserResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Estate/SecurityUserResponse.cs @@ -1,17 +1,21 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Estate { + /// Data transfer object for SecurityUserResponse. + /// {"emailAddress":"customer@example.com","securityUserId":"00000000-0000-0000-0000-000000000170"} [ExcludeFromCodeCoverage] public class SecurityUserResponse { #region Properties + /// The email address. public String EmailAddress { get; set; } + /// The security user id. public Guid SecurityUserId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/File/FileDetailsResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/File/FileDetailsResponse.cs index 6fdd4fea..7c15fa53 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/File/FileDetailsResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/File/FileDetailsResponse.cs @@ -1,17 +1,24 @@ -using System; +using System; using System.Collections.Generic; using TransactionProcessor.DataTransferObjects.Responses.Merchant; namespace TransactionProcessor.DataTransferObjects.Responses.File{ + /// Data transfer object for FileDetailsResponse. + /// {"fileId":"00000000-0000-0000-0000-000000000048","fileReceivedDate":"2026-01-15T10:30:00Z","fileReceivedDateTime":"2026-01-15T10:30:00Z","fileLineDetails":[{"fileLineData":"example","fileLineNumber":1,"status":"example","transaction":{"transactionId":"00000000-0000-0000-0000-000000000057","authCode":"VOUCHER-2026-001","isAuthorised":false,"isCompleted":false,"responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionNumber":"EXAMPLE-001"}}],"merchant":{"addresses":[{"addressId":"00000000-0000-0000-0000-000000000119","addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"}],"contacts":[{"contactEmailAddress":"customer@example.com","contactId":"00000000-0000-0000-0000-000000000125","contactName":"Example Name","contactPhoneNumber":"+15550100"}],"estateId":"00000000-0000-0000-0000-000000000023","estateReportingId":1,"merchantId":"00000000-0000-0000-0000-000000000227","merchantReportingId":1,"merchantName":"Example Name","merchantReference":"REF-2026-001","nextStatementDate":"2026-01-15T10:30:00Z","operators":[{"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","merchantNumber":"EXAMPLE-001","terminalNumber":"EXAMPLE-001","isDeleted":false}],"settlementSchedule":0,"contracts":[{"contractId":"00000000-0000-0000-0000-000000000239","isDeleted":false,"contractProducts":[]}],"schedules":[{"year":1,"months":[{"month":1,"closedDays":[]}]}]}} public class FileDetailsResponse{ + /// The file id. public Guid FileId{ get; set; } + /// The file received date. public DateTime FileReceivedDate{ get; set; } + /// The file received date time. public DateTime FileReceivedDateTime{ get; set; } + /// The file line details. public List FileLineDetails{ get; set; } + /// The merchant. public MerchantResponse Merchant{ get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/File/FileLineDetailsResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/File/FileLineDetailsResponse.cs index 25483774..064e7d5d 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/File/FileLineDetailsResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/File/FileLineDetailsResponse.cs @@ -1,10 +1,16 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.File{ + /// Data transfer object for FileLineDetailsResponse. + /// {"fileLineData":"example","fileLineNumber":1,"status":"example","transaction":{"transactionId":"00000000-0000-0000-0000-000000000057","authCode":"VOUCHER-2026-001","isAuthorised":false,"isCompleted":false,"responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionNumber":"EXAMPLE-001"}} public class FileLineDetailsResponse{ + /// The file line data. public String FileLineData{ get; set; } + /// The file line number. public Int32 FileLineNumber { get; set; } + /// The status. public String Status{ get; set; } + /// The transaction. public TransactionResponse Transaction{ get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/File/TransactionResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/File/TransactionResponse.cs index b9649035..f3becb5b 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/File/TransactionResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/File/TransactionResponse.cs @@ -1,15 +1,24 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.File { + /// Data transfer object for TransactionResponse. + /// {"transactionId":"00000000-0000-0000-0000-000000000057","authCode":"VOUCHER-2026-001","isAuthorised":false,"isCompleted":false,"responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionNumber":"EXAMPLE-001"} public class TransactionResponse { + /// The transaction id. public Guid TransactionId { get; set; } + /// The auth code. public String AuthCode { get; set; } + /// The is authorised. public Boolean IsAuthorised { get; set; } + /// The is completed. public Boolean IsCompleted { get; set; } + /// The response code. public String ResponseCode { get; set; } + /// The response message. public String ResponseMessage { get; set; } + /// The transaction number. public String TransactionNumber { get; set; } } } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddMerchantDeviceResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddMerchantDeviceResponse.cs index 2443d7ee..99a884f3 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddMerchantDeviceResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddMerchantDeviceResponse.cs @@ -1,16 +1,21 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for AddMerchantDeviceResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","deviceId":"00000000-0000-0000-0000-000000000001"} [ExcludeFromCodeCoverage] public class AddMerchantDeviceResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The device id. public Guid DeviceId { get; set; } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddressResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddressResponse.cs index d39825e0..4debb29b 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddressResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AddressResponse.cs @@ -1,31 +1,42 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for AddressResponse. + /// {"addressId":"00000000-0000-0000-0000-000000000119","addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"} [ExcludeFromCodeCoverage] public class AddressResponse { #region Properties + /// The address id. public Guid AddressId { get; set; } + /// The address line1. public String AddressLine1 { get; set; } + /// The address line2. public String AddressLine2 { get; set; } + /// The address line3. public String AddressLine3 { get; set; } + /// The address line4. public String AddressLine4 { get; set; } + /// The country. public String Country { get; set; } + /// The postal code. public String PostalCode { get; set; } + /// The region. public String Region { get; set; } + /// The town. public String Town { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AssignOperatorResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AssignOperatorResponse.cs index da7ff612..ac6640a9 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/AssignOperatorResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/AssignOperatorResponse.cs @@ -1,22 +1,26 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { /// - /// + /// Identifiers of the estate, merchant, and assigned operator. /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","operatorId":"00000000-0000-0000-0000-000000000253"} [ExcludeFromCodeCoverage] public class AssignOperatorResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The operator id. public Guid OperatorId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantResponse.cs index 7efbcf0d..755f87f7 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantResponse.cs @@ -1,20 +1,23 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { /// - /// + /// Identifiers of the estate and newly created merchant. /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227"} [ExcludeFromCodeCoverage] public class CreateMerchantResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantUserResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantUserResponse.cs index 95827e92..d308810b 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantUserResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/CreateMerchantUserResponse.cs @@ -1,19 +1,24 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for CreateMerchantUserResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","userId":"00000000-0000-0000-0000-000000000079"} [ExcludeFromCodeCoverage] public class CreateMerchantUserResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The user id. public Guid UserId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/GenerateMerchantStatementResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/GenerateMerchantStatementResponse.cs index f40ceaa7..0d8a2397 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/GenerateMerchantStatementResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/GenerateMerchantStatementResponse.cs @@ -1,11 +1,16 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for GenerateMerchantStatementResponse. + /// {"merchantStatementId":"00000000-0000-0000-0000-000000000156","merchantId":"00000000-0000-0000-0000-000000000227","estateId":"00000000-0000-0000-0000-000000000023"} public class GenerateMerchantStatementResponse { + /// The merchant statement id. public Guid MerchantStatementId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The estate id. public Guid EstateId { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantDepositResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantDepositResponse.cs index 928e68fa..0b7a4e1e 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantDepositResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantDepositResponse.cs @@ -1,16 +1,21 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for MakeMerchantDepositResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","depositId":"00000000-0000-0000-0000-000000000137"} [ExcludeFromCodeCoverage] public class MakeMerchantDepositResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The deposit id. public Guid DepositId { get; set; } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantWithdrawalResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantWithdrawalResponse.cs index 5a506b27..fcc62b36 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantWithdrawalResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MakeMerchantWithdrawalResponse.cs @@ -1,19 +1,24 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for MakeMerchantWithdrawalResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","withdrawalId":"00000000-0000-0000-0000-000000000201"} [ExcludeFromCodeCoverage] public class MakeMerchantWithdrawalResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The withdrawal id. public Guid WithdrawalId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantContractResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantContractResponse.cs index 8533f00f..a7cda399 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantContractResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantContractResponse.cs @@ -1,13 +1,18 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant{ + /// Data transfer object for MerchantContractResponse. + /// {"contractId":"00000000-0000-0000-0000-000000000239","isDeleted":false,"contractProducts":[]} [ExcludeFromCodeCoverage] public class MerchantContractResponse { + /// The contract id. public Guid ContractId { get; set; } + /// The is deleted. public Boolean IsDeleted { get; set; } + /// The contract products. public List ContractProducts { get; set; } @@ -16,4 +21,4 @@ public MerchantContractResponse() this.ContractProducts = new List(); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantOperatorResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantOperatorResponse.cs index acf38a9d..fd1d19fc 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantOperatorResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantOperatorResponse.cs @@ -1,22 +1,29 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for MerchantOperatorResponse. + /// {"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","merchantNumber":"EXAMPLE-001","terminalNumber":"EXAMPLE-001","isDeleted":false} [ExcludeFromCodeCoverage] public class MerchantOperatorResponse { #region Properties + /// The name. public String Name { get; set; } + /// The operator id. public Guid OperatorId { get; set; } + /// The merchant number. public String MerchantNumber { get; set; } + /// The terminal number. public String TerminalNumber { get; set; } + /// The is deleted. public Boolean IsDeleted { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantResponse.cs index 97ee7ed9..c4a0c8f7 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using TransactionProcessor.DataTransferObjects.Requests.Merchant; using TransactionProcessor.DataTransferObjects.Responses.Contract; @@ -6,39 +6,55 @@ namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { /// - /// + /// Merchant details including contacts, devices, contracts, schedule, and opening hours. /// + /// {"addresses":[{"addressId":"00000000-0000-0000-0000-000000000119","addressLine1":"123 Example Street","addressLine2":"123 Example Street","addressLine3":"123 Example Street","addressLine4":"123 Example Street","country":"US","postalCode":"10001","region":"Example City","town":"Example City"}],"contacts":[{"contactEmailAddress":"customer@example.com","contactId":"00000000-0000-0000-0000-000000000125","contactName":"Example Name","contactPhoneNumber":"+15550100"}],"devices":{"00000000-0000-0000-0000-000000000001":"EXAMPLE-001"},"estateId":"00000000-0000-0000-0000-000000000023","estateReportingId":1,"merchantId":"00000000-0000-0000-0000-000000000227","merchantReportingId":1,"merchantName":"Example Name","merchantReference":"REF-2026-001","nextStatementDate":"2026-01-15T10:30:00Z","operators":[{"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","merchantNumber":"EXAMPLE-001","terminalNumber":"EXAMPLE-001","isDeleted":false}],"settlementSchedule":0,"contracts":[{"contractId":"00000000-0000-0000-0000-000000000239","isDeleted":false,"contractProducts":[]}],"openingHours":{"1":{"opening":"09:00","closing":"17:00"}},"schedules":[{"year":1,"months":[{"month":1,"closedDays":[]}] }]} public class MerchantResponse { #region Properties + /// The addresses. public List Addresses { get; set; } + /// The contacts. public List Contacts { get; set; } + /// Devices registered to the merchant, keyed by device ID. public Dictionary Devices { get; set; } + /// The estate id. public Guid EstateId { get; set; } + /// The estate reporting id. public Int32 EstateReportingId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The merchant reporting id. public Int32 MerchantReportingId { get; set; } + /// The merchant name. public String MerchantName { get; set; } + /// The merchant reference. public String MerchantReference { get; set; } + /// The next statement date. public DateTime NextStatementDate { get; set; } + /// The operators. public List Operators { get; set; } + /// The settlement schedule. public SettlementSchedule SettlementSchedule { get; set; } + /// The contracts. public List Contracts { get; set; } + /// Opening and closing times keyed by day of week. public Dictionary OpeningHours { get; set; } + /// The schedules. public List Schedules { get; set; } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantScheduleResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantScheduleResponse.cs index da2c8bca..a07137f8 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantScheduleResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/MerchantScheduleResponse.cs @@ -1,18 +1,30 @@ -using System; +using System; using System.Collections.Generic; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant; +/// +/// Merchant schedule for a year, grouped by month and closed day. +/// + /// {"year":1,"months":[{"month":1,"closedDays":[]}]} public class MerchantScheduleResponse { + /// The year. public Int32 Year { get; set; } + /// The months. public List Months { get; set; } = []; } +/// +/// One schedule month and its closed day numbers. +/// + /// {"month":1,"closedDays":[]} public class MerchantScheduleMonthResponse { + /// The month. public Int32 Month { get; set; } + /// The closed days. public List ClosedDays { get; set; } = []; } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/SettlementSchedule.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/SettlementSchedule.cs index bd875a58..09386319 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/SettlementSchedule.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/SettlementSchedule.cs @@ -1,10 +1,16 @@ -namespace TransactionProcessor.DataTransferObjects.Responses.Merchant +namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Schedules that determine when merchant fees are settled. + /// 0 public enum SettlementSchedule { + /// No settlement schedule has been selected. NotSet, + /// Settle fees as soon as they are eligible. Immediate, + /// Settle fees once each week. Weekly, + /// Settle fees once each month. Monthly } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Merchant/SwapMerchantDeviceResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Merchant/SwapMerchantDeviceResponse.cs index f610d8ec..792752fa 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Merchant/SwapMerchantDeviceResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Merchant/SwapMerchantDeviceResponse.cs @@ -1,19 +1,24 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Merchant { + /// Data transfer object for SwapMerchantDeviceResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","deviceId":"00000000-0000-0000-0000-000000000001"} [ExcludeFromCodeCoverage] public class SwapMerchantDeviceResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The device id. public Guid DeviceId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Operator/CreateOperatorResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Operator/CreateOperatorResponse.cs index 76bcd017..d8707460 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Operator/CreateOperatorResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Operator/CreateOperatorResponse.cs @@ -1,17 +1,21 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Operator { + /// Data transfer object for CreateOperatorResponse. + /// {"estateId":"00000000-0000-0000-0000-000000000023","operatorId":"00000000-0000-0000-0000-000000000253"} [ExcludeFromCodeCoverage] public class CreateOperatorResponse { #region Properties + /// The estate id. public Guid EstateId { get; set; } + /// The operator id. public Guid OperatorId { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Operator/OperatorResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Operator/OperatorResponse.cs index ceb891c3..505cd2fd 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Operator/OperatorResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Operator/OperatorResponse.cs @@ -1,14 +1,22 @@ -using System; +using System; namespace TransactionProcessor.DataTransferObjects.Responses.Operator { + /// + /// Operator identity and merchant or terminal number configuration. + /// + /// {"name":"Example Name","operatorId":"00000000-0000-0000-0000-000000000253","requireCustomMerchantNumber":false,"requireCustomTerminalNumber":false} public class OperatorResponse { + /// The name. public String Name { get; set; } + /// The operator id. public Guid OperatorId { get; set; } + /// The require custom merchant number. public Boolean RequireCustomMerchantNumber { get; set; } + /// The require custom terminal number. public Boolean RequireCustomTerminalNumber { get; set; } diff --git a/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementFeeResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementFeeResponse.cs index 9f9edd01..81db2e07 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementFeeResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementFeeResponse.cs @@ -1,30 +1,43 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Settlement { + /// + /// One fee included in a settlement, with merchant, transaction, operator, amount, and status. + /// + /// {"calculatedValue":42.5,"feeDescription":"Example description","isSettled":false,"merchantId":"00000000-0000-0000-0000-000000000227","merchantName":"Example Name","settlementDate":"2026-01-15T10:30:00Z","settlementId":"00000000-0000-0000-0000-000000000215","transactionId":"00000000-0000-0000-0000-000000000057","operatorIdentifier":"EXAMPLE-001"} [ExcludeFromCodeCoverage] public class SettlementFeeResponse { #region Properties + /// The calculated value. public Decimal CalculatedValue { get; set; } + /// The fee description. public String FeeDescription { get; set; } + /// The is settled. public Boolean IsSettled { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The merchant name. public String MerchantName { get; set; } + /// The settlement date. public DateTime SettlementDate { get; set; } + /// The settlement id. public Guid SettlementId { get; set; } + /// The transaction id. public Guid TransactionId { get; set; } + /// The operator identifier. public String OperatorIdentifier { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementResponse.cs b/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementResponse.cs index 2a463622..2c34dfd4 100644 --- a/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementResponse.cs +++ b/TransactionProcessor.DataTransferObjects/Responses/Settlement/SettlementResponse.cs @@ -1,9 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace TransactionProcessor.DataTransferObjects.Responses.Settlement { + /// + /// Settlement totals, completion status, and individual settlement fees. + /// + /// {"isCompleted":false,"numberOfFeesSettled":1,"settlementDate":"2026-01-15T10:30:00Z","settlementFees":[{"calculatedValue":42.5,"feeDescription":"Example description","isSettled":false,"merchantId":"00000000-0000-0000-0000-000000000227","merchantName":"Example Name","settlementDate":"2026-01-15T10:30:00Z","settlementId":"00000000-0000-0000-0000-000000000215","transactionId":"00000000-0000-0000-0000-000000000057","operatorIdentifier":"EXAMPLE-001"}],"settlementId":"00000000-0000-0000-0000-000000000215","valueOfFeesSettled":42.5} [ExcludeFromCodeCoverage] public class SettlementResponse { @@ -17,19 +21,25 @@ public SettlementResponse() #endregion #region Properties + /// The is completed. public Boolean IsCompleted { get; set; } + /// The number of fees settled. public Int32 NumberOfFeesSettled { get; set; } + /// The settlement date. public DateTime SettlementDate { get; set; } + /// The settlement fees. public List SettlementFees { get; set; } + /// The settlement id. public Guid SettlementId { get; set; } + /// The value of fees settled. public Decimal ValueOfFeesSettled { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs index 60cb1fca..b4fb5817 100644 --- a/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs +++ b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs @@ -1,30 +1,58 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + /// + /// Sale details including merchant, device, operator, contract, product, and optional metadata. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","transactionDateTime":"2026-01-15T10:30:00Z","transactionType":"example","contractId":"00000000-0000-0000-0000-000000000239","customerEmailAddress":"customer@example.com","deviceIdentifier":"EXAMPLE-001","operatorId":"00000000-0000-0000-0000-000000000253","productId":"00000000-0000-0000-0000-000000000146","transactionNumber":"EXAMPLE-001","transactionSource":1,"additionalTransactionMetadata":{"channel":"terminal"}} [ExcludeFromCodeCoverage] public class SaleTransactionRequest : TransactionRequest { #region Properties + /// + /// Optional key/value metadata to include with the transaction. + /// public Dictionary AdditionalTransactionMetadata { get; set; } + /// + /// Contract associated with the product or transaction. + /// public Guid ContractId { get; set; } + /// + /// Optional customer email address associated with the sale. + /// public String CustomerEmailAddress { get; set; } + /// + /// Identifier assigned to the terminal device submitting the sale. + /// public String DeviceIdentifier { get; set; } + /// + /// Operator associated with the operation. + /// public Guid OperatorId { get; set; } + /// + /// Product associated with the contract or transaction. + /// public Guid ProductId { get; set; } + /// + /// Reference number assigned to the transaction by the terminal. + /// public String TransactionNumber { get; set; } + /// + /// Numeric transaction source code; omitted values default to online source code 1. + /// public Int32? TransactionSource { get; set; } #endregion } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs b/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs index 542e2207..63c2e774 100644 --- a/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs +++ b/TransactionProcessor.DataTransferObjects/SaleTransactionResponse.cs @@ -1,12 +1,17 @@ -namespace TransactionProcessor.DataTransferObjects +namespace TransactionProcessor.DataTransferObjects { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + /// + /// Processor response to a sale transaction, including returned transaction metadata. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","responseCode":"VOUCHER-2026-001","responseMessage":"Example message","transactionId":"00000000-0000-0000-0000-000000000057","transactionType":"example","additionalTransactionMetadata":{"example":"example"}} [ExcludeFromCodeCoverage] public class SaleTransactionResponse : TransactionResponse { + /// Metadata returned with the sale transaction response. public Dictionary AdditionalTransactionMetadata { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/SettlementResponse.cs b/TransactionProcessor.DataTransferObjects/SettlementResponse.cs index c01118d2..a9d54a5e 100644 --- a/TransactionProcessor.DataTransferObjects/SettlementResponse.cs +++ b/TransactionProcessor.DataTransferObjects/SettlementResponse.cs @@ -1,19 +1,29 @@ - + namespace TransactionProcessor.DataTransferObjects { using System; using System.Diagnostics.CodeAnalysis; + /// + /// Pending and completed fee counts and status for a merchant settlement date. + /// + /// {"estateId":"00000000-0000-0000-0000-000000000023","merchantId":"00000000-0000-0000-0000-000000000227","settlementDate":"2026-01-15T10:30:00Z","numberOfFeesPendingSettlement":1,"numberOfFeesSettled":1,"settlementCompleted":false} [ExcludeFromCodeCoverage] public class SettlementResponse { + /// The estate id. public Guid EstateId { get; set; } + /// The merchant id. public Guid MerchantId { get; set; } + /// The settlement date. public DateTime SettlementDate { get; set; } + /// The number of fees pending settlement. public Int32 NumberOfFeesPendingSettlement { get; set; } + /// The number of fees settled. public Int32 NumberOfFeesSettled { get; set; } + /// The settlement completed. public Boolean SettlementCompleted { get; set; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj b/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj index 6a29b12a..3de819ad 100644 --- a/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj +++ b/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj @@ -3,11 +3,12 @@ net10.0 None + true + $(NoWarn);1591 - diff --git a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs index 542ce5df..842668a9 100644 --- a/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs +++ b/TransactionProcessor.IntegrationTesting.Helpers/TransactionProcessorSteps.cs @@ -1,4 +1,5 @@ using EventStore.Client; +using KurrentDB.Client; using Shared.Serialisation; using SimpleResults; using TestHosts.Clients; @@ -30,11 +31,11 @@ public class TransactionProcessorSteps private readonly ITransactionProcessorClient TransactionProcessorClient; private readonly HttpClient TestHostHttpClient; - private readonly EventStoreProjectionManagementClient ProjectionManagementClient; + private readonly KurrentDBProjectionManagementClient ProjectionManagementClient; private readonly IAgencyBankingClient AgencyBankingClient; public TransactionProcessorSteps(ITransactionProcessorClient transactionProcessorClient, - HttpClient testHostHttpClient, EventStoreProjectionManagementClient projectionManagementClient, + HttpClient testHostHttpClient, KurrentDBProjectionManagementClient projectionManagementClient, IAgencyBankingClient agencyBankingClient) { this.TransactionProcessorClient = transactionProcessorClient; diff --git a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs index 1bba5dfe..317d7b72 100644 --- a/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/TransactionProcessor.IntegrationTests/Common/DockerHelper.cs @@ -1,4 +1,5 @@ using DotNet.Testcontainers.Builders; +using KurrentDB.Client; using System.IO; using TestHosts.Clients; using TransactionProcessor.Database.Contexts; @@ -49,8 +50,8 @@ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.Doc private readonly TestingContext TestingContext; - public EventStoreProjectionManagementClient ProjectionManagementClient; - + public KurrentDBProjectionManagementClient ProjectionManagementClient; + public IAgencyBankingClient AgencyBankingClient; #endregion @@ -209,7 +210,7 @@ public override async Task StartContainersForScenarioRun(String scenarioName, Do await this.ConfigureTestBank(DockerHelper.TestBankSortCode, DockerHelper.TestBankAccountNumber, callbackUrl); this.Trace("Test Bank Configured"); - this.ProjectionManagementClient = new EventStoreProjectionManagementClient(ConfigureEventStoreSettings()); + this.ProjectionManagementClient = new KurrentDBProjectionManagementClient(this.ConfigureKurrentDbSettings()); SimpleResults.Result bootstrapToken = await this.SecurityServiceClient.GetToken("management-bootstrap", "management-bootstrap-secret", CancellationToken.None); if (bootstrapToken.IsFailed || String.IsNullOrWhiteSpace(bootstrapToken.Data?.AccessToken)) diff --git a/TransactionProcessor.IntegrationTests/Common/GenericSteps.cs b/TransactionProcessor.IntegrationTests/Common/GenericSteps.cs index b4058a87..fd24e23d 100644 --- a/TransactionProcessor.IntegrationTests/Common/GenericSteps.cs +++ b/TransactionProcessor.IntegrationTests/Common/GenericSteps.cs @@ -33,7 +33,7 @@ public async Task StartSystem() logger.Initialise(LogManager.GetLogger(scenarioName), scenarioName); LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly); - DockerServices dockerServices = DockerServices.SqlServer | DockerServices.CallbackHandler | DockerServices.EventStore | + DockerServices dockerServices = DockerServices.SqlServer | DockerServices.CallbackHandler | DockerServices.KurrentDb | DockerServices.FileProcessor | DockerServices.MessagingService | DockerServices.SecurityService | DockerServices.SqlServer | DockerServices.TestHost | DockerServices.TransactionProcessor | DockerServices.TransactionProcessorAcl; diff --git a/TransactionProcessor.Tests/Common/TransactionProcessorWebFactory.cs b/TransactionProcessor.Tests/Common/TransactionProcessorWebFactory.cs index 7705e75e..c6e75aa4 100644 --- a/TransactionProcessor.Tests/Common/TransactionProcessorWebFactory.cs +++ b/TransactionProcessor.Tests/Common/TransactionProcessorWebFactory.cs @@ -15,7 +15,7 @@ namespace TransactionProcessor.Tests.Common using Imposter.Abstractions; using Xunit; - public class TransactionProcessorWebFactory : WebApplicationFactory where TStartup : class + public class TransactionProcessorWebFactory : WebApplicationFactory where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { @@ -33,7 +33,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { options.Filters.Add(new AllowAnonymousFilter()); }) - .AddApplicationPart(typeof(Startup).Assembly); + .AddApplicationPart(typeof(Program).Assembly); }); ; } @@ -51,9 +51,9 @@ private IMediatorImposter CreateMediatorMock() /// /// - /// + /// [CollectionDefinition("TestCollection")] - public class TestCollection : ICollectionFixture> + public class TestCollection : ICollectionFixture> { // A class with no code, only used to define the collection } diff --git a/TransactionProcessor.Tests/ControllerTests/DomainEventControllerTests.cs b/TransactionProcessor.Tests/ControllerTests/DomainEventControllerTests.cs index c8ec9a66..763d970d 100644 --- a/TransactionProcessor.Tests/ControllerTests/DomainEventControllerTests.cs +++ b/TransactionProcessor.Tests/ControllerTests/DomainEventControllerTests.cs @@ -1,4 +1,5 @@ using Imposter.Abstractions; +using Lamar; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -98,7 +99,7 @@ private static DomainEventController CreateController(DomainEvent domainEvent, p resolver.GetDomainEventHandlers(Arg.Any()) .Returns(Result.Success(handlers.ToList())); - DomainEventController controller = new(resolver.Instance()) + DomainEventController controller = new(resolver.Instance(), new Container(new ServiceRegistry())) { ControllerContext = new ControllerContext { diff --git a/TransactionProcessor.Tests/ControllerTests/TransactionControllerTests.cs b/TransactionProcessor.Tests/ControllerTests/TransactionControllerTests.cs index e81e0c23..a7b0fa5a 100644 --- a/TransactionProcessor.Tests/ControllerTests/TransactionControllerTests.cs +++ b/TransactionProcessor.Tests/ControllerTests/TransactionControllerTests.cs @@ -17,19 +17,19 @@ namespace TransactionProcessor.Tests.ControllerTests { using Xunit; [Collection("TestCollection")] - public class TransactionControllerTests : IClassFixture> { + public class TransactionControllerTests : IClassFixture> { #region Fields /// /// The web application factory /// - private readonly TransactionProcessorWebFactory WebApplicationFactory; + private readonly TransactionProcessorWebFactory WebApplicationFactory; #endregion #region Constructors - public TransactionControllerTests(TransactionProcessorWebFactory webApplicationFactory) { + public TransactionControllerTests(TransactionProcessorWebFactory webApplicationFactory) { this.WebApplicationFactory = webApplicationFactory; } diff --git a/TransactionProcessor.Tests/General/AutoLogonWorkerServiceTests.cs b/TransactionProcessor.Tests/General/AutoLogonWorkerServiceTests.cs index 87f55d05..f36b025e 100644 --- a/TransactionProcessor.Tests/General/AutoLogonWorkerServiceTests.cs +++ b/TransactionProcessor.Tests/General/AutoLogonWorkerServiceTests.cs @@ -1,7 +1,6 @@ using Grpc.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using Imposter.Abstractions; +using Microsoft.Extensions.Logging.Abstractions; using Shared.Logger; using Shouldly; using System; @@ -9,65 +8,39 @@ using System.Threading.Tasks; using TransactionProcessor.BusinessLogic.OperatorInterfaces; using Xunit; +using NullLogger = Shared.Logger.NullLogger; namespace TransactionProcessor.Tests.General; public class AutoLogonWorkerServiceTests { public AutoLogonWorkerServiceTests() { - Logger.Initialise(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + Logger.Initialise(NullLogger.Instance); } [Fact] public async Task OperatorLogonAsync_TransientGrpcUnavailable_ExceptionIsHandled() { - IOperatorProxyImposter proxy = new IOperatorProxyImposter(); + IOperatorProxyImposter proxy = new(); proxy.ProcessLogonMessage(Arg.Any()) .Throws(new RpcException(new Status(StatusCode.Unavailable, "RPC server unavailable"))); + AutoLogonWorkerService worker = CreateWorker(proxy.Instance()); - ServiceCollection services = new ServiceCollection(); - services.AddSingleton>(_ => _ => proxy.Instance()); - IServiceProvider previousServiceProvider = Startup.ServiceProvider; - - try { - Startup.ServiceProvider = services.BuildServiceProvider(); - - AutoLogonWorkerService worker = new AutoLogonWorkerService(); - - Exception exception = await Record.ExceptionAsync(async () => - { - await worker.OperatorLogonAsync("operator-1", CancellationToken.None); - }); + Exception exception = await Record.ExceptionAsync(() => worker.OperatorLogonAsync("operator-1", CancellationToken.None)); - exception.ShouldBeNull(); - } - finally { - Startup.ServiceProvider = previousServiceProvider; - } + exception.ShouldBeNull(); } [Fact] public async Task OperatorLogonAsync_NonTransientException_ExceptionIsHandled() { - IOperatorProxyImposter proxy = new IOperatorProxyImposter(); + IOperatorProxyImposter proxy = new(); proxy.ProcessLogonMessage(Arg.Any()) .Throws(new InvalidOperationException("boom")); + AutoLogonWorkerService worker = CreateWorker(proxy.Instance()); - ServiceCollection services = new ServiceCollection(); - services.AddSingleton>(_ => _ => proxy.Instance()); - IServiceProvider previousServiceProvider = Startup.ServiceProvider; + Exception exception = await Record.ExceptionAsync(() => worker.OperatorLogonAsync("operator-2", CancellationToken.None)); - try { - Startup.ServiceProvider = services.BuildServiceProvider(); - - AutoLogonWorkerService worker = new AutoLogonWorkerService(); - - Exception exception = await Record.ExceptionAsync(async () => - { - await worker.OperatorLogonAsync("operator-2", CancellationToken.None); - }); - - exception.ShouldBeNull(); - } - finally { - Startup.ServiceProvider = previousServiceProvider; - } + exception.ShouldBeNull(); } + + private static AutoLogonWorkerService CreateWorker(IOperatorProxy proxy) => + new(new AutoLogonOperatorConfiguration(Array.Empty()), _ => proxy); } diff --git a/TransactionProcessor.Tests/General/BootstrapperTests.cs b/TransactionProcessor.Tests/General/BootstrapperTests.cs index 212a2159..30f5545f 100644 --- a/TransactionProcessor.Tests/General/BootstrapperTests.cs +++ b/TransactionProcessor.Tests/General/BootstrapperTests.cs @@ -3,12 +3,8 @@ namespace TransactionProcessor.Tests.General { using Lamar; - using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Imposter.Abstractions; - using Shared.Serialisation; + using TransactionProcessor.Bootstrapper; using System; using System.Collections.Generic; using System.Diagnostics; @@ -24,27 +20,12 @@ public class BootstrapperTests #region Methods [Fact] - public void ConfigureContainer_PopulatesAutoApiLogonOperators_WithoutDuplicates() + public void OperatorRegistry_PopulatesAutoApiLogonOperators() { - IWebHostEnvironmentImposter hostingEnvironment = new IWebHostEnvironmentImposter(); - hostingEnvironment.EnvironmentName.Getter().Returns("Development"); - hostingEnvironment.ContentRootPath.Getter().Returns("/home"); - hostingEnvironment.ApplicationName.Getter().Returns("Test Application"); + OperatorRegistry registry = new(this.SetupMemoryConfiguration()); + Container container = new(registry); - Startup s = new Startup(hostingEnvironment.Instance()); - Startup.Configuration = this.SetupMemoryConfiguration(); - - ServiceRegistry firstServices = new ServiceRegistry(); - this.AddTestRegistrations(firstServices, hostingEnvironment.Instance()); - s.ConfigureContainer(firstServices); - - Assert.Equal(new[] { "Safaricom", "PataPawaPostPay" }, Startup.AutoApiLogonOperators.ToArray()); - - ServiceRegistry secondServices = new ServiceRegistry(); - this.AddTestRegistrations(secondServices, hostingEnvironment.Instance()); - s.ConfigureContainer(secondServices); - - Assert.Equal(new[] { "Safaricom", "PataPawaPostPay" }, Startup.AutoApiLogonOperators.ToArray()); + Assert.Equal(new[] { "Safaricom", "PataPawaPostPay" }, container.GetInstance().OperatorIds); } private IConfigurationRoot SetupMemoryConfiguration() @@ -73,23 +54,6 @@ private IConfigurationRoot SetupMemoryConfiguration() return builder.Build(); } - /// - /// Adds the test registrations. - /// - /// The services. - /// The hosting environment. - private void AddTestRegistrations(IServiceCollection services, - IWebHostEnvironment hostingEnvironment) - { - services.AddLogging(); - DiagnosticListener diagnosticSource = new DiagnosticListener(hostingEnvironment.ApplicationName); - services.AddSingleton(diagnosticSource); - services.AddSingleton(diagnosticSource); - services.AddSingleton(hostingEnvironment); - services.AddSingleton(hostingEnvironment); - services.AddSingleton(Startup.Configuration); - } - #endregion } } diff --git a/TransactionProcessor/Bootstrapper/DomainEventHandlerRegistry.cs b/TransactionProcessor/Bootstrapper/DomainEventHandlerRegistry.cs index 5182767b..39083768 100644 --- a/TransactionProcessor/Bootstrapper/DomainEventHandlerRegistry.cs +++ b/TransactionProcessor/Bootstrapper/DomainEventHandlerRegistry.cs @@ -33,23 +33,23 @@ public class DomainEventHandlerRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public DomainEventHandlerRegistry() + public DomainEventHandlerRegistry(IConfiguration configuration) { - this.RegisterServices(); + this.RegisterServices(configuration); } #endregion #region Methods - private void RegisterServices() + private void RegisterServices(IConfiguration configuration) { Dictionary eventHandlersConfiguration = - this.GetEventHandlerConfiguration($"AppSettings:{MainRegistrationName}", MainRegistrationName); + this.GetEventHandlerConfiguration(configuration, $"AppSettings:{MainRegistrationName}", MainRegistrationName); Dictionary eventHandlersConfigurationDomain = - this.GetEventHandlerConfiguration($"AppSettings:{DomainRegistrationName}", DomainRegistrationName); + this.GetEventHandlerConfiguration(configuration, $"AppSettings:{DomainRegistrationName}", DomainRegistrationName); Dictionary eventHandlersConfigurationOrdered = - this.GetEventHandlerConfiguration($"AppSettings:{OrderedRegistrationName}", OrderedRegistrationName); + this.GetEventHandlerConfiguration(configuration, $"AppSettings:{OrderedRegistrationName}", OrderedRegistrationName); this.RegisterEventHandlers(); this.RegisterProjections(); @@ -57,14 +57,15 @@ private void RegisterServices() this.AddSingleton, DomainEventFactory>(); } - private Dictionary GetEventHandlerConfiguration(String sectionName, + private Dictionary GetEventHandlerConfiguration(IConfiguration hostConfiguration, + String sectionName, String registrationName) { Dictionary configuration = new Dictionary(); - if (Startup.Configuration != null) + if (hostConfiguration != null) { - IConfigurationSection section = Startup.Configuration.GetSection(sectionName); + IConfigurationSection section = hostConfiguration.GetSection(sectionName); if (section != null) { diff --git a/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs b/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs index cbd7ed45..60ccb289 100644 --- a/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs +++ b/TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs @@ -1,7 +1,9 @@ using KurrentDB.Client; using Microsoft.IdentityModel.Logging; -using Microsoft.OpenApi; +using Microsoft.AspNetCore.OpenApi; using OpenIddict.Validation.AspNetCore; +using TransactionProcessing.ApiDefaults; +using TransactionProcessor.OpenApi; namespace TransactionProcessor.Bootstrapper { @@ -15,7 +17,6 @@ namespace TransactionProcessor.Bootstrapper using Shared.Extensions; using Shared.General; using Shared.Serialisation; - using Swashbuckle.AspNetCore.Filters; using System; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -35,9 +36,9 @@ public class MiddlewareRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public MiddlewareRegistry() + public MiddlewareRegistry(IConfiguration configuration) { - String connectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + String connectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); KurrentDBClientSettings eventStoreClientSettings = KurrentDBClientSettings.Create(connectionString); this.ConfigureHealthChecks(eventStoreClientSettings); @@ -47,7 +48,13 @@ public MiddlewareRegistry() this.ConfigureAuthorization(); this.ConfigureJsonOptions(); - this.AddHealthMonitoringRegistration(Startup.Configuration); + this.AddHealthMonitoringRegistration(configuration); + this.AddApiDefaults(options => { + options.Title = "Transaction Processor"; + options.Description = "API for all aspects of Transaction Processing and Estate Management"; + }); + this.ConfigureAll(options => + options.AddDocumentTransformer()); } #endregion @@ -126,38 +133,6 @@ private void ConfigureMvc() this.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); } - private void ConfigureSwagger() - { - this.AddSwaggerGen(c => - { - c.CustomSchemaIds(type => type.FullName); // Uses the full namespace as schema ID - c.SwaggerDoc("v1", - new OpenApiInfo - { - Title = "Transaction Processor API", - Version = "1.0", - Description = "A REST Api to manage the processing and routing of transactions to local and remote operators.", - Contact = new OpenApiContact - { - Name = "Stuart Ferguson", - Email = "golfhandicapping@btinternet.com" - } - }); - // add a custom operation filter which sets default values - c.ExampleFilters(); - - //Locate the XML files being generated by ASP.NET... - var directory = new DirectoryInfo(AppContext.BaseDirectory); - var xmlFiles = directory.GetFiles("*.xml"); - - //... and tell Swagger to use those XML comments. - foreach (FileInfo fileInfo in xmlFiles) - { - c.IncludeXmlComments(fileInfo.FullName); - } - }); - } - /// /// APIs the endpoint HTTP handler. /// diff --git a/TransactionProcessor/Bootstrapper/OperatorRegistry.cs b/TransactionProcessor/Bootstrapper/OperatorRegistry.cs index 5095c3ce..9147475a 100644 --- a/TransactionProcessor/Bootstrapper/OperatorRegistry.cs +++ b/TransactionProcessor/Bootstrapper/OperatorRegistry.cs @@ -4,6 +4,7 @@ namespace TransactionProcessor.Bootstrapper { using System; + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.ServiceModel; using BusinessLogic.OperatorInterfaces; @@ -29,12 +30,14 @@ public class OperatorRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public OperatorRegistry() + public OperatorRegistry(IConfiguration configuration) { - this.ConfigureOperator("Safaricom"); - this.ConfigureOperator("PataPawaPostPay"); - this.ConfigureOperator("PataPawaPrePay"); - this.ConfigureOperator("AgencyBanking"); + List autoLogonOperators = new(); + this.ConfigureOperator(configuration, autoLogonOperators, "Safaricom"); + this.ConfigureOperator(configuration, autoLogonOperators, "PataPawaPostPay"); + this.ConfigureOperator(configuration, autoLogonOperators, "PataPawaPrePay"); + this.ConfigureOperator(configuration, autoLogonOperators, "AgencyBanking"); + this.AddSingleton(new AutoLogonOperatorConfiguration(autoLogonOperators)); this.AddSingleton(new PataPawaPostPayServiceClient(PataPawaPostPayServiceClient.EndpointConfiguration.BasicHttpBinding_IPataPawaPostPayService)); this.For().Add().Named("Safaricom").Singleton(); @@ -48,7 +51,7 @@ public OperatorRegistry() (client, clientName, url) => { - var loggingConfig = Startup.Container.GetInstance(); + var loggingConfig = context.GetService().GetInstance(); client.Endpoint.SetTraceLogging(clientName, loggingConfig); IPataPawaPostPayService channel = client.ChannelFactory.CreateChannel(new EndpointAddress(url)); @@ -57,21 +60,21 @@ public OperatorRegistry() this.AddTransient>(context => operatorIdentifier => { - return Startup.Container.GetInstance(operatorIdentifier); + return context.GetService().GetInstance(operatorIdentifier); }); } - private void ConfigureOperator(String operatorId) where T : class { + private void ConfigureOperator(IConfiguration configuration, List autoLogonOperators, String operatorId) where T : class { T configObject = (T)Activator.CreateInstance(typeof(T)); - Startup.Configuration.GetSection($"OperatorConfiguration:{operatorId}").Bind(configObject); + configuration.GetSection($"OperatorConfiguration:{operatorId}").Bind(configObject); this.AddSingleton(configObject); BaseOperatorConfiguration cfg = configObject as BaseOperatorConfiguration; if (cfg.ApiLogonRequired) { - Startup.AddAutoApiLogonOperator(operatorId); + autoLogonOperators.Add(operatorId); } } diff --git a/TransactionProcessor/Bootstrapper/RepositoryRegistry.cs b/TransactionProcessor/Bootstrapper/RepositoryRegistry.cs index 6f5799f0..fe2e08a9 100644 --- a/TransactionProcessor/Bootstrapper/RepositoryRegistry.cs +++ b/TransactionProcessor/Bootstrapper/RepositoryRegistry.cs @@ -50,8 +50,8 @@ public class RepositoryRegistry : ServiceRegistry { private static bool CachedAggregatesAdded; private static readonly object CachedAggregatesLock = new(); - public RepositoryRegistry() { - string eventStoreConnectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + public RepositoryRegistry(IConfiguration configuration, IHostEnvironment environment) { + string eventStoreConnectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); this.AddKurrentDBProjectionManagementClient(eventStoreConnectionString); this.AddKurrentDBPersistentSubscriptionsClient(eventStoreConnectionString); @@ -62,7 +62,7 @@ public RepositoryRegistry() { this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolver<>)); - if (Startup.WebHostEnvironment.IsEnvironment("IntegrationTest") || Startup.Configuration.GetValue("ServiceOptions:UseInMemoryDatabase")) { + if (environment.IsEnvironment("IntegrationTest") || configuration.GetValue("ServiceOptions:UseInMemoryDatabase")) { this.AddDbContext(builder => builder.UseInMemoryDatabase("TransactionProcessorReadModel")); } else { diff --git a/TransactionProcessor/Controllers/DomainEventController.cs b/TransactionProcessor/Controllers/DomainEventController.cs index 715716bd..3d8f62cd 100644 --- a/TransactionProcessor/Controllers/DomainEventController.cs +++ b/TransactionProcessor/Controllers/DomainEventController.cs @@ -12,6 +12,7 @@ namespace TransactionProcessor.Controllers { using Microsoft.AspNetCore.Mvc; + using Lamar; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.EventStore.EventHandling; @@ -25,7 +26,6 @@ namespace TransactionProcessor.Controllers [Route(DomainEventController.ControllerRoute)] [ApiController] [ExcludeFromCodeCoverage] - [ApiExplorerSettings(IgnoreApi = true)] public class DomainEventController : ControllerBase { #region Fields @@ -34,6 +34,7 @@ public class DomainEventController : ControllerBase /// The domain event handler resolver /// private readonly IDomainEventHandlerResolver DomainEventHandlerResolver; + private readonly IContainer Container; #endregion @@ -43,9 +44,11 @@ public class DomainEventController : ControllerBase /// Initializes a new instance of the class. /// /// The domain event handler resolver. - public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver) + /// The application service container. + public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver, IContainer container) { this.DomainEventHandlerResolver = domainEventHandlerResolver; + this.Container = container; } #endregion @@ -55,21 +58,31 @@ public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResol /// /// Posts the event asynchronous. /// - /// The domain event. + /// The event payload. This example is for MerchantCreatedEvent; event fields vary with eventType. /// The cancellation token. + /// Required eventType header naming the domain event type. + /// Optional subscriptionId header selecting a named event handler resolver. /// + /// + /// Set eventType to MerchantCreatedEvent when using the body example. Other event types require their own payload shape. + /// [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task PostEventAsync([FromBody] Object request, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + [FromHeader(Name = "eventType")] String eventType, + [FromHeader(Name = "subscriptionId")] String subscriptionId = null) { - Result domainEventResult = await this.GetDomainEvent(request); + Result domainEventResult = await this.GetDomainEvent(request, eventType); if (domainEventResult.IsFailed) return ResponseFactory.FromResult(domainEventResult, SuccessFactory); IDomainEvent domainEvent = domainEventResult.Data; - List eventHandlers = this.GetDomainEventHandlers(domainEvent); + List eventHandlers = this.GetDomainEventHandlers(domainEvent, subscriptionId); cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); @@ -123,6 +136,18 @@ public async Task PostEventAsync([FromBody] Object request, } } + [NonAction] + public Task PostEventAsync(Object request, CancellationToken cancellationToken) + { + String subscriptionId = this.Request.Headers.ContainsKey("subscriptionId") + ? this.Request.Headers["subscriptionId"].ToString() + : null; + return this.PostEventAsync(request, + cancellationToken, + this.Request.Headers["eventType"].ToString(), + subscriptionId); + } + private IDomainEvent SuccessFactory(IDomainEvent arg) { return arg; } @@ -169,11 +194,10 @@ private HandlerExecution ExecuteHandler(IDomainEventHandler handler, } } - private List GetDomainEventHandlers(IDomainEvent domainEvent) { + private List GetDomainEventHandlers(IDomainEvent domainEvent, String subscriptionId) { - if (this.Request.Headers.ContainsKey("subscriptionId")) { - var eventHandlerType = this.Request.Headers["subscriptionId"]; - var resolver = Startup.Container.GetInstance(eventHandlerType); + if (subscriptionId != null) { + var resolver = this.Container.GetInstance(subscriptionId); // We are being told by the caller to use a specific handler var allhandlers = resolver.GetDomainEventHandlers(domainEvent); @@ -190,10 +214,8 @@ private List GetDomainEventHandlers(IDomainEvent domainEven return eventHandlersResult.Data; } - private async Task> GetDomainEvent(Object domainEvent) + private async Task> GetDomainEvent(Object domainEvent, String eventType) { - String eventType = this.Request.Headers["eventType"].ToString(); - Type type; try { diff --git a/TransactionProcessor/Endpoints/ContractEndpoints.cs b/TransactionProcessor/Endpoints/ContractEndpoints.cs index c513d925..2a784ba7 100644 --- a/TransactionProcessor/Endpoints/ContractEndpoints.cs +++ b/TransactionProcessor/Endpoints/ContractEndpoints.cs @@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Routing; using Shared.Authorisation; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects.Responses.Contract; +using System.Collections.Generic; namespace TransactionProcessor.Endpoints { @@ -21,23 +23,33 @@ public static IEndpointRouteBuilder MapContractEndpoints(this IEndpointRouteBuil .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy); // Read - contractGroup.MapGet("/{contractId:guid}", ContractHandlers.GetContract).WithName("GetContract"); - contractGroup.MapGet("/", ContractHandlers.GetContracts).WithName("GetContracts"); + contractGroup.MapGet("/{contractId:guid}", ContractHandlers.GetContract) + .WithName("GetContract").WithSummary("Get a contract") + .WithDescription("Returns one contract in the specified estate, including its products and transaction fees.") + .Produces(); + contractGroup.MapGet("/", ContractHandlers.GetContracts) + .WithName("GetContracts").WithSummary("List estate contracts") + .WithDescription("Returns all contracts belonging to the specified estate.") + .Produces>(); // Write / Modify - require client credentials only contractGroup.MapPatch("/{contractId:guid}/products", ContractHandlers.AddProductToContract) - .WithName("AddProductToContract"); + .WithName("AddProductToContract").WithSummary("Add a product to a contract") + .WithDescription("Adds the supplied product definition to the contract. The service generates the product ID."); contractGroup.MapPatch("/{contractId:guid}/products/{productId:guid}/transactionFees", ContractHandlers.AddTransactionFeeForProductToContract) - .WithName("AddTransactionFeeForProductToContract"); + .WithName("AddTransactionFeeForProductToContract").WithSummary("Add a transaction fee") + .WithDescription("Adds a fee definition to the specified contract product. The service generates the transaction fee ID."); contractGroup.MapDelete("/{contractId:guid}/products/{productId:guid}/transactionFees/{transactionFeeId:guid}", ContractHandlers.DisableTransactionFeeForProduct) - .WithName("DisableTransactionFeeForProduct"); + .WithName("DisableTransactionFeeForProduct").WithSummary("Disable a transaction fee") + .WithDescription("Disables the selected transaction fee on the specified contract product."); contractGroup.MapPost("/", ContractHandlers.CreateContract) - .WithName("CreateContract"); + .WithName("CreateContract").WithSummary("Create a contract") + .WithDescription("Creates a contract in the specified estate for the supplied operator."); return endpoints; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Endpoints/EstateEndpoints.cs b/TransactionProcessor/Endpoints/EstateEndpoints.cs index f76cc309..375d5fe7 100644 --- a/TransactionProcessor/Endpoints/EstateEndpoints.cs +++ b/TransactionProcessor/Endpoints/EstateEndpoints.cs @@ -1,4 +1,4 @@ -using MessagingService.DataTransferObjects; +using MessagingService.DataTransferObjects; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects.Responses.Estate; +using System.Collections.Generic; namespace TransactionProcessor.Endpoints { @@ -24,22 +26,28 @@ public static IEndpointRouteBuilder MapEstateEndpoints(this IEndpointRouteBuilde .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy); estateGroup.MapPost("/", EstateHandlers.CreateEstate) - .WithName("CreateEstate"); + .WithName("CreateEstate").WithSummary("Create an estate") + .WithDescription("Creates an estate using the supplied estate ID and display name."); estateGroup.MapGet("/{estateId:guid}", EstateHandlers.GetEstate) - .WithName("GetEstate"); + .WithName("GetEstate").WithSummary("Get an estate") + .WithDescription("Returns estate details, assigned operators, and security users.").Produces(); estateGroup.MapGet("/{estateId:guid}/all", EstateHandlers.GetEstates) - .WithName("GetEstates"); + .WithName("GetEstates").WithSummary("List estate details") + .WithDescription("Returns the estate collection result for the supplied estate ID.").Produces>(); estateGroup.MapPatch("/{estateId:guid}/users", EstateHandlers.CreateEstateUser) - .WithName("CreateEstateUser"); + .WithName("CreateEstateUser").WithSummary("Create an estate user") + .WithDescription("Creates a security user in the specified estate from the supplied identity details."); estateGroup.MapPatch("/{estateId:guid}/operators", EstateHandlers.AssignOperator) - .WithName("AssignEstateOperator"); + .WithName("AssignEstateOperator").WithSummary("Assign an operator to an estate") + .WithDescription("Associates the operator ID in the request body with the specified estate."); estateGroup.MapDelete("/{estateId:guid}/operators/{operatorId:guid}", EstateHandlers.RemoveOperator) - .WithName("RemoveEstateOperator"); + .WithName("RemoveEstateOperator").WithSummary("Remove an operator from an estate") + .WithDescription("Removes the specified operator association from the estate."); return endpoints; } diff --git a/TransactionProcessor/Endpoints/FloatEndpoints.cs b/TransactionProcessor/Endpoints/FloatEndpoints.cs index 4be0fe51..2ff6eceb 100644 --- a/TransactionProcessor/Endpoints/FloatEndpoints.cs +++ b/TransactionProcessor/Endpoints/FloatEndpoints.cs @@ -20,12 +20,14 @@ public static IEndpointRouteBuilder MapFloatEndpoints(this IEndpointRouteBuilder .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy); group.MapPost("/", FloatHandlers.CreateFloat) - .WithName("CreateFloat"); + .WithName("CreateFloat").WithSummary("Create an estate float") + .WithDescription("Creates the float identified by the request body for the specified estate."); group.MapPut("/", FloatHandlers.RecordFloatCreditPurchase) - .WithName("RecordFloatCreditPurchase"); + .WithName("RecordFloatCreditPurchase").WithSummary("Record a float credit purchase") + .WithDescription("Records a credit purchase, amount, cost price, and purchase time against the specified estate float."); return endpoints; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Endpoints/MerchantEndpoints.cs b/TransactionProcessor/Endpoints/MerchantEndpoints.cs index 8b57ed89..8f15a58e 100644 --- a/TransactionProcessor/Endpoints/MerchantEndpoints.cs +++ b/TransactionProcessor/Endpoints/MerchantEndpoints.cs @@ -1,9 +1,13 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Shared.Authorisation; using System.Diagnostics.CodeAnalysis; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects; +using TransactionProcessor.DataTransferObjects.Responses.Contract; +using TransactionProcessor.DataTransferObjects.Responses.Merchant; +using System.Collections.Generic; namespace TransactionProcessor.Endpoints; @@ -22,46 +26,84 @@ public static IEndpointRouteBuilder MapMerchantEndpoints(this IEndpointRouteBuil .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy); // Read endpoints (now also protected by client-credentials-only policy via the group) - merchantGroup.MapGet("/{merchantId:guid}/balance", MerchantHandlers.GetMerchantBalance).WithName("GetMerchantBalance"); - merchantGroup.MapGet("/{merchantId:guid}/livebalance", MerchantHandlers.GetMerchantBalanceLive).WithName("GetMerchantBalanceLive"); - merchantGroup.MapGet("/{merchantId:guid}/balancehistory", MerchantHandlers.GetMerchantBalanceHistory).WithName("GetMerchantBalanceHistory"); - merchantGroup.MapGet("/", MerchantHandlers.GetMerchants).WithName("GetMerchants"); - merchantGroup.MapGet("/{merchantId:guid}", MerchantHandlers.GetMerchant).WithName("GetMerchant"); - merchantGroup.MapGet("/{merchantId:guid}/contracts", MerchantHandlers.GetMerchantContracts).WithName("GetMerchantContracts"); - merchantGroup.MapGet("/{merchantId:guid}/schedules/{year:int}", MerchantHandlers.GetMerchantSchedule).WithName("GetMerchantSchedule"); - merchantGroup.MapGet("/{merchantId:guid}/schedules/{year:int}/readmodel", MerchantHandlers.GetMerchantScheduleFromReadModel).WithName("GetMerchantScheduleFromReadModel"); + merchantGroup.MapGet("/{merchantId:guid}/balance", MerchantHandlers.GetMerchantBalance) + .WithName("GetMerchantBalance").WithSummary("Get merchant balance") + .WithDescription("Returns the stored balance and available balance for this estate merchant.").Produces(); + merchantGroup.MapGet("/{merchantId:guid}/livebalance", MerchantHandlers.GetMerchantBalanceLive) + .WithName("GetMerchantBalanceLive").WithSummary("Get live merchant balance") + .WithDescription("Returns the current balance from the live balance projection for this merchant.").Produces(); + merchantGroup.MapGet("/{merchantId:guid}/balancehistory", MerchantHandlers.GetMerchantBalanceHistory) + .WithName("GetMerchantBalanceHistory").WithSummary("Get merchant balance history") + .WithDescription("Returns balance changes for the merchant between the required startDate and endDate query values.").Produces>(); + merchantGroup.MapGet("/", MerchantHandlers.GetMerchants) + .WithName("GetMerchants").WithSummary("List estate merchants") + .WithDescription("Returns all merchants belonging to the specified estate.").Produces>(); + merchantGroup.MapGet("/{merchantId:guid}", MerchantHandlers.GetMerchant) + .WithName("GetMerchant").WithSummary("Get a merchant") + .WithDescription("Returns merchant details, including addresses, contacts, devices, operators, contracts, opening hours, and schedules.").Produces(); + merchantGroup.MapGet("/{merchantId:guid}/contracts", MerchantHandlers.GetMerchantContracts) + .WithName("GetMerchantContracts").WithSummary("List merchant contracts") + .WithDescription("Returns contracts associated with the specified merchant.").Produces>(); + merchantGroup.MapGet("/{merchantId:guid}/schedules/{year:int}", MerchantHandlers.GetMerchantSchedule) + .WithName("GetMerchantSchedule").WithSummary("Get merchant schedule") + .WithDescription("Returns the merchant schedule for the requested year from the primary schedule store.").Produces(); + merchantGroup.MapGet("/{merchantId:guid}/schedules/{year:int}/readmodel", MerchantHandlers.GetMerchantScheduleFromReadModel) + .WithName("GetMerchantScheduleFromReadModel").WithSummary("Get merchant schedule read model") + .WithDescription("Returns the requested year's merchant schedule from the read model.").Produces(); merchantGroup.MapGet("/{merchantId:guid}/contracts/{contractId:guid}/products/{productId:guid}/transactionFees", - MerchantHandlers.GetTransactionFeesForProduct).WithName("GetTransactionFeesForProduct"); + MerchantHandlers.GetTransactionFeesForProduct).WithName("GetTransactionFeesForProduct") + .WithSummary("List merchant product transaction fees") + .WithDescription("Returns transaction fee definitions for the selected product on a merchant contract.").Produces>(); // Write/modify endpoints - merchantGroup.MapPost("/", MerchantHandlers.CreateMerchant).WithName("CreateMerchant"); + merchantGroup.MapPost("/", MerchantHandlers.CreateMerchant).WithName("CreateMerchant") + .WithSummary("Create a merchant") + .WithDescription("Creates a merchant in the specified estate, including its address, contact, and settlement schedule."); - merchantGroup.MapPatch("/{merchantId:guid}/operators", MerchantHandlers.AssignOperator).WithName("AssignMerchantOperator"); - merchantGroup.MapDelete("/{merchantId:guid}/operators/{operatorId:guid}", MerchantHandlers.RemoveOperator).WithName("RemoveMerchantOperator"); + merchantGroup.MapPatch("/{merchantId:guid}/operators", MerchantHandlers.AssignOperator).WithName("AssignMerchantOperator") + .WithSummary("Assign an operator to a merchant").WithDescription("Associates the request's operator and terminal identifiers with the merchant."); + merchantGroup.MapDelete("/{merchantId:guid}/operators/{operatorId:guid}", MerchantHandlers.RemoveOperator).WithName("RemoveMerchantOperator") + .WithSummary("Remove an operator from a merchant").WithDescription("Removes the specified operator association from the merchant."); - merchantGroup.MapPatch("/{merchantId:guid}/devices",MerchantHandlers.AddDevice).WithName("AddDevice"); - merchantGroup.MapPatch("/{merchantId:guid}/devices/{deviceIdentifier}", MerchantHandlers.SwapMerchantDevice).WithName("SwapMerchantDevice"); + merchantGroup.MapPatch("/{merchantId:guid}/devices",MerchantHandlers.AddDevice).WithName("AddDevice") + .WithSummary("Add a merchant device").WithDescription("Registers the device identifier supplied in the request body for this merchant."); + merchantGroup.MapPatch("/{merchantId:guid}/devices/{deviceIdentifier}", MerchantHandlers.SwapMerchantDevice).WithName("SwapMerchantDevice") + .WithSummary("Replace a merchant device").WithDescription("Replaces the device identified by the deviceIdentifier path value with the new identifier in the request body."); - merchantGroup.MapPatch("/{merchantId:guid}/contracts", MerchantHandlers.AddContract).WithName("AddContract"); - merchantGroup.MapDelete("/{merchantId:guid}/contracts/{contractId:guid}", MerchantHandlers.RemoveContract).WithName("RemoveContract"); + merchantGroup.MapPatch("/{merchantId:guid}/contracts", MerchantHandlers.AddContract).WithName("AddContract") + .WithSummary("Associate a contract with a merchant").WithDescription("Adds the contract ID in the request body to the merchant."); + merchantGroup.MapDelete("/{merchantId:guid}/contracts/{contractId:guid}", MerchantHandlers.RemoveContract).WithName("RemoveContract") + .WithSummary("Remove a merchant contract").WithDescription("Removes the selected contract association from the merchant."); - merchantGroup.MapPatch("/{merchantId:guid}/users", MerchantHandlers.CreateMerchantUser).WithName("CreateMerchantUser"); + merchantGroup.MapPatch("/{merchantId:guid}/users", MerchantHandlers.CreateMerchantUser).WithName("CreateMerchantUser") + .WithSummary("Create a merchant user").WithDescription("Creates a security user associated with this merchant."); - merchantGroup.MapPost("/{merchantId:guid}/deposits", MerchantHandlers.MakeDeposit).WithName("MakeDeposit"); - merchantGroup.MapPost("/{merchantId:guid}/withdrawals", MerchantHandlers.MakeWithdrawal).WithName("MakeWithdrawal"); + merchantGroup.MapPost("/{merchantId:guid}/deposits", MerchantHandlers.MakeDeposit).WithName("MakeDeposit") + .WithSummary("Record a merchant deposit").WithDescription("Records a manual deposit with amount, timestamp, and reference."); + merchantGroup.MapPost("/{merchantId:guid}/withdrawals", MerchantHandlers.MakeWithdrawal).WithName("MakeWithdrawal") + .WithSummary("Record a merchant withdrawal").WithDescription("Records a withdrawal with amount, timestamp, and reference."); - merchantGroup.MapPatch("/{merchantId:guid}/addresses", MerchantHandlers.AddMerchantAddress).WithName("AddMerchantAddress"); - merchantGroup.MapPatch("/{merchantId:guid}/addresses/{addressId:guid}", MerchantHandlers.UpdateMerchantAddress).WithName("UpdateMerchantAddress"); + merchantGroup.MapPatch("/{merchantId:guid}/addresses", MerchantHandlers.AddMerchantAddress).WithName("AddMerchantAddress") + .WithSummary("Add a merchant address").WithDescription("Adds the address in the request body to the merchant."); + merchantGroup.MapPatch("/{merchantId:guid}/addresses/{addressId:guid}", MerchantHandlers.UpdateMerchantAddress).WithName("UpdateMerchantAddress") + .WithSummary("Update a merchant address").WithDescription("Replaces the selected merchant address with the address in the request body."); - merchantGroup.MapPatch("/{merchantId:guid}/contacts", MerchantHandlers.AddMerchantContact).WithName("AddMerchantContact"); - merchantGroup.MapPatch("/{merchantId:guid}/contacts/{contactId:guid}", MerchantHandlers.UpdateMerchantContact).WithName("UpdateMerchantContact"); + merchantGroup.MapPatch("/{merchantId:guid}/contacts", MerchantHandlers.AddMerchantContact).WithName("AddMerchantContact") + .WithSummary("Add a merchant contact").WithDescription("Adds the contact in the request body to the merchant."); + merchantGroup.MapPatch("/{merchantId:guid}/contacts/{contactId:guid}", MerchantHandlers.UpdateMerchantContact).WithName("UpdateMerchantContact") + .WithSummary("Update a merchant contact").WithDescription("Replaces the selected merchant contact with the contact in the request body."); - merchantGroup.MapPost("/{merchantId:guid}/statements", MerchantHandlers.GenerateMerchantStatement).WithName("GenerateMerchantStatement"); + merchantGroup.MapPost("/{merchantId:guid}/statements", MerchantHandlers.GenerateMerchantStatement).WithName("GenerateMerchantStatement") + .WithSummary("Generate a merchant statement").WithDescription("Generates a statement for the merchant and statement date in the request body."); - merchantGroup.MapPatch("/{merchantId:guid}", MerchantHandlers.UpdateMerchant).WithName("UpdateMerchant"); - merchantGroup.MapPatch("/{merchantId:guid}/opening", MerchantHandlers.UpdateMerchantOpening).WithName("UpdateMerchantOpening"); - merchantGroup.MapPost("/{merchantId:guid}/schedules", MerchantHandlers.CreateMerchantSchedule).WithName("CreateMerchantSchedule"); - merchantGroup.MapPatch("/{merchantId:guid}/schedules/{year:int}", MerchantHandlers.UpdateMerchantSchedule).WithName("UpdateMerchantSchedule"); + merchantGroup.MapPatch("/{merchantId:guid}", MerchantHandlers.UpdateMerchant).WithName("UpdateMerchant") + .WithSummary("Update a merchant").WithDescription("Updates the merchant's name and settlement schedule."); + merchantGroup.MapPatch("/{merchantId:guid}/opening", MerchantHandlers.UpdateMerchantOpening).WithName("UpdateMerchantOpening") + .WithSummary("Update merchant opening hours").WithDescription("Sets opening and closing times for each day supplied in the request body."); + merchantGroup.MapPost("/{merchantId:guid}/schedules", MerchantHandlers.CreateMerchantSchedule).WithName("CreateMerchantSchedule") + .WithSummary("Create a merchant schedule").WithDescription("Creates the merchant schedule for the year and month closure days in the request body."); + merchantGroup.MapPatch("/{merchantId:guid}/schedules/{year:int}", MerchantHandlers.UpdateMerchantSchedule).WithName("UpdateMerchantSchedule") + .WithSummary("Update a merchant schedule").WithDescription("Updates month closure days for the year in the route."); return endpoints; } diff --git a/TransactionProcessor/Endpoints/OperatorEndpoints.cs b/TransactionProcessor/Endpoints/OperatorEndpoints.cs index 6997a256..fdfb305c 100644 --- a/TransactionProcessor/Endpoints/OperatorEndpoints.cs +++ b/TransactionProcessor/Endpoints/OperatorEndpoints.cs @@ -4,6 +4,8 @@ using Shared.Authorisation; using System.Diagnostics.CodeAnalysis; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects.Responses.Operator; +using System.Collections.Generic; namespace TransactionProcessor.Endpoints { @@ -23,17 +25,23 @@ public static IEndpointRouteBuilder MapOperatorEndpoints(this IEndpointRouteBuil // Create operatorGroup.MapPost("/", OperatorHandlers.CreateOperator) - .WithName("CreateOperator"); + .WithName("CreateOperator").WithSummary("Create an operator") + .WithDescription("Creates an operator in the specified estate. The operator ID is supplied in the request body."); // Update operatorGroup.MapPost("/{operatorId:guid}", OperatorHandlers.UpdateOperator) - .WithName("UpdateOperator"); + .WithName("UpdateOperator").WithSummary("Update an operator") + .WithDescription("Updates the selected operator's name and custom merchant or terminal number requirements."); // Read endpoints - operatorGroup.MapGet("/{operatorId:guid}", OperatorHandlers.GetOperator).WithName("GetOperator"); - operatorGroup.MapGet("/", OperatorHandlers.GetOperators).WithName("GetOperators"); + operatorGroup.MapGet("/{operatorId:guid}", OperatorHandlers.GetOperator) + .WithName("GetOperator").WithSummary("Get an operator") + .WithDescription("Returns the selected operator's configuration.").Produces(); + operatorGroup.MapGet("/", OperatorHandlers.GetOperators) + .WithName("GetOperators").WithSummary("List estate operators") + .WithDescription("Returns operators configured for the specified estate.").Produces>(); return endpoints; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Endpoints/SettlementEndpoints.cs b/TransactionProcessor/Endpoints/SettlementEndpoints.cs index 0678e7e6..38029943 100644 --- a/TransactionProcessor/Endpoints/SettlementEndpoints.cs +++ b/TransactionProcessor/Endpoints/SettlementEndpoints.cs @@ -2,8 +2,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Shared.Authorisation; +using System; using System.Diagnostics.CodeAnalysis; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects.Responses.Settlement; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Threading.Tasks; namespace TransactionProcessor.Endpoints { @@ -20,18 +25,38 @@ public static IEndpointRouteBuilder MapSettlementEndpoints(this IEndpointRouteBu .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy); group.MapGet("/{settlementDate:datetime}/merchants/{merchantId:guid}/pending", SettlementHandlers.GetPendingSettlement) - .WithName("GetPendingSettlement"); + .WithName("GetPendingSettlement").WithSummary("Get pending merchant settlement") + .WithDescription("Returns settlement counts and completion status for the merchant and settlement date.") + .Produces(); group.MapPost("/{settlementDate:datetime}/merchants/{merchantId:guid}", SettlementHandlers.ProcessSettlement) - .WithName("ProcessSettlement"); + .WithName("ProcessSettlement").WithSummary("Process a merchant settlement") + .WithDescription("Processes eligible fees for the merchant and settlement date, and returns the settlement ID.") + .Produces() + .AddOpenApiOperationTransformer((operation, _, _) => + { + if (operation.Responses.TryGetValue("200", out var response)) + { + foreach (var mediaType in response.Content.Values) + { + mediaType.Example = JsonNode.Parse("\"00000000-0000-0000-0000-000000000888\""); + } + } + + return Task.CompletedTask; + }); group.MapGet("/{settlementId:guid}", SettlementHandlers.GetSettlement) - .WithName("GetSettlement"); + .WithName("GetSettlement").WithSummary("Get a settlement") + .WithDescription("Returns settlement details and fees. The merchantId query parameter is also required by the handler.") + .Produces(); group.MapGet("/", SettlementHandlers.GetSettlements) - .WithName("GetSettlements"); + .WithName("GetSettlements").WithSummary("List settlements") + .WithDescription("Returns settlements for this estate, optionally filtered by merchantId, start_date, and end_date. Date filters are passed as strings to the business layer.") + .Produces>(); return endpoints; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Endpoints/TransactionEndpoints.cs b/TransactionProcessor/Endpoints/TransactionEndpoints.cs index e6e3f8c3..6c479593 100644 --- a/TransactionProcessor/Endpoints/TransactionEndpoints.cs +++ b/TransactionProcessor/Endpoints/TransactionEndpoints.cs @@ -4,6 +4,7 @@ using Shared.Authorisation; using System.Diagnostics.CodeAnalysis; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects; namespace TransactionProcessor.Endpoints { @@ -19,22 +20,27 @@ public static IEndpointRouteBuilder MapTransactionEndpoints(this IEndpointRouteB // POST /api/transactions - rejects password tokens => require client credentials group.MapPost("/logon", TransactionHandlers.PerformLogonTransaction) - .WithName("PerformLogonTransaction"); + .WithName("PerformLogonTransaction").WithSummary("Process a logon transaction") + .WithDescription("Submits a terminal logon transaction and returns the processor response.").Produces(); group.MapPost("/sale", TransactionHandlers.PerformSaleTransaction) - .WithName("PerformSaleTransaction"); + .WithName("PerformSaleTransaction").WithSummary("Process a sale transaction") + .WithDescription("Submits a sale transaction, including merchant, operator, contract, product, and optional metadata.").Produces(); group.MapPost("/reconciliation", TransactionHandlers.PerformReconciliationTransaction) - .WithName("PerformReconciliationTransaction"); + .WithName("PerformReconciliationTransaction").WithSummary("Reconcile terminal transactions") + .WithDescription("Submits the transaction count and value reported by a terminal, optionally broken down by operator and contract.").Produces(); // POST /api/{estateId}/transactions/{transactionId}/resendreceipt // note: controller used absolute route; map both forms — keep support for the original absolute route group.MapPost("/{transactionId:guid}/resendreceipt", TransactionHandlers.ResendTransactionReceipt) - .WithName("ResendTransactionReceipt"); + .WithName("ResendTransactionReceipt").WithSummary("Resend a transaction receipt") + .WithDescription("Resends the receipt for the transactionId. estateId is currently bound from the query string on this route."); // Also map legacy absolute route (controller had "/api/{estateId}/transactions/{transactionId}/resendreceipt") endpoints.MapPost("/api/{estateId:guid}/transactions/{transactionId:guid}/resendreceipt", TransactionHandlers.ResendTransactionReceipt) - .WithName("ResendTransactionReceiptLegacy"); + .WithName("ResendTransactionReceiptLegacy").WithSummary("Resend a transaction receipt (legacy route)") + .WithDescription("Resends the receipt for the transaction identified by estateId and transactionId in the route."); return endpoints; } diff --git a/TransactionProcessor/Endpoints/VoucherEndpoints.cs b/TransactionProcessor/Endpoints/VoucherEndpoints.cs index 66fbf015..54e6e603 100644 --- a/TransactionProcessor/Endpoints/VoucherEndpoints.cs +++ b/TransactionProcessor/Endpoints/VoucherEndpoints.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Shared.Authorisation; using System.Diagnostics.CodeAnalysis; using TransactionProcessor.Handlers; +using TransactionProcessor.DataTransferObjects; namespace TransactionProcessor.Endpoints { @@ -21,13 +23,18 @@ public static IEndpointRouteBuilder MapVoucherEndpoints(this IEndpointRouteBuild // Redeem and Get both reject password tokens in controller -> require client credentials group.MapPut("/", VoucherHandlers.RedeemVoucher) - .WithName("RedeemVoucher"); + .WithName("RedeemVoucher").WithSummary("Redeem a voucher") + .WithDescription("Redeems the voucher identified by its code. If RedeemedDateTime is omitted, the server uses the current local time.") + .Produces(); // GET with query parameters estateId, voucherCode, transactionId group.MapGet("/", VoucherHandlers.GetVoucher) - .WithName("GetVoucher"); + .WithName("GetVoucher").WithSummary("Get a voucher") + .WithDescription("Looks up a voucher by voucherCode when provided; otherwise it uses transactionId. estateId is required. At least one lookup key must be provided.") + + .Produces(); return endpoints; } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Extensions.cs b/TransactionProcessor/Extensions.cs index 9ffffcae..be00b29c 100644 --- a/TransactionProcessor/Extensions.cs +++ b/TransactionProcessor/Extensions.cs @@ -67,26 +67,28 @@ public static class Extensions { public static void PreWarm(this IApplicationBuilder applicationBuilder) { TypeProvider.LoadDomainEventsTypeDynamically(); - IConfigurationSection subscriptionConfigSection = Startup.Configuration.GetSection("AppSettings:SubscriptionConfiguration"); + IConfiguration configuration = applicationBuilder.ApplicationServices.GetRequiredService(); + IConfigurationSection subscriptionConfigSection = configuration.GetSection("AppSettings:SubscriptionConfiguration"); SubscriptionWorkersRoot subscriptionWorkersRoot = new SubscriptionWorkersRoot(); subscriptionConfigSection.Bind(subscriptionWorkersRoot); String eventStoreConnectionString = ConfigurationReader.GetValue("EventStoreSettings", "ConnectionString"); - IDomainEventHandlerResolver mainEventHandlerResolver = Startup.Container.GetInstance("Main"); - IDomainEventHandlerResolver domainEventHandlerResolver = Startup.Container.GetInstance("Domain"); - IDomainEventHandlerResolver orderedEventHandlerResolver = Startup.Container.GetInstance("Ordered"); + IServiceProvider services = applicationBuilder.ApplicationServices; + IDomainEventHandlerResolver mainEventHandlerResolver = services.GetRequiredService().GetInstance("Main"); + IDomainEventHandlerResolver domainEventHandlerResolver = services.GetRequiredService().GetInstance("Domain"); + IDomainEventHandlerResolver orderedEventHandlerResolver = services.GetRequiredService().GetInstance("Ordered"); Dictionary eventHandlerResolvers = new Dictionary { { "Main", mainEventHandlerResolver }, { "Domain", domainEventHandlerResolver }, { "Ordered", orderedEventHandlerResolver } }; - Func subscriptionRepositoryResolver = Startup.Container.GetInstance>(); + Func subscriptionRepositoryResolver = services.GetRequiredService>(); applicationBuilder.ConfigureSubscriptionService(subscriptionWorkersRoot, eventStoreConnectionString, eventHandlerResolvers, Extensions.log, subscriptionRepositoryResolver).Wait(CancellationToken.None); - IMediator mediator = Startup.Container.GetInstance(); - StatementFilePollerService statementFilePollerService = new(mediator); + IMediator mediator = services.GetRequiredService(); + StatementFilePollerService statementFilePollerService = new(mediator, configuration); statementFilePollerService.Start(); } @@ -95,15 +97,23 @@ public static void PreWarm(this IApplicationBuilder applicationBuilder) { [ExcludeFromCodeCoverage] public record FileProfile(String Name, String ListeningDirectory, String Filter, Boolean IsEnabled); + public sealed class AutoLogonOperatorConfiguration { + public AutoLogonOperatorConfiguration(IEnumerable operatorIds) { + this.OperatorIds = operatorIds.ToArray(); + } + + public IReadOnlyCollection OperatorIds { get; } + } + [ExcludeFromCodeCoverage] public class StatementFilePollerService { private readonly IMediator Mediator; private readonly IEnumerable FileProfiles; - public StatementFilePollerService(IMediator mediator) { + public StatementFilePollerService(IMediator mediator, IConfiguration configuration) { this.Mediator = mediator; // Load up the file configuration - this.FileProfiles = Startup.Configuration.GetSection("AppSettings:FileProfiles").GetChildren().ToList().Select(x => new + this.FileProfiles = configuration.GetSection("AppSettings:FileProfiles").GetChildren().ToList().Select(x => new { Name = x.GetValue("Name"), ListeningDirectory = x.GetValue("ListeningDirectory"), @@ -202,12 +212,21 @@ private void FileHandler(FileSystemEventArgs e, FileProfile fileProfile) [ExcludeFromCodeCoverage] public class AutoLogonWorkerService : BackgroundService { + private readonly AutoLogonOperatorConfiguration AutoLogonConfiguration; + private readonly Func OperatorProxyResolver; + + public AutoLogonWorkerService(AutoLogonOperatorConfiguration autoLogonConfiguration, + Func operatorProxyResolver) { + this.AutoLogonConfiguration = autoLogonConfiguration; + this.OperatorProxyResolver = operatorProxyResolver; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (stoppingToken.IsCancellationRequested == false) { - if (Startup.AutoApiLogonOperators.Any()) { - foreach (String autoApiLogonOperator in Startup.AutoApiLogonOperators) { + if (this.AutoLogonConfiguration.OperatorIds.Any()) { + foreach (String autoApiLogonOperator in this.AutoLogonConfiguration.OperatorIds) { await this.OperatorLogonAsync(autoApiLogonOperator, stoppingToken); } } @@ -222,7 +241,7 @@ internal async Task OperatorLogonAsync(String operatorId, CancellationToken canc Logger.LogInformation($"About to do auto logon for operator Id [{operatorId}]"); try { - Func resolver = Startup.ServiceProvider?.GetService>(); + Func resolver = this.OperatorProxyResolver; if (resolver == null) { Logger.LogWarning($"Auto logon for operator Id [{operatorId}] skipped because no operator proxy resolver was registered"); return; diff --git a/TransactionProcessor/Handlers/MerchantHandlers.cs b/TransactionProcessor/Handlers/MerchantHandlers.cs index 4decfd1d..a3549f63 100644 --- a/TransactionProcessor/Handlers/MerchantHandlers.cs +++ b/TransactionProcessor/Handlers/MerchantHandlers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -40,6 +40,11 @@ public static async Task GetMerchantBalanceLive(IMediator mediator, return ResponseFactory.FromResult(result, r => new MerchantBalanceResponse { Balance = r.merchant.balance, MerchantId = Guid.Parse(r.merchant.Id), AvailableBalance = r.merchant.balance, EstateId = estateId }); } + /// Gets the balance change history for a merchant over a date range. + /// Estate that owns the merchant. + /// Merchant whose balance history is requested. + /// Inclusive start of the history range, supplied as a query parameter. + /// Inclusive end of the history range, supplied as a query parameter. public static async Task GetMerchantBalanceHistory(IMediator mediator, HttpContext ctx, Guid estateId, diff --git a/TransactionProcessor/Handlers/SettlementHandlers.cs b/TransactionProcessor/Handlers/SettlementHandlers.cs index 2ee92148..9f7a7718 100644 --- a/TransactionProcessor/Handlers/SettlementHandlers.cs +++ b/TransactionProcessor/Handlers/SettlementHandlers.cs @@ -39,6 +39,10 @@ public static async Task ProcessSettlement(IMediator mediator, HttpCont return ResponseFactory.FromResult(result, guid => guid); } + /// Gets settlement details and its fees. + /// Estate that owns the settlement. + /// Settlement identifier from the route. + /// Merchant identifier, bound from the query string because it is not in the route. public static async Task GetSettlement(IMediator mediator, HttpContext ctx, [FromRoute] Guid estateId, Guid settlementId, Guid merchantId, CancellationToken cancellationToken) { SettlementQueries.GetSettlementQuery query = new(estateId, merchantId, settlementId); @@ -76,6 +80,11 @@ public static async Task GetSettlement(IMediator mediator, HttpContext }); } + /// Lists settlements for an estate with optional merchant and date filtering. + /// Estate whose settlements are listed. + /// Optional merchant filter. + /// Optional lower date bound from the start_date query parameter. + /// Optional upper date bound from the end_date query parameter. public static async Task GetSettlements(IMediator mediator, HttpContext ctx, [FromRoute] Guid estateId, [FromQuery] Guid? merchantId, [FromQuery(Name = "start_date")] string startDate, [FromQuery(Name = "end_date")] string endDate, CancellationToken cancellationToken) { @@ -120,4 +129,4 @@ public static async Task GetSettlements(IMediator mediator, HttpContext }); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/Handlers/TransactionHandlers.cs b/TransactionProcessor/Handlers/TransactionHandlers.cs index 1e4a468b..e9b9e0c2 100644 --- a/TransactionProcessor/Handlers/TransactionHandlers.cs +++ b/TransactionProcessor/Handlers/TransactionHandlers.cs @@ -40,6 +40,9 @@ public static async Task PerformReconciliationTransaction(IMediator med return ResponseFactory.FromResult(transactionResult, message => message); } + /// Resends the receipt for a transaction. + /// Estate owning the transaction. On the short route this is bound from the query string. + /// Transaction identifier from the route. public static async Task ResendTransactionReceipt(IMediator mediator, HttpContext ctx, Guid estateId, Guid transactionId, CancellationToken cancellationToken) { TransactionCommands.ResendTransactionReceiptCommand command = new(transactionId, estateId); diff --git a/TransactionProcessor/Handlers/VoucherHandlers.cs b/TransactionProcessor/Handlers/VoucherHandlers.cs index f8254837..4358e976 100644 --- a/TransactionProcessor/Handlers/VoucherHandlers.cs +++ b/TransactionProcessor/Handlers/VoucherHandlers.cs @@ -23,6 +23,10 @@ public static async Task RedeemVoucher(IMediator mediator, HttpContext return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } + /// Looks up a voucher by voucher code or transaction ID. + /// Estate that owns the voucher; supplied as a query parameter. + /// Voucher code query parameter. Takes precedence when non-empty. + /// Optional transaction ID query parameter used when voucherCode is empty. public static async Task GetVoucher(IMediator mediator, HttpContext ctx, Guid estateId, string voucherCode, Guid? transactionId, CancellationToken cancellationToken) { if (String.IsNullOrEmpty(voucherCode) == false) @@ -44,4 +48,4 @@ public static async Task GetVoucher(IMediator mediator, HttpContext ctx return ResponseFactory.FromResult(Result.Invalid()); } } -} \ No newline at end of file +} diff --git a/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs b/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs new file mode 100644 index 00000000..680f8daa --- /dev/null +++ b/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs @@ -0,0 +1,399 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace TransactionProcessor.OpenApi; + +/// +/// Adds DTO XML comments and examples to the OpenAPI document generated by ApiDefaults. +/// +public sealed class DtoDocumentationTransformer : IOpenApiDocumentTransformer +{ + private const string DocumentationFileName = "TransactionProcessor.DataTransferObjects.xml"; + + /// + public Task TransformAsync(OpenApiDocument document, + OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + string documentationPath = Path.Combine(AppContext.BaseDirectory, DocumentationFileName); + if (!File.Exists(documentationPath)) + { + return Task.CompletedTask; + } + + Dictionary members = XDocument.Load(documentationPath) + .Root? + .Element("members")? + .Elements("member") + .Where(member => member.Attribute("name") is not null) + .ToDictionary(member => member.Attribute("name")!.Value, + member => member, + StringComparer.Ordinal) + ?? new Dictionary(StringComparer.Ordinal); + + IDictionary componentSchemas = document.Components?.Schemas ?? + new Dictionary(StringComparer.Ordinal); + foreach (KeyValuePair schemaEntry in componentSchemas) + { + if (schemaEntry.Value is not OpenApiSchema schema) + { + continue; + } + + schema.Properties ??= new Dictionary(StringComparer.Ordinal); + if (string.Equals(schemaEntry.Key, "ProblemDetails", StringComparison.Ordinal)) + { + schema.Description ??= "RFC 9457 problem details returned when a request fails."; + schema.Examples ??= new List(); + if (schema.Examples.Count == 0 && ReadExample("""{"type":"about:blank","title":"An error occurred.","detail":"The request could not be completed."}""") is JsonNode problemExample) + { + schema.Examples.Add(problemExample); + } + + AddProblemDetailsPropertyDescriptions(schema); + } + + string? typeMemberName = FindTypeMember(schemaEntry.Key, schema, members); + if (typeMemberName is null) + { + continue; + } + + XElement typeMember = members[typeMemberName]; + schema.Description ??= ReadDocumentationText(typeMember.Element("summary")); + schema.Examples ??= new List(); + if (schema.Examples.Count == 0 && ReadExample(typeMember.Element("example")) is JsonNode example) + { + schema.Examples.Add(NormalizeExample(example, schema)); + } + + foreach (string documentedTypeMemberName in GetTypeMemberHierarchy(typeMemberName, members)) + { + string memberPrefix = $"P:{documentedTypeMemberName[2..]}."; + foreach (KeyValuePair propertyEntry in schema.Properties.ToArray()) + { + string propertyName = ToPascalCase(propertyEntry.Key); + if (members.TryGetValue(memberPrefix + propertyName, out XElement? propertyMember)) + { + schema.Properties[propertyEntry.Key] = WithDescription( + propertyEntry.Value, + ReadDocumentationText(propertyMember.Element("summary"))); + } + } + } + + } + + foreach (KeyValuePair pathEntry in document.Paths ?? + new Dictionary(StringComparer.Ordinal)) + { + IOpenApiPathItem path = pathEntry.Value; + foreach (OpenApiOperation operation in path.Operations?.Values ?? Enumerable.Empty()) + { + AddPathParameterDescriptions(pathEntry.Key, operation); + + if (operation.RequestBody is OpenApiRequestBody requestBody) + { + foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= GetExample(mediaType.Schema); + } + } + + foreach (IOpenApiResponse response in operation.Responses?.Values ?? Enumerable.Empty()) + { + foreach (OpenApiMediaType mediaType in response.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= GetExample(mediaType.Schema); + } + } + } + } + + AddDomainEventExample(document); + + return Task.CompletedTask; + } + + private static void AddPathParameterDescriptions(string route, OpenApiOperation operation) + { + foreach (IOpenApiParameter parameter in operation.Parameters ?? Enumerable.Empty()) + { + if (parameter.In != ParameterLocation.Path) + { + continue; + } + + string? description = parameter.Name switch + { + "estateId" => "Unique identifier of the estate that owns or scopes this resource.", + "merchantId" => "Unique identifier of the merchant within the specified estate.", + "operatorId" => "Unique identifier of the operator associated with the estate or merchant.", + "contractId" => "Unique identifier of the contract within the specified estate or merchant.", + "productId" => "Unique identifier of the product on the specified contract.", + "transactionFeeId" => "Unique identifier of the transaction fee to disable.", + "deviceIdentifier" => "Identifier of the merchant device to replace.", + "addressId" => "Unique identifier of the merchant address to update.", + "contactId" => "Unique identifier of the merchant contact to update.", + "year" => "Calendar year for the merchant schedule.", + "settlementDate" => "Settlement date, supplied as a date value (for example, 2026-01-15).", + "settlementId" => "Unique identifier of the settlement.", + "transactionId" => "Unique identifier of the transaction whose receipt is being resent.", + _ => null + }; + + if (description is not null) + { + // Parameter XML docs are shared by both receipt routes; use the route context + // so the legacy route's estateId is documented as a path value. + parameter.Description = parameter.Name == "estateId" && + route.Equals("/api/{estateId}/transactions/{transactionId}/resendreceipt", StringComparison.Ordinal) + ? "Unique identifier of the estate that owns the transaction." + : description; + } + } + } + + private static void AddDomainEventExample(OpenApiDocument document) + { + string documentationPath = Path.Combine(AppContext.BaseDirectory, "TransactionProcessor.xml"); + if (!File.Exists(documentationPath) || + document.Paths is null || + !document.Paths.TryGetValue("/api/domainevents", out IOpenApiPathItem? path) || + path.Operations is null || + !path.Operations.TryGetValue(System.Net.Http.HttpMethod.Post, out OpenApiOperation? operation) || + operation.RequestBody is not OpenApiRequestBody requestBody) + { + return; + } + + XElement? parameterDocumentation = XDocument.Load(documentationPath) + .Root? + .Element("members")? + .Elements("member") + .Where(member => member.Attribute("name")?.Value.StartsWith( + "M:TransactionProcessor.Controllers.DomainEventController.PostEventAsync(", + StringComparison.Ordinal) == true) + .SelectMany(member => member.Elements("param")) + .FirstOrDefault(parameter => parameter.Attribute("name")?.Value == "request"); + string? example = parameterDocumentation?.Attribute("example")?.Value; + if (string.IsNullOrWhiteSpace(example)) + { + return; + } + + JsonNode? parsedExample = ReadExample(example); + foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= parsedExample?.DeepClone(); + } + } + + private static string? FindTypeMember(string schemaName, + OpenApiSchema schema, + IReadOnlyDictionary members) + { + string[] candidates = members.Keys.Where(name => name.StartsWith("T:", StringComparison.Ordinal) && + name.EndsWith($".{schemaName}", StringComparison.Ordinal)) + .ToArray(); + if (candidates.Length == 0) + { + return null; + } + + IDictionary properties = schema.Properties ?? + new Dictionary(StringComparer.Ordinal); + + return candidates.OrderByDescending(candidate => + { + string prefix = $"P:{candidate[2..]}."; + return properties.Keys.Count(propertyName => + members.ContainsKey(prefix + ToPascalCase(propertyName))); + }).First(); + } + + private static JsonNode? GetExample(IOpenApiSchema? openApiSchema) + { + if (openApiSchema is OpenApiSchemaReference reference) + { + return GetExample(reference.Target); + } + + if (openApiSchema is not OpenApiSchema schema) + { + return null; + } + + if (schema.Examples?.FirstOrDefault() is JsonNode example) + { + return example.DeepClone(); + } + + if (schema.Items is not null && GetExample(schema.Items) is JsonNode itemExample) + { + return new JsonArray(itemExample); + } + + return null; + } + + private static JsonNode NormalizeExample(JsonNode example, IOpenApiSchema schema) + { + if (schema is OpenApiSchemaReference reference) + { + return NormalizeExample(example, reference.Target); + } + + if (schema is not OpenApiSchema openApiSchema) + { + return example.DeepClone(); + } + + IDictionary properties = openApiSchema.Properties ?? + new Dictionary(StringComparer.Ordinal); + if (example is JsonObject exampleObject && properties.Count > 0) + { + JsonObject normalizedObject = new(); + foreach (KeyValuePair property in exampleObject) + { + string schemaPropertyName = properties.Keys.FirstOrDefault(name => + string.Equals(ToPascalCase(name), ToPascalCase(property.Key), StringComparison.OrdinalIgnoreCase)) + ?? property.Key; + JsonNode? value = property.Value; + normalizedObject[schemaPropertyName] = value is not null && + properties.TryGetValue(schemaPropertyName, + out IOpenApiSchema? propertySchema) + ? NormalizeExample(value, propertySchema) + : value?.DeepClone(); + } + + return normalizedObject; + } + + IOpenApiSchema? itemsSchema = openApiSchema.Items; + if (example is JsonArray exampleArray && itemsSchema is not null) + { + JsonArray normalizedArray = new(); + foreach (JsonNode? item in exampleArray) + { + normalizedArray.Add(item is null ? null : NormalizeExample(item, itemsSchema)); + } + + return normalizedArray; + } + + return example.DeepClone(); + } + + private static IEnumerable GetTypeMemberHierarchy(string typeMemberName, + IReadOnlyDictionary members) + { + yield return typeMemberName; + + string typeName = typeMemberName[2..]; + Assembly? dtoAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(assembly => string.Equals( + assembly.GetName().Name, + "TransactionProcessor.DataTransferObjects", + StringComparison.Ordinal)); + Type? dtoType = dtoAssembly?.GetType(typeName, throwOnError: false); + for (Type? baseType = dtoType?.BaseType; + baseType is not null && baseType != typeof(object); + baseType = baseType.BaseType) + { + string baseTypeMemberName = $"T:{baseType.FullName}"; + if (members.ContainsKey(baseTypeMemberName)) + { + yield return baseTypeMemberName; + } + } + } + + private static IOpenApiSchema WithDescription(IOpenApiSchema schema, string? description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return schema; + } + + if (schema is OpenApiSchema openApiSchema) + { + openApiSchema.Description ??= description; + return openApiSchema; + } + + // OpenAPI serializes schema references without sibling descriptions. Wrap the reference + // in allOf so property documentation remains visible to clients such as Scalar. + return schema is OpenApiSchemaReference + ? new OpenApiSchema + { + Description = description, + AllOf = new List { schema } + } + : schema; + } + + private static void AddProblemDetailsPropertyDescriptions(OpenApiSchema schema) + { + (string Name, string Description)[] descriptions = + [ + ("type", "URI reference identifying the problem type. Defaults to about:blank."), + ("title", "Short, human-readable summary of the problem."), + ("status", "HTTP status code for this occurrence of the problem."), + ("detail", "Human-readable explanation specific to this occurrence."), + ("instance", "URI reference identifying this specific occurrence of the problem.") + ]; + + foreach ((string name, string description) in descriptions) + { + if (schema.Properties.TryGetValue(name, out IOpenApiSchema? propertySchema)) + { + schema.Properties[name] = WithDescription(propertySchema, description); + } + } + } + + private static JsonNode? ReadExample(XElement? exampleElement) + { + return ReadExample(exampleElement?.Value); + } + + private static JsonNode? ReadExample(string? example) + { + string? value = example?.Trim(); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + try + { + return JsonNode.Parse(value); + } + catch (JsonException) + { + return JsonValue.Create(value); + } + } + + private static string? ReadDocumentationText(XElement? element) + { + string value = string.Join(" ", element?.Value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) ?? []); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + private static string ToPascalCase(string value) => + string.Concat(value.Split('_', StringSplitOptions.RemoveEmptyEntries) + .Select(part => char.ToUpperInvariant(part[0]) + part[1..])); +} diff --git a/TransactionProcessor/Program.cs b/TransactionProcessor/Program.cs index ba296ea1..f367a4f4 100644 --- a/TransactionProcessor/Program.cs +++ b/TransactionProcessor/Program.cs @@ -1,121 +1,197 @@ +using Lamar; +using Lamar.Microsoft.DependencyInjection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog; using NLog.Extensions.Logging; -using Shared.Logger; +using Sentry.Extensibility; +using Shared.General; +using Logger = Shared.Logger.Logger; +using NlogLogger = Shared.Logger.NlogLogger; using Shared.Middleware; +using Shared.Serialisation; +using System.Diagnostics.CodeAnalysis; using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using TransactionProcessor.Bootstrapper; +using TransactionProcessor.Endpoints; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Prometheus; +using Shared.EventStore.Aggregate; +using Shared.Extensions; +using TransactionProcessing.ApiDefaults; -namespace TransactionProcessor +namespace TransactionProcessor; + +[ExcludeFromCodeCoverage] +public partial class Program { - using Lamar.Microsoft.DependencyInjection; - using Microsoft.Extensions.DependencyInjection; - using NLog; - using Sentry.Extensibility; - using Shared.General; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Reflection; - - [ExcludeFromCodeCoverage] - public class Program + public static void Main(string[] args) + { + BuildApplication(args).Run(); + } + + public static WebApplication BuildApplication(string[] args) { - public static void Main(string[] args) + string contentRoot = Directory.GetCurrentDirectory(); + string nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); + + LogManager.Setup(b => { - Program.CreateHostBuilder(args).Build().Run(); - } + b.SetupLogFactory(setup => + { + setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); + }); + b.LoadConfigurationFromFile(nlogConfigPath); + }); + + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + builder.Host.UseWindowsService(); + builder.Host.UseLamar(); + + ConfigureConfiguration(builder); + ConfigureLogging(builder); + + IConfigurationRoot configuration = builder.Configuration; + IWebHostEnvironment environment = builder.Environment; + ConfigurationReader.Initialise(configuration); + ConfigureSentry(builder, configuration, environment); - public static IHostBuilder CreateHostBuilder(string[] args) + builder.Host.ConfigureContainer((_, services) => { - String contentRoot = Directory.GetCurrentDirectory(); - String nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); + ConfigureServices(services, configuration, environment); + }); - LogManager.Setup(b => + WebApplication app = builder.Build(); + Logger.Initialise(app.Services.GetRequiredService().CreateLogger("TransactionProcessor")); + configuration.LogConfiguration(Logger.LogWarning); + StringSerialiser.Initialise(app.Services.GetRequiredService()); + TypeProvider.LoadDomainEventsTypeDynamically(); + LogTypeMappings(); + + if (environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMiddleware(); + app.AddRequestResponseLogging(); + app.AddExceptionHandler(); + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseMetricServer(); + ConfigureEndpoints(app); + app.PreWarm(); + TraceLoadedTokenAssemblies(); + app.UseApiDefaults(); + return app; + } + + public static void ConfigureServices(ServiceRegistry services, IConfigurationRoot configuration, IWebHostEnvironment environment) + { + ConfigurationReader.Initialise(configuration); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(new RepositoryRegistry(configuration, environment)); + services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(); + services.IncludeRegistry(new OperatorRegistry(configuration)); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(new DomainEventHandlerRegistry(configuration)); + services.AddMemoryCache(); + services.AddHostedService(); + } + + private static void ConfigureConfiguration(WebApplicationBuilder builder) + { + FileInfo assemblyFile = new(Assembly.GetExecutingAssembly().Location); + builder.Configuration.SetBasePath(assemblyFile.Directory!.FullName) + .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 { - b.SetupLogFactory(setup => - { - setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); - }); - b.LoadConfigurationFromFile(nlogConfigPath); + ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" }); + } - IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); - hostBuilder.UseWindowsService(); - hostBuilder.UseLamar(); - ConfigureWebHost(hostBuilder); - hostBuilder.ConfigureLogging(logging => { - logging.AddConsole(); - logging.AddNLog(); + private static void ConfigureLogging(WebApplicationBuilder builder) + { + builder.Logging.AddConsole(); + builder.Logging.AddNLog(); + } + private static void ConfigureSentry(WebApplicationBuilder builder, IConfiguration configuration, IWebHostEnvironment environment) + { + IConfigurationSection sentrySection = configuration.GetSection("SentryConfiguration"); + if (sentrySection.Exists() && !environment.IsDevelopment()) + { + builder.WebHost.UseSentry(options => + { + options.Dsn = configuration["SentryConfiguration:Dsn"]; + options.SendDefaultPii = true; + options.MaxRequestBodySize = RequestSize.Always; + options.CaptureBlockingCalls = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "CaptureBlockingCalls", false); + options.IncludeActivityData = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "IncludeActivityData", false); + options.Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; }); - hostBuilder.ConfigureServices(services => - { - services.AddHostedService(provider => - { - AutoLogonWorkerService worker = new AutoLogonWorkerService(); - return worker; - }); - }); - return hostBuilder; } + } - private static void ConfigureWebHost(IHostBuilder hostBuilder) { - hostBuilder.ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureAppConfiguration((context, configBuilder) => - { - IWebHostEnvironment env = context.HostingEnvironment; - - FileInfo fi = new(Assembly.GetExecutingAssembly().Location); - - configBuilder.SetBasePath(fi.Directory.FullName) - .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" - }); - - // Build a snapshot of configuration so we can use it immediately (e.g. for Sentry) - IConfigurationRoot builtConfig = configBuilder.Build(); - - // Keep existing static usage (if you must), and initialise the ConfigurationReader now. - Startup.Configuration = builtConfig; - ConfigurationReader.Initialise(Startup.Configuration); - - // Configure Sentry on the webBuilder using the config snapshot. - IConfigurationSection sentrySection = builtConfig.GetSection("SentryConfiguration"); - if (sentrySection.Exists()) - { - // Replace the condition below if you intended to only enable Sentry in certain environments. - if (env.IsDevelopment() == false) - { - webBuilder.UseSentry(o => - { - o.Dsn = builtConfig["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"; - }); - } - } - }); - - webBuilder.UseStartup(); - webBuilder.UseKestrel(); - }); + private static void ConfigureEndpoints(WebApplication app) + { + app.MapControllers(); + app.MapHealthChecks("health", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse + }); + app.MapHealthChecks("healthui", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + app.MapEstateEndpoints(); + app.MapMerchantEndpoints(); + app.MapContractEndpoints(); + app.MapOperatorEndpoints(); + app.MapFloatEndpoints(); + app.MapVoucherEndpoints(); + app.MapTransactionEndpoints(); + app.MapSettlementEndpoints(); + + } + + private static void LogTypeMappings() + { + foreach (KeyValuePair type in TypeMap.Map) + { + Logger.LogInformation($"Type name {type.Value} mapped to {type.Key.Name}"); + } + } + + private static void TraceLoadedTokenAssemblies() + { + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "Microsoft.IdentityModel.Tokens")) + { + Console.WriteLine($"[Trace] Tokens Assembly: {assembly.Location} - Version: {assembly.GetName().Version}"); } } } diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs deleted file mode 100644 index 55f053fb..00000000 --- a/TransactionProcessor/Startup.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Prometheus; -using System.Linq; -using TransactionProcessor.Endpoints; - -namespace TransactionProcessor -{ - using Bootstrapper; - using HealthChecks.UI.Client; - using Lamar; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Diagnostics.HealthChecks; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Shared.EventStore.Aggregate; - using Shared.Extensions; - using Shared.General; - using Shared.Middleware; - using Shared.Serialisation; - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Reflection; - using ILogger = Microsoft.Extensions.Logging.ILogger; - using Logger = Shared.Logger.Logger; - - /// - /// - /// - [ExcludeFromCodeCoverage] - public class Startup - { - #region Fields - - public static Container Container; - - private static readonly List AutoApiLogonOperatorsInternal = new List(); - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The web host environment. - public Startup(IWebHostEnvironment webHostEnvironment) { - Startup.WebHostEnvironment = webHostEnvironment; - } - - #endregion - - #region Properties - - /// - /// Gets or sets the configuration. - /// - /// - /// The configuration. - /// - public static IConfigurationRoot Configuration { get; set; } - - public static IServiceProvider ServiceProvider { get; set; } - - public static IReadOnlyCollection AutoApiLogonOperators => AutoApiLogonOperatorsInternal.AsReadOnly(); - - /// - /// Gets or sets the web host environment. - /// - /// - /// The web host environment. - /// - public static IWebHostEnvironment WebHostEnvironment { get; set; } - - #endregion - - #region Methods - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - /// - /// Configures the specified application. - /// - /// The application. - /// The env. - /// The logger factory. - /// The provider. - public void Configure(IApplicationBuilder app, - IWebHostEnvironment env, - ILoggerFactory loggerFactory, - IHostApplicationLifetime lifetime, IHost host) { - ConfigurationReader.Initialise(Startup.Configuration); - - if (env.IsDevelopment()) { - app.UseDeveloperExceptionPage(); - } - - ILogger logger = loggerFactory.CreateLogger("TransactionProcessor"); - - Logger.Initialise(logger); - - Startup.Configuration.LogConfiguration(Logger.LogWarning); - this.LogTypeMappings(); - app.UseMiddleware(); - app.AddRequestResponseLogging(); - app.AddExceptionHandler(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - app.UseMetricServer(s => { - }); - this.ConfigureEndpoints(app); - - app.PreWarm(); - - this.TraceLoadedTokenAssemblies(); - } - - public void ConfigureContainer(ServiceRegistry services) { - - ConfigurationReader.Initialise(Startup.Configuration); - Startup.AutoApiLogonOperatorsInternal.Clear(); - - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - - services.AddMemoryCache(); - - TypeProvider.LoadDomainEventsTypeDynamically(); - - Startup.Container = new Container(services); - - Startup.ServiceProvider = services.BuildServiceProvider(); - - var serialiser = Container.GetRequiredService(); - StringSerialiser.Initialise(serialiser); - } - - internal static void AddAutoApiLogonOperator(String operatorId) { - if (Startup.AutoApiLogonOperatorsInternal.Contains(operatorId) == false) { - Startup.AutoApiLogonOperatorsInternal.Add(operatorId); - } - } - - private void ConfigureEndpoints(IApplicationBuilder app) { - app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - endpoints.MapHealthChecks("health", - new HealthCheckOptions { - Predicate = _ => true, - ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse - }); - endpoints.MapHealthChecks("healthui", - new HealthCheckOptions { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - endpoints.MapEstateEndpoints(); - endpoints.MapMerchantEndpoints(); - endpoints.MapContractEndpoints(); - endpoints.MapOperatorEndpoints(); - endpoints.MapFloatEndpoints(); - endpoints.MapVoucherEndpoints(); - endpoints.MapTransactionEndpoints(); - endpoints.MapSettlementEndpoints(); - }); - } - - private void LogTypeMappings() { - foreach (KeyValuePair type in TypeMap.Map) { - Logger.LogInformation($"Type name {type.Value} mapped to {type.Key.Name}"); - } - } - - private void TraceLoadedTokenAssemblies() { - List loadedTokensAssemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => a.GetName().Name == "Microsoft.IdentityModel.Tokens") - .ToList(); - - foreach (Assembly asm in loadedTokensAssemblies) { - Console.WriteLine($"[Trace] Tokens Assembly: {asm.Location} - Version: {asm.GetName().Version}"); - } - } - } - - #endregion -} diff --git a/TransactionProcessor/TransactionProcessor.csproj b/TransactionProcessor/TransactionProcessor.csproj index ffce1157..49f19665 100644 --- a/TransactionProcessor/TransactionProcessor.csproj +++ b/TransactionProcessor/TransactionProcessor.csproj @@ -3,6 +3,8 @@ net10.0 Linux + true + $(NoWarn);1591 @@ -23,6 +25,7 @@ + all @@ -40,18 +43,13 @@ - - - - - - + @@ -60,6 +58,15 @@ + + + + TransactionProcessor.DataTransferObjects.xml + PreserveNewest + PreserveNewest + + + Always diff --git a/TransactionProcessor/TransactionProcessor.xml b/TransactionProcessor/TransactionProcessor.xml index 177becdd..81a45809 100644 --- a/TransactionProcessor/TransactionProcessor.xml +++ b/TransactionProcessor/TransactionProcessor.xml @@ -280,7 +280,7 @@ The domain event handler resolver - + Initializes a new instance of the class. @@ -443,64 +443,5 @@ The trace. The log level. - - - - - - - - Initializes a new instance of the class. - - The web host environment. - - - - Gets or sets the configuration. - - - The configuration. - - - - - Gets or sets the web host environment. - - - The web host environment. - - - - - Configures the services. - - The services. - - - - The event store client settings - - - - - Configures the event store settings. - - The settings. - - - - Configures the middleware services. - - The services. - - - - Configures the specified application. - - The application. - The env. - The logger factory. - The provider. - diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 00000000..c2d1deca --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,210 @@ +# Transaction Processor API reference + +This reference covers the routes currently registered by `TransactionProcessor` (without host, port, or deployment path prefix). JSON property names are shown in CLR/PascalCase form; wire casing follows the shared serializer defaults configured at runtime. `Guid` values use UUID strings, decimals are JSON numbers, and date/time values are JSON strings accepted by the configured .NET converters. + +## Common behavior + +- All endpoint groups below, except health checks and the domain-event controller, require authentication and the `ClientCredentialsOnlyPolicy`. Tokens use the configured OpenIddict validation issuer and API audience. Send `Authorization: Bearer `. +- Handler outcomes pass through `ResponseFactory`. Generic success results serialize the mapped response DTO/model listed below. Successful non-generic command results return status-only responses with no JSON body. Failure status and problem bodies are centralized in `Shared.Results.Web` and can vary by result status. +- No pagination parameters are declared by these routes. Query parameters are listed only where the handler signature binds them. +- Nullable DTO fields may be omitted or `null`; the code does not declare per-property validation attributes for these request DTOs. + +## Endpoint index + +### Health + +| Method | Path | Parameters / body | Success response | +|---|---|---|---| +| GET | `/health` | None | Health check JSON from `Shared.HealthChecks.HealthCheckMiddleware.WriteResponse`. | +| GET | `/healthui` | None | Health check UI JSON. | + +### Estates — `/api/estates` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| POST | `/api/estates/` | — | `CreateEstateRequest` | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}` | — | — | `EstateResponse` | +| GET | `/api/estates/{estateId}/all` | — | — | `EstateResponse[]` | +| PATCH | `/api/estates/{estateId}/users` | — | `CreateEstateUserRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/operators` | — | `Estate.AssignOperatorRequest` | Result envelope/body from command result. | +| DELETE | `/api/estates/{estateId}/operators/{operatorId}` | — | — | Result envelope/body from command result. | + +### Contracts — `/api/estates/{estateId}/contracts` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| GET | `/api/estates/{estateId}/contracts/` | — | — | `ContractResponse[]` | +| GET | `/api/estates/{estateId}/contracts/{contractId}` | — | — | `ContractResponse` | +| POST | `/api/estates/{estateId}/contracts/` | — | `CreateContractRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/contracts/{contractId}/products` | — | `AddProductToContractRequest` | Result envelope/body from command result. Product ID is generated by the server. | +| PATCH | `/api/estates/{estateId}/contracts/{contractId}/products/{productId}/transactionFees` | — | `AddTransactionFeeForProductToContractRequest` | Result envelope/body from command result. Fee ID is generated by the server. | +| DELETE | `/api/estates/{estateId}/contracts/{contractId}/products/{productId}/transactionFees/{transactionFeeId}` | — | — | Result envelope/body from command result. | + +### Floats — `/api/estates/{estateId}/floats` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| POST | `/api/estates/{estateId}/floats/` | — | `CreateFloatRequest` | Result envelope/body from command result. | +| PUT | `/api/estates/{estateId}/floats/` | — | `RecordFloatCreditPurchaseRequest` | Result envelope/body from command result. | + +### Merchants — `/api/estates/{estateId}/merchants` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| GET | `/api/estates/{estateId}/merchants/` | — | — | `MerchantResponse[]` | +| POST | `/api/estates/{estateId}/merchants/` | — | `CreateMerchantRequest` | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}/merchants/{merchantId}` | — | — | `MerchantResponse` | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}` | — | `UpdateMerchantRequest` | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/balance` | — | — | `MerchantBalanceResponse` | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/livebalance` | — | — | `MerchantBalanceResponse` | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/balancehistory` | `startDate` (DateTime), `endDate` (DateTime) | — | `MerchantBalanceChangedEntryResponse[]` | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/contracts` | — | — | `ContractResponse[]` | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/contracts` | — | `AddMerchantContractRequest` | Result envelope/body from command result. | +| DELETE | `/api/estates/{estateId}/merchants/{merchantId}/contracts/{contractId}` | — | — | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/contracts/{contractId}/products/{productId}/transactionFees` | — | — | `ContractProductTransactionFee[]` | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/operators` | — | `Merchant.AssignOperatorRequest` | Result envelope/body from command result. | +| DELETE | `/api/estates/{estateId}/merchants/{merchantId}/operators/{operatorId}` | — | — | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/devices` | — | `AddMerchantDeviceRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/devices/{deviceIdentifier}` | — | `SwapMerchantDeviceRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/users` | — | `CreateMerchantUserRequest` | Result envelope/body from command result. | +| POST | `/api/estates/{estateId}/merchants/{merchantId}/deposits` | — | `MakeMerchantDepositRequest` | Result envelope/body from command result. | +| POST | `/api/estates/{estateId}/merchants/{merchantId}/withdrawals` | — | `MakeMerchantWithdrawalRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/addresses` | — | `Address` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/addresses/{addressId}` | — | `Address` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/contacts` | — | `Contact` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/contacts/{contactId}` | — | `Contact` | Result envelope/body from command result. | +| POST | `/api/estates/{estateId}/merchants/{merchantId}/statements` | — | `GenerateMerchantStatementRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/opening` | — | `MerchantOpeningRequest` | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/schedules/{year}` | — | — | `MerchantScheduleResponse` | +| GET | `/api/estates/{estateId}/merchants/{merchantId}/schedules/{year}/readmodel` | — | — | `MerchantScheduleResponse` | +| POST | `/api/estates/{estateId}/merchants/{merchantId}/schedules` | — | `CreateMerchantScheduleRequest` | Result envelope/body from command result. | +| PATCH | `/api/estates/{estateId}/merchants/{merchantId}/schedules/{year}` | — | `UpdateMerchantScheduleRequest` | Result envelope/body from command result. | + +### Operators — `/api/estates/{estateId}/operators` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| POST | `/api/estates/{estateId}/operators/` | — | `CreateOperatorRequest` | Result envelope/body from command result. | +| POST | `/api/estates/{estateId}/operators/{operatorId}` | — | `UpdateOperatorRequest` | Result envelope/body from command result. | +| GET | `/api/estates/{estateId}/operators/` | — | — | `OperatorResponse[]` | +| GET | `/api/estates/{estateId}/operators/{operatorId}` | — | — | `OperatorResponse` | + +### Settlements — `/api/estates/{estateId}/settlements` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| GET | `/api/estates/{estateId}/settlements/` | `merchantId` (optional Guid), `start_date` (string), `end_date` (string) | — | `SettlementResponse[]` | +| GET | `/api/estates/{estateId}/settlements/{settlementId}` | `merchantId` (Guid; bound by handler but absent from route) | — | `Responses.Settlement.SettlementResponse` | +| GET | `/api/estates/{estateId}/settlements/{settlementDate}/merchants/{merchantId}/pending` | — | — | Root `SettlementResponse` | +| POST | `/api/estates/{estateId}/settlements/{settlementDate}/merchants/{merchantId}` | — | — | Settlement ID (`Guid`) | + +`settlementDate` is bound as `DateTime`. `start_date` and `end_date` are passed to business logic as strings; their accepted formats are not constrained at the HTTP binding layer. The single-settlement GET handler also requires a `merchantId` argument not represented in its route, so callers must supply it as a query parameter. + +### Transactions — `/api/transactions` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| POST | `/api/transactions/logon` | — | `LogonTransactionRequest` | `LogonTransactionResponse` | +| POST | `/api/transactions/sale` | — | `SaleTransactionRequest` | `SaleTransactionResponse` | +| POST | `/api/transactions/reconciliation` | — | `ReconciliationRequest` | `ReconciliationResponse` | +| POST | `/api/transactions/{transactionId}/resendreceipt` | `estateId` (Guid; bound but absent from route) | — | Result envelope/body from command result. | +| POST | `/api/{estateId}/transactions/{transactionId}/resendreceipt` (legacy) | — | — | Result envelope/body from command result. | + +### Vouchers — `/api/vouchers` + +| Method | Path | Query | Request body | Success response | +|---|---|---|---|---| +| PUT | `/api/vouchers/` | — | `RedeemVoucherRequest` | `RedeemVoucherResponse` | +| GET | `/api/vouchers/` | `estateId` (Guid), `voucherCode` (string), `transactionId` (optional Guid) | — | `GetVoucherResponse` | + +Voucher lookup uses `voucherCode` when non-empty; otherwise it looks up by non-empty `transactionId`. If neither is supplied, it returns an invalid-result response. + +### Domain event ingress + +| Method | Path | Query / headers | Request body | Success response | +|---|---|---|---|---| +| POST | `/api/domainevents` | `eventType` header (required to resolve event type); optional `subscriptionId` header selects a named resolver | Event JSON object matching the event identified by `eventType` | Empty `200 OK`; failures may return `500` Problem Details with `eventId`, `eventType`, and `failures`. | + +This endpoint has no authorization metadata in its controller declaration. Event payload properties are event-type-specific and are not modeled by a single DTO; the body schema is therefore a generic object. + +## Request DTOs + +Fields below are the declared DTO properties. Requiredness is not inferred from non-nullable C# reference annotations because runtime nullability/validation behavior is not consistently configured. + +| DTO | Fields | +|---|---| +| `CreateEstateRequest` | `EstateId: Guid`, `EstateName: string` | +| `CreateEstateUserRequest` / `CreateMerchantUserRequest` | `EmailAddress: string`, `Password: string`, `GivenName: string`, `MiddleName: string`, `FamilyName: string` | +| `Estate.AssignOperatorRequest` | `OperatorId: Guid` | +| `CreateContractRequest` | `OperatorId: Guid`, `Description: string` | +| `AddProductToContractRequest` | `DisplayText: string`, `ProductName: string`, `Value: decimal?`, `ProductType: ProductType` | +| `AddTransactionFeeForProductToContractRequest` | `CalculationType: CalculationType`, `Description: string`, `FeeType: FeeType`, `Value: decimal` | +| `CreateFloatRequest` | `FloatId: Guid`, `CreateDateTime: DateTime` | +| `RecordFloatCreditPurchaseRequest` | `FloatId: Guid`, `PurchaseDateTime: DateTime`, `CreditAmount: decimal`, `CostPrice: decimal` | +| `CreateMerchantRequest` | `Address: Address`, `Contact: Contact`, `Name: string`, `SettlementSchedule: SettlementSchedule`, `MerchantId: Guid?`, `CreatedDateTime: DateTime?` | +| `Address` | `AddressLine1: string`, `AddressLine2: string`, `AddressLine3: string`, `AddressLine4: string`, `Country: string`, `PostalCode: string`, `Region: string`, `Town: string` | +| `Contact` | `ContactName: string`, `EmailAddress: string`, `PhoneNumber: string` | +| `UpdateMerchantRequest` | `Name: string`, `SettlementSchedule: SettlementSchedule` | +| `MerchantOpeningRequest` | `Sunday`, `Monday`, `Tuesday`, `Wednesday`, `Thursday`, `Friday`, `Saturday`: each `OpeningHours` (`Opening: string`, `Closing: string`) | +| `AddMerchantContractRequest` | `ContractId: Guid` | +| `Merchant.AssignOperatorRequest` | `MerchantNumber: string`, `OperatorId: Guid`, `TerminalNumber: string` | +| `AddMerchantDeviceRequest` | `DeviceIdentifier: string` | +| `SwapMerchantDeviceRequest` | `NewDeviceIdentifier: string` | +| `CreateMerchantScheduleRequest` | `Year: int`, `Months: MerchantScheduleMonthRequest[]` | +| `UpdateMerchantScheduleRequest` | `Months: MerchantScheduleMonthRequest[]` | +| `MerchantScheduleMonthRequest` | `Month: int`, `ClosedDays: int[]` | +| `MakeMerchantDepositRequest` | `Amount: decimal`, `DepositDateTime: DateTime`, `Reference: string` | +| `MakeMerchantWithdrawalRequest` | `Amount: decimal`, `WithdrawalDateTime: DateTime`, `Reference: string` | +| `GenerateMerchantStatementRequest` | `MerchantStatementDate: DateTime` | +| `CreateOperatorRequest` | `Name: string`, `OperatorId: Guid`, `RequireCustomMerchantNumber: bool?`, `RequireCustomTerminalNumber: bool?` | +| `UpdateOperatorRequest` | `Name: string`, `RequireCustomMerchantNumber: bool?`, `RequireCustomTerminalNumber: bool?` | +| `LogonTransactionRequest` | Inherited `TransactionRequest` fields plus `DeviceIdentifier: string`, `TransactionNumber: string`; it redeclares the base `TransactionType: string` property | +| `SaleTransactionRequest` | Inherited `TransactionRequest` fields plus `AdditionalTransactionMetadata: Dictionary`, `ContractId: Guid`, `CustomerEmailAddress: string`, `DeviceIdentifier: string`, `OperatorId: Guid`, `ProductId: Guid`, `TransactionNumber: string`, `TransactionSource: int?` | +| `ReconciliationRequest` | Inherited `TransactionRequest` fields plus `DeviceIdentifier: string`, `OperatorTotals: OperatorTotalRequest[]`, `TransactionCount: int`, `TransactionValue: decimal` | +| `TransactionRequest` (base) | `EstateId: Guid`, `MerchantId: Guid`, `TransactionDateTime: DateTime`, `TransactionType: string` | +| `OperatorTotalRequest` | `ContractId: Guid`, `OperatorId: Guid`, `TransactionCount: int`, `TransactionValue: decimal` | +| `RedeemVoucherRequest` | `VoucherCode: string`, `EstateId: Guid`, `RedeemedDateTime: DateTime?` | + +## Principal response DTOs + +The following fields describe the DTO types explicitly returned by handlers or model conversion. Collections are JSON arrays; nested DTOs are described in this table as well. + +| DTO | Fields | +|---|---| +| `EstateResponse` | `EstateId`, `EstateReportingId`, `EstateName`, `EstateReference`, `Operators: EstateOperatorResponse[]`, `SecurityUsers: SecurityUserResponse[]` | +| `EstateOperatorResponse` | `Name`, `OperatorId`, `RequireCustomMerchantNumber`, `RequireCustomTerminalNumber`, `IsDeleted` | +| `SecurityUserResponse` | `EmailAddress`, `SecurityUserId` | +| `ContractResponse` | `ContractId`, `ContractReportingId`, `Description`, `EstateId`, `EstateReportingId`, `OperatorId`, `OperatorName`, `Products: ContractProduct[]` | +| `ContractProduct` | `DisplayText`, `Name`, `ProductId`, `ProductReportingId`, `TransactionFees: ContractProductTransactionFee[]`, `Value`, `ProductType` | +| `ContractProductTransactionFee` | `CalculationType`, `FeeType`, `Description`, `TransactionFeeId`, `TransactionFeeReportingId`, `Value` | +| `MerchantResponse` | `Addresses: AddressResponse[]`, `Contacts: ContactResponse[]`, `Devices: Dictionary`, `EstateId`, `EstateReportingId`, `MerchantId`, `MerchantReportingId`, `MerchantName`, `MerchantReference`, `NextStatementDate`, `Operators: MerchantOperatorResponse[]`, `SettlementSchedule`, `Contracts: MerchantContractResponse[]`, `OpeningHours`, `Schedules: MerchantScheduleResponse[]` | +| `AddressResponse` | `AddressId`, `AddressLine1`, `AddressLine2`, `AddressLine3`, `AddressLine4`, `Country`, `PostalCode`, `Region`, `Town` | +| `ContactResponse` | `ContactEmailAddress`, `ContactId`, `ContactName`, `ContactPhoneNumber` | +| `MerchantOperatorResponse` | `Name`, `OperatorId`, `MerchantNumber`, `TerminalNumber`, `IsDeleted` | +| `MerchantContractResponse` | `ContractId`, `IsDeleted`, `ContractProducts: Guid[]` | +| `MerchantScheduleResponse` | `Year`, `Months: MerchantScheduleMonthResponse[]` | +| `MerchantScheduleMonthResponse` | `Month`, `ClosedDays: int[]` | +| `OperatorResponse` | `Name`, `OperatorId`, `RequireCustomMerchantNumber`, `RequireCustomTerminalNumber` | +| `MerchantBalanceResponse` | `EstateId`, `MerchantId`, `AvailableBalance`, `Balance` | +| `MerchantBalanceChangedEntryResponse` | `OriginalEventId`, `EstateId`, `MerchantId`, `ChangeAmount`, `DateTime`, `Reference`, `DebitOrCredit`, `Balance` | +| `SettlementResponse` (root DTO, pending endpoint) | `EstateId`, `MerchantId`, `SettlementDate`, `NumberOfFeesPendingSettlement`, `NumberOfFeesSettled`, `SettlementCompleted` | +| `Responses.Settlement.SettlementResponse` | `IsCompleted`, `NumberOfFeesSettled`, `SettlementDate`, `SettlementFees: SettlementFeeResponse[]`, `SettlementId`, `ValueOfFeesSettled` | +| `SettlementFeeResponse` | `CalculatedValue`, `FeeDescription`, `IsSettled`, `MerchantId`, `MerchantName`, `SettlementDate`, `SettlementId`, `TransactionId`, `OperatorIdentifier` | +| `TransactionResponse` (base) | `EstateId`, `MerchantId`, `ResponseCode`, `ResponseMessage`, `TransactionId`, `TransactionType` | +| `LogonTransactionResponse` | Inherits `TransactionResponse` | +| `SaleTransactionResponse` | Inherits `TransactionResponse`, plus `AdditionalTransactionMetadata: Dictionary` | +| `ReconciliationResponse` | Inherits `TransactionResponse` | +| `GetVoucherResponse` | `VoucherId`, `ExpiryDate`, `Value`, `Balance`, `VoucherCode`, `IsGenerated`, `IsIssued`, `IsRedeemed`, `IssuedDateTime`, `GeneratedDateTime`, `RedeemedDateTime`, `TransactionId` | +| `RedeemVoucherResponse` | `ExpiryDate`, `VoucherCode`, `RemainingBalance` | + +## Contract enums + +`ProductType`, `CalculationType`, and `FeeType` are enum properties in the DTOs. The project does not configure an enum-to-string JSON converter in `JsonSerializerConfiguration`; confirm the deployed serializer defaults before sending enum values as strings. Merchant `SettlementSchedule` and day-of-week keys are likewise enum-backed. + +## Source and known contract caveats + +- Minimal API handlers infer body binding for their DTO parameters and query binding for unmatched scalar parameters. Only handler parameters explicitly present in the route are path values. +- The settlement GET-by-ID route handler accepts `merchantId` in addition to `estateId` and `settlementId`; it is not part of the route template and is therefore a query parameter. +- The primary transaction receipt route similarly binds `estateId` as a query parameter because it is omitted from that route. A legacy route carries both IDs in its path. +- `DomainEventController.GetDomainEvent` expects `eventType` in a header and the body to serialize as the dynamically selected domain-event type. Its invalid/unrecognized-event paths are not represented as a stable response DTO. +- Some command success bodies are produced indirectly by `ResponseFactory` and business result types, rather than an endpoint-specific response mapping. Those are documented as result-factory responses instead of guessing a DTO. diff --git a/docs/superpowers/plans/2026-10-21-modern-startup.md b/docs/superpowers/plans/2026-10-21-modern-startup.md new file mode 100644 index 00000000..e1ce8b13 --- /dev/null +++ b/docs/superpowers/plans/2026-10-21-modern-startup.md @@ -0,0 +1,75 @@ +# Modern Startup Configuration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate TransactionProcessor to .NET 10 minimal hosting and remove its `Startup`-based bootstrapping. + +**Architecture:** `Program` owns builder, registrations, pipeline, endpoints, and lifecycle. Configuration/environment are injected, Lamar is the single host provider, and hosted services resolve dependencies normally. + +**Tech Stack:** .NET 10, ASP.NET Core minimal hosting, Lamar, xUnit, `WebApplicationFactory`. + +**Spec:** `docs/superpowers/specs/2026-10-21-modern-startup-design.md` + +## Global Constraints + +- Preserve existing configuration sources, order, reload flags, and version override. +- Preserve Windows service, NLog, Sentry, Kestrel, middleware, endpoint, health, metrics, and prewarm behavior. +- Use one DI container and eliminate app runtime dependencies on static `Startup` state. +- Keep IntegrationTest and in-memory database selection working. + +## Review Focus + +- Configuration source precedence and reload behavior: add a host-level configuration assertion for overlapping values and reload-enabled JSON sources. +- IntegrationTest environment database choice: assert the registry selects the in-memory database for that environment. +- Development Sentry decision: assert Sentry remains disabled in Development and configured otherwise when a section exists. +- Named event-handler resolution: assert subscription ID chooses the matching named resolver. +- Hosted worker dependency resolution and cancellation: update focused worker tests to construct/host the worker with its dependencies. + +--- + +### Task 1: Host configuration and service registration + +**Files:** +- Modify: `TransactionProcessor/Program.cs` +- Modify: `TransactionProcessor/Bootstrapper/RepositoryRegistry.cs` +- Modify: `TransactionProcessor/Bootstrapper/MiddlewareRegistry.cs` +- Modify: `TransactionProcessor/Bootstrapper/DomainEventHandlerRegistry.cs` +- Modify: `TransactionProcessor/Bootstrapper/OperatorRegistry.cs` +- Modify tests that construct the registries directly. + +**Interfaces:** +- `Program.BuildApplication(string[] args)` returns a configured `WebApplication` for test hosting. +- Registries receive `IConfiguration` and `IWebHostEnvironment` as needed; no registry reads `Startup` globals. + +- [ ] Configure `WebApplication.CreateBuilder`, source ordering, host settings, logging, Sentry, Lamar, registrations, and hosted workers. +- [ ] Move all registry configuration to injected host configuration/environment and remove manual `BuildServiceProvider`. +- [ ] Update focused registry tests to provide explicit host context. + +### Task 2: Application pipeline and runtime dependencies + +**Files:** +- Modify: `TransactionProcessor/Program.cs` +- Modify: `TransactionProcessor/Extensions.cs` +- Modify: `TransactionProcessor/Controllers/DomainEventController.cs` +- Modify: `TransactionProcessor/AutoLogonWorkerService.cs` or its current source file. +- Modify: dependent tests. + +**Interfaces:** +- Prewarm resolves registrations from the app's provider. +- `AutoLogonWorkerService` receives operator resolution through constructor injection. +- Controller receives `IServiceProvider` for named resolver selection. + +- [ ] Move logging/serialization/type initialization, middleware, route mapping, prewarm, and run lifecycle to the WebApplication pipeline. +- [ ] Replace static runtime container/config/service-provider access with injected services. +- [ ] Preserve middleware order, route mappings, worker lifecycle, and cancellation semantics. + +### Task 3: Test host and legacy bootstrap removal + +**Files:** +- Modify: `TransactionProcessor.Tests/Common/TransactionProcessorWebFactory.cs` +- Modify: controller and bootstrapper tests under `TransactionProcessor.Tests` and `TransactionProcessor.BusinessLogic.Tests`. +- Modify: `TransactionProcessor/Startup.cs` (remove) and generated XML documentation if maintained. + +- [ ] Switch integration test factory to `WebApplicationFactory` and preserve mocked mediator/controller behavior. +- [ ] Replace direct `Startup` construction and static global setup in unit tests. +- [ ] Remove `Startup` and all stale references/documentation. diff --git a/docs/superpowers/specs/2026-10-21-modern-startup-design.md b/docs/superpowers/specs/2026-10-21-modern-startup-design.md new file mode 100644 index 00000000..62e99c1b --- /dev/null +++ b/docs/superpowers/specs/2026-10-21-modern-startup-design.md @@ -0,0 +1,30 @@ +# Modern Startup Configuration Design + +## Goal + +Move TransactionProcessor's web host to the .NET 10 minimal hosting model while preserving configuration precedence, service registrations, request behavior, hosted services, and deployment behavior. + +## Existing constraints + +- Target framework is `net10.0`. +- Configuration files are loaded from the application directory and `/home/txnproc/config`; environment variables and the version override are applied afterward. +- Lamar registries provide application registrations and must share the host's single service provider. +- Sentry is configured from the built configuration and remains disabled in Development. +- Middleware, endpoint order, Windows service support, NLog, Kestrel, and host settings remain compatible. +- Existing static `Startup` state has consumers in registries, extensions, a controller, and tests; replace runtime access with host services or explicit dependencies. + +## Design + +`Program.Main` will use `WebApplication.CreateBuilder(args)` and configure host services, custom configuration sources, logging, Windows service support, Lamar, and Sentry before `Build()`. Registrations will receive `IConfiguration` and `IWebHostEnvironment` from DI rather than global `Startup` properties. Lamar registry constructors will accept the required host context through constructor injection or explicit registration callbacks, and the host container will be the only provider. + +After building, `Program` will initialize shared logging/serialization/type infrastructure, configure middleware in the current order, map controllers and health/custom endpoints, and run prewarming with the application service provider. Hosted workers will receive dependencies through constructors. Existing APIs that need an alternate named domain-event resolver will resolve it from the host provider. + +Tests will use `WebApplicationFactory` and test-host service overrides. Focused unit tests will construct service registrations with explicit configuration and environment rather than assigning `Startup` globals. The obsolete `Startup` class and XML documentation entries will be removed after all references migrate. + +## Compatibility checks + +- Verify the effective configuration precedence and reload behavior. +- Verify Development and non-Development Sentry decisions. +- Verify Windows service, Kestrel, NLog, and hosted worker wiring. +- Verify route, authentication, authorization, health, metrics, prewarm, and shutdown behavior. +- Verify IntegrationTest/in-memory database selection. diff --git a/openapi-runtime.json b/openapi-runtime.json new file mode 100644 index 00000000..e69de29b From cbb0a39c9b7f8bf0d21819c9ad4a732a5470c968 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 19:40:14 +0100 Subject: [PATCH 2/5] Fix API documentation review findings --- .../OpenApi/DtoDocumentationTransformer.cs | 290 +++++++++++------- TransactionProcessor/Program.cs | 4 + .../TransactionProcessor.csproj | 21 +- 3 files changed, 193 insertions(+), 122 deletions(-) diff --git a/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs b/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs index 680f8daa..2ac82345 100644 --- a/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs +++ b/TransactionProcessor/OpenApi/DtoDocumentationTransformer.cs @@ -25,105 +25,148 @@ public sealed class DtoDocumentationTransformer : IOpenApiDocumentTransformer public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + if (!TryLoadDocumentationMembers(out Dictionary members)) + { + return Task.CompletedTask; + } + + DocumentSchemas(document, members); + DocumentOperations(document); + AddDomainEventExample(document); + + return Task.CompletedTask; + } + + private static bool TryLoadDocumentationMembers(out Dictionary members) { string documentationPath = Path.Combine(AppContext.BaseDirectory, DocumentationFileName); if (!File.Exists(documentationPath)) { - return Task.CompletedTask; + members = new Dictionary(StringComparer.Ordinal); + return false; } - Dictionary members = XDocument.Load(documentationPath) - .Root? - .Element("members")? - .Elements("member") - .Where(member => member.Attribute("name") is not null) - .ToDictionary(member => member.Attribute("name")!.Value, - member => member, - StringComparer.Ordinal) + members = XDocument.Load(documentationPath) + .Root? + .Element("members")? + .Elements("member") + .Where(member => member.Attribute("name") is not null) + .ToDictionary(member => member.Attribute("name")!.Value, + member => member, + StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + return true; + } - IDictionary componentSchemas = document.Components?.Schemas ?? + private static void DocumentSchemas(OpenApiDocument document, IReadOnlyDictionary members) + { + IDictionary schemas = document.Components?.Schemas ?? new Dictionary(StringComparer.Ordinal); - foreach (KeyValuePair schemaEntry in componentSchemas) + foreach (KeyValuePair schemaEntry in schemas) { - if (schemaEntry.Value is not OpenApiSchema schema) + if (schemaEntry.Value is OpenApiSchema schema) { - continue; + DocumentSchema(schemaEntry.Key, schema, members); } + } + } - schema.Properties ??= new Dictionary(StringComparer.Ordinal); - if (string.Equals(schemaEntry.Key, "ProblemDetails", StringComparison.Ordinal)) - { - schema.Description ??= "RFC 9457 problem details returned when a request fails."; - schema.Examples ??= new List(); - if (schema.Examples.Count == 0 && ReadExample("""{"type":"about:blank","title":"An error occurred.","detail":"The request could not be completed."}""") is JsonNode problemExample) - { - schema.Examples.Add(problemExample); - } + private static void DocumentSchema(string schemaName, + OpenApiSchema schema, + IReadOnlyDictionary members) + { + schema.Properties ??= new Dictionary(StringComparer.Ordinal); + if (string.Equals(schemaName, "ProblemDetails", StringComparison.Ordinal)) + { + DocumentProblemDetails(schema); + } - AddProblemDetailsPropertyDescriptions(schema); - } + string? typeMemberName = FindTypeMember(schemaName, schema, members); + if (typeMemberName is null) + { + return; + } - string? typeMemberName = FindTypeMember(schemaEntry.Key, schema, members); - if (typeMemberName is null) - { - continue; - } + XElement typeMember = members[typeMemberName]; + schema.Description ??= ReadDocumentationText(typeMember.Element("summary")); + schema.Examples ??= new List(); + if (schema.Examples.Count == 0 && ReadExample(typeMember.Element("example")) is JsonNode example) + { + schema.Examples.Add(NormalizeExample(example, schema)); + } - XElement typeMember = members[typeMemberName]; - schema.Description ??= ReadDocumentationText(typeMember.Element("summary")); - schema.Examples ??= new List(); - if (schema.Examples.Count == 0 && ReadExample(typeMember.Element("example")) is JsonNode example) - { - schema.Examples.Add(NormalizeExample(example, schema)); - } + DocumentSchemaProperties(schema, typeMemberName, members); + } + + private static void DocumentProblemDetails(OpenApiSchema schema) + { + schema.Description ??= "RFC 9457 problem details returned when a request fails."; + schema.Examples ??= new List(); + if (schema.Examples.Count == 0 && ReadExample("""{"type":"about:blank","title":"An error occurred.","detail":"The request could not be completed."}""") is JsonNode example) + { + schema.Examples.Add(example); + } - foreach (string documentedTypeMemberName in GetTypeMemberHierarchy(typeMemberName, members)) + AddProblemDetailsPropertyDescriptions(schema); + } + + private static void DocumentSchemaProperties(OpenApiSchema schema, + string typeMemberName, + IReadOnlyDictionary members) + { + foreach (string documentedTypeMemberName in GetTypeMemberHierarchy(typeMemberName, members)) + { + string memberPrefix = $"P:{documentedTypeMemberName[2..]}."; + foreach (KeyValuePair propertyEntry in schema.Properties.ToArray()) { - string memberPrefix = $"P:{documentedTypeMemberName[2..]}."; - foreach (KeyValuePair propertyEntry in schema.Properties.ToArray()) + string propertyName = ToPascalCase(propertyEntry.Key); + if (members.TryGetValue(memberPrefix + propertyName, out XElement? propertyMember)) { - string propertyName = ToPascalCase(propertyEntry.Key); - if (members.TryGetValue(memberPrefix + propertyName, out XElement? propertyMember)) - { - schema.Properties[propertyEntry.Key] = WithDescription( - propertyEntry.Value, - ReadDocumentationText(propertyMember.Element("summary"))); - } + schema.Properties[propertyEntry.Key] = WithDescription( + propertyEntry.Value, + ReadDocumentationText(propertyMember.Element("summary"))); } } - } + } + private static void DocumentOperations(OpenApiDocument document) + { foreach (KeyValuePair pathEntry in document.Paths ?? new Dictionary(StringComparer.Ordinal)) { - IOpenApiPathItem path = pathEntry.Value; - foreach (OpenApiOperation operation in path.Operations?.Values ?? Enumerable.Empty()) + foreach (OpenApiOperation operation in pathEntry.Value.Operations?.Values ?? Enumerable.Empty()) { AddPathParameterDescriptions(pathEntry.Key, operation); - - if (operation.RequestBody is OpenApiRequestBody requestBody) - { - foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) - { - mediaType.Example ??= GetExample(mediaType.Schema); - } - } - - foreach (IOpenApiResponse response in operation.Responses?.Values ?? Enumerable.Empty()) - { - foreach (OpenApiMediaType mediaType in response.Content?.Values ?? Enumerable.Empty()) - { - mediaType.Example ??= GetExample(mediaType.Schema); - } - } + AddRequestExamples(operation); + AddResponseExamples(operation); } } + } - AddDomainEventExample(document); + private static void AddRequestExamples(OpenApiOperation operation) + { + if (operation.RequestBody is not OpenApiRequestBody requestBody) + { + return; + } - return Task.CompletedTask; + foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= GetExample(mediaType.Schema); + } + } + + private static void AddResponseExamples(OpenApiOperation operation) + { + foreach (IOpenApiResponse response in operation.Responses?.Values ?? Enumerable.Empty()) + { + foreach (OpenApiMediaType mediaType in response.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= GetExample(mediaType.Schema); + } + } } private static void AddPathParameterDescriptions(string route, OpenApiOperation operation) @@ -166,6 +209,22 @@ private static void AddPathParameterDescriptions(string route, OpenApiOperation } private static void AddDomainEventExample(OpenApiDocument document) + { + if (!TryGetDomainEventRequestBody(document, out OpenApiRequestBody? requestBody)) + { + return; + } + + string? example = ReadDomainEventRequestExample(); + JsonNode? parsedExample = ReadExample(example); + foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) + { + mediaType.Example ??= parsedExample?.DeepClone(); + } + } + + private static bool TryGetDomainEventRequestBody(OpenApiDocument document, + out OpenApiRequestBody? requestBody) { string documentationPath = Path.Combine(AppContext.BaseDirectory, "TransactionProcessor.xml"); if (!File.Exists(documentationPath) || @@ -173,31 +232,28 @@ document.Paths is null || !document.Paths.TryGetValue("/api/domainevents", out IOpenApiPathItem? path) || path.Operations is null || !path.Operations.TryGetValue(System.Net.Http.HttpMethod.Post, out OpenApiOperation? operation) || - operation.RequestBody is not OpenApiRequestBody requestBody) + operation.RequestBody is not OpenApiRequestBody foundRequestBody) { - return; + requestBody = null; + return false; } - XElement? parameterDocumentation = XDocument.Load(documentationPath) - .Root? - .Element("members")? - .Elements("member") - .Where(member => member.Attribute("name")?.Value.StartsWith( - "M:TransactionProcessor.Controllers.DomainEventController.PostEventAsync(", - StringComparison.Ordinal) == true) - .SelectMany(member => member.Elements("param")) - .FirstOrDefault(parameter => parameter.Attribute("name")?.Value == "request"); - string? example = parameterDocumentation?.Attribute("example")?.Value; - if (string.IsNullOrWhiteSpace(example)) - { - return; - } + requestBody = foundRequestBody; + return true; + } - JsonNode? parsedExample = ReadExample(example); - foreach (OpenApiMediaType mediaType in requestBody.Content?.Values ?? Enumerable.Empty()) - { - mediaType.Example ??= parsedExample?.DeepClone(); - } + private static string? ReadDomainEventRequestExample() + { + string documentationPath = Path.Combine(AppContext.BaseDirectory, "TransactionProcessor.xml"); + XElement? parameterDocumentation = XDocument.Load(documentationPath).Root? + .Element("members")?.Elements("member") + .Where(member => member.Attribute("name")?.Value.StartsWith( + "M:TransactionProcessor.Controllers.DomainEventController.PostEventAsync(", + StringComparison.Ordinal) == true) + .SelectMany(member => member.Elements("param")) + .FirstOrDefault(parameter => parameter.Attribute("name")?.Value == "request"); + string? example = parameterDocumentation?.Attribute("example")?.Value; + return string.IsNullOrWhiteSpace(example) ? null : example; } private static string? FindTypeMember(string schemaName, @@ -260,40 +316,46 @@ private static JsonNode NormalizeExample(JsonNode example, IOpenApiSchema schema return example.DeepClone(); } - IDictionary properties = openApiSchema.Properties ?? - new Dictionary(StringComparer.Ordinal); - if (example is JsonObject exampleObject && properties.Count > 0) + if (example is JsonObject exampleObject && openApiSchema.Properties?.Count > 0) { - JsonObject normalizedObject = new(); - foreach (KeyValuePair property in exampleObject) - { - string schemaPropertyName = properties.Keys.FirstOrDefault(name => - string.Equals(ToPascalCase(name), ToPascalCase(property.Key), StringComparison.OrdinalIgnoreCase)) - ?? property.Key; - JsonNode? value = property.Value; - normalizedObject[schemaPropertyName] = value is not null && - properties.TryGetValue(schemaPropertyName, - out IOpenApiSchema? propertySchema) - ? NormalizeExample(value, propertySchema) - : value?.DeepClone(); - } + return NormalizeObjectExample(exampleObject, openApiSchema.Properties); + } - return normalizedObject; + if (example is JsonArray exampleArray && openApiSchema.Items is not null) + { + return NormalizeArrayExample(exampleArray, openApiSchema.Items); } - IOpenApiSchema? itemsSchema = openApiSchema.Items; - if (example is JsonArray exampleArray && itemsSchema is not null) + return example.DeepClone(); + } + + private static JsonObject NormalizeObjectExample(JsonObject example, + IDictionary properties) + { + JsonObject normalized = new(); + foreach (KeyValuePair property in example) { - JsonArray normalizedArray = new(); - foreach (JsonNode? item in exampleArray) - { - normalizedArray.Add(item is null ? null : NormalizeExample(item, itemsSchema)); - } + string schemaName = properties.Keys.FirstOrDefault(name => + string.Equals(ToPascalCase(name), ToPascalCase(property.Key), StringComparison.OrdinalIgnoreCase)) + ?? property.Key; + JsonNode? value = property.Value; + normalized[schemaName] = value is not null && properties.TryGetValue(schemaName, out IOpenApiSchema? propertySchema) + ? NormalizeExample(value, propertySchema) + : value?.DeepClone(); + } + + return normalized; + } - return normalizedArray; + private static JsonArray NormalizeArrayExample(JsonArray example, IOpenApiSchema itemsSchema) + { + JsonArray normalized = new(); + foreach (JsonNode? item in example) + { + normalized.Add(item is null ? null : NormalizeExample(item, itemsSchema)); } - return example.DeepClone(); + return normalized; } private static IEnumerable GetTypeMemberHierarchy(string typeMemberName, diff --git a/TransactionProcessor/Program.cs b/TransactionProcessor/Program.cs index f367a4f4..c91ef077 100644 --- a/TransactionProcessor/Program.cs +++ b/TransactionProcessor/Program.cs @@ -34,6 +34,10 @@ namespace TransactionProcessor; [ExcludeFromCodeCoverage] public partial class Program { + protected Program() + { + } + public static void Main(string[] args) { BuildApplication(args).Run(); diff --git a/TransactionProcessor/TransactionProcessor.csproj b/TransactionProcessor/TransactionProcessor.csproj index 49f19665..56ed0afb 100644 --- a/TransactionProcessor/TransactionProcessor.csproj +++ b/TransactionProcessor/TransactionProcessor.csproj @@ -58,14 +58,19 @@ - - - - TransactionProcessor.DataTransferObjects.xml - PreserveNewest - PreserveNewest - - + + + <_DtoAssembly Include="@(ReferenceCopyLocalPaths)" + Condition="'%(ReferenceCopyLocalPaths.Filename)' == 'TransactionProcessor.DataTransferObjects'" /> + <_DtoXml Include="@(_DtoAssembly->'%(RootDir)%(Directory)TransactionProcessor.DataTransferObjects.xml')" /> + + + TransactionProcessor.DataTransferObjects.xml + PreserveNewest + PreserveNewest + + + From bde5800f7b647cde39605d3709bc3ca1e1fec7cb Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 19:57:25 +0100 Subject: [PATCH 3/5] Cap ImageSharp updates below license enforcement --- .github/dependabot.yml | 11 +++++++++++ Directory.Packages.props | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..042ebec4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + allow: + - dependency-name: "SixLabors.ImageSharp" + ignore: + - dependency-name: "SixLabors.ImageSharp" + versions: [">= 3.2.0"] diff --git a/Directory.Packages.props b/Directory.Packages.props index ecd38517..6fa73c7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ - + From 3414967b8c486ec3a981a50eeb4ad63a1bc34600 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 20:02:12 +0100 Subject: [PATCH 4/5] Map API defaults package to Feedz --- NuGet.Config | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGet.Config b/NuGet.Config index ee4d94d4..fe0f7878 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -16,6 +16,7 @@ + From bd75ea773e5bf30ec15a86fdcd88e4529f1e684c Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 20:04:25 +0100 Subject: [PATCH 5/5] Disable NuGet package source mapping --- NuGet.Config | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index fe0f7878..66b50e02 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -4,24 +4,6 @@ - - - - - - - - - - - - - - - - - -