diff --git a/.github/workflows/createrelease.yml b/.github/workflows/createrelease.yml index 9c40669..b67e0ae 100644 --- a/.github/workflows/createrelease.yml +++ b/.github/workflows/createrelease.yml @@ -71,6 +71,47 @@ jobs: name: "Deploy to Staging" steps: + - uses: actions/checkout@v4 + + - name: Install scheduled tasks + shell: pwsh + env: + SCHEDULED_TASK_USER: ${{ secrets.SCHEDULED_TASK_USER }} + SCHEDULED_TASK_PASSWORD: ${{ secrets.SCHEDULED_TASK_PASSWORD }} + run: | + $taskScriptDir = 'C:\txnproc\transactionprocessing\powershell' + $scheduledTasksRoot = Join-Path $env:GITHUB_WORKSPACE 'ScheduledTasks' + + New-Item -ItemType Directory -Path $taskScriptDir -Force | Out-Null + + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.ps1 | + Where-Object { $_.FullName -notmatch '\\Tests\\' } | + Copy-Item -Destination $taskScriptDir -Force + + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml | + Where-Object { $_.FullName -notmatch '\\Tests\\' } | + ForEach-Object { + [xml]$taskDefinition = Get-Content -Path $_.FullName -Raw + $taskName = [string]$taskDefinition.Task.RegistrationInfo.URI + + if ([string]::IsNullOrWhiteSpace($taskName)) { + $taskName = $_.BaseName + } + + $registerParams = @{ + Xml = $taskDefinition.OuterXml + TaskName = $taskName.TrimStart('\') + TaskPath = '\' + Force = $true + } + + if ($env:SCHEDULED_TASK_USER -and $env:SCHEDULED_TASK_PASSWORD) { + $registerParams.User = $env:SCHEDULED_TASK_USER + $registerParams.Password = $env:SCHEDULED_TASK_PASSWORD + } + + Register-ScheduledTask @registerParams | Out-Null + } - name: Download the artifact (Merchant Pos) uses: actions/download-artifact@v4.1.8 @@ -131,6 +172,47 @@ jobs: name: "Deploy to Production" steps: + - uses: actions/checkout@v4 + + - name: Install scheduled tasks + shell: pwsh + env: + SCHEDULED_TASK_USER: ${{ secrets.SCHEDULED_TASK_USER }} + SCHEDULED_TASK_PASSWORD: ${{ secrets.SCHEDULED_TASK_PASSWORD }} + run: | + $taskScriptDir = 'C:\txnproc\transactionprocessing\powershell' + $scheduledTasksRoot = Join-Path $env:GITHUB_WORKSPACE 'ScheduledTasks' + + New-Item -ItemType Directory -Path $taskScriptDir -Force | Out-Null + + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.ps1 | + Where-Object { $_.FullName -notmatch '\\Tests\\' } | + Copy-Item -Destination $taskScriptDir -Force + + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml | + Where-Object { $_.FullName -notmatch '\\Tests\\' } | + ForEach-Object { + [xml]$taskDefinition = Get-Content -Path $_.FullName -Raw + $taskName = [string]$taskDefinition.Task.RegistrationInfo.URI + + if ([string]::IsNullOrWhiteSpace($taskName)) { + $taskName = $_.BaseName + } + + $registerParams = @{ + Xml = $taskDefinition.OuterXml + TaskName = $taskName.TrimStart('\') + TaskPath = '\' + Force = $true + } + + if ($env:SCHEDULED_TASK_USER -and $env:SCHEDULED_TASK_PASSWORD) { + $registerParams.User = $env:SCHEDULED_TASK_USER + $registerParams.Password = $env:SCHEDULED_TASK_PASSWORD + } + + Register-ScheduledTask @registerParams | Out-Null + } - name: Remove existing Windows service (Merchant Pos) run: | diff --git a/.github/workflows/powershell_nightly.yml b/.github/workflows/powershell_nightly.yml index f9887bd..4e091cb 100644 --- a/.github/workflows/powershell_nightly.yml +++ b/.github/workflows/powershell_nightly.yml @@ -24,4 +24,8 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Pester -Path ./ScheduledTasks/SettlementProcessing/Scripts/Tests -CI + Invoke-Pester -Path @( + './ScheduledTasks/Tests' + './ScheduledTasks/ReplayParkedQueue/Tests' + './ScheduledTasks/SettlementProcessing/Tests' + ) -CI diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 33fab4d..40f6d6d 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -53,4 +53,8 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Pester -Path ./ScheduledTasks/SettlementProcessing/Tests -CI + Invoke-Pester -Path @( + './ScheduledTasks/Tests' + './ScheduledTasks/ReplayParkedQueue/Tests' + './ScheduledTasks/SettlementProcessing/Tests' + ) -CI diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..fa626cc --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Scripts/Scavenge.xml b/ScheduledTasks/AutoScavenge/Scavenge.xml similarity index 100% rename from Scripts/Scavenge.xml rename to ScheduledTasks/AutoScavenge/Scavenge.xml diff --git a/Scripts/scavenge.ps1 b/ScheduledTasks/AutoScavenge/scavenge.ps1 similarity index 100% rename from Scripts/scavenge.ps1 rename to ScheduledTasks/AutoScavenge/scavenge.ps1 diff --git a/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 b/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 index 1120f78..dda6cf4 100644 --- a/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 +++ b/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 @@ -2,11 +2,8 @@ [Parameter(Mandatory = $true)] [string]$BaseUrl, - [Parameter] - [string]$Username, - - [Parameter] - [string]$Password, + [Parameter()] + [System.Management.Automation.PSCredential]$Credential, [string]$LogDirectory = "C:\home\txnproc\trace", [int]$LogRetentionDays = 7 @@ -63,10 +60,15 @@ Write-Trace "INFO" "Script started. BaseUrl=$BaseUrl" # ========================= # Auth header # ========================= -$AuthHeader = @{ - Authorization = "Basic " + +$AuthHeader = @{} +if ($null -ne $Credential) { + $networkCredential = $Credential.GetNetworkCredential() + $username = $networkCredential.UserName + $password = $networkCredential.Password + + $AuthHeader.Authorization = "Basic " + [Convert]::ToBase64String( - [Text.Encoding]::ASCII.GetBytes("$Username`:$Password") + [Text.Encoding]::ASCII.GetBytes("$username`:$password") ) } @@ -140,4 +142,4 @@ foreach ($sub in $subscriptions) { #} } -Write-Trace "INFO" "Script completed" \ No newline at end of file +Write-Trace "INFO" "Script completed" diff --git a/ScheduledTasks/ReplayParkedQueue/Tests/ReplayParkedQueue.Tests.ps1 b/ScheduledTasks/ReplayParkedQueue/Tests/ReplayParkedQueue.Tests.ps1 new file mode 100644 index 0000000..f17cf70 --- /dev/null +++ b/ScheduledTasks/ReplayParkedQueue/Tests/ReplayParkedQueue.Tests.ps1 @@ -0,0 +1,139 @@ +Describe 'ReplayParkedQueue.ps1' { + It 'replays parked messages for subscriptions with parked counts and encodes $all streams' { + $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path + $logDirectory = Join-Path $TestDrive 'replay-logs-1' + $securePassword = [System.Security.SecureString]::new() + foreach ($code in 115, 101, 99, 114, 101, 116) { + $securePassword.AppendChar([char]$code) + } + $credential = [pscredential]::new('alice', $securePassword) + $subscriptions = @( + [pscustomobject]@{ + eventStreamId = 'stream-1' + groupName = 'group-1' + }, + [pscustomobject]@{ + eventStreamId = '$all' + groupName = 'group-2' + } + ) + $captured = [pscustomobject]@{ + SubscriptionCalls = 0 + InfoUris = [System.Collections.Generic.List[string]]::new() + ReplayUris = [System.Collections.Generic.List[string]]::new() + } + + Mock Invoke-RestMethod { + $captured.SubscriptionCalls++ + $subscriptions + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions' + } + + Mock Invoke-RestMethod { + $captured.InfoUris.Add($Uri) | Out-Null + @{ parkedMessageCount = 2 } + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/info' + } + + Mock Invoke-RestMethod { + $captured.InfoUris.Add($Uri) | Out-Null + @{ parkedMessageCount = 0 } + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions/%24all/group-2/info' + } + + Mock Invoke-RestMethod { + $captured.ReplayUris.Add($Uri) | Out-Null + } -ParameterFilter { + $Method -eq 'POST' -and + $Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/replayParked?from=0' + } + + & $scriptUnderTest 'https://queue.example' -Credential $credential -LogDirectory $logDirectory + + $captured.SubscriptionCalls | Should -Be 1 + $captured.InfoUris.Count | Should -Be 2 + $captured.InfoUris | Should -Be @( + 'https://queue.example/subscriptions/stream-1/group-1/info', + 'https://queue.example/subscriptions/%24all/group-2/info' + ) + $captured.ReplayUris | Should -Be @( + 'https://queue.example/subscriptions/stream-1/group-1/replayParked?from=0' + ) + } + + It 'does not post a replay when no subscription has parked messages' { + $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path + $logDirectory = Join-Path $TestDrive 'replay-logs-2' + $securePassword = [System.Security.SecureString]::new() + foreach ($code in 115, 101, 99, 114, 101, 116) { + $securePassword.AppendChar([char]$code) + } + $credential = [pscredential]::new('alice', $securePassword) + $subscriptions = @( + [pscustomobject]@{ + eventStreamId = 'stream-1' + groupName = 'group-1' + } + ) + $captured = [pscustomobject]@{ + SubscriptionCalls = 0 + InfoCalls = 0 + ReplayCalls = 0 + } + + Mock Invoke-RestMethod { + $captured.SubscriptionCalls++ + $subscriptions + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions' + } + + Mock Invoke-RestMethod { + $captured.InfoCalls++ + @{ parkedMessageCount = 0 } + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/info' + } + + Mock Invoke-RestMethod { + $captured.ReplayCalls++ + } -ParameterFilter { + $Method -eq 'POST' -and + $Uri -like 'https://queue.example/subscriptions/*/*/replayParked?from=0' + } + + & $scriptUnderTest 'https://queue.example' -Credential $credential -LogDirectory $logDirectory + + $captured.SubscriptionCalls | Should -Be 1 + $captured.InfoCalls | Should -Be 1 + $captured.ReplayCalls | Should -Be 0 + } + + It 'does not add an Authorization header when no credential is supplied' { + $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path + $logDirectory = Join-Path $TestDrive 'replay-logs-3' + $captured = [pscustomobject]@{ + Headers = $null + } + + Mock Invoke-RestMethod { + $captured.Headers = $Headers + @() + } -ParameterFilter { + $Method -eq 'GET' -and + $Uri -eq 'https://queue.example/subscriptions' + } + + & $scriptUnderTest 'https://queue.example' -LogDirectory $logDirectory + + $captured.Headers.ContainsKey('Authorization') | Should -BeFalse + } +} diff --git a/ScheduledTasks/SettlementProcessing/Tests/Invoke-DailySettlementProcessing.Tests.ps1 b/ScheduledTasks/SettlementProcessing/Tests/Invoke-DailySettlementProcessing.Tests.ps1 index 813e246..7b0a537 100644 --- a/ScheduledTasks/SettlementProcessing/Tests/Invoke-DailySettlementProcessing.Tests.ps1 +++ b/ScheduledTasks/SettlementProcessing/Tests/Invoke-DailySettlementProcessing.Tests.ps1 @@ -1,10 +1,15 @@ Describe 'Invoke-DailySettlementProcessing.ps1' { It 'gets merchants and posts a settlement for each merchant returned' { + $scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path $postedUris = [System.Collections.Generic.List[string]]::new() $settlementDate = (Get-Date).ToString('yyyy-MM-dd') - $scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path + $captured = [pscustomobject]@{ + TokenCalls = 0 + MerchantCalls = 0 + } Mock Invoke-RestMethod { + $captured.TokenCalls++ @{ access_token = 'access-token' } @@ -18,6 +23,7 @@ Describe 'Invoke-DailySettlementProcessing.ps1' { } Mock Invoke-RestMethod { + $captured.MerchantCalls++ @{ merchants = @( @{ merchantId = 'merchant-1' }, @@ -45,17 +51,61 @@ Describe 'Invoke-DailySettlementProcessing.ps1' { -ClientId 'client-id' ` -ClientSecret 'client-secret' - Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter { - $Method -eq 'Post' -and $Uri -eq 'https://security.example/connect/token' - } - - Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.example/api/estates/estate-123/merchants' - } - + $captured.TokenCalls | Should -Be 1 + $captured.MerchantCalls | Should -Be 1 $postedUris | Should -Be @( "https://api.example/api/estates/estate-123/settlements/$settlementDate/merchants/merchant-1", "https://api.example/api/estates/estate-123/settlements/$settlementDate/merchants/merchant-2" ) } + + It 'does not post settlements when no merchants are returned' { + $scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path + $captured = [pscustomobject]@{ + TokenCalls = 0 + MerchantCalls = 0 + SettlementCalls = 0 + } + + Mock Invoke-RestMethod { + $captured.TokenCalls++ + @{ + access_token = 'access-token' + } + } -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://security.example/connect/token' -and + $ContentType -eq 'application/x-www-form-urlencoded' -and + $Body.grant_type -eq 'client_credentials' -and + $Body.client_id -eq 'client-id' -and + $Body.client_secret -eq 'client-secret' + } + + Mock Invoke-RestMethod { + $captured.MerchantCalls++ + @() + } -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.example/api/estates/estate-123/merchants' -and + $Headers.Authorization -eq 'Bearer access-token' + } + + Mock Invoke-RestMethod { + $captured.SettlementCalls++ + } -ParameterFilter { + $Method -eq 'Post' -and + $Uri -like 'https://api.example/api/estates/estate-123/settlements/*/merchants/*' + } + + & $scriptUnderTest ` + -EstateId 'estate-123' ` + -BaseUrl 'https://api.example/' ` + -SecurityServiceUrl 'https://security.example/' ` + -ClientId 'client-id' ` + -ClientSecret 'client-secret' + + $captured.TokenCalls | Should -Be 1 + $captured.MerchantCalls | Should -Be 1 + $captured.SettlementCalls | Should -Be 0 + } } diff --git a/Scripts/Tests/Scavenge.Tests.ps1 b/ScheduledTasks/Tests/Scavenge.Tests.ps1 similarity index 65% rename from Scripts/Tests/Scavenge.Tests.ps1 rename to ScheduledTasks/Tests/Scavenge.Tests.ps1 index 47957be..ccedb10 100644 --- a/Scripts/Tests/Scavenge.Tests.ps1 +++ b/ScheduledTasks/Tests/Scavenge.Tests.ps1 @@ -1,6 +1,9 @@ -Describe 'scavenge.ps1' { +BeforeAll { + $script:scriptUnderTest = (Resolve-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'AutoScavenge') 'scavenge.ps1')).Path +} + +Describe 'AutoScavenge\scavenge.ps1' { It 'posts to /admin/scavenge after trimming a trailing slash from BaseUrl' { - $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'scavenge.ps1')).Path $expectedUri = 'http://localhost:2113/admin/scavenge' $captured = [pscustomobject]@{ Count = 0 @@ -17,7 +20,7 @@ Describe 'scavenge.ps1' { $Method -eq 'Post' } - & $scriptUnderTest -BaseUrl 'http://localhost:2113/' + & $script:scriptUnderTest -BaseUrl 'http://localhost:2113/' $captured.Count | Should -Be 1 $captured.Uri | Should -Be $expectedUri @@ -27,7 +30,6 @@ Describe 'scavenge.ps1' { } It 'adds a Basic Authorization header when username and password are both provided' { - $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'scavenge.ps1')).Path $expectedUri = 'http://localhost:2113/admin/scavenge' $expectedAuth = 'Basic ' + [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes('alice:secret')) $captured = [pscustomobject]@{ @@ -45,7 +47,7 @@ Describe 'scavenge.ps1' { $Method -eq 'Post' } - & $scriptUnderTest -BaseUrl 'http://localhost:2113' -Username 'alice' -Password 'secret' + & $script:scriptUnderTest -BaseUrl 'http://localhost:2113' -Username 'alice' -Password 'secret' $captured.Count | Should -Be 1 $captured.Uri | Should -Be $expectedUri @@ -53,7 +55,6 @@ Describe 'scavenge.ps1' { } It 'does not add an Authorization header when only one credential is supplied' { - $scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'scavenge.ps1')).Path $captured = [pscustomobject]@{ Count = 0 Uri = $null @@ -69,7 +70,30 @@ Describe 'scavenge.ps1' { $Method -eq 'Post' } - & $scriptUnderTest -BaseUrl 'http://localhost:2113' -Username 'alice' + & $script:scriptUnderTest -BaseUrl 'http://localhost:2113' -Username 'alice' + + $captured.Count | Should -Be 1 + $captured.Uri | Should -Be 'http://localhost:2113/admin/scavenge' + $captured.Headers.ContainsKey('Authorization') | Should -BeFalse + } + + It 'does not add an Authorization header when only a password is supplied' { + $captured = [pscustomobject]@{ + Count = 0 + Uri = $null + Headers = $null + } + + Mock Invoke-RestMethod { + $captured.Count++ + $captured.Uri = $Uri + $captured.Headers = $Headers + @{ status = 'ok' } + } -ParameterFilter { + $Method -eq 'Post' + } + + & $script:scriptUnderTest -BaseUrl 'http://localhost:2113' -Password 'secret' $captured.Count | Should -Be 1 $captured.Uri | Should -Be 'http://localhost:2113/admin/scavenge' diff --git a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs index 945ddd1..7fcdd37 100644 --- a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs +++ b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs @@ -53,9 +53,9 @@ public async Task SetupEstate(CancellationToken cancellationToken) { return ResultHelpers.CreateFailure(createEstateResult); this.EstateId = createEstateResult.Data; - var createUserResult = await this.CreateEstateUser(cancellationToken); - if (createUserResult.IsFailed) - return ResultHelpers.CreateFailure(createUserResult); + //var createUserResult = await this.CreateEstateUser(cancellationToken); + //if (createUserResult.IsFailed) + // return ResultHelpers.CreateFailure(createUserResult); var createOperatorsResult = await this.CreateOperators(cancellationToken); if (createOperatorsResult.IsFailed) @@ -336,6 +336,7 @@ public async Task CreateMerchant(Merchant merchant, CreateMerchantRequest createMerchantRequest = new CreateMerchantRequest { Address = new Address { AddressLine1 = merchant.Address.AddressLine1, Country = merchant.Address.Country, Region = merchant.Address.Region, Town = merchant.Address.Town, + PostalCode = merchant.Address.Postcode }, Name = merchant.Name, Contact = new Contact { ContactName = merchant.Contact.ContactName, EmailAddress = merchant.Contact.EmailAddress }, diff --git a/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs b/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs index 71693e6..5ee0b5d 100644 --- a/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs +++ b/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs @@ -45,6 +45,7 @@ private async Task SetupSubscriptions(CancellationToken cancellationToke ("$ce-FileAggregate", "Transaction Processor", 0), ("$ce-FileImportLogAggregate", "Transaction Processor", 0), ("$ce-OperatorAggregate", "Transaction Processor", 0), + ("$ce-MerchantBalanceArchive", "Transaction Processor", 0), ("$ce-TransactionAggregate", "Transaction Processor - Domain", 0), ("$ce-SettlementAggregate", "Transaction Processor - Domain", 0), @@ -100,7 +101,6 @@ private async Task DeployProjections(CancellationToken cancellationToken projection.Contains("TransactionProcessorSubscriptionStreamBuilder") || projection.Contains("EstateAggregator") || projection.Contains("MerchantAggregator") || - projection.Contains("MerchantBalanceAggregator") || projection.Contains("CallbackHandlerEnricher")) { continue; diff --git a/TransactionProcessor.SystemSetupTool/Program.cs b/TransactionProcessor.SystemSetupTool/Program.cs index 5ebd498..69aa60e 100644 --- a/TransactionProcessor.SystemSetupTool/Program.cs +++ b/TransactionProcessor.SystemSetupTool/Program.cs @@ -1,9 +1,13 @@ using System; +using System.Data; +using FileProcessor.Client; +using FileProcessor.DataTransferObjects.Requests; using KurrentDB.Client; using SecurityService.DataTransferObjects; using Shared.Results; using Shared.Serialisation; using SimpleResults; +using TransactionProcessor.SystemSetupTool.fileprofileconfig; namespace TransactionProcessor.SystemSetupTool { @@ -21,6 +25,7 @@ namespace TransactionProcessor.SystemSetupTool class Program { private static TransactionProcessorClient TransactionProcessorClient; + private static FileProcessorClient FileProcessorClient; private static SecurityServiceClient SecurityServiceClient; @@ -43,6 +48,7 @@ static async Task Main(string[] args) { Func securityResolver = s => { return ConfigurationReader.GetValue("SecurityServiceUri"); }; Func transactionProcessorResolver = s => { return ConfigurationReader.GetValue("TransactionProcessorApi"); }; + Func fileProcessorResolver = s => { return ConfigurationReader.GetValue("FileProcessorApi"); }; HttpClientHandler handler = new() { ServerCertificateCustomValidationCallback = (message, cert, @@ -56,14 +62,17 @@ static async Task Main(string[] args) { Program.SecurityServiceClient = new SecurityServiceClient(securityResolver, client, Serialise, Deserialise); Program.TransactionProcessorClient = new TransactionProcessorClient(transactionProcessorResolver, client, Serialise, Deserialise); + Program.FileProcessorClient = new FileProcessorClient(fileProcessorResolver, client, Serialise, Deserialise); + KurrentDBClientSettings settings = KurrentDBClientSettings.Create(ConfigurationReader.GetValue("EventStoreAddress")); Program.ProjectionClient = new (settings); Program.PersistentSubscriptionsClient = new (settings); - Mode setupMode = Mode.EstateSetup; + Mode setupMode = Mode.FileProcessorSetup; - String configFileName = "setupconfig.json"; + String configFileName = "setupconfig.staging.json"; + FileProcessingOptions fileProcessingOptions = await Program.GetFileProfileConfig(cancellationToken); IdentityServerConfiguration identityServerConfiguration = await Program.GetIdentityServerConfig(cancellationToken); IdentityServerFunctions identityServerFunctions = new(Program.SecurityServiceClient, identityServerConfiguration); EventStoreFunctions eventStoreFunctions = new(Program.ProjectionClient, Program.PersistentSubscriptionsClient); @@ -72,6 +81,7 @@ static async Task Main(string[] args) { Mode.SecuritySetup => await identityServerFunctions.CreateConfig(cancellationToken), Mode.EventStoreSetup => await eventStoreFunctions.SetupEventStore(cancellationToken), Mode.EstateSetup => await SetupEstates(configFileName,cancellationToken), + Mode.FileProcessorSetup => await SetupFileProcessors(fileProcessingOptions, cancellationToken), _ => Result.Invalid($"Invalid mode {setupMode}") }; @@ -83,6 +93,29 @@ static async Task Main(string[] args) { } } + private static async Task SetupFileProcessors(FileProcessingOptions fileProcessingOptions, + CancellationToken cancellationToken) { + Result tokenResult = await SecurityServiceClient.GetToken("serviceClient", "d192cbc46d834d0da90e8a9d50ded543", CancellationToken.None); + if (tokenResult.IsFailed) + return ResultHelpers.CreateFailure(tokenResult); + foreach (FileProfile fileProfile in fileProcessingOptions.FileProfiles) { + CreateFileProfileRequest createFileProfileRequest = new() + { + FileProfileId = fileProfile.Id, + Name = fileProfile.Name, + ListeningDirectory = fileProfile.ListeningDirectory, + RequestType = fileProfile.RequestType, + OperatorName = fileProfile.OperatorName, + LineTerminator = Enum.Parse(fileProfile.LineTerminator), + FileFormatHandler = fileProfile.FileFormatHandler + }; + Result result = await Program.FileProcessorClient.CreateFileProfile(tokenResult.Data.AccessToken, createFileProfileRequest, cancellationToken); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + } + return Result.Success(); + } + static String Serialise(Object arg) { return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); @@ -108,7 +141,8 @@ private static async Task SetupEstates(String configFileName, Cancellati public enum Mode { SecuritySetup, EventStoreSetup, - EstateSetup + EstateSetup, + FileProcessorSetup, } private static async Task GetIdentityServerConfig(CancellationToken cancellationToken) { @@ -123,6 +157,18 @@ private static async Task GetIdentityServerConfig(C return identityServerConfiguration; } + private static async Task GetFileProfileConfig(CancellationToken cancellationToken) + { + // Read the file profile config json string + String fileProfileJsonData; + using StreamReader sr = new("fileprofilesconfig.json"); + fileProfileJsonData = await sr.ReadToEndAsync(cancellationToken); + + FileProcessingOptions fileProcessingOptions = StringSerialiser.Deserialise(fileProfileJsonData); + + return fileProcessingOptions; + } + private static async Task GetEstatesConfig(String configFileName, CancellationToken cancellationToken) { // Read the estate config json string diff --git a/TransactionProcessor.SystemSetupTool/TransactionProcessor.SystemSetupTool.csproj b/TransactionProcessor.SystemSetupTool/TransactionProcessor.SystemSetupTool.csproj index ccda701..1950022 100644 --- a/TransactionProcessor.SystemSetupTool/TransactionProcessor.SystemSetupTool.csproj +++ b/TransactionProcessor.SystemSetupTool/TransactionProcessor.SystemSetupTool.csproj @@ -21,6 +21,7 @@ + @@ -57,6 +58,9 @@ Always + + PreserveNewest + Always diff --git a/TransactionProcessor.SystemSetupTool/appsettings.json b/TransactionProcessor.SystemSetupTool/appsettings.json index 7deaa3b..fde9f02 100644 --- a/TransactionProcessor.SystemSetupTool/appsettings.json +++ b/TransactionProcessor.SystemSetupTool/appsettings.json @@ -3,16 +3,19 @@ // Local (Docker) //"SecurityServiceUri": "https://127.0.0.1:5001", //"TransactionProcessorApi": "http://127.0.0.1:5002", + //"FileProcessorApi": "http://127.0.0.1:5009", //"EventStoreAddress": "esdb://admin:changeit@127.0.0.1:4113?tls=false&tlsVerifyCert=false" // Staging "SecurityServiceUri": "https://192.168.1.163:5001", "TransactionProcessorApi": "http://192.168.1.163:5002", - "EventStoreAddress": "esdb://admin:changeit@192.168.1.163:2113?tls=false&tlsVerifyCert=false" + "FileProcessorApi": "http://192.168.1.163:5009", + "EventStoreAddress": "esdb://admin:changeit@192.168.1.163:2113?tls=false&tlsVerifyCert=false" // Production //"SecurityServiceUri": "https://192.168.1.155:5001", //"TransactionProcessorApi": "http://192.168.1.155:5002", + //"FileProcessorApi": "http://192.168.1.155:5009", //"EventStoreAddress": "esdb://admin:changeit@192.168.1.155:2113?tls=false&tlsVerifyCert=false" } } diff --git a/TransactionProcessor.SystemSetupTool/estateconfig/Address.cs b/TransactionProcessor.SystemSetupTool/estateconfig/Address.cs index 811c2b1..c2902f1 100644 --- a/TransactionProcessor.SystemSetupTool/estateconfig/Address.cs +++ b/TransactionProcessor.SystemSetupTool/estateconfig/Address.cs @@ -11,5 +11,6 @@ public class Address public string Region { get; set; } public string Town { get; set; } + public string Postcode { get; set; } } } \ No newline at end of file diff --git a/TransactionProcessor.SystemSetupTool/fileprofileconfig/FileProcessingOptions.cs b/TransactionProcessor.SystemSetupTool/fileprofileconfig/FileProcessingOptions.cs new file mode 100644 index 0000000..1b990c7 --- /dev/null +++ b/TransactionProcessor.SystemSetupTool/fileprofileconfig/FileProcessingOptions.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace TransactionProcessor.SystemSetupTool.fileprofileconfig; + +public sealed class FileProcessingOptions +{ + public List FileProfiles { get; init; } = []; +} + +public sealed class FileProfile +{ + /// + /// Unique identifier for the file profile. + /// + public Guid Id { get; init; } + + /// + /// Friendly name of the profile. + /// + public required string Name { get; init; } + + /// + /// Directory to monitor for incoming files. + /// + public required string ListeningDirectory { get; init; } + + /// + /// Request type that should be created for each record. + /// + public required string RequestType { get; init; } + + /// + /// Name of the operator associated with the profile. + /// + public required string OperatorName { get; init; } + + /// + /// Line terminator used in the file (e.g. "\n" or "\r\n"). + /// + public required string LineTerminator { get; init; } + + /// + /// Name of the class responsible for parsing the file. + /// + public required string FileFormatHandler { get; init; } +} \ No newline at end of file diff --git a/TransactionProcessor.SystemSetupTool/fileprofilesconfig.json b/TransactionProcessor.SystemSetupTool/fileprofilesconfig.json new file mode 100644 index 0000000..c71e20e --- /dev/null +++ b/TransactionProcessor.SystemSetupTool/fileprofilesconfig.json @@ -0,0 +1,22 @@ +{ + "file_profiles": [ + { + "id": "B2A59ABF-293D-4A6B-B81B-7007503C3476", + "name": "Safaricom Topup", + "listening_directory": "/home/txnproc/bulkfiles/safaricom", + "request_type": "SafaricomTopupRequest", + "operator_name": "Safaricom", + "line_terminator": "LineFeed", + "file_format_handler": "SafaricomFileFormatHandler" + }, + { + "id": "8806EDBC-3ED6-406B-9E5F-A9078356BE99", + "name": "Voucher Issue", + "listening_directory": "/home/txnproc/bulkfiles/voucher", + "request_type": "VoucherRequest", + "operator_name": "Voucher", + "line_terminator": "LineFeed", + "file_format_handler": "VoucherFileFormatHandler" + } + ] +} diff --git a/TransactionProcessor.SystemSetupTool/setupconfig.json b/TransactionProcessor.SystemSetupTool/setupconfig.json index ccc0f77..5247308 100644 --- a/TransactionProcessor.SystemSetupTool/setupconfig.json +++ b/TransactionProcessor.SystemSetupTool/setupconfig.json @@ -15,12 +15,13 @@ { "merchant_id": "AB1C99FB-1C6C-4694-9A32-B71BE5D1DA33", "name": "Test Merchant 1", - "settlementschedule": "Weekly", + "settlement_schedule": "Weekly", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Test Contact", @@ -42,7 +43,7 @@ "device": { "device_identifier": "testmerchant1device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" } ], "operators": [ diff --git a/TransactionProcessor.SystemSetupTool/setupconfig.production.json b/TransactionProcessor.SystemSetupTool/setupconfig.production.json index 4c4a3a1..c310110 100644 --- a/TransactionProcessor.SystemSetupTool/setupconfig.production.json +++ b/TransactionProcessor.SystemSetupTool/setupconfig.production.json @@ -15,12 +15,13 @@ { "merchant_id": "AB1C99FB-1C6C-4694-9A32-B71BE5D1DA33", "name": "Live Merchant 1", - "settlementschedule": "Weekly", + "settlement_schedule": "Weekly", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -42,17 +43,18 @@ "device": { "device_identifier": "livemerchant1device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "AF4D7C7C-9B8D-4E58-A12A-28D3E0B89DF6", "name": "Live Merchant 2", - "settlementschedule": "Monthly", + "settlement_schedule": "Monthly", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -74,17 +76,18 @@ "device": { "device_identifier": "livemerchant2device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "8BC8434D-41F9-4CC3-83BC-E73F20C02E1D", "name": "Live Merchant 3", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -106,17 +109,18 @@ "device": { "device_identifier": "livemerchant3device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "22549CD1-AFDD-44F9-882F-51C3EA71CCC0", "name": "Live Merchant 4", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -138,17 +142,18 @@ "device": { "device_identifier": "livemerchant4device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "115EF339-6FB2-43E8-9C7F-7F270059AD08", "name": "Live Merchant 5", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -170,17 +175,18 @@ "device": { "device_identifier": "livemerchant5device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "E8064ED9-BEDC-4159-9708-888BDE070BE4", "name": "Live Merchant 6", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -202,17 +208,18 @@ "device": { "device_identifier": "livemerchant6device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "870D41FA-FD73-40B8-A8C2-A2673D6C7428", "name": "Live Merchant 7", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -234,17 +241,18 @@ "device": { "device_identifier": "livemerchant7device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "279502C6-EA18-4E5A-A678-5F1CA044E828", "name": "Live Merchant 8", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Live Contact", @@ -266,7 +274,7 @@ "device": { "device_identifier": "livemerchant8device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" } ], diff --git a/TransactionProcessor.SystemSetupTool/setupconfig.staging.json b/TransactionProcessor.SystemSetupTool/setupconfig.staging.json index e9801ef..ccbd7dd 100644 --- a/TransactionProcessor.SystemSetupTool/setupconfig.staging.json +++ b/TransactionProcessor.SystemSetupTool/setupconfig.staging.json @@ -15,12 +15,13 @@ { "merchant_id": "AB1C99FB-1C6C-4694-9A32-B71BE5D1DA33", "name": "Staging Merchant 1", - "settlementschedule": "Weekly", + "settlement_schedule": "Weekly", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Staging Contact", @@ -42,17 +43,18 @@ "device": { "device_identifier": "stagingmerchant1device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "AF4D7C7C-9B8D-4E58-A12A-28D3E0B89DF6", "name": "Staging Merchant 2", - "settlementschedule": "Monthly", + "settlement_schedule": "Monthly", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Staging Contact", @@ -74,17 +76,18 @@ "device": { "device_identifier": "stagingmerchant2device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" }, { "merchant_id": "8BC8434D-41F9-4CC3-83BC-E73F20C02E1D", "name": "Staging Merchant 3", - "settlementschedule": "Immediate", + "settlement_schedule": "Immediate", "address": { - "address_line_1": "test address line 1", + "address_line1": "test address line 1", "country": "United Kingdom", "region": "Region", - "town": "MyTown" + "town": "MyTown", + "postcode": "AB12 3CD" }, "contact": { "contact_name": "Staging Contact", @@ -106,7 +109,7 @@ "device": { "device_identifier": "stagingmerchant3device" }, - "createdate": "2022-08-31" + "create_date": "2022-08-31" } ], "operators": [ diff --git a/docker-compose-master.yml b/docker-compose-master.yml index e3d3e49..fe32d67 100644 --- a/docker-compose-master.yml +++ b/docker-compose-master.yml @@ -119,6 +119,7 @@ services: environment: - ConnectionStrings:TestBankReadModel=server=sqlserver;user id=sa;password=thisisalongpassword123!;database=TestBankReadModel;Encrypt=false - ConnectionStrings:PataPawaReadModel=server=sqlserver;user id=sa;password=thisisalongpassword123!;database=PataPawaReadModel;Encrypt=false + - ConnectionStrings:AgencyBankingReadModel=server=sqlserver;user id=sa;password=thisisalongpassword123!;database=AgencyBankingReadModel;Encrypt=false ports: - "9000:9000" restart: on-failure @@ -165,3 +166,23 @@ services: - EventStoreSettings:Insecure=true depends_on: - kurrentdb + + estatereporting: + image: stuartferguson/estatereportingapi + container_name: estatereporting_master + volumes: + - E:\home\txnproc\docker\trace:/home/txnproc/trace + ports: + - "5011:5011" + environment: + - EventStoreSettings:ConnectionString=esdb://admin:changeit@kurrentdb:2113?tls=false&tlsVerifyCert=false + - EventStoreSettings:Insecure=true + - AppSettings:SecurityService=https://securityservice:5001 + - SecurityConfiguration:Authority=https://securityservice:5001 + - urls=http://*:5011 + - ConnectionStrings:TransactionProcessorReadModel=server=sqlserver;user id=sa;password=thisisalongpassword123!;database=TransactionProcessorReadModel;Encrypt=false + - ConnectionStrings:HealthCheck=server=sqlserver;database=master;user id=sa;password=thisisalongpassword123!;Encrypt=false + depends_on: + - sqlserver + - kurrentdb + - securityservice