diff --git a/Directory.Packages.props b/Directory.Packages.props index 90b4e97..396b601 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,8 @@ - - + + @@ -13,10 +13,10 @@ - + - + @@ -37,27 +37,22 @@ - + - - - - - - - - - - - - - - + + + + + + + + - - - + + + + diff --git a/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs b/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs index 63aa666..bf4b3b3 100644 --- a/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs +++ b/FileProcessor.BusinessLogic.Tests/FileHandlersTests.cs @@ -66,17 +66,12 @@ public async Task UploadFile_WhenEventStoreAcceptanceFails_DeletesTemporaryFile( FormFileCollection files = new() { formFile }; FormCollection formCollection = new(new Dictionary(), files); + DefaultHttpContext httpContext = new(); + httpContext.Request.Form = formCollection; await FileHandlers.UploadFile( mediator.Instance(), - new FileProcessor.DataTransferObjects.UploadFileRequest - { - FileId = TestData.FileId, - EstateId = TestData.EstateId, - MerchantId = TestData.MerchantId, - UserId = TestData.UserId, - FileProfileId = TestData.FileProfileId - }, - formCollection, + CreateUploadRequest(formFile), + httpContext.Request, CancellationToken.None); Directory.GetFiles(temporaryDirectory).ShouldBeEmpty(); @@ -107,8 +102,15 @@ public async Task UploadFile_UsesUniqueTemporaryPathForRepeatedClientFilename() IMediatorImposter mediator = new(); mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success()); - await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(), CreateFormCollection(), CancellationToken.None); - await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(), CreateFormCollection(), CancellationToken.None); + FormCollection firstForm = CreateFormCollection(); + DefaultHttpContext firstContext = new(); + firstContext.Request.Form = firstForm; + await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(firstForm.Files[0]), firstContext.Request, CancellationToken.None); + + FormCollection secondForm = CreateFormCollection(); + DefaultHttpContext secondContext = new(); + secondContext.Request.Form = secondForm; + await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(secondForm.Files[0]), secondContext.Request, CancellationToken.None); Directory.GetFiles(temporaryDirectory).Length.ShouldBe(2); } @@ -138,7 +140,10 @@ public async Task UploadFile_DoesNotUseClientFilenameCharactersInTemporaryPath() IMediatorImposter mediator = new(); mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success()); - await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(), CreateFormCollection("invoice:payload.csv"), CancellationToken.None); + FormCollection formCollection = CreateFormCollection("invoice:payload.csv"); + DefaultHttpContext httpContext = new(); + httpContext.Request.Form = formCollection; + await FileHandlers.UploadFile(mediator.Instance(), CreateUploadRequest(formCollection.Files[0]), httpContext.Request, CancellationToken.None); string storedFileName = Path.GetFileName(Directory.GetFiles(temporaryDirectory).Single()); storedFileName.ShouldNotContain(":"); @@ -151,13 +156,14 @@ public async Task UploadFile_DoesNotUseClientFilenameCharactersInTemporaryPath() } } - private static FileProcessor.DataTransferObjects.UploadFileRequest CreateUploadRequest() => new() + private static UploadFileForm CreateUploadRequest(IFormFile file) => new() { FileId = TestData.FileId, EstateId = TestData.EstateId, MerchantId = TestData.MerchantId, UserId = TestData.UserId, - FileProfileId = TestData.FileProfileId + FileProfileId = TestData.FileProfileId, + File = file }; private static FormCollection CreateFormCollection(string fileName = "source.csv") diff --git a/FileProcessor.BusinessLogic.Tests/MediatorTests.cs b/FileProcessor.BusinessLogic.Tests/MediatorTests.cs index a79c670..962e11d 100644 --- a/FileProcessor.BusinessLogic.Tests/MediatorTests.cs +++ b/FileProcessor.BusinessLogic.Tests/MediatorTests.cs @@ -11,6 +11,8 @@ using System.Text; using System.Threading.Tasks; using FileProcessor.BusinessLogic.Managers; +using FileProcessor.Bootstrapper; +using FileProcessor.Common; using Xunit; namespace FileProcessor.BusinessLogic.Tests @@ -20,6 +22,7 @@ namespace FileProcessor.BusinessLogic.Tests using Services; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; + using Shared.General; public class MediatorTests { @@ -44,14 +47,24 @@ 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(); - - this.AddTestRegistrations(services, hostingEnvironment.Instance()); - s.ConfigureContainer(services); + IConfigurationRoot configuration = this.SetupMemoryConfiguration(); + ConfigurationReader.Initialise(configuration); + + this.AddTestRegistrations(services, hostingEnvironment.Instance(), configuration); + TypeProvider.LoadDomainEventsTypeDynamically(); + services.IncludeRegistry(); + services.IncludeRegistry(new DomainEventHandlerRegistry(configuration)); + services.IncludeRegistry(new RepositoryRegistry(configuration, hostingEnvironment.Instance())); + services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(); + + using Container container = new Container(services); List errors = new List(); - IMediator mediator = Startup.Container.GetService(); + IMediator mediator = container.GetService(); foreach (IBaseRequest baseRequest in this.Requests) { try @@ -95,7 +108,8 @@ private IConfigurationRoot SetupMemoryConfiguration() } private void AddTestRegistrations(ServiceRegistry services, - IWebHostEnvironment hostingEnvironment) + IWebHostEnvironment hostingEnvironment, + IConfiguration configuration) { services.AddLogging(); DiagnosticListener diagnosticSource = new DiagnosticListener(hostingEnvironment.ApplicationName); @@ -103,7 +117,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/FileProcessor.BusinessLogic/Services/FileProfileVersionMigrationService.cs b/FileProcessor.BusinessLogic/Services/FileProfileVersionMigrationService.cs index 7a12b66..5c11d0e 100644 --- a/FileProcessor.BusinessLogic/Services/FileProfileVersionMigrationService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProfileVersionMigrationService.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.ComponentModel; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -125,17 +126,31 @@ private Dictionary GetInitialVersions(List profile private sealed record ExistingFile(Guid FileId, Guid FileProfileId); } +/// Summary of the file-profile version migration. +[Description("Summary of the file-profile version migration.")] public sealed class FileProfileVersionMigrationReport { + /// Gets the number of profiles checked. + [Description("Number of profiles checked.")] public Int32 ProfilesChecked { get; set; } + /// Gets the number of profiles that received an initial version. + [Description("Number of profiles that received an initial version.")] public Int32 ProfilesWithInitialVersion { get; set; } + /// Gets the number of files checked. + [Description("Number of files checked.")] public Int32 FilesChecked { get; set; } + /// Gets the number of files assigned to a profile version. + [Description("Number of files assigned to a profile version.")] public Int32 FilesAssigned { get; set; } + /// Gets the number of files skipped by the migration. + [Description("Number of files skipped by the migration.")] public Int32 FilesSkipped { get; set; } + /// Gets the migration failure messages. + [Description("Migration failure messages, if any.")] public List Failures { get; } = new(); } diff --git a/FileProcessor.DataTransferObjects/FileProcessor.DataTransferObjects.csproj b/FileProcessor.DataTransferObjects/FileProcessor.DataTransferObjects.csproj index 89657bc..87ece4d 100644 --- a/FileProcessor.DataTransferObjects/FileProcessor.DataTransferObjects.csproj +++ b/FileProcessor.DataTransferObjects/FileProcessor.DataTransferObjects.csproj @@ -3,6 +3,7 @@ net10.0 None + true diff --git a/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs b/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs index 53639c6..4cb3b23 100644 --- a/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs +++ b/FileProcessor.DataTransferObjects/Requests/CreateFileProfileRequest.cs @@ -1,20 +1,37 @@ -using System; +using System; +using System.ComponentModel; namespace FileProcessor.DataTransferObjects.Requests; +/// Configuration for creating a file profile. +[Description("Configuration for creating a file profile.")] public class CreateFileProfileRequest { + /// Gets or sets the identifier to assign to the new profile. + [Description("The identifier to assign to the new profile.")] public Guid FileProfileId { get; set; } + /// Gets or sets the profile's display name. + [Description("The profile's display name.")] public string Name { get; set; } + /// Gets or sets the directory from which files for this profile are collected. + [Description("The directory from which files for this profile are collected.")] public string ListeningDirectory { get; set; } + /// Gets or sets the request or transaction type associated with files using this profile. + [Description("The request or transaction type associated with files using this profile.")] public string RequestType { get; set; } + /// Gets or sets the operator name recorded for the profile. + [Description("The operator name recorded for the profile.")] public string OperatorName { get; set; } + /// Gets or sets the required line ending used to separate records. + [Description("The required line ending used to separate records.")] public LineTerminatorType? LineTerminator { get; set; } + /// Gets or sets the registered file format handler used to parse files for this profile. + [Description("The registered file format handler used to parse files for this profile.")] public string FileFormatHandler { get; set; } } diff --git a/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs b/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs index 5805298..2ff0af7 100644 --- a/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs +++ b/FileProcessor.DataTransferObjects/Requests/LineTerminatorType.cs @@ -1,14 +1,23 @@ -using System; +using System; +using System.ComponentModel; using System.Text.Json.Serialization; namespace FileProcessor.DataTransferObjects.Requests; +/// Line ending values accepted by file profile requests. +[Description("Line ending values accepted by file profile requests.")] [JsonConverter(typeof(JsonStringEnumConverter))] public enum LineTerminatorType { + /// Line feed (LF, `\n`). + [Description("Line feed (LF, `\\n`).")] LineFeed = 1, + /// Carriage return followed by line feed (CRLF, `\r\n`). + [Description("Carriage return followed by line feed (CRLF, `\\r\\n`).")] CarriageReturnLineFeed = 2, + /// Carriage return (CR, `\r`). + [Description("Carriage return (CR, `\\r`).")] CarriageReturn = 3 } diff --git a/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs b/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs index c09527c..1cbe488 100644 --- a/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs +++ b/FileProcessor.DataTransferObjects/Requests/UpdateFileProfileRequest.cs @@ -1,16 +1,31 @@ +using System.ComponentModel; namespace FileProcessor.DataTransferObjects.Requests; +/// Configuration values to apply when updating a file profile. +[Description("Configuration values to apply when updating a file profile.")] public class UpdateFileProfileRequest { + /// Gets or sets the profile's display name. + [Description("The profile's display name.")] public string Name { get; set; } + /// Gets or sets the directory from which files for this profile are collected. + [Description("The directory from which files for this profile are collected.")] public string ListeningDirectory { get; set; } + /// Gets or sets the request or transaction type associated with files using this profile. + [Description("The request or transaction type associated with files using this profile.")] public string RequestType { get; set; } + /// Gets or sets the operator name recorded for the profile. + [Description("The operator name recorded for the profile.")] public string OperatorName { get; set; } + /// Gets or sets the line ending used to separate records. When omitted, the profile's current value is retained. + [Description("The line ending used to separate records. When omitted, the profile's current value is retained.")] public LineTerminatorType? LineTerminator { get; set; } + /// Gets or sets the registered file format handler used to parse files for this profile. + [Description("The registered file format handler used to parse files for this profile.")] public string FileFormatHandler { get; set; } } diff --git a/FileProcessor.DataTransferObjects/Requests/UploadFileRequest.cs b/FileProcessor.DataTransferObjects/Requests/UploadFileRequest.cs index 354462a..3611d92 100644 --- a/FileProcessor.DataTransferObjects/Requests/UploadFileRequest.cs +++ b/FileProcessor.DataTransferObjects/Requests/UploadFileRequest.cs @@ -1,22 +1,34 @@ using System; +using System.ComponentModel; namespace FileProcessor.DataTransferObjects { - /// - /// - /// + /// Metadata submitted with a file upload. + [Description("Metadata submitted with a file upload.")] public class UploadFileRequest { + /// Gets or sets the caller-supplied identifier for the file being uploaded. + [Description("The caller-supplied identifier for the file being uploaded.")] public Guid FileId { get; set; } + /// Gets or sets the identifier of the estate that owns the file. + [Description("The identifier of the estate that owns the file.")] public Guid EstateId { get; set; } + /// Gets or sets the identifier of the merchant associated with the file. + [Description("The identifier of the merchant associated with the file.")] public Guid MerchantId { get; set; } + /// Gets or sets the identifier of the user submitting the file. + [Description("The identifier of the user submitting the file.")] public Guid UserId { get; set; } + /// Gets or sets the identifier of the file profile used to process the file. + [Description("The identifier of the file profile used to process the file.")] public Guid FileProfileId { get; set; } + /// Gets or sets the time the file was uploaded. If omitted or set to the minimum date value, the server uses the current local time. + [Description("The time the file was uploaded. If omitted or set to the minimum date value, the server uses the current local time.")] public DateTime UploadDateTime { get; set; } } } diff --git a/FileProcessor.DataTransferObjects/Responses/FileDetails.cs b/FileProcessor.DataTransferObjects/Responses/FileDetails.cs index 3063652..ae858eb 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileDetails.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileDetails.cs @@ -1,46 +1,87 @@ namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; using System.Collections.Generic; + /// File metadata, processing state, line results, and summary returned by file queries. + [Description("File metadata, processing state, line results, and summary returned by file queries.")] public class FileDetails { + /// Gets or sets the file identifier. + [Description("The file identifier.")] public Guid FileId { get; set; } + /// Gets or sets whether processing of the file has completed. + [Description("Whether processing of the file has completed.")] public Boolean ProcessingCompleted { get; set; } + /// Gets or sets whether the file was rejected before or during processing. + [Description("Whether the file was rejected before or during processing.")] public Boolean IsRejected { get; set; } + /// Gets or sets the machine-readable reason code when the file was rejected. + [Description("The machine-readable reason code when the file was rejected.")] public String RejectionCode { get; set; } + /// Gets or sets the human-readable reason when the file was rejected. + [Description("The human-readable reason when the file was rejected.")] public String RejectionReason { get; set; } + /// Gets or sets the number of detail records observed in the uploaded file. + [Description("The number of detail records observed in the uploaded file.")] public Int32 ObservedDetailRecordCount { get; set; } + /// Gets or sets the configured maximum number of detail records allowed. + [Description("The configured maximum number of detail records allowed.")] public Int32 MaximumDetailRecordCount { get; set; } + /// Gets or sets the identifier of the owning estate. + [Description("The identifier of the owning estate.")] public Guid EstateId { get; set; } + /// Gets or sets the identifier of the uploading user. + [Description("The identifier of the uploading user.")] public Guid UserId { get; set; } + /// Gets or sets the email address of the uploading user, when available. + [Description("The email address of the uploading user, when available.")] public String UserEmailAddress { get; set; } + /// Gets or sets the identifier of the associated merchant. + [Description("The identifier of the associated merchant.")] public Guid MerchantId { get; set; } + /// Gets or sets the name of the associated merchant. + [Description("The name of the associated merchant.")] public String MerchantName { get; set; } + /// Gets or sets the identifier of the file profile used to process the file. + [Description("The identifier of the file profile used to process the file.")] public Guid FileProfileId { get; set; } + /// Gets or sets the identifier of the file profile version used to process the file. + [Description("The identifier of the file profile version used to process the file.")] public Guid FileProfileVersionId { get; set; } + /// Gets or sets the name of the file profile used to process the file. + [Description("The name of the file profile used to process the file.")] public String FileProfileName { get; set; } + /// Gets or sets the import log that contains this file. + [Description("The import log that contains this file.")] public Guid FileImportLogId { get; set; } + /// Gets or sets the stored file location. + [Description("The stored file location.")] public String FileLocation { get; set; } + /// Gets or sets the individual processing results for the file's lines. + [Description("The individual processing results for the file's lines.")] public List FileLines { get; set; } + /// Gets or sets counts of lines by processing outcome. + [Description("Counts of lines by processing outcome.")] public FileProcessingSummary ProcessingSummary { get; set; } } } diff --git a/FileProcessor.DataTransferObjects/Responses/FileImportLog.cs b/FileProcessor.DataTransferObjects/Responses/FileImportLog.cs index 7951d5d..f3521ad 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileImportLog.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileImportLog.cs @@ -1,20 +1,35 @@ namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; using System.Collections.Generic; + /// A summary of files uploaded during one import period. + [Description("A summary of files uploaded during one import period.")] public class FileImportLog { + /// Gets or sets the identifier of this import log. + [Description("The identifier of this import log.")] public Guid FileImportLogId { get; set; } + /// Gets or sets the import log timestamp. + [Description("The import log timestamp.")] public DateTime ImportLogDateTime { get; set; } + /// Gets or sets the calendar date of the import log. + [Description("The calendar date of the import log.")] public DateTime ImportLogDate { get; set; } + /// Gets or sets the time of day of the import log. + [Description("The time of day of the import log.")] public TimeSpan ImportLogTime { get; set; } + /// Gets or sets the number of files in this log. + [Description("The number of files in this log.")] public Int32 FileCount { get; set; } + /// Gets or sets the files included in this log. + [Description("The files included in this log.")] public List Files { get; set; } } -} \ No newline at end of file +} diff --git a/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs b/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs index f851692..17425e7 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileImportLogFile.cs @@ -1,28 +1,45 @@ namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; + /// File metadata contained in an import log response. + [Description("File metadata contained in an import log response.")] public class FileImportLogFile { - #region Properties - + /// Gets or sets the identifier of the uploaded file. + [Description("The identifier of the uploaded file.")] public Guid FileId { get; set; } + /// Gets or sets the identifier of the import log containing the file. + [Description("The identifier of the import log containing the file.")] public Guid FileImportLogId { get; set; } + /// Gets or sets the stored file path. + [Description("The stored file path.")] public String FilePath { get; set; } + /// Gets or sets the identifier of the file profile used to process the file. + [Description("The identifier of the file profile used to process the file.")] public Guid FileProfileId { get; set; } + /// Gets or sets the identifier of the file profile version used to process the file. + [Description("The identifier of the file profile version used to process the file.")] public Guid FileProfileVersionId { get; set; } + /// Gets or sets when the file was uploaded. + [Description("When the file was uploaded.")] public DateTime FileUploadedDateTime { get; set; } + /// Gets or sets the identifier of the associated merchant. + [Description("The identifier of the associated merchant.")] public Guid MerchantId { get; set; } + /// Gets or sets the original name of the uploaded file. + [Description("The original name of the uploaded file.")] public String OriginalFileName { get; set; } + /// Gets or sets the identifier of the user who uploaded the file. + [Description("The identifier of the user who uploaded the file.")] public Guid UserId { get; set; } - - #endregion } } diff --git a/FileProcessor.DataTransferObjects/Responses/FileImportLogList.cs b/FileProcessor.DataTransferObjects/Responses/FileImportLogList.cs index eadb0d1..4840615 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileImportLogList.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileImportLogList.cs @@ -1,24 +1,30 @@ -namespace FileProcessor.DataTransferObjects.Responses +using System.ComponentModel; +namespace FileProcessor.DataTransferObjects.Responses { using System.Collections.Generic; - /// - /// - /// + /// A paginated collection of file import logs. + [Description("A paginated collection of file import logs.")] public class FileImportLogList { - #region Fields - + /// Gets or sets the import logs on the current page. + [Description("The import logs on the current page.")] public List FileImportLogs { get; set; } + /// Gets or sets the one-based current page number. + [Description("The one-based current page number.")] public int PageNumber { get; set; } + /// Gets or sets the maximum number of results requested per page. + [Description("The maximum number of results requested per page.")] public int PageSize { get; set; } + /// Gets or sets the total number of matching logs across all pages. + [Description("The total number of matching logs across all pages.")] public int TotalCount { get; set; } + /// Gets or sets whether additional matching results are available on a later page. + [Description("Whether additional matching results are available on a later page.")] public bool HasMore { get; set; } - - #endregion } } diff --git a/FileProcessor.DataTransferObjects/Responses/FileLine.cs b/FileProcessor.DataTransferObjects/Responses/FileLine.cs index 51561ad..278818c 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileLine.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileLine.cs @@ -1,27 +1,50 @@ namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; using System.Collections.Generic; + /// Processing details for one line in an uploaded file. + [Description("Processing details for one line in an uploaded file.")] public class FileLine { + /// Gets or sets the original text of this line. + [Description("The original text of this line.")] public String LineData { get; set; } + /// Gets or sets the one-based line number in the uploaded file. + [Description("The one-based line number in the uploaded file.")] public Int32 LineNumber { get; set; } + /// Gets or sets the processing outcome for this line. + [Description("The processing outcome for this line.")] public FileLineProcessingResult ProcessingResult { get; set; } + /// Gets or sets the transaction identifier created for this line, when available. + [Description("The transaction identifier created for this line, when available.")] public Guid TransactionId { get; set; } + /// Gets or sets the domain event identifier produced for this line, when available. + [Description("The domain event identifier produced for this line, when available.")] public Guid EventId { get; set; } + /// Gets or sets the rejection reason when the line was rejected. + [Description("The rejection reason when the line was rejected.")] public String RejectionReason { get; set; } + /// Gets or sets the failure reason when processing the line failed. + [Description("The failure reason when processing the line failed.")] public String FailureReason { get; set; } + /// Gets or sets the response code returned for the line, when available. + [Description("The response code returned for the line, when available.")] public String ResponseCode { get; set; } + /// Gets or sets the number of dispatch attempts made for the line. + [Description("The number of dispatch attempts made for the line.")] public Int32 DispatchAttemptCount { get; set; } + /// Gets or sets failed transaction dispatch attempts associated with the line. + [Description("Failed transaction dispatch attempts associated with the line.")] public List FailedDispatchAttempts { get; set; } = new(); } } diff --git a/FileProcessor.DataTransferObjects/Responses/FileLineProcessingResult.cs b/FileProcessor.DataTransferObjects/Responses/FileLineProcessingResult.cs index 4a102f4..76f63b2 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileLineProcessingResult.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileLineProcessingResult.cs @@ -1,35 +1,32 @@ -namespace FileProcessor.DataTransferObjects.Responses +using System.ComponentModel; +namespace FileProcessor.DataTransferObjects.Responses { + /// Possible outcomes for a processed file line. + [Description("Possible outcomes for a processed file line.")] public enum FileLineProcessingResult { - /// - /// The unknown - /// + /// The processing outcome is not known. + [Description("The processing outcome is not known.")] Unknown, - /// - /// The not processed - /// + /// The line has not been processed. + [Description("The line has not been processed.")] NotProcessed, - /// - /// The successful - /// + /// The line was processed successfully. + [Description("The line was processed successfully.")] Successful, - /// - /// The failed - /// + /// Processing the line failed. + [Description("Processing the line failed.")] Failed, - /// - /// The ignored - /// + /// The line was intentionally ignored. + [Description("The line was intentionally ignored.")] Ignored, - /// - /// The rejected - /// + /// The line or file was rejected by validation. + [Description("The line or file was rejected by validation.")] Rejected } -} \ No newline at end of file +} diff --git a/FileProcessor.DataTransferObjects/Responses/FileProcessingSummary.cs b/FileProcessor.DataTransferObjects/Responses/FileProcessingSummary.cs index 79e3e67..c2c9ba4 100644 --- a/FileProcessor.DataTransferObjects/Responses/FileProcessingSummary.cs +++ b/FileProcessor.DataTransferObjects/Responses/FileProcessingSummary.cs @@ -1,19 +1,34 @@ namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; + /// Counts of file lines grouped by processing outcome. + [Description("Counts of file lines grouped by processing outcome.")] public class FileProcessingSummary { + /// Gets or sets the number of lines that failed during processing. + [Description("The number of lines that failed during processing.")] public Int32 FailedLines { get; set; } + /// Gets or sets the number of lines intentionally ignored. + [Description("The number of lines intentionally ignored.")] public Int32 IgnoredLines { get; set; } + /// Gets or sets the number of lines not processed. + [Description("The number of lines not processed.")] public Int32 NotProcessedLines { get; set; } + /// Gets or sets the number of rejected lines. + [Description("The number of rejected lines.")] public Int32 RejectedLines { get; set; } + /// Gets or sets the number of successfully processed lines. + [Description("The number of successfully processed lines.")] public Int32 SuccessfullyProcessedLines { get; set; } + /// Gets or sets the total number of lines in the file. + [Description("The total number of lines in the file.")] public Int32 TotalLines { get; set; } } -} \ No newline at end of file +} diff --git a/FileProcessor.DataTransferObjects/Responses/FileProfileResponse.cs b/FileProcessor.DataTransferObjects/Responses/FileProfileResponse.cs new file mode 100644 index 0000000..0ac1640 --- /dev/null +++ b/FileProcessor.DataTransferObjects/Responses/FileProfileResponse.cs @@ -0,0 +1,53 @@ +using System; +using System.ComponentModel; + +namespace FileProcessor.DataTransferObjects.Responses; + +/// File profile configuration returned by the API. +[Description("File profile configuration returned by the API.")] +public class FileProfileResponse +{ + /// Gets or sets the identifier of the profile. + [Description("The identifier of the profile.")] + public Guid FileProfileId { get; set; } + + /// Gets or sets the identifier of this profile version. + [Description("The identifier of this profile version.")] + public Guid FileProfileVersionId { get; set; } + + /// Gets or sets the version number, starting at one. + [Description("The version number, starting at one.")] + public Int32 FileProfileVersion { get; set; } + + /// Gets or sets the profile's display name. + [Description("The profile's display name.")] + public String Name { get; set; } + + /// Gets or sets the operator name. + [Description("The operator name.")] + public String OperatorName { get; set; } + + /// Gets or sets the directory from which files are collected. + [Description("The directory from which files are collected.")] + public String ListeningDirectory { get; set; } + + /// Gets or sets the directory where successfully processed files are placed. + [Description("The directory where successfully processed files are placed.")] + public String ProcessedDirectory { get; set; } + + /// Gets or sets the directory where failed files are placed. + [Description("The directory where failed files are placed.")] + public String FailedDirectory { get; set; } + + /// Gets or sets the registered file format handler. + [Description("The registered file format handler.")] + public String FileFormatHandler { get; set; } + + /// Gets or sets the request or transaction type. + [Description("The request or transaction type.")] + public String RequestType { get; set; } + + /// Gets or sets the line terminator used to separate records. + [Description("The line terminator used to separate records.")] + public String LineTerminator { get; set; } +} diff --git a/FileProcessor.DataTransferObjects/Responses/TransactionDispatchAttempt.cs b/FileProcessor.DataTransferObjects/Responses/TransactionDispatchAttempt.cs index f604a75..5f8d6ca 100644 --- a/FileProcessor.DataTransferObjects/Responses/TransactionDispatchAttempt.cs +++ b/FileProcessor.DataTransferObjects/Responses/TransactionDispatchAttempt.cs @@ -1,13 +1,22 @@ -namespace FileProcessor.DataTransferObjects.Responses +namespace FileProcessor.DataTransferObjects.Responses { using System; + using System.ComponentModel; + /// Details of a failed attempt to dispatch a transaction from a file line. + [Description("Details of a failed attempt to dispatch a transaction from a file line.")] public class TransactionDispatchAttempt { + /// Gets or sets the transaction's position in the dispatch batch. + [Description("The transaction's position in the dispatch batch.")] public Int32 TransactionNumber { get; set; } + /// Gets or sets the type or category of the dispatch failure. + [Description("The type or category of the dispatch failure.")] public String FailureType { get; set; } + /// Gets or sets when the dispatch attempt occurred. + [Description("When the dispatch attempt occurred.")] public DateTime AttemptedAt { get; set; } } } diff --git a/FileProcessor.IntegrationTests/Common/DockerHelper.cs b/FileProcessor.IntegrationTests/Common/DockerHelper.cs index 0d114cf..ceddffc 100644 --- a/FileProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/FileProcessor.IntegrationTests/Common/DockerHelper.cs @@ -1,4 +1,5 @@ using DotNet.Testcontainers.Builders; +using KurrentDB.Client; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -34,7 +35,7 @@ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.Doc private readonly TestingContext TestingContext; - public EventStoreProjectionManagementClient ProjectionManagementClient; + public KurrentDBProjectionManagementClient ProjectionManagementClient; public IAgencyBankingClient AgencyBankingClient; @@ -83,9 +84,9 @@ public override ContainerBuilder SetupFileProcessorContainer() { public String AccessToken; - protected override String GenerateEventStoreConnectionString(String userName = "admin", String password = "changeit") + protected override String GenerateKurrentDbConnectionString(String userName = "admin", String password = "changeit") { - String eventStoreAddress = $"esdb://{userName}:{password}@{this.EventStoreContainerName}:{DockerPorts.EventStoreHttpDockerPort}?tls=false&defaultDeadline=240000"; + String eventStoreAddress = $"esdb://{userName}:{password}@{this.KurrentDbContainerName}:{DockerPorts.KurrentDbHttpDockerPort}?tls=false&defaultDeadline=240000"; return eventStoreAddress; } @@ -126,7 +127,7 @@ public override async Task StartContainersForScenarioRun(String scenarioName, Do this.TestHostHttpClient.BaseAddress = new Uri($"http://127.0.0.1:{this.TestHostServicePort}"); this.AgencyBankingClient = new AgencyBankingClient(TestHostServiceBaseAddressResolver, httpClient, Serialise_CamelCase, this.Deserialise_CamelCase); - 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/FileProcessor.IntegrationTests/Common/GenericSteps.cs b/FileProcessor.IntegrationTests/Common/GenericSteps.cs index 8f99710..c2df755 100644 --- a/FileProcessor.IntegrationTests/Common/GenericSteps.cs +++ b/FileProcessor.IntegrationTests/Common/GenericSteps.cs @@ -67,11 +67,11 @@ private DockerServices GetDockerServicesForScenario() // the rest of the transaction-processing stack is unnecessary for these scenarios. if (scenarioTitle.Contains("file profile", StringComparison.OrdinalIgnoreCase)) { - return DockerServices.EventStore | DockerServices.FileProcessor | DockerServices.SecurityService | + return DockerServices.KurrentDb | DockerServices.FileProcessor | DockerServices.SecurityService | DockerServices.SqlServer | DockerServices.TransactionProcessor; } - return DockerServices.EventStore | DockerServices.FileProcessor | DockerServices.MessagingService | + return DockerServices.KurrentDb | DockerServices.FileProcessor | DockerServices.MessagingService | DockerServices.SecurityService | DockerServices.SqlServer | DockerServices.TestHost | DockerServices.TransactionProcessor; } diff --git a/FileProcessor.IntegrationTests/Features/FileProfile.feature.cs b/FileProcessor.IntegrationTests/Features/FileProfile.feature.cs index 2330048..c1d7bc3 100644 --- a/FileProcessor.IntegrationTests/Features/FileProfile.feature.cs +++ b/FileProcessor.IntegrationTests/Features/FileProfile.feature.cs @@ -25,7 +25,7 @@ namespace FileProcessor.IntegrationTests.Features [global::NUnit.Framework.CategoryAttribute("fileprofiles")] public partial class FileProfileFeature { - + private global::Reqnroll.ITestRunner testRunner; private static string[] featureTags = new string[] { @@ -168,7 +168,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and { return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Features/FileProfile.feature.ndjson", 5); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Create, update and list file profiles")] [global::NUnit.Framework.CategoryAttribute("PRTest")] @@ -276,7 +276,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Reject duplicate file profile name and request type")] public async global::System.Threading.Tasks.Task RejectDuplicateFileProfileNameAndRequestType() @@ -348,7 +348,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Reject unsupported processing configuration")] public async global::System.Threading.Tasks.Task RejectUnsupportedProcessingConfiguration() diff --git a/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature.cs b/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature.cs index 22ba604..f7d7433 100644 --- a/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature.cs +++ b/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature.cs @@ -115,66 +115,66 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa { #line 4 #line hidden - global::Reqnroll.Table table10 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table11 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Description"}); - table10.AddRow(new string[] { + table11.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST Scope", "A scope for Transaction Processor REST"}); - table10.AddRow(new string[] { + table11.AddRow(new string[] { "fileProcessor", "File Processor REST Scope", "A scope for File Processor REST"}); #line 5 - await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table10, "Given "); + await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table11, "Given "); #line hidden - global::Reqnroll.Table table11 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table12 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Secret", "Scopes", "UserClaims"}); - table11.AddRow(new string[] { + table12.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST", "Secret1", "transactionProcessor", "MerchantId, EstateId, role"}); - table11.AddRow(new string[] { + table12.AddRow(new string[] { "fileProcessor", "File Processor REST", "Secret1", "fileProcessor", ""}); #line 10 - await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table11, "Given "); + await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table12, "Given "); #line hidden - global::Reqnroll.Table table12 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table13 = new global::Reqnroll.Table(new string[] { "ClientId", "ClientName", "Secret", "Scopes", "GrantTypes"}); - table12.AddRow(new string[] { + table13.AddRow(new string[] { "serviceClient", "Service Client", "Secret1", "transactionProcessor,fileProcessor", "client_credentials"}); #line 15 - await testRunner.GivenAsync("the following clients exist", ((string)(null)), table12, "Given "); + await testRunner.GivenAsync("the following clients exist", ((string)(null)), table13, "Given "); #line hidden - global::Reqnroll.Table table13 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table14 = new global::Reqnroll.Table(new string[] { "ClientId"}); - table13.AddRow(new string[] { + table14.AddRow(new string[] { "serviceClient"}); #line 19 await testRunner.GivenAsync("I have a token to access the estate management and transaction processor resource" + - "s", ((string)(null)), table13, "Given "); + "s", ((string)(null)), table14, "Given "); #line hidden - global::Reqnroll.Table table14 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table15 = new global::Reqnroll.Table(new string[] { "Alias", "FileProfileId", "Name", @@ -183,7 +183,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "OperatorName", "LineTerminator", "FileFormatHandler"}); - table14.AddRow(new string[] { + table15.AddRow(new string[] { "Safaricom Topup", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "Safaricom Topup", @@ -192,7 +192,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Safaricom", "1", "SafaricomFileFormatHandler"}); - table14.AddRow(new string[] { + table15.AddRow(new string[] { "Voucher Issue", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "Voucher Issue", @@ -202,61 +202,61 @@ await testRunner.GivenAsync("I have a token to access the estate management and "1", "VoucherFileFormatHandler"}); #line 23 - await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table14, "When "); + await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table15, "When "); #line hidden - global::Reqnroll.Table table15 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table16 = new global::Reqnroll.Table(new string[] { "EstateName"}); - table15.AddRow(new string[] { + table16.AddRow(new string[] { "Test Estate 1"}); #line 28 - await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table15, "Given "); + await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table16, "Given "); #line hidden - global::Reqnroll.Table table16 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table17 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "RequireCustomMerchantNumber", "RequireCustomTerminalNumber"}); - table16.AddRow(new string[] { + table17.AddRow(new string[] { "Test Estate 1", "Safaricom", "True", "True"}); - table16.AddRow(new string[] { + table17.AddRow(new string[] { "Test Estate 1", "Voucher", "True", "True"}); #line 32 - await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table16, "Given "); + await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table17, "Given "); #line hidden - global::Reqnroll.Table table17 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table18 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName"}); - table17.AddRow(new string[] { + table18.AddRow(new string[] { "Test Estate 1", "Safaricom"}); - table17.AddRow(new string[] { + table18.AddRow(new string[] { "Test Estate 1", "Voucher"}); #line 37 - await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table17, "And "); + await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table18, "And "); #line hidden - global::Reqnroll.Table table18 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table19 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription"}); - table18.AddRow(new string[] { + table19.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract"}); - table18.AddRow(new string[] { + table19.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract"}); #line 42 - await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table18, "Given "); + await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table19, "Given "); #line hidden - global::Reqnroll.Table table19 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table20 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -264,7 +264,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "DisplayText", "Value", "ProductType"}); - table19.AddRow(new string[] { + table20.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -272,7 +272,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Custom", "", "MobileTopup"}); - table19.AddRow(new string[] { + table20.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract", @@ -281,9 +281,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "", "Voucher"}); #line 47 - await testRunner.WhenAsync("I create the following Products", ((string)(null)), table19, "When "); + await testRunner.WhenAsync("I create the following Products", ((string)(null)), table20, "When "); #line hidden - global::Reqnroll.Table table20 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table21 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -291,7 +291,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "CalculationType", "FeeDescription", "Value"}); - table20.AddRow(new string[] { + table21.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -300,9 +300,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Merchant Commission", "2.50"}); #line 52 - await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table20, "When "); + await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table21, "When "); #line hidden - global::Reqnroll.Table table21 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table22 = new global::Reqnroll.Table(new string[] { "MerchantName", "AddressLine1", "Town", @@ -312,7 +312,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "ContactName", "EmailAddress", "EstateName"}); - table21.AddRow(new string[] { + table22.AddRow(new string[] { "Test Merchant 1", "Address Line 1", "TestTown", @@ -322,7 +322,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Test Contact 1", "testcontact1@merchant1.co.uk", "Test Estate 1"}); - table21.AddRow(new string[] { + table22.AddRow(new string[] { "Test Merchant 2", "Address Line 1", "TestTown", @@ -333,99 +333,99 @@ await testRunner.GivenAsync("I have a token to access the estate management and "testcontact1@merchant2.co.uk", "Test Estate 1"}); #line 56 - await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table21, "Given "); + await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table22, "Given "); #line hidden - global::Reqnroll.Table table22 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table23 = new global::Reqnroll.Table(new string[] { "OperatorName", "MerchantName", "MerchantNumber", "TerminalNumber", "EstateName"}); - table22.AddRow(new string[] { + table23.AddRow(new string[] { "Safaricom", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); - table22.AddRow(new string[] { + table23.AddRow(new string[] { "Voucher", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); - table22.AddRow(new string[] { + table23.AddRow(new string[] { "Safaricom", "Test Merchant 2", "00000002", "10000002", "Test Estate 1"}); - table22.AddRow(new string[] { + table23.AddRow(new string[] { "Voucher", "Test Merchant 2", "00000002", "10000002", "Test Estate 1"}); #line 61 - await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table22, "Given "); + await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table23, "Given "); #line hidden - global::Reqnroll.Table table23 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table24 = new global::Reqnroll.Table(new string[] { "DeviceIdentifier", "MerchantName", "EstateName"}); - table23.AddRow(new string[] { + table24.AddRow(new string[] { "123456780", "Test Merchant 1", "Test Estate 1"}); - table23.AddRow(new string[] { + table24.AddRow(new string[] { "123456781", "Test Merchant 2", "Test Estate 1"}); #line 68 - await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table23, "Given "); + await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table24, "Given "); #line hidden - global::Reqnroll.Table table24 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table25 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "ContractDescription"}); - table24.AddRow(new string[] { + table25.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Safaricom Contract"}); - table24.AddRow(new string[] { + table25.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Hospital 1 Contract"}); - table24.AddRow(new string[] { + table25.AddRow(new string[] { "Test Estate 1", "Test Merchant 2", "Safaricom Contract"}); - table24.AddRow(new string[] { + table25.AddRow(new string[] { "Test Estate 1", "Test Merchant 2", "Hospital 1 Contract"}); #line 73 - await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table24, "When "); + await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table25, "When "); #line hidden - global::Reqnroll.Table table25 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table26 = new global::Reqnroll.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table25.AddRow(new string[] { + table26.AddRow(new string[] { "Deposit1", "300.00", "Today", "Test Merchant 1", "Test Estate 1"}); - table25.AddRow(new string[] { + table26.AddRow(new string[] { "Deposit1", "300.00", "Today", "Test Merchant 2", "Test Estate 1"}); #line 80 - await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table25, "Given "); + await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table26, "Given "); #line hidden } @@ -459,144 +459,144 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table26 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table27 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table26.AddRow(new string[] { + table27.AddRow(new string[] { "H", "20210508", ""}); - table26.AddRow(new string[] { + table27.AddRow(new string[] { "D", "07777777775", "100"}); - table26.AddRow(new string[] { + table27.AddRow(new string[] { "T", "1", ""}); #line 87 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table26, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table27, "Given "); #line hidden - global::Reqnroll.Table table27 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table28 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId", "UploadDateTime"}); - table27.AddRow(new string[] { + table28.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476", "Today"}); #line 92 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table27, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table28, "And "); #line hidden - global::Reqnroll.Table table28 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table29 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table28.AddRow(new string[] { + table29.AddRow(new string[] { "H", "20210508", ""}); - table28.AddRow(new string[] { + table29.AddRow(new string[] { "D", "07777777776", "150"}); - table28.AddRow(new string[] { + table29.AddRow(new string[] { "T", "1", ""}); #line 96 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table28, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table29, "Given "); #line hidden - global::Reqnroll.Table table29 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table30 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId", "UploadDateTime"}); - table29.AddRow(new string[] { + table30.AddRow(new string[] { "Test Estate 1", "Test Merchant 2", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476", "Today"}); #line 101 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table29, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table30, "And "); #line hidden - global::Reqnroll.Table table30 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table31 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "H", "20210508", "", ""}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "10"}); - table30.AddRow(new string[] { + table31.AddRow(new string[] { "T", "1", "", ""}); #line 105 - await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table30, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table31, "Given "); #line hidden - global::Reqnroll.Table table31 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table32 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId", "UploadDateTime"}); - table31.AddRow(new string[] { + table32.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476", "Today"}); #line 111 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table31, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table32, "And "); #line hidden - global::Reqnroll.Table table32 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table33 = new global::Reqnroll.Table(new string[] { "ImportLogDate", "FileCount"}); - table32.AddRow(new string[] { + table33.AddRow(new string[] { "Today", "3"}); #line 115 await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Yesterday\' and \'Today\' the followi" + - "ng data is returned", ((string)(null)), table32, "When "); + "ng data is returned", ((string)(null)), table33, "When "); #line hidden - global::Reqnroll.Table table33 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table34 = new global::Reqnroll.Table(new string[] { "MerchantName", "OriginalFileName"}); - table33.AddRow(new string[] { + table34.AddRow(new string[] { "Test Merchant 1", "SafarcomTopup1.txt"}); - table33.AddRow(new string[] { + table34.AddRow(new string[] { "Test Merchant 2", "SafarcomTopup2.txt"}); - table33.AddRow(new string[] { + table34.AddRow(new string[] { "Test Merchant 1", "VoucherIssue1.txt"}); #line 119 await testRunner.WhenAsync("I get the \'Test Estate 1\' import log for \'Today\' the following file information i" + - "s returned", ((string)(null)), table33, "When "); + "s returned", ((string)(null)), table34, "When "); #line hidden - global::Reqnroll.Table table34 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table35 = new global::Reqnroll.Table(new string[] { "ProcessingCompleted", "NumberOfLines", "TotaLines", @@ -604,7 +604,7 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "IgnoredLines", "FailedLines", "NotProcessedLines"}); - table34.AddRow(new string[] { + table35.AddRow(new string[] { "True", "3", "3", @@ -614,29 +614,29 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "0"}); #line 125 await testRunner.WhenAsync("I get the file \'SafarcomTopup1.txt\' for Estate \'Test Estate 1\' the following file" + - " information is returned", ((string)(null)), table34, "When "); + " information is returned", ((string)(null)), table35, "When "); #line hidden - global::Reqnroll.Table table35 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table36 = new global::Reqnroll.Table(new string[] { "LineNumber", "Data", "Result"}); - table35.AddRow(new string[] { + table36.AddRow(new string[] { "1", "H,20210508,", "Ignored"}); - table35.AddRow(new string[] { + table36.AddRow(new string[] { "2", "D,07777777775,100", "Successful"}); - table35.AddRow(new string[] { + table36.AddRow(new string[] { "3", "T,1,", "Ignored"}); #line 129 await testRunner.WhenAsync("I get the file \'SafarcomTopup1.txt\' for Estate \'Test Estate 1\' the following file" + - " lines are returned", ((string)(null)), table35, "When "); + " lines are returned", ((string)(null)), table36, "When "); #line hidden - global::Reqnroll.Table table36 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table37 = new global::Reqnroll.Table(new string[] { "ProcessingCompleted", "NumberOfLines", "TotaLines", @@ -644,7 +644,7 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "IgnoredLines", "FailedLines", "NotProcessedLines"}); - table36.AddRow(new string[] { + table37.AddRow(new string[] { "True", "3", "3", @@ -654,29 +654,29 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "0"}); #line 135 await testRunner.WhenAsync("I get the file \'SafarcomTopup2.txt\' for Estate \'Test Estate 1\' the following file" + - " information is returned", ((string)(null)), table36, "When "); + " information is returned", ((string)(null)), table37, "When "); #line hidden - global::Reqnroll.Table table37 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table38 = new global::Reqnroll.Table(new string[] { "LineNumber", "Data", "Result"}); - table37.AddRow(new string[] { + table38.AddRow(new string[] { "1", "H,20210508,", "Ignored"}); - table37.AddRow(new string[] { + table38.AddRow(new string[] { "2", "D,07777777776,150", "Successful"}); - table37.AddRow(new string[] { + table38.AddRow(new string[] { "3", "T,1,", "Ignored"}); #line 139 await testRunner.WhenAsync("I get the file \'SafarcomTopup2.txt\' for Estate \'Test Estate 1\' the following file" + - " lines are returned", ((string)(null)), table37, "When "); + " lines are returned", ((string)(null)), table38, "When "); #line hidden - global::Reqnroll.Table table38 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table39 = new global::Reqnroll.Table(new string[] { "ProcessingCompleted", "NumberOfLines", "TotaLines", @@ -684,7 +684,7 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "IgnoredLines", "FailedLines", "NotProcessedLines"}); - table38.AddRow(new string[] { + table39.AddRow(new string[] { "True", "4", "4", @@ -694,31 +694,31 @@ await testRunner.WhenAsync("I get the \'Test Estate 1\' import logs between \'Ye "0"}); #line 145 await testRunner.WhenAsync("I get the file \'VoucherIssue1.txt\' for Estate \'Test Estate 1\' the following file " + - "information is returned", ((string)(null)), table38, "When "); + "information is returned", ((string)(null)), table39, "When "); #line hidden - global::Reqnroll.Table table39 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table40 = new global::Reqnroll.Table(new string[] { "LineNumber", "Data", "Result"}); - table39.AddRow(new string[] { + table40.AddRow(new string[] { "1", "H,20210508,,", "Ignored"}); - table39.AddRow(new string[] { + table40.AddRow(new string[] { "2", "D,Hospital 1,07777777775,10", "Successful"}); - table39.AddRow(new string[] { + table40.AddRow(new string[] { "3", "D,Hospital 1,testrecipient1@recipient.com,10", "Successful"}); - table39.AddRow(new string[] { + table40.AddRow(new string[] { "4", "T,1,,", "Ignored"}); #line 149 await testRunner.WhenAsync("I get the file \'VoucherIssue1.txt\' for Estate \'Test Estate 1\' the following file " + - "lines are returned", ((string)(null)), table39, "When "); + "lines are returned", ((string)(null)), table40, "When "); #line hidden } await this.ScenarioCleanupAsync(); diff --git a/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature.cs b/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature.cs index de199ad..c2bc4b8 100644 --- a/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature.cs +++ b/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------ // // This code was generated by Reqnroll (https://reqnroll.net/). // Reqnroll Version:3.0.0.0 @@ -13,8 +13,8 @@ using Reqnroll; namespace FileProcessor.IntegrationTests.Features { - - + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "3.0.0.0")] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::NUnit.Framework.TestFixtureAttribute()] @@ -25,30 +25,30 @@ namespace FileProcessor.IntegrationTests.Features [global::NUnit.Framework.CategoryAttribute("fileprofiles")] public partial class ProcessTopupCSVFilesFeature { - + private global::Reqnroll.ITestRunner testRunner; - + private static string[] featureTags = new string[] { "base", "shared", "fileprofiles"}; - + private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new global::System.Globalization.CultureInfo("en-US"), "Features", "Process Topup CSV Files", null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags, InitializeCucumberMessages()); - + #line 1 "ProcessTopupCSV.feature" #line hidden - + [global::NUnit.Framework.OneTimeSetUpAttribute()] public static async global::System.Threading.Tasks.Task FeatureSetupAsync() { } - + [global::NUnit.Framework.OneTimeTearDownAttribute()] public static async global::System.Threading.Tasks.Task FeatureTearDownAsync() { await global::Reqnroll.TestRunnerManager.ReleaseFeatureAsync(featureInfo); } - + [global::NUnit.Framework.SetUpAttribute()] public async global::System.Threading.Tasks.Task TestInitializeAsync() { @@ -74,7 +74,7 @@ public partial class ProcessTopupCSVFilesFeature } } } - + [global::NUnit.Framework.TearDownAttribute()] public async global::System.Threading.Tasks.Task TestTearDownAsync() { @@ -92,87 +92,87 @@ public partial class ProcessTopupCSVFilesFeature testRunner = null; } } - + public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, global::Reqnroll.RuleInfo ruleInfo) { testRunner.OnScenarioInitialize(scenarioInfo, ruleInfo); testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(global::NUnit.Framework.TestContext.CurrentContext); } - + public async global::System.Threading.Tasks.Task ScenarioStartAsync() { await testRunner.OnScenarioStartAsync(); } - + public async global::System.Threading.Tasks.Task ScenarioCleanupAsync() { await testRunner.CollectScenarioErrorsAsync(); } - + public virtual async global::System.Threading.Tasks.Task FeatureBackgroundAsync() { #line 4 #line hidden - global::Reqnroll.Table table1 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table41 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Description"}); - table1.AddRow(new string[] { + table41.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST Scope", "A scope for Transaction Processor REST"}); - table1.AddRow(new string[] { + table41.AddRow(new string[] { "fileProcessor", "File Processor REST Scope", "A scope for File Processor REST"}); #line 5 - await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table1, "Given "); + await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table41, "Given "); #line hidden - global::Reqnroll.Table table2 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table42 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Secret", "Scopes", "UserClaims"}); - table2.AddRow(new string[] { + table42.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST", "Secret1", "transactionProcessor", "MerchantId, EstateId, role"}); - table2.AddRow(new string[] { + table42.AddRow(new string[] { "fileProcessor", "File Processor REST", "Secret1", "fileProcessor", ""}); #line 10 - await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table2, "Given "); + await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table42, "Given "); #line hidden - global::Reqnroll.Table table3 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table43 = new global::Reqnroll.Table(new string[] { "ClientId", "ClientName", "Secret", "Scopes", "GrantTypes"}); - table3.AddRow(new string[] { + table43.AddRow(new string[] { "serviceClient", "Service Client", "Secret1", "transactionProcessor,fileProcessor", "client_credentials"}); #line 15 - await testRunner.GivenAsync("the following clients exist", ((string)(null)), table3, "Given "); + await testRunner.GivenAsync("the following clients exist", ((string)(null)), table43, "Given "); #line hidden - global::Reqnroll.Table table4 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table44 = new global::Reqnroll.Table(new string[] { "ClientId"}); - table4.AddRow(new string[] { + table44.AddRow(new string[] { "serviceClient"}); #line 19 await testRunner.GivenAsync("I have a token to access the estate management and transaction processor resource" + - "s", ((string)(null)), table4, "Given "); + "s", ((string)(null)), table44, "Given "); #line hidden - global::Reqnroll.Table table5 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table45 = new global::Reqnroll.Table(new string[] { "Alias", "FileProfileId", "Name", @@ -181,7 +181,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "OperatorName", "LineTerminator", "FileFormatHandler"}); - table5.AddRow(new string[] { + table45.AddRow(new string[] { "Safaricom Topup", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "Safaricom Topup", @@ -190,7 +190,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Safaricom", "1", "SafaricomFileFormatHandler"}); - table5.AddRow(new string[] { + table45.AddRow(new string[] { "Voucher Issue", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "Voucher Issue", @@ -200,61 +200,61 @@ await testRunner.GivenAsync("I have a token to access the estate management and "1", "VoucherFileFormatHandler"}); #line 23 - await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table5, "When "); + await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table45, "When "); #line hidden - global::Reqnroll.Table table6 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table46 = new global::Reqnroll.Table(new string[] { "EstateName"}); - table6.AddRow(new string[] { + table46.AddRow(new string[] { "Test Estate 1"}); #line 28 - await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table6, "Given "); + await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table46, "Given "); #line hidden - global::Reqnroll.Table table7 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table47 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "RequireCustomMerchantNumber", "RequireCustomTerminalNumber"}); - table7.AddRow(new string[] { + table47.AddRow(new string[] { "Test Estate 1", "Safaricom", "True", "True"}); - table7.AddRow(new string[] { + table47.AddRow(new string[] { "Test Estate 1", "Voucher", "True", "True"}); #line 32 - await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table7, "Given "); + await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table47, "Given "); #line hidden - global::Reqnroll.Table table8 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table48 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName"}); - table8.AddRow(new string[] { + table48.AddRow(new string[] { "Test Estate 1", "Safaricom"}); - table8.AddRow(new string[] { + table48.AddRow(new string[] { "Test Estate 1", "Voucher"}); #line 37 - await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table8, "And "); + await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table48, "And "); #line hidden - global::Reqnroll.Table table9 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table49 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription"}); - table9.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract"}); - table9.AddRow(new string[] { + table49.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract"}); #line 42 - await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table9, "Given "); + await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table49, "Given "); #line hidden - global::Reqnroll.Table table10 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table50 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -262,7 +262,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "DisplayText", "Value", "ProductType"}); - table10.AddRow(new string[] { + table50.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -270,7 +270,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Custom", "", "MobileTopup"}); - table10.AddRow(new string[] { + table50.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract", @@ -279,9 +279,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "", "Voucher"}); #line 47 - await testRunner.WhenAsync("I create the following Products", ((string)(null)), table10, "When "); + await testRunner.WhenAsync("I create the following Products", ((string)(null)), table50, "When "); #line hidden - global::Reqnroll.Table table11 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table51 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -289,7 +289,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "CalculationType", "FeeDescription", "Value"}); - table11.AddRow(new string[] { + table51.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -298,9 +298,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Merchant Commission", "2.50"}); #line 52 - await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table11, "When "); + await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table51, "When "); #line hidden - global::Reqnroll.Table table12 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table52 = new global::Reqnroll.Table(new string[] { "MerchantName", "AddressLine1", "Town", @@ -310,7 +310,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "ContactName", "EmailAddress", "EstateName"}); - table12.AddRow(new string[] { + table52.AddRow(new string[] { "Test Merchant 1", "Address Line 1", "TestTown", @@ -321,77 +321,77 @@ await testRunner.GivenAsync("I have a token to access the estate management and "testcontact1@merchant1.co.uk", "Test Estate 1"}); #line 56 - await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table12, "Given "); + await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table52, "Given "); #line hidden - global::Reqnroll.Table table13 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table53 = new global::Reqnroll.Table(new string[] { "OperatorName", "MerchantName", "MerchantNumber", "TerminalNumber", "EstateName"}); - table13.AddRow(new string[] { + table53.AddRow(new string[] { "Safaricom", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); - table13.AddRow(new string[] { + table53.AddRow(new string[] { "Voucher", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); #line 60 - await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table13, "Given "); + await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table53, "Given "); #line hidden - global::Reqnroll.Table table14 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table54 = new global::Reqnroll.Table(new string[] { "DeviceIdentifier", "MerchantName", "EstateName"}); - table14.AddRow(new string[] { + table54.AddRow(new string[] { "123456780", "Test Merchant 1", "Test Estate 1"}); #line 65 - await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table14, "Given "); + await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table54, "Given "); #line hidden - global::Reqnroll.Table table15 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table55 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "ContractDescription"}); - table15.AddRow(new string[] { + table55.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Safaricom Contract"}); - table15.AddRow(new string[] { + table55.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Hospital 1 Contract"}); #line 69 - await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table15, "When "); + await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table55, "When "); #line hidden - global::Reqnroll.Table table16 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table56 = new global::Reqnroll.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table16.AddRow(new string[] { + table56.AddRow(new string[] { "Deposit1", "300.00", "Today", "Test Merchant 1", "Test Estate 1"}); #line 74 - await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table16, "Given "); + await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table56, "Given "); #line hidden } - + private static global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages InitializeCucumberMessages() { return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Features/ProcessTopupCSV.feature.ndjson", 9); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Process Safaricom Topup File with 1 detail row")] public async global::System.Threading.Tasks.Task ProcessSafaricomTopupFileWith1DetailRow() @@ -415,42 +415,42 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table17 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table57 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table17.AddRow(new string[] { + table57.AddRow(new string[] { "H", "20210508", ""}); - table17.AddRow(new string[] { + table57.AddRow(new string[] { "D", "07777777775", "100"}); - table17.AddRow(new string[] { + table57.AddRow(new string[] { "T", "1", ""}); #line 80 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table17, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table57, "Given "); #line hidden - global::Reqnroll.Table table18 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table58 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table18.AddRow(new string[] { + table58.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 85 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table18, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table58, "And "); #line hidden } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Process Safaricom Topup File with 2 detail rows")] public async global::System.Threading.Tasks.Task ProcessSafaricomTopupFileWith2DetailRows() @@ -474,46 +474,46 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table19 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table59 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table19.AddRow(new string[] { + table59.AddRow(new string[] { "H", "20210508", ""}); - table19.AddRow(new string[] { + table59.AddRow(new string[] { "D", "07777777775", "100"}); - table19.AddRow(new string[] { + table59.AddRow(new string[] { "D", "07777777776", "200"}); - table19.AddRow(new string[] { + table59.AddRow(new string[] { "T", "2", ""}); #line 92 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table19, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table59, "Given "); #line hidden - global::Reqnroll.Table table20 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table60 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table20.AddRow(new string[] { + table60.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 98 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table20, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table60, "And "); #line hidden } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Process 2 Safaricom Topup Files")] public async global::System.Threading.Tasks.Task Process2SafaricomTopupFiles() @@ -537,78 +537,78 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table21 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table61 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table21.AddRow(new string[] { + table61.AddRow(new string[] { "H", "20210508", ""}); - table21.AddRow(new string[] { + table61.AddRow(new string[] { "D", "07777777775", "100"}); - table21.AddRow(new string[] { + table61.AddRow(new string[] { "T", "1", ""}); #line 105 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table21, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table61, "Given "); #line hidden - global::Reqnroll.Table table22 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table62 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table22.AddRow(new string[] { + table62.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 110 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table22, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table62, "And "); #line hidden - global::Reqnroll.Table table23 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table63 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table23.AddRow(new string[] { + table63.AddRow(new string[] { "H", "20210508", ""}); - table23.AddRow(new string[] { + table63.AddRow(new string[] { "D", "07777777776", "150"}); - table23.AddRow(new string[] { + table63.AddRow(new string[] { "D", "07777777777", "50"}); - table23.AddRow(new string[] { + table63.AddRow(new string[] { "T", "2", ""}); #line 114 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table23, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table63, "Given "); #line hidden - global::Reqnroll.Table table24 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table64 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table24.AddRow(new string[] { + table64.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 120 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table24, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table64, "And "); #line hidden } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Process Duplicate Safaricom Topup File with 1 detail row")] [global::NUnit.Framework.CategoryAttribute("PRTest")] @@ -634,83 +634,82 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table25 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table65 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table25.AddRow(new string[] { + table65.AddRow(new string[] { "H", "20210508", ""}); - table25.AddRow(new string[] { + table65.AddRow(new string[] { "D", "07777777775", "100"}); - table25.AddRow(new string[] { + table65.AddRow(new string[] { "D", "07777777776", "200"}); - table25.AddRow(new string[] { + table65.AddRow(new string[] { "T", "1", ""}); #line 128 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table25, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table65, "Given "); #line hidden - global::Reqnroll.Table table26 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table66 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table26.AddRow(new string[] { + table66.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 134 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table26, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table66, "And "); #line hidden - global::Reqnroll.Table table27 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table67 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table27.AddRow(new string[] { + table67.AddRow(new string[] { "H", "20210508", ""}); - table27.AddRow(new string[] { + table67.AddRow(new string[] { "D", "07777777775", "100"}); - table27.AddRow(new string[] { + table67.AddRow(new string[] { "D", "07777777776", "200"}); - table27.AddRow(new string[] { + table67.AddRow(new string[] { "T", "1", ""}); #line 140 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table27, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup2.txt\' with the following contents", ((string)(null)), table67, "Given "); #line hidden - global::Reqnroll.Table table28 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table68 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table28.AddRow(new string[] { + table68.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 146 - await testRunner.AndAsync("I upload this file for processing an error should be returned indicating the file" + - " is a duplicate", ((string)(null)), table28, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table68, "And "); #line hidden } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Process Safaricom Topup File with Upload Date Time")] public async global::System.Threading.Tasks.Task ProcessSafaricomTopupFileWithUploadDateTime() @@ -734,77 +733,77 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table29 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table69 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table29.AddRow(new string[] { + table69.AddRow(new string[] { "H", "20210508", ""}); - table29.AddRow(new string[] { + table69.AddRow(new string[] { "D", "07777777775", "100"}); - table29.AddRow(new string[] { + table69.AddRow(new string[] { "T", "1", ""}); #line 152 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table29, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup.txt\' with the following contents", ((string)(null)), table69, "Given "); #line hidden - global::Reqnroll.Table table30 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table70 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId", "UploadDateTime"}); - table30.AddRow(new string[] { + table70.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476", "Today"}); #line 157 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table30, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table70, "And "); #line hidden #line 161 await testRunner.WhenAsync("I get the import log for estate \'Test Estate 1\' the date on the import log is \'To" + "day\'", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); #line hidden - global::Reqnroll.Table table31 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table71 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3"}); - table31.AddRow(new string[] { + table71.AddRow(new string[] { "H", "20210508", ""}); - table31.AddRow(new string[] { + table71.AddRow(new string[] { "D", "07777777775", "200"}); - table31.AddRow(new string[] { + table71.AddRow(new string[] { "T", "1", ""}); #line 163 - await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table31, "Given "); + await testRunner.GivenAsync("I have a file named \'SafarcomTopup1.txt\' with the following contents", ((string)(null)), table71, "Given "); #line hidden - global::Reqnroll.Table table32 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table72 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId", "UploadDateTime"}); - table32.AddRow(new string[] { + table72.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476", "01/09/2021"}); #line 168 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table32, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table72, "And "); #line hidden #line 172 await testRunner.WhenAsync("I get the import log for estate \'Test Estate 1\' the date on the import log is \'01" + @@ -813,7 +812,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Upload a generated Safaricom Topup file with 1000 detail rows")] [global::NUnit.Framework.CategoryAttribute("largefile")] @@ -841,39 +840,39 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table33 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table73 = new global::Reqnroll.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table33.AddRow(new string[] { + table73.AddRow(new string[] { "Deposit1", "600000.00", "Today", "Test Merchant 1", "Test Estate 1"}); #line 175 - await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table33, "Given "); + await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table73, "Given "); #line hidden #line 178 await testRunner.GivenAsync("I have a generated Safaricom Topup file named \'LargeSafarcomTopup.txt\' with 1000 " + "detail rows", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); #line hidden - global::Reqnroll.Table table34 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table74 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table34.AddRow(new string[] { + table74.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 179 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table34, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table74, "And "); #line hidden - global::Reqnroll.Table table35 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table75 = new global::Reqnroll.Table(new string[] { "ProcessingCompleted", "NumberOfLines", "TotaLines", @@ -881,7 +880,7 @@ await testRunner.GivenAsync("I have a generated Safaricom Topup file named \'Lar "IgnoredLines", "FailedLines", "NotProcessedLines"}); - table35.AddRow(new string[] { + table75.AddRow(new string[] { "True", "1002", "1002", @@ -891,12 +890,12 @@ await testRunner.GivenAsync("I have a generated Safaricom Topup file named \'Lar "0"}); #line 183 await testRunner.WhenAsync("I get the file \'LargeSafarcomTopup.txt\' for Estate \'Test Estate 1\' the following " + - "file information is returned", ((string)(null)), table35, "When "); + "file information is returned", ((string)(null)), table75, "When "); #line hidden } await this.ScenarioCleanupAsync(); } - + [global::NUnit.Framework.TestAttribute()] [global::NUnit.Framework.DescriptionAttribute("Reject a generated Safaricom Topup file with more than 1000 detail rows")] [global::NUnit.Framework.CategoryAttribute("largefile")] @@ -924,37 +923,37 @@ await testRunner.GivenAsync("I have a generated Safaricom Topup file named \'Lar #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table36 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table76 = new global::Reqnroll.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table36.AddRow(new string[] { + table76.AddRow(new string[] { "Deposit1", "600000.00", "Today", "Test Merchant 1", "Test Estate 1"}); #line 189 - await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table36, "Given "); + await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table76, "Given "); #line hidden #line 192 await testRunner.GivenAsync("I have a generated Safaricom Topup file named \'OversizedSafarcomTopup.txt\' with 1" + "001 detail rows", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); #line hidden - global::Reqnroll.Table table37 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table77 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table37.AddRow(new string[] { + table77.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 193 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table37, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table77, "And "); #line hidden #line 197 await testRunner.WhenAsync("I get the file \'OversizedSafarcomTopup.txt\' for Estate \'Test Estate 1\' it is reje" + diff --git a/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature.cs b/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature.cs index 88e63fb..5d64d78 100644 --- a/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature.cs +++ b/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature.cs @@ -115,66 +115,66 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa { #line 4 #line hidden - global::Reqnroll.Table table72 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table78 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Description"}); - table72.AddRow(new string[] { + table78.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST Scope", "A scope for Transaction Processor REST"}); - table72.AddRow(new string[] { + table78.AddRow(new string[] { "fileProcessor", "File Processor REST Scope", "A scope for File Processor REST"}); #line 5 - await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table72, "Given "); + await testRunner.GivenAsync("I create the following api scopes", ((string)(null)), table78, "Given "); #line hidden - global::Reqnroll.Table table73 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table79 = new global::Reqnroll.Table(new string[] { "Name", "DisplayName", "Secret", "Scopes", "UserClaims"}); - table73.AddRow(new string[] { + table79.AddRow(new string[] { "transactionProcessor", "Transaction Processor REST", "Secret1", "transactionProcessor", "MerchantId, EstateId, role"}); - table73.AddRow(new string[] { + table79.AddRow(new string[] { "fileProcessor", "File Processor REST", "Secret1", "fileProcessor", ""}); #line 10 - await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table73, "Given "); + await testRunner.GivenAsync("the following api resources exist", ((string)(null)), table79, "Given "); #line hidden - global::Reqnroll.Table table74 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table80 = new global::Reqnroll.Table(new string[] { "ClientId", "ClientName", "Secret", "Scopes", "GrantTypes"}); - table74.AddRow(new string[] { + table80.AddRow(new string[] { "serviceClient", "Service Client", "Secret1", "transactionProcessor,fileProcessor", "client_credentials"}); #line 15 - await testRunner.GivenAsync("the following clients exist", ((string)(null)), table74, "Given "); + await testRunner.GivenAsync("the following clients exist", ((string)(null)), table80, "Given "); #line hidden - global::Reqnroll.Table table75 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table81 = new global::Reqnroll.Table(new string[] { "ClientId"}); - table75.AddRow(new string[] { + table81.AddRow(new string[] { "serviceClient"}); #line 19 await testRunner.GivenAsync("I have a token to access the estate management and transaction processor resource" + - "s", ((string)(null)), table75, "Given "); + "s", ((string)(null)), table81, "Given "); #line hidden - global::Reqnroll.Table table76 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table82 = new global::Reqnroll.Table(new string[] { "Alias", "FileProfileId", "Name", @@ -183,7 +183,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "OperatorName", "LineTerminator", "FileFormatHandler"}); - table76.AddRow(new string[] { + table82.AddRow(new string[] { "Safaricom Topup", "B2A59ABF-293D-4A6B-B81B-7007503C3476", "Safaricom Topup", @@ -192,7 +192,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Safaricom", "1", "SafaricomFileFormatHandler"}); - table76.AddRow(new string[] { + table82.AddRow(new string[] { "Voucher Issue", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "Voucher Issue", @@ -202,61 +202,61 @@ await testRunner.GivenAsync("I have a token to access the estate management and "1", "VoucherFileFormatHandler"}); #line 23 - await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table76, "When "); + await testRunner.WhenAsync("I create the following file profiles", ((string)(null)), table82, "When "); #line hidden - global::Reqnroll.Table table77 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table83 = new global::Reqnroll.Table(new string[] { "EstateName"}); - table77.AddRow(new string[] { + table83.AddRow(new string[] { "Test Estate 1"}); #line 28 - await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table77, "Given "); + await testRunner.GivenAsync("I have created the following estates", ((string)(null)), table83, "Given "); #line hidden - global::Reqnroll.Table table78 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table84 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "RequireCustomMerchantNumber", "RequireCustomTerminalNumber"}); - table78.AddRow(new string[] { + table84.AddRow(new string[] { "Test Estate 1", "Safaricom", "True", "True"}); - table78.AddRow(new string[] { + table84.AddRow(new string[] { "Test Estate 1", "Voucher", "True", "True"}); #line 32 - await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table78, "Given "); + await testRunner.GivenAsync("I have created the following operators", ((string)(null)), table84, "Given "); #line hidden - global::Reqnroll.Table table79 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table85 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName"}); - table79.AddRow(new string[] { + table85.AddRow(new string[] { "Test Estate 1", "Safaricom"}); - table79.AddRow(new string[] { + table85.AddRow(new string[] { "Test Estate 1", "Voucher"}); #line 37 - await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table79, "And "); + await testRunner.AndAsync("I have assigned the following operators to the estates", ((string)(null)), table85, "And "); #line hidden - global::Reqnroll.Table table80 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table86 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription"}); - table80.AddRow(new string[] { + table86.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract"}); - table80.AddRow(new string[] { + table86.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract"}); #line 42 - await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table80, "Given "); + await testRunner.GivenAsync("I create a contract with the following values", ((string)(null)), table86, "Given "); #line hidden - global::Reqnroll.Table table81 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table87 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -264,7 +264,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "DisplayText", "Value", "ProductType"}); - table81.AddRow(new string[] { + table87.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -272,7 +272,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Custom", "", "MobileTopup"}); - table81.AddRow(new string[] { + table87.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract", @@ -281,9 +281,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "", "Voucher"}); #line 47 - await testRunner.WhenAsync("I create the following Products", ((string)(null)), table81, "When "); + await testRunner.WhenAsync("I create the following Products", ((string)(null)), table87, "When "); #line hidden - global::Reqnroll.Table table82 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table88 = new global::Reqnroll.Table(new string[] { "EstateName", "OperatorName", "ContractDescription", @@ -291,7 +291,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "CalculationType", "FeeDescription", "Value"}); - table82.AddRow(new string[] { + table88.AddRow(new string[] { "Test Estate 1", "Safaricom", "Safaricom Contract", @@ -299,7 +299,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Fixed", "Merchant Commission", "2.50"}); - table82.AddRow(new string[] { + table88.AddRow(new string[] { "Test Estate 1", "Voucher", "Hospital 1 Contract", @@ -308,9 +308,9 @@ await testRunner.GivenAsync("I have a token to access the estate management and "Merchant Commission", "2.50"}); #line 52 - await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table82, "When "); + await testRunner.WhenAsync("I add the following Transaction Fees", ((string)(null)), table88, "When "); #line hidden - global::Reqnroll.Table table83 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table89 = new global::Reqnroll.Table(new string[] { "MerchantName", "AddressLine1", "Town", @@ -320,7 +320,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and "ContactName", "EmailAddress", "EstateName"}); - table83.AddRow(new string[] { + table89.AddRow(new string[] { "Test Merchant 1", "Address Line 1", "TestTown", @@ -331,69 +331,69 @@ await testRunner.GivenAsync("I have a token to access the estate management and "testcontact1@merchant1.co.uk", "Test Estate 1"}); #line 57 - await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table83, "Given "); + await testRunner.GivenAsync("I create the following merchants", ((string)(null)), table89, "Given "); #line hidden - global::Reqnroll.Table table84 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table90 = new global::Reqnroll.Table(new string[] { "OperatorName", "MerchantName", "MerchantNumber", "TerminalNumber", "EstateName"}); - table84.AddRow(new string[] { + table90.AddRow(new string[] { "Safaricom", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); - table84.AddRow(new string[] { + table90.AddRow(new string[] { "Voucher", "Test Merchant 1", "00000001", "10000001", "Test Estate 1"}); #line 61 - await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table84, "Given "); + await testRunner.GivenAsync("I have assigned the following operator to the merchants", ((string)(null)), table90, "Given "); #line hidden - global::Reqnroll.Table table85 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table91 = new global::Reqnroll.Table(new string[] { "DeviceIdentifier", "MerchantName", "EstateName"}); - table85.AddRow(new string[] { + table91.AddRow(new string[] { "123456780", "Test Merchant 1", "Test Estate 1"}); #line 66 - await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table85, "Given "); + await testRunner.GivenAsync("I have assigned the following devices to the merchants", ((string)(null)), table91, "Given "); #line hidden - global::Reqnroll.Table table86 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table92 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "ContractDescription"}); - table86.AddRow(new string[] { + table92.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Safaricom Contract"}); - table86.AddRow(new string[] { + table92.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "Hospital 1 Contract"}); #line 70 - await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table86, "When "); + await testRunner.WhenAsync("I add the following contracts to the following merchants", ((string)(null)), table92, "When "); #line hidden - global::Reqnroll.Table table87 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table93 = new global::Reqnroll.Table(new string[] { "Reference", "Amount", "DateTime", "MerchantName", "EstateName"}); - table87.AddRow(new string[] { + table93.AddRow(new string[] { "Deposit1", "300.00", "Today", "Test Merchant 1", "Test Estate 1"}); #line 75 - await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table87, "Given "); + await testRunner.GivenAsync("I make the following manual merchant deposits", ((string)(null)), table93, "Given "); #line hidden } @@ -425,41 +425,41 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table88 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table94 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table88.AddRow(new string[] { + table94.AddRow(new string[] { "H", "20210508", "", ""}); - table88.AddRow(new string[] { + table94.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "10"}); - table88.AddRow(new string[] { + table94.AddRow(new string[] { "T", "1", "", ""}); #line 80 - await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table88, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table94, "Given "); #line hidden - global::Reqnroll.Table table89 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table95 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table89.AddRow(new string[] { + table95.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 85 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table89, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table95, "And "); #line hidden } await this.ScenarioCleanupAsync(); @@ -488,41 +488,41 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table90 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table96 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table90.AddRow(new string[] { + table96.AddRow(new string[] { "H", "20210508", "", ""}); - table90.AddRow(new string[] { + table96.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table90.AddRow(new string[] { + table96.AddRow(new string[] { "T", "1", "", ""}); #line 92 - await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table90, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table96, "Given "); #line hidden - global::Reqnroll.Table table91 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table97 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table91.AddRow(new string[] { + table97.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 97 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table91, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table97, "And "); #line hidden } await this.ScenarioCleanupAsync(); @@ -551,46 +551,46 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table92 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table98 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table92.AddRow(new string[] { + table98.AddRow(new string[] { "H", "20210508", "", ""}); - table92.AddRow(new string[] { + table98.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table92.AddRow(new string[] { + table98.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "10"}); - table92.AddRow(new string[] { + table98.AddRow(new string[] { "T", "1", "", ""}); #line 104 - await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table92, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue.txt\' with the following contents", ((string)(null)), table98, "Given "); #line hidden - global::Reqnroll.Table table93 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table99 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table93.AddRow(new string[] { + table99.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 110 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table93, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table99, "And "); #line hidden } await this.ScenarioCleanupAsync(); @@ -619,87 +619,87 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table94 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table100 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table94.AddRow(new string[] { + table100.AddRow(new string[] { "H", "20210508", "", ""}); - table94.AddRow(new string[] { + table100.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table94.AddRow(new string[] { + table100.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "10"}); - table94.AddRow(new string[] { + table100.AddRow(new string[] { "T", "1", "", ""}); #line 117 - await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table94, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table100, "Given "); #line hidden - global::Reqnroll.Table table95 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table101 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table95.AddRow(new string[] { + table101.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 123 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table95, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table101, "And "); #line hidden - global::Reqnroll.Table table96 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table102 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table96.AddRow(new string[] { + table102.AddRow(new string[] { "H", "20210508", "", ""}); - table96.AddRow(new string[] { + table102.AddRow(new string[] { "D", "Hospital 1", "07777777775", "20"}); - table96.AddRow(new string[] { + table102.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "20"}); - table96.AddRow(new string[] { + table102.AddRow(new string[] { "T", "1", "", ""}); #line 127 - await testRunner.GivenAsync("I have a file named \'VoucherIssue2.txt\' with the following contents", ((string)(null)), table96, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue2.txt\' with the following contents", ((string)(null)), table102, "Given "); #line hidden - global::Reqnroll.Table table97 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table103 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table97.AddRow(new string[] { + table103.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 133 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table97, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table103, "And "); #line hidden } await this.ScenarioCleanupAsync(); @@ -730,88 +730,87 @@ await testRunner.GivenAsync("I have a token to access the estate management and #line 4 await this.FeatureBackgroundAsync(); #line hidden - global::Reqnroll.Table table98 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table104 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table98.AddRow(new string[] { + table104.AddRow(new string[] { "H", "20210508", "", ""}); - table98.AddRow(new string[] { + table104.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table98.AddRow(new string[] { + table104.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "20"}); - table98.AddRow(new string[] { + table104.AddRow(new string[] { "T", "1", "", ""}); #line 141 - await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table98, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue1.txt\' with the following contents", ((string)(null)), table104, "Given "); #line hidden - global::Reqnroll.Table table99 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table105 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table99.AddRow(new string[] { + table105.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 147 - await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table99, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table105, "And "); #line hidden - global::Reqnroll.Table table100 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table106 = new global::Reqnroll.Table(new string[] { "Column1", "Column2", "Column3", "Column4"}); - table100.AddRow(new string[] { + table106.AddRow(new string[] { "H", "20210508", "", ""}); - table100.AddRow(new string[] { + table106.AddRow(new string[] { "D", "Hospital 1", "07777777775", "10"}); - table100.AddRow(new string[] { + table106.AddRow(new string[] { "D", "Hospital 1", "testrecipient1@recipient.com", "20"}); - table100.AddRow(new string[] { + table106.AddRow(new string[] { "T", "1", "", ""}); #line 153 - await testRunner.GivenAsync("I have a file named \'VoucherIssue2.txt\' with the following contents", ((string)(null)), table100, "Given "); + await testRunner.GivenAsync("I have a file named \'VoucherIssue2.txt\' with the following contents", ((string)(null)), table106, "Given "); #line hidden - global::Reqnroll.Table table101 = new global::Reqnroll.Table(new string[] { + global::Reqnroll.Table table107 = new global::Reqnroll.Table(new string[] { "EstateName", "MerchantName", "FileProfileId", "UserId"}); - table101.AddRow(new string[] { + table107.AddRow(new string[] { "Test Estate 1", "Test Merchant 1", "8806EDBC-3ED6-406B-9E5F-A9078356BE99", "ABA59ABF-293D-4A6B-B81B-7007503C3476"}); #line 160 - await testRunner.AndAsync("I upload this file for processing an error should be returned indicating the file" + - " is a duplicate", ((string)(null)), table101, "And "); + await testRunner.AndAsync("I upload this file for processing", ((string)(null)), table107, "And "); #line hidden } await this.ScenarioCleanupAsync(); diff --git a/FileProcessor/Bootstrapper/DomainEventHandlerRegistry.cs b/FileProcessor/Bootstrapper/DomainEventHandlerRegistry.cs index 6a5e18a..cd459c8 100644 --- a/FileProcessor/Bootstrapper/DomainEventHandlerRegistry.cs +++ b/FileProcessor/Bootstrapper/DomainEventHandlerRegistry.cs @@ -19,17 +19,17 @@ public class DomainEventHandlerRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public DomainEventHandlerRegistry() + public DomainEventHandlerRegistry(IConfiguration configuration) { Dictionary eventHandlersConfiguration = new Dictionary(); - if (Startup.Configuration != null) + if (configuration != null) { - IConfigurationSection section = Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration"); + IConfigurationSection section = configuration.GetSection("AppSettings:EventHandlerConfiguration"); if (section != null) { - Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration").Bind(eventHandlersConfiguration); + configuration.GetSection("AppSettings:EventHandlerConfiguration").Bind(eventHandlersConfiguration); } } @@ -48,4 +48,4 @@ public DomainEventHandlerRegistry() } #endregion -} \ No newline at end of file +} diff --git a/FileProcessor/Bootstrapper/MiddlewareRegistry.cs b/FileProcessor/Bootstrapper/MiddlewareRegistry.cs index 2c56b4e..df0b82a 100644 --- a/FileProcessor/Bootstrapper/MiddlewareRegistry.cs +++ b/FileProcessor/Bootstrapper/MiddlewareRegistry.cs @@ -1,6 +1,8 @@ using HealthMonitoring.Client; using KurrentDB.Client; using Microsoft.OpenApi; +using Microsoft.Extensions.Configuration; +using TransactionProcessing.ApiDefaults; namespace FileProcessor.Bootstrapper { @@ -30,7 +32,7 @@ public class MiddlewareRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public MiddlewareRegistry() + public MiddlewareRegistry(IConfiguration configuration) { String eventStoreConnectionString = ConfigurationReader.GetValue("EventStoreSettings", "ConnectionString"); @@ -42,35 +44,7 @@ public MiddlewareRegistry() failureStatus:HealthStatus.Unhealthy, tags:new[] {"db", "eventstore"}).AddTransactionProcessorService() .AddSecurityService(this.ApiEndpointHttpHandler); - - this.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", - new OpenApiInfo - { - Title = "File Processor API", - Version = "1.0", - Description = "A REST Api to manage importing and processing bulk transaction files.", - Contact = new OpenApiContact - { - Name = "Stuart Ferguson", - Email = "golfhandicapping@btinternet.com" - } - }); - // add a custom operation filter which sets default values - - //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); - } - }); - - + this.AddAuthentication(options => { options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; @@ -119,7 +93,12 @@ public MiddlewareRegistry() options.MemoryBufferThreshold = 64 * 1024; }); - this.AddHealthMonitoringRegistration(Startup.Configuration); + this.AddHealthMonitoringRegistration(configuration); + + this.AddApiDefaults(options => { + options.Title = "File Processor"; + options.Description = "API for all aspects of bulk file processing"; + }); } #endregion diff --git a/FileProcessor/Bootstrapper/RepositoryRegistry.cs b/FileProcessor/Bootstrapper/RepositoryRegistry.cs index 44a1054..5cd7a12 100644 --- a/FileProcessor/Bootstrapper/RepositoryRegistry.cs +++ b/FileProcessor/Bootstrapper/RepositoryRegistry.cs @@ -20,6 +20,7 @@ namespace FileProcessor.Bootstrapper; using System; using System.Diagnostics.CodeAnalysis; using FileProfileAggregateRoot = FileProcessor.FileProfileAggregate.FileProfileAggregate; +using Microsoft.AspNetCore.Hosting; [ExcludeFromCodeCoverage] public class RepositoryRegistry : ServiceRegistry @@ -29,10 +30,10 @@ public class RepositoryRegistry : ServiceRegistry /// /// Initializes a new instance of the class. /// - public RepositoryRegistry() + public RepositoryRegistry(IConfiguration configuration, IWebHostEnvironment environment) { this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolver<>)); - if (Startup.WebHostEnvironment.IsEnvironment("IntegrationTest") || Startup.Configuration.GetValue("ServiceOptions:UseInMemoryDatabase") == true) + if (environment.IsEnvironment("IntegrationTest") || configuration.GetValue("ServiceOptions:UseInMemoryDatabase") == true) { this.AddDbContext(builder => builder.UseInMemoryDatabase("TransactionProcessorReadModel")); } @@ -42,7 +43,7 @@ public RepositoryRegistry() options.UseSqlServer(ConfigurationReader.GetConnectionString("TransactionProcessorReadModel"))); } - String connectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + String connectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); this.AddKurrentDBProjectionManagementClient(connectionString); this.AddKurrentDBPersistentSubscriptionsClient(connectionString); diff --git a/FileProcessor/Common/Extensions.cs b/FileProcessor/Common/Extensions.cs index 401596c..c59fe2a 100644 --- a/FileProcessor/Common/Extensions.cs +++ b/FileProcessor/Common/Extensions.cs @@ -66,10 +66,13 @@ public static IServiceCollection AddInSecureEventStoreClient( } }; - public static void PreWarm(this IApplicationBuilder applicationBuilder){ - IFileSystem fileSystem = Startup.Container.GetInstance(); - IFileProfileDirectorySynchronizer directorySynchronizer = Startup.Container.GetInstance(); - IFileProfileDirectoryRecoveryService recoveryService = Startup.Container.GetInstance(); + public static void PreWarm(this IApplicationBuilder applicationBuilder, + IFileSystem fileSystem, + IFileProfileDirectorySynchronizer directorySynchronizer, + IFileProfileDirectoryRecoveryService recoveryService, + IConfiguration configuration, + IDomainEventHandlerResolver mainEventHandlerResolver, + Func subscriptionRepositoryResolver){ var temporaryFileLocation = ConfigurationReader.GetValue("AppSettings", "TemporaryFileLocation"); Logger.LogInformation($"Starting up, TemporaryFileLocation is [{temporaryFileLocation}]"); @@ -90,20 +93,16 @@ public static void PreWarm(this IApplicationBuilder applicationBuilder){ TypeProvider.LoadDomainEventsTypeDynamically(); - IConfigurationSection subscriptionConfigSection = Startup.Configuration.GetSection("AppSettings:SubscriptionConfiguration"); + 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"); - Dictionary eventHandlerResolvers = new Dictionary { {"Main", mainEventHandlerResolver} }; - Func subscriptionRepositoryResolver = Startup.Container.GetInstance>(); - applicationBuilder.ConfigureSubscriptionService(subscriptionWorkersRoot, eventStoreConnectionString, eventHandlerResolvers, diff --git a/FileProcessor/Common/ModelFactory.cs b/FileProcessor/Common/ModelFactory.cs index 6c9d912..327ee7d 100644 --- a/FileProcessor/Common/ModelFactory.cs +++ b/FileProcessor/Common/ModelFactory.cs @@ -8,11 +8,35 @@ namespace FileProcessor.Common using FileImportLogResponse = DataTransferObjects.Responses.FileImportLog; using FileDetailsResponse = DataTransferObjects.Responses.FileDetails; using FileLineResponse = DataTransferObjects.Responses.FileLine; + using FileProfileResponse = DataTransferObjects.Responses.FileProfileResponse; public static class ModelFactory { #region Methods + public static List ConvertFrom(List fileProfiles) + { + return fileProfiles.Select(ConvertFrom).ToList(); + } + + public static FileProfileResponse ConvertFrom(Models.FileProfile fileProfile) + { + return new FileProfileResponse + { + FileProfileId = fileProfile.FileProfileId, + FileProfileVersionId = fileProfile.FileProfileVersionId, + FileProfileVersion = fileProfile.FileProfileVersion, + Name = fileProfile.Name, + OperatorName = fileProfile.OperatorName, + ListeningDirectory = fileProfile.ListeningDirectory, + ProcessedDirectory = fileProfile.ProcessedDirectory, + FailedDirectory = fileProfile.FailedDirectory, + FileFormatHandler = fileProfile.FileFormatHandler, + RequestType = fileProfile.RequestType, + LineTerminator = fileProfile.LineTerminator + }; + } + /// /// Converts from. /// diff --git a/FileProcessor/Endpoints/DomainEventEndpoints.cs b/FileProcessor/Endpoints/DomainEventEndpoints.cs index e7785e9..ecfb303 100644 --- a/FileProcessor/Endpoints/DomainEventEndpoints.cs +++ b/FileProcessor/Endpoints/DomainEventEndpoints.cs @@ -13,7 +13,11 @@ public static class DomainEventEndpoints public static void MapDomainEventEndpoint(this IEndpointRouteBuilder app) { app.MapPost(Route, DomainEventHandlers.HandleDomainEvent) - .ExcludeFromDescription() // hides from Swagger - .WithName("DomainEvent"); + .WithName("DomainEvent") + .WithSummary("Process a domain event") + .WithDescription("Internal event-delivery endpoint. Send the event JSON body and identify its registered event type in the `eventType` request header. Returns 200 when all configured handlers succeed or no handlers are configured; returns 500 when one or more handlers fail.") + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithEndpointExamples(); } -} \ No newline at end of file +} diff --git a/FileProcessor/Endpoints/FileEndpoints.cs b/FileProcessor/Endpoints/FileEndpoints.cs index a160268..6567ed0 100644 --- a/FileProcessor/Endpoints/FileEndpoints.cs +++ b/FileProcessor/Endpoints/FileEndpoints.cs @@ -1,5 +1,6 @@ using System; using FileProcessor.Handlers; +using FileProcessor.DataTransferObjects.Responses; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -18,13 +19,27 @@ public static void MapFileEndpoints(this IEndpointRouteBuilder app) group.MapPost("/", FileHandlers.UploadFile) .DisableAntiforgery() - .Accepts("multipart/form-data") - .WithName("UploadFile"); + .Accepts("multipart/form-data") + .WithName("UploadFile") + .WithSummary("Upload a transaction file") + .WithDescription("Requires authorization. Submit the upload metadata fields from `UploadFileRequest` and exactly one non-empty file as multipart form data. The file name must have a configured allowed extension and the file must be within the configured size limit. The upload time defaults to the current local time when omitted or `DateTime.MinValue`. Returns 202 Accepted with the file ID when the upload is accepted.") + .Produces(StatusCodes.Status202Accepted) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status413PayloadTooLarge) + .WithEndpointExamples(); group.MapGet("/{fileId:guid}", FileHandlers.GetFile) - .WithName("GetFile"); + .WithName("GetFile") + .WithSummary("Get a file and its processing details") + .WithDescription("Requires authorization. Required GUID path parameter `fileId` identifies the file; required GUID query parameter `estateId` identifies the estate that owns it. Returns the file metadata, processing state, line results, and processing summary.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapPost("/{fileId:guid}/lines/{lineNumber:int}/retry", FileHandlers.ReplayLine) - .WithName("ReplayLine"); + .WithName("ReplayLine") + .WithSummary("Retry a file line") + .WithDescription("Requires authorization. Required GUID path parameter `fileId` and integer path parameter `lineNumber` identify the file and line to replay; required GUID query parameter `estateId` identifies the owning estate. The request has no body. Returns the operation result.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); } } diff --git a/FileProcessor/Endpoints/FileImportLogEndpoints.cs b/FileProcessor/Endpoints/FileImportLogEndpoints.cs index f227850..029bd8c 100644 --- a/FileProcessor/Endpoints/FileImportLogEndpoints.cs +++ b/FileProcessor/Endpoints/FileImportLogEndpoints.cs @@ -1,5 +1,6 @@ using System; using FileProcessor.Handlers; +using FileProcessor.DataTransferObjects.Responses; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -17,9 +18,17 @@ public static void MapFileImportLogEndpoints(this IEndpointRouteBuilder app) .RequireAuthorization(); group.MapGet("/", FileImportLogHandlers.GetImportLogsAsync) - .WithName("GetImportLogs"); + .WithName("GetImportLogs") + .WithSummary("List file import logs") + .WithDescription("Requires authorization. Query parameters: `estateId` (required), `startDate` and `endDate` (required ISO dates; end date must be on or after start date), `merchantId` (optional), `pageNumber` (defaults to 1), `pageSize` (defaults to 50; allowed range 1–200), and `descending` (defaults to false). The date range includes both calendar dates. Returns a page of import logs and pagination metadata.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapGet("/{fileImportLogId:guid}", FileImportLogHandlers.GetImportLogAsync) - .WithName("GetImportLog"); + .WithName("GetImportLog") + .WithSummary("Get a file import log") + .WithDescription("Requires authorization. Required GUID path parameter `fileImportLogId` identifies the log; required GUID query parameter `estateId` scopes the lookup, and optional GUID query parameter `merchantId` filters its files. Returns the import log and its files.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); } -} \ No newline at end of file +} diff --git a/FileProcessor/Endpoints/FileProfileEndpoints.cs b/FileProcessor/Endpoints/FileProfileEndpoints.cs index 3897295..f9ed473 100644 --- a/FileProcessor/Endpoints/FileProfileEndpoints.cs +++ b/FileProcessor/Endpoints/FileProfileEndpoints.cs @@ -1,5 +1,6 @@ -using System; +using System; using FileProcessor.Handlers; +using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -17,24 +18,52 @@ public static void MapFileProfileEndpoints(this IEndpointRouteBuilder app) .RequireAuthorization(); group.MapGet("/", FileProfileHandlers.GetFileProfiles) - .WithName("GetFileProfiles"); + .WithName("GetFileProfiles") + .WithSummary("List file profiles") + .WithDescription("Requires authorization. The request has no path parameters, query parameters, or body. Returns all available file profile versions.") + .Produces>(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapGet("/{fileProfileId:guid}", FileProfileHandlers.GetFileProfile) - .WithName("GetFileProfile"); + .WithName("GetFileProfile") + .WithSummary("Get the current file profile") + .WithDescription("Requires authorization. Path parameter `fileProfileId` identifies the file profile. The request has no query parameters or body. Returns the current profile details.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapGet("/{fileProfileId:guid}/versions", FileProfileHandlers.GetFileProfileVersions) - .WithName("GetFileProfileVersions"); + .WithName("GetFileProfileVersions") + .WithSummary("List file profile versions") + .WithDescription("Requires authorization. Path parameter `fileProfileId` identifies the file profile. The request has no query parameters or body. Returns the profile's versions.") + .Produces>(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapGet("/{fileProfileId:guid}/versions/{fileProfileVersionId:guid}", FileProfileHandlers.GetFileProfileVersion) - .WithName("GetFileProfileVersion"); + .WithName("GetFileProfileVersion") + .WithSummary("Get a file profile version") + .WithDescription("Requires authorization. Path parameters `fileProfileId` and `fileProfileVersionId` identify the profile and version. The request has no query parameters or body. Returns the selected version details.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapPost("/", FileProfileHandlers.CreateFileProfile) - .WithName("CreateFileProfile"); + .WithName("CreateFileProfile") + .WithSummary("Create a file profile") + .WithDescription("Requires authorization. Supply a `CreateFileProfileRequest` JSON body containing the new profile identifier and configuration. The request has no path or query parameters. Returns the created profile.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapPatch("/{fileProfileId:guid}", FileProfileHandlers.UpdateFileProfile) - .WithName("UpdateFileProfile"); + .WithName("UpdateFileProfile") + .WithSummary("Update a file profile") + .WithDescription("Requires authorization. Path parameter `fileProfileId` identifies the profile. Supply an `UpdateFileProfileRequest` JSON body with the profile configuration to apply. The request has no query parameters. Returns the updated profile.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); group.MapDelete("/{fileProfileId:guid}", FileProfileHandlers.ArchiveFileProfile) - .WithName("ArchiveFileProfile"); + .WithName("ArchiveFileProfile") + .WithSummary("Archive a file profile") + .WithDescription("Requires authorization. Path parameter `fileProfileId` identifies the profile. The request has no query parameters or body. Returns 204 No Content when the profile is archived successfully.") + .Produces(StatusCodes.Status204NoContent) + .WithEndpointExamples(); } } diff --git a/FileProcessor/Endpoints/MigrationEndpoints.cs b/FileProcessor/Endpoints/MigrationEndpoints.cs index a12ce7e..b581a05 100644 --- a/FileProcessor/Endpoints/MigrationEndpoints.cs +++ b/FileProcessor/Endpoints/MigrationEndpoints.cs @@ -1,5 +1,6 @@ -using System; +using System; using FileProcessor.Handlers; +using FileProcessor.BusinessLogic.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -17,6 +18,10 @@ public static void MapMigrationEndpoints(this IEndpointRouteBuilder app) .RequireAuthorization() .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) .WithTags("Migrations") - .WithName("MigrateProfileVersions"); + .WithName("MigrateProfileVersions") + .WithSummary("Migrate file profile versions") + .WithDescription("Requires the client-credentials-only authorization policy. The request has no path parameters, query parameters, or body. Runs the file profile version migration and returns its report.") + .Produces(StatusCodes.Status200OK) + .WithEndpointExamples(); } } diff --git a/FileProcessor/Endpoints/OpenApiExampleExtensions.cs b/FileProcessor/Endpoints/OpenApiExampleExtensions.cs new file mode 100644 index 0000000..8c0b446 --- /dev/null +++ b/FileProcessor/Endpoints/OpenApiExampleExtensions.cs @@ -0,0 +1,488 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace FileProcessor.Endpoints; + +/// Adds the documented request, parameter, and response examples to an endpoint. +public static class OpenApiExampleExtensions +{ + private const string EstateIdParameterName = "estateId"; + private const string FileProfileIdParameterName = "fileProfileId"; + private const string EstateIdExampleValue = "435613ac-a468-47a3-ac4f-649d89764c22"; + private const string FileProfileIdExampleValue = "9c89c59e-8608-47b1-b280-052a75bcf84b"; + + private static readonly IReadOnlyDictionary Examples = new Dictionary + { + ["DomainEvent"] = new() + { + RequestBody = """ + { + "fileId": "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + "estateId": "435613ac-a468-47a3-ac4f-649d89764c22", + "merchantId": "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + "lineNumber": 1, + "fileLine": "D,Example Operator,01234567890,10.00", + "EventId": "42cae457-c061-42d0-b544-f1767d812ae5" + } + """, + Parameters = new Dictionary { ["eventType"] = "FileLineAddedEvent" }, + Responses = new Dictionary + { + ["500"] = """ + { + "type": "about:blank", + "title": "One or more event handlers failed", + "status": 500, + "eventId": "42cae457-c061-42d0-b544-f1767d812ae5", + "eventType": "FileLineAddedEvent", + "failures": [ + { "handler": "FileDomainEventHandler", "error": "The transaction could not be processed." } + ] + } + """ + } + }, + ["MigrateProfileVersions"] = new() + { + Responses = new Dictionary + { + ["200"] = """ + { + "profiles_checked": 12, + "profiles_with_initial_version": 12, + "files_checked": 48, + "files_assigned": 45, + "files_skipped": 3, + "failures": [] + } + """ + } + }, + ["UploadFile"] = new() + { + RequestBody = """ + { + "FileId": "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + "EstateId": "435613ac-a468-47a3-ac4f-649d89764c22", + "MerchantId": "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + "UserId": "a782891c-5c7d-45de-b002-1585a6db28df", + "FileProfileId": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "UploadDateTime": "2026-10-22T09:30:00Z", + "File": "H,Operator,Phone,Amount\nD,Example Operator,01234567890,10.00" + } + """, + Responses = new Dictionary + { + ["202"] = "\"c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90\"", + ["400"] = "\"The uploaded file must not be empty.\"" + } + }, + ["GetFile"] = new() + { + Parameters = new Dictionary + { + ["fileId"] = "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + [EstateIdParameterName] = EstateIdExampleValue + }, + Responses = new Dictionary + { + ["200"] = """ + { + "file_id": "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + "processing_completed": true, + "is_rejected": false, + "rejection_code": null, + "rejection_reason": null, + "observed_detail_record_count": 1, + "maximum_detail_record_count": 1000, + "estate_id": "435613ac-a468-47a3-ac4f-649d89764c22", + "user_id": "a782891c-5c7d-45de-b002-1585a6db28df", + "user_email_address": "operator@example.com", + "merchant_id": "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + "merchant_name": "Example Merchant", + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_profile_name": "Voucher transactions", + "file_import_log_id": "9eeab611-33a4-4e31-9e84-b61d1302f743", + "file_location": "files/2026/10/vouchers.csv", + "file_lines": [ + { + "line_data": "D,Example Operator,01234567890,10.00", + "line_number": 1, + "processing_result": 2, + "transaction_id": "cb95782a-1c7f-49bb-a96d-8cf4dbeb1f57", + "event_id": "42cae457-c061-42d0-b544-f1767d812ae5", + "rejection_reason": null, + "failure_reason": null, + "response_code": "0000", + "dispatch_attempt_count": 1, + "failed_dispatch_attempts": [] + } + ], + "processing_summary": { + "failed_lines": 0, + "ignored_lines": 0, + "not_processed_lines": 0, + "rejected_lines": 0, + "successfully_processed_lines": 1, + "total_lines": 1 + } + } + """ + } + }, + ["ReplayLine"] = new() + { + Parameters = new Dictionary + { + ["fileId"] = "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + [EstateIdParameterName] = EstateIdExampleValue, + ["lineNumber"] = 1 + } + }, + ["GetImportLogs"] = new() + { + Parameters = new Dictionary + { + [EstateIdParameterName] = EstateIdExampleValue, + ["startDate"] = "2026-10-21", + ["endDate"] = "2026-10-22", + ["merchantId"] = "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + ["pageNumber"] = 1, + ["pageSize"] = 50, + ["descending"] = true + }, + Responses = new Dictionary + { + ["200"] = """ + { + "file_import_logs": [ + { + "file_import_log_id": "9eeab611-33a4-4e31-9e84-b61d1302f743", + "import_log_date_time": "2026-10-22T09:30:00Z", + "import_log_date": "2026-10-22T00:00:00Z", + "import_log_time": "09:30:00", + "file_count": 1, + "files": [ + { + "file_id": "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + "file_import_log_id": "9eeab611-33a4-4e31-9e84-b61d1302f743", + "file_path": "files/2026/10/vouchers.csv", + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_uploaded_date_time": "2026-10-22T09:30:00Z", + "merchant_id": "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + "original_file_name": "vouchers.csv", + "user_id": "a782891c-5c7d-45de-b002-1585a6db28df" + } + ] + } + ], + "page_number": 1, + "page_size": 50, + "total_count": 1, + "has_more": false + } + """ + } + }, + ["GetImportLog"] = new() + { + Parameters = new Dictionary + { + ["fileImportLogId"] = "9eeab611-33a4-4e31-9e84-b61d1302f743", + [EstateIdParameterName] = EstateIdExampleValue, + ["merchantId"] = "ab1c99fb-1c6c-4694-9a32-b71be5d1da33" + }, + Responses = new Dictionary + { + ["200"] = """ + { + "file_import_log_id": "9eeab611-33a4-4e31-9e84-b61d1302f743", + "import_log_date_time": "2026-10-22T09:30:00Z", + "import_log_date": "2026-10-22T00:00:00Z", + "import_log_time": "09:30:00", + "file_count": 1, + "files": [ + { + "file_id": "c31f6a4b-8cd1-4d7e-9a2c-1f8a5b6d7e90", + "file_import_log_id": "9eeab611-33a4-4e31-9e84-b61d1302f743", + "file_path": "files/2026/10/vouchers.csv", + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_uploaded_date_time": "2026-10-22T09:30:00Z", + "merchant_id": "ab1c99fb-1c6c-4694-9a32-b71be5d1da33", + "original_file_name": "vouchers.csv", + "user_id": "a782891c-5c7d-45de-b002-1585a6db28df" + } + ] + } + """ + } + }, + ["GetFileProfiles"] = new() + { + Responses = new Dictionary + { + ["200"] = """ + [ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_profile_version": 1, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + ] + """ + } + }, + ["CreateFileProfile"] = new() + { + RequestBody = """ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "name": "Voucher transactions", + "listening_directory": "files/incoming/vouchers", + "request_type": "Voucher", + "operator_name": "Example Operator", + "line_terminator": "LineFeed", + "file_format_handler": "CsvFileFormatHandler" + } + """, + Responses = new Dictionary + { + ["200"] = """ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_profile_version": 1, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + """ + } + }, + ["GetFileProfile"] = new() + { + Parameters = new Dictionary { [FileProfileIdParameterName] = FileProfileIdExampleValue }, + Responses = new Dictionary + { + ["200"] = """ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_profile_version": 1, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + """ + } + }, + ["UpdateFileProfile"] = new() + { + Parameters = new Dictionary { [FileProfileIdParameterName] = FileProfileIdExampleValue }, + RequestBody = """ + { + "name": "Voucher transactions", + "listening_directory": "files/incoming/vouchers", + "request_type": "Voucher", + "operator_name": "Example Operator", + "line_terminator": "LineFeed", + "file_format_handler": "CsvFileFormatHandler" + } + """, + Responses = new Dictionary + { + ["200"] = """ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "7738df59-9b15-44cb-8d7a-df4845bb8bd4", + "file_profile_version": 2, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + """ + } + }, + ["ArchiveFileProfile"] = new() + { + Parameters = new Dictionary { [FileProfileIdParameterName] = FileProfileIdExampleValue } + }, + ["GetFileProfileVersions"] = new() + { + Parameters = new Dictionary { [FileProfileIdParameterName] = FileProfileIdExampleValue }, + Responses = new Dictionary + { + ["200"] = """ + [ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "469752f8-e6f8-42c3-bff9-a79717c11d84", + "file_profile_version": 1, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + }, + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "7738df59-9b15-44cb-8d7a-df4845bb8bd4", + "file_profile_version": 2, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + ] + """ + } + }, + ["GetFileProfileVersion"] = new() + { + Parameters = new Dictionary + { + [FileProfileIdParameterName] = FileProfileIdExampleValue, + ["fileProfileVersionId"] = "7738df59-9b15-44cb-8d7a-df4845bb8bd4" + }, + Responses = new Dictionary + { + ["200"] = """ + { + "file_profile_id": "9c89c59e-8608-47b1-b280-052a75bcf84b", + "file_profile_version_id": "7738df59-9b15-44cb-8d7a-df4845bb8bd4", + "file_profile_version": 2, + "name": "Voucher transactions", + "operator_name": "Example Operator", + "listening_directory": "files/incoming/vouchers", + "processed_directory": "files/incoming/vouchers/processed", + "failed_directory": "files/incoming/vouchers/failed", + "file_format_handler": "CsvFileFormatHandler", + "request_type": "Voucher", + "line_terminator": "\n" + } + """ + } + } + }; + + /// Attaches the examples for the endpoint's OpenAPI operation. + public static RouteHandlerBuilder WithEndpointExamples(this RouteHandlerBuilder builder) + { + return builder.AddOpenApiOperationTransformer((operation, _, _) => ApplyExamplesAsync(operation)); + } + + private static Task ApplyExamplesAsync(OpenApiOperation operation) + { + if (Examples.TryGetValue(operation.OperationId ?? string.Empty, out EndpointExamples? examples) == false || examples is null) + return Task.CompletedTask; + + AddRequestBodyExample(operation, examples); + AddResponseExamples(operation, examples); + AddParameterExamples(operation, examples); + + return Task.CompletedTask; + } + + private static void AddRequestBodyExample(OpenApiOperation operation, EndpointExamples examples) + { + if (examples.RequestBody is not string requestExample || operation.RequestBody?.Content is not { } requestContent) + return; + + foreach (OpenApiMediaType mediaType in requestContent.Values) + mediaType.Example = JsonNode.Parse(requestExample); + } + + private static void AddResponseExamples(OpenApiOperation operation, EndpointExamples examples) + { + if (examples.Responses is null) + return; + + foreach ((string statusCode, string example) in examples.Responses) + { + if (operation.Responses?.TryGetValue(statusCode, out IOpenApiResponse? response) != true || response?.Content is not { } content) + continue; + + foreach (OpenApiMediaType mediaType in content.Values) + mediaType.Example = JsonNode.Parse(example); + } + } + + private static void AddParameterExamples(OpenApiOperation operation, EndpointExamples examples) + { + if (examples.Parameters is null) + return; + + AddDomainEventHeaderParameter(operation); + + foreach (OpenApiParameter parameter in (operation.Parameters ?? []).OfType()) + { + if (parameter.Name is string name && examples.Parameters.TryGetValue(name, out object? value) && value is not null) + parameter.Example = JsonSerializer.SerializeToNode(value, value.GetType()); + } + } + + private static void AddDomainEventHeaderParameter(OpenApiOperation operation) + { + if (operation.OperationId != "DomainEvent" || (operation.Parameters ?? []).Any(parameter => parameter.Name == "eventType")) + return; + + operation.Parameters ??= []; + operation.Parameters.Add(new OpenApiParameter + { + Name = "eventType", + In = ParameterLocation.Header, + Description = "Registered event type name used to deserialize the JSON body.", + Required = true, + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Example = JsonValue.Create("FileLineAddedEvent") + }); + } + + private sealed class EndpointExamples + { + public string? RequestBody { get; init; } + public IReadOnlyDictionary? Responses { get; init; } + public IReadOnlyDictionary? Parameters { get; init; } + } +} diff --git a/FileProcessor/FileProcessor.csproj b/FileProcessor/FileProcessor.csproj index 2c46205..1772290 100644 --- a/FileProcessor/FileProcessor.csproj +++ b/FileProcessor/FileProcessor.csproj @@ -24,14 +24,9 @@ - - - - - - + @@ -43,4 +38,20 @@ + + + + + + + + + + + + + + diff --git a/FileProcessor/Handlers/FileHandlers.cs b/FileProcessor/Handlers/FileHandlers.cs index bba8b42..18c05ab 100644 --- a/FileProcessor/Handlers/FileHandlers.cs +++ b/FileProcessor/Handlers/FileHandlers.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using System.Linq; using System.Threading; @@ -20,10 +21,11 @@ namespace FileProcessor.Handlers; public static class FileHandlers { public static async Task UploadFile(IMediator mediator, - [FromForm] UploadFileRequest request, - [FromForm] IFormCollection formCollection, - CancellationToken cancellationToken) + [FromForm, Description("Multipart form metadata and the file to upload.")] UploadFileForm request, + HttpRequest httpRequest, + CancellationToken cancellationToken) { + IFormCollection formCollection = await httpRequest.ReadFormAsync(cancellationToken); if (formCollection?.Files == null || formCollection.Files.Count != 1) return Results.BadRequest("Exactly one file must be supplied."); @@ -120,8 +122,8 @@ private static String SanitizeFileName(String fileName) } public static async Task GetFile(IMediator mediator, - [FromRoute] Guid fileId, - [FromQuery] Guid estateId, + [FromRoute, Description("GUID identifying the uploaded file.")] Guid fileId, + [FromQuery, Description("GUID identifying the estate that owns the file.")] Guid estateId, CancellationToken cancellationToken) { FileQueries.GetFileQuery query = new(fileId, estateId); @@ -131,9 +133,9 @@ public static async Task GetFile(IMediator mediator, } public static async Task ReplayLine(IMediator mediator, - [FromRoute] Guid fileId, - [FromQuery] Guid estateId, - [FromRoute] Int32 lineNumber, + [FromRoute, Description("GUID identifying the uploaded file.")] Guid fileId, + [FromQuery, Description("GUID identifying the estate that owns the file.")] Guid estateId, + [FromRoute, Description("One-based line number to retry.")] Int32 lineNumber, CancellationToken cancellationToken) { FileCommands.ReplayFileLineCommand command = new(fileId, estateId, lineNumber); diff --git a/FileProcessor/Handlers/FileImportLogHandlers.cs b/FileProcessor/Handlers/FileImportLogHandlers.cs index 0c9ce22..c9aff63 100644 --- a/FileProcessor/Handlers/FileImportLogHandlers.cs +++ b/FileProcessor/Handlers/FileImportLogHandlers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using FileProcessor.BusinessLogic.Requests; @@ -15,13 +16,13 @@ namespace FileProcessor.Handlers; public static class FileImportLogHandlers { public static async Task GetImportLogsAsync(IMediator mediator, - [FromQuery] Guid estateId, - [FromQuery] DateOnly startDate, - [FromQuery] DateOnly endDate, - [FromQuery] Guid? merchantId, - [FromQuery] Int32 pageNumber, - [FromQuery] Int32 pageSize, - [FromQuery] Boolean descending, + [FromQuery, Description("Required estate GUID to scope the logs or log lookup.")] Guid estateId, + [FromQuery, Description("Required inclusive start date in ISO 8601 date format (yyyy-MM-dd). Must be on or before endDate.")] DateOnly startDate, + [FromQuery, Description("Required inclusive end date in ISO 8601 date format (yyyy-MM-dd). Must be on or after startDate.")] DateOnly endDate, + [FromQuery, Description("Optional merchant GUID used to filter logs or files.")] Guid? merchantId, + [FromQuery, Description("One-based page number. Defaults to 1 when omitted or zero.")] Int32 pageNumber, + [FromQuery, Description("Number of logs per page (1–200). Defaults to 50 when omitted or zero.")] Int32 pageSize, + [FromQuery, Description("Sort newest first when true; defaults to false.")] Boolean descending, CancellationToken cancellationToken) { pageNumber = pageNumber == 0 ? 1 : pageNumber; @@ -33,9 +34,9 @@ public static async Task GetImportLogsAsync(IMediator mediator, } public static async Task GetImportLogAsync(IMediator mediator, - [FromRoute] Guid fileImportLogId, - [FromQuery] Guid estateId, - [FromQuery] Guid? merchantId, + [FromRoute, Description("GUID identifying the file import log.")] Guid fileImportLogId, + [FromQuery, Description("Required estate GUID to scope the logs or log lookup.")] Guid estateId, + [FromQuery, Description("Optional merchant GUID used to filter logs or files.")] Guid? merchantId, CancellationToken cancellationToken) { FileQueries.GetImportLogQuery query = new(fileImportLogId, estateId, merchantId); diff --git a/FileProcessor/Handlers/FileProfileHandlers.cs b/FileProcessor/Handlers/FileProfileHandlers.cs index 7ad9d5e..f48a42b 100644 --- a/FileProcessor/Handlers/FileProfileHandlers.cs +++ b/FileProcessor/Handlers/FileProfileHandlers.cs @@ -1,7 +1,9 @@ -using System; +using System; +using System.ComponentModel; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using FileProcessor.Common; using FileProcessor.BusinessLogic.Requests; using FileProcessor.DataTransferObjects.Requests; using MediatR; @@ -20,38 +22,38 @@ public static async Task GetFileProfiles(IMediator mediator, Cancellati FileProfileQueries.GetFileProfilesQuery query = new(); Result> result = await mediator.Send(query, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task GetFileProfile(IMediator mediator, - [FromRoute] Guid fileProfileId, + [FromRoute, Description("GUID identifying the file profile.")] Guid fileProfileId, CancellationToken cancellationToken) { FileProfileQueries.GetFileProfileQuery query = new(fileProfileId); Result result = await mediator.Send(query, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task GetFileProfileVersions(IMediator mediator, - [FromRoute] Guid fileProfileId, + [FromRoute, Description("GUID identifying the file profile.")] Guid fileProfileId, CancellationToken cancellationToken) { FileProfileQueries.GetFileProfileVersionsQuery query = new(fileProfileId); Result> result = await mediator.Send(query, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task GetFileProfileVersion(IMediator mediator, - [FromRoute] Guid fileProfileId, - [FromRoute] Guid fileProfileVersionId, + [FromRoute, Description("GUID identifying the file profile.")] Guid fileProfileId, + [FromRoute, Description("GUID identifying the requested file profile version.")] Guid fileProfileVersionId, CancellationToken cancellationToken) { FileProfileQueries.GetFileProfileVersionQuery query = new(fileProfileId, fileProfileVersionId); Result result = await mediator.Send(query, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task CreateFileProfile(IMediator mediator, @@ -61,22 +63,22 @@ public static async Task CreateFileProfile(IMediator mediator, FileProfileCommands.CreateFileProfileCommand command = new(request); Result result = await mediator.Send(command, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task UpdateFileProfile(IMediator mediator, - [FromRoute] Guid fileProfileId, + [FromRoute, Description("GUID identifying the file profile.")] Guid fileProfileId, [FromBody] UpdateFileProfileRequest request, CancellationToken cancellationToken) { FileProfileCommands.UpdateFileProfileCommand command = new(fileProfileId, request); Result result = await mediator.Send(command, cancellationToken); - return ResponseFactory.FromResult(result, x => x); + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); } public static async Task ArchiveFileProfile(IMediator mediator, - [FromRoute] Guid fileProfileId, + [FromRoute, Description("GUID identifying the file profile.")] Guid fileProfileId, CancellationToken cancellationToken) { FileProfileCommands.ArchiveFileProfileCommand command = new(fileProfileId); diff --git a/FileProcessor/Handlers/UploadFileForm.cs b/FileProcessor/Handlers/UploadFileForm.cs new file mode 100644 index 0000000..5f84e92 --- /dev/null +++ b/FileProcessor/Handlers/UploadFileForm.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Microsoft.AspNetCore.Http; +using FileProcessor.DataTransferObjects; + +namespace FileProcessor.Handlers; + +/// Multipart form fields accepted by the file upload endpoint. +[Description("Multipart metadata and the file submitted to the upload endpoint.")] +public sealed class UploadFileForm : UploadFileRequest +{ + /// The single non-empty file to upload. + [Description("The single non-empty file to upload.")] + public IFormFile File { get; set; } = default!; +} diff --git a/FileProcessor/Program.cs b/FileProcessor/Program.cs index b50a377..a24c064 100644 --- a/FileProcessor/Program.cs +++ b/FileProcessor/Program.cs @@ -1,137 +1,190 @@ +using FileProcessor.Bootstrapper; +using FileProcessor.Common; +using FileProcessor.Endpoints; +using FileProcessor.Services; +using FileProcessor.BusinessLogic.Services; +using Lamar; +using Lamar.Microsoft.DependencyInjection; +using MediatR; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; +using LogManager = NLog.LogManager; +using Sentry.Extensibility; +using Shared.EventStore.EventHandling; +using Shared.Extensions; +using Shared.General; +using Shared.Logger; using Shared.Middleware; -using System; +using Shared.Serialisation; +using Shared.DomainDrivenDesign.EventSourcing; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System; +using System.IO; +using System.Reflection; +using NLog; +using Logger = Shared.Logger.Logger; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using Shared.EventStore.Aggregate; +using Shared.EventStore.SubscriptionWorker; +using System.IO.Abstractions; +using TransactionProcessing.ApiDefaults; -namespace FileProcessor +namespace FileProcessor; + +[ExcludeFromCodeCoverage] +public class Program { - using BusinessLogic.Managers; - using EventStore.Client; - using File.DomainEvents; - using FileImportLog.DomainEvents; - using FileProcessor.Services; - using Lamar.Microsoft.DependencyInjection; - using MediatR; - using Microsoft.Extensions.DependencyInjection; - using NLog; - using NLog.Extensions.Logging; - using Sentry.Extensibility; - using Shared.EventStore.EventHandling; - using Shared.General; - using Shared.Logger; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.IO.Abstractions; - using System.Net.Http; - using System.Reflection; - - [ExcludeFromCodeCoverage] - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - Program.CreateHostBuilder(args).Build().Run(); - } + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + + FileInfo assemblyFile = new FileInfo(Assembly.GetExecutingAssembly().Location); + string assemblyDirectory = assemblyFile.DirectoryName!; + string contentRoot = Directory.GetCurrentDirectory(); + string nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); - public static IHostBuilder CreateHostBuilder(string[] args) + LogManager.Setup(b => { - //At this stage, we only need our hosting file for ip and ports - FileInfo fi = new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location); + b.SetupLogFactory(setup => + { + setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); + }); + b.LoadConfigurationFromFile(nlogConfigPath); + }); - IConfigurationRoot config = new ConfigurationBuilder().SetBasePath(fi.Directory.FullName) - .AddJsonFile("hosting.json", optional: true) - .AddJsonFile("hosting.development.json", optional: true) - .AddEnvironmentVariables().Build(); + builder.Host.UseWindowsService(); + builder.Host.UseLamar(); + builder.Logging.AddConsole(); + builder.Logging.AddNLog(); - String contentRoot = Directory.GetCurrentDirectory(); - String nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); + ConfigureApplicationConfiguration(builder, assemblyDirectory); + ConfigurationReader.Initialise((IConfigurationRoot)builder.Configuration); + builder.Services.AddHostedService(); - LogManager.Setup(b => + builder.Host.ConfigureContainer((context, services) => + { + TypeProvider.LoadDomainEventsTypeDynamically(); + services.IncludeRegistry(); + services.IncludeRegistry(new DomainEventHandlerRegistry(builder.Configuration)); + services.IncludeRegistry(new RepositoryRegistry(builder.Configuration, builder.Environment)); + services.IncludeRegistry(new MiddlewareRegistry(builder.Configuration)); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(); + services.IncludeRegistry(); + }); + + builder.WebHost.UseConfiguration(BuildHostingConfiguration(assemblyDirectory)); + builder.WebHost.UseKestrel(); + + if (builder.Configuration.GetSection("SentryConfiguration").Exists() && builder.Environment.IsDevelopment() == false) + { + IConfiguration sentryConfiguration = builder.Configuration; + builder.WebHost.UseSentry(options => { - b.SetupLogFactory(setup => - { - setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); - }); - b.LoadConfigurationFromFile(nlogConfigPath); + options.Dsn = sentryConfiguration["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"; }); + } + + builder.WebHost.ConfigureKestrel(serverOptions => + { + serverOptions.Limits.MaxRequestBodySize = UploadFileSettings.GetMaximumMultipartRequestSizeBytes(); + }); + + WebApplication app = builder.Build(); + StringSerialiser.Initialise(app.Services.GetRequiredService()); - IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); - hostBuilder.UseWindowsService(); - hostBuilder.UseLamar(); - hostBuilder.ConfigureLogging(logging => - { - logging.AddConsole(); - logging.AddNLog(); - }); - hostBuilder.ConfigureServices((context, services) => + ConfigurePipeline(app); + app.Run(); + } + + private static void ConfigureApplicationConfiguration(WebApplicationBuilder builder, string assemblyDirectory) + { + IWebHostEnvironment environment = builder.Environment; + builder.Configuration.SetBasePath(assemblyDirectory) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile($"hosting.{environment.EnvironmentName}.json", optional: true) + .AddJsonFile("/home/txnproc/config/appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"/home/txnproc/config/appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("/home/txnproc/config/appsettings.local.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddInMemoryCollection(new Dictionary { - services.AddHostedService(); + ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" }); - hostBuilder.ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureAppConfiguration((context, configBuilder) => - { - var env = context.HostingEnvironment; - - 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) - var 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. - var 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.UseConfiguration(config); - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => - { - serverOptions.Limits.MaxRequestBodySize = Common.UploadFileSettings.GetMaximumMultipartRequestSizeBytes(); - }); - }); - - - return hostBuilder; + } + + private static IConfiguration BuildHostingConfiguration(string assemblyDirectory) + { + return new ConfigurationBuilder() + .SetBasePath(assemblyDirectory) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile("hosting.development.json", optional: true) + .AddEnvironmentVariables() + .Build(); + } + + private static void ConfigurePipeline(WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); } + + ILogger logger = app.Services.GetRequiredService().CreateLogger("FileProcessor"); + Logger.Initialise(logger); + IConfigurationRoot configuration = (IConfigurationRoot)app.Configuration; + configuration.LogConfiguration(Logger.LogWarning); + ConfigurationReader.Initialise(configuration); + + app.UseMiddleware(); + app.AddRequestResponseLogging(); + app.AddExceptionHandler(); + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapDomainEventEndpoint(); + app.MapFileEndpoints(); + app.MapFileImportLogEndpoints(); + app.MapFileProfileEndpoints(); + app.MapMigrationEndpoints(); + // Liveness details for every registered health check. + app.MapHealthChecks("health", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse + }); + // Health-check payload formatted for the Health Checks UI client. + app.MapHealthChecks("healthui", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse + }); + + IContainer container = (IContainer)app.Services; + app.PreWarm(container.GetInstance(), + container.GetInstance(), + container.GetInstance(), + app.Configuration, + container.GetInstance("Main"), + container.GetInstance>()); + app.UseApiDefaults(); } } diff --git a/FileProcessor/Startup.cs b/FileProcessor/Startup.cs deleted file mode 100644 index 3b7e263..0000000 --- a/FileProcessor/Startup.cs +++ /dev/null @@ -1,151 +0,0 @@ -using FileProcessor.Endpoints; -using ImTools; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Shared.Serialisation; -using System; -using System.Collections.Generic; -using System.Linq; -using FileProcessor.Bootstrapper; -using FileProcessor.Common; -using HealthChecks.UI.Client; -using Lamar; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Shared.DomainDrivenDesign.EventSourcing; -using Shared.EventStore.Aggregate; -using Shared.Extensions; -using Shared.General; -using Shared.Logger; -using Shared.Middleware; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace FileProcessor -{ - [ExcludeFromCodeCoverage] - public class Startup - { - /// - /// Gets or sets the configuration. - /// - /// - /// The configuration. - /// - public static IConfigurationRoot Configuration { get; set; } - - /// - /// Gets or sets the web host environment. - /// - /// - /// The web host environment. - /// - public static IWebHostEnvironment WebHostEnvironment { get; set; } - - public Startup(IWebHostEnvironment webHostEnvironment) - { - Startup.WebHostEnvironment = webHostEnvironment; - } - - public static Container Container; - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureContainer(ServiceRegistry services) - { - ConfigurationReader.Initialise(Startup.Configuration); - - TypeProvider.LoadDomainEventsTypeDynamically(); - - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - - Startup.Container = new Container(services); - - Startup.ServiceProvider = services.BuildServiceProvider(); - - var serialiser = Container.GetRequiredService(); - StringSerialiser.Initialise(serialiser); - } - - public static IServiceProvider ServiceProvider { get; set; } - - public static void LoadDomainEventsTypeDynamically(List assemblyFilters = null) - { - Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - List source = new List(); - foreach (string assemblyFilter in assemblyFilters) - { - List filteredAssemblies = assemblies.Where(a => a.FullName.Contains(assemblyFilter) == true).ToList(); - foreach (Assembly a in filteredAssemblies) { - assemblies = assemblies.Remove(a); - } - } - source.AddRange(assemblies.SelectMany((Func>)(a => (IEnumerable)a.GetTypes()))); - - foreach (Type type in source.Where((Func)(t => t.IsSubclassOf(typeof(DomainEvent)))).OrderBy((Func)(e => e.Name)).ToList()) - TypeMap.AddType(type, type.Name); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - IHostApplicationLifetime lifetime, IHost host) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - ILogger logger = loggerFactory.CreateLogger("FileProcessor"); - - Logger.Initialise(logger); - - Startup.Configuration.LogConfiguration(Logger.LogWarning); - - ConfigurationReader.Initialise(Startup.Configuration); - app.UseMiddleware(); - app.AddRequestResponseLogging(); - app.AddExceptionHandler(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => { - endpoints.MapDomainEventEndpoint(); - endpoints.MapFileEndpoints(); - endpoints.MapFileImportLogEndpoints(); - endpoints.MapFileProfileEndpoints(); - endpoints.MapMigrationEndpoints(); - - endpoints.MapHealthChecks("health", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse - }); - endpoints.MapHealthChecks("healthui", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - }); - - app.UseSwagger(); - - app.UseSwaggerUI(); - app.PreWarm(); - } - } -}