diff --git a/.azuredevops/pipelines/build-dr-func.yml b/.azuredevops/pipelines/build-dr-func.yml
new file mode 100644
index 0000000..669856a
--- /dev/null
+++ b/.azuredevops/pipelines/build-dr-func.yml
@@ -0,0 +1,43 @@
+trigger:
+ - develop
+ - main
+ - releases/*
+
+pool:
+ vmImage: windows-latest
+
+steps:
+ - task: UseDotNet@2
+ displayName: 'Install .NET 6 SDK'
+ inputs:
+ packageType: 'sdk'
+ version: '6.0.x'
+ performMultiLevelLookup: true
+
+ - script: |
+ ls
+ cd Source/CDR.GetDataRecipients
+ ls
+ dotnet restore
+ dotnet build --configuration Release
+
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: publish
+ arguments: '--configuration Release --output publish_output'
+ projects: 'Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj'
+ publishWebProjects: false
+ modifyOutputPath: false
+ zipAfterPublish: false
+
+ - task: ArchiveFiles@2
+ displayName: 'Archive Files'
+ inputs:
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/publish_output'
+ includeRootFolder: false
+ archiveFile: '$(System.DefaultWorkingDirectory)/CDR.GetDataRecipients.zip'
+
+ - task: PublishBuildArtifacts@1
+ inputs:
+ PathToPublish: '$(System.DefaultWorkingDirectory)/CDR.GetDataRecipients.zip'
+ artifactName: 'functions'
diff --git a/.azuredevops/pipelines/build-v2.yml b/.azuredevops/pipelines/build-v2.yml
index a448f7c..d06f523 100644
--- a/.azuredevops/pipelines/build-v2.yml
+++ b/.azuredevops/pipelines/build-v2.yml
@@ -9,6 +9,8 @@ resources:
trigger:
- develop
+ - main
+ - releases/*
pool:
vmImage: ubuntu-latest
@@ -24,7 +26,7 @@ steps:
displayName: Build mock-register image
inputs:
command: build
- Dockerfile: $(Build.SourcesDirectory)/sb-mock-register/Source/Dockerfile
+ Dockerfile: $(Build.SourcesDirectory)/sb-mock-register/Source/Dockerfile.for-testing
buildContext: $(Build.SourcesDirectory)/sb-mock-register/Source
repository: mock-register
tags: latest
@@ -39,16 +41,6 @@ steps:
repository: mock-data-holder-energy
tags: latest
-# Build mock-data-holder-energy-unit-tests
-- task: Docker@2
- displayName: Build mock-data-holder-energy-unit-tests image
- inputs:
- command: build
- Dockerfile: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/Dockerfile.unit-tests
- buildContext: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source
- repository: mock-data-holder-energy-unit-tests
- tags: latest
-
# Build mock-data-holder-energy-integration-tests
- task: Docker@2
displayName: Build mock-data-holder-energy-integration-tests image
@@ -59,6 +51,16 @@ steps:
repository: mock-data-holder-energy-integration-tests
tags: latest
+# Build mock-data-holder-energy-for-testing
+- task: Docker@2
+ displayName: Build mock-data-holder-energy-for-testing image
+ inputs:
+ command: build
+ Dockerfile: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/Dockerfile.for-testing
+ buildContext: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source
+ repository: mock-data-holder-energy-for-testing
+ tags: latest
+
# List docker images
- task: Docker@2
displayName: List Docker images
@@ -66,23 +68,6 @@ steps:
inputs:
command: images
-# Run unit tests
-- task: DockerCompose@0
- displayName: Unit Tests - Up
- inputs:
- action: Run a Docker Compose command
- dockerComposeFile: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/docker-compose.UnitTests.yml
- dockerComposeCommand: up --abort-on-container-exit --exit-code-from mock-data-holder-energy-unit-tests
-
-# Remove unit tests
-- task: DockerCompose@0
- displayName: Unit Tests - Down
- condition: always()
- inputs:
- action: Run a Docker Compose command
- dockerComposeFile: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/docker-compose.UnitTests.yml
- dockerComposeCommand: down
-
# Run integration tests
- task: DockerCompose@0
displayName: Integration Tests - Up
@@ -131,12 +116,6 @@ steps:
condition: always()
artifact: Mock-Data-Holder-Energy - Logs
-# Publish mock-data-holder unit tests results
-- publish: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/_temp/mock-data-holder-energy-unit-tests/testresults
- displayName: Publish unit tests
- condition: always()
- artifact: Mock-Data-Holder-Energy - Unit tests
-
# Publish mock-data-holder integration tests results
- publish: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/_temp/mock-data-holder-energy-integration-tests/testresults
displayName: Publish integration tests
@@ -175,4 +154,18 @@ steps:
- publish: $(Build.SourcesDirectory)/sb-mock-data-holder-energy/Source/CDR.DataHolder.Repository/efbundle
displayName: Publish EF Migration bundle
condition: always()
- artifact: Database Migration Scripts
\ No newline at end of file
+ artifact: Database Migration Scripts
+
+- task: PublishTestResults@2
+ displayName: 'Surface Integration Test TRX results to devops'
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest
+ testResultsFiles: '**/results.trx'
+ #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional
+ #mergeTestResults: false # Optional
+ #failTaskOnFailedTests: false # Optional
+ #testRunTitle: # Optional
+ #buildPlatform: # Optional
+ #buildConfiguration: # Optional
+ #publishRunAttachments: true # Optional
\ No newline at end of file
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index a7e3e5a..1f731ce 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -71,7 +71,7 @@ jobs:
uses: docker/build-push-action@v2
with:
context: ./Source
- file: ./Source/Dockerfile
+ file: ./Source/Dockerfile.for-testing
platforms: linux/amd64,linux/arm64
push: ${{ github.repository_owner == 'ConsumerDataRight' && github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 041690b..b0e87b2 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -67,7 +67,7 @@ jobs:
# Build mock-data-holder-energy image
- name: Build the mock-data-holder-energy image
run: |
- docker build ./mock-data-holder-energy/Source --file ./mock-data-holder-energy/Source/Dockerfile --tag mock-data-holder-energy:latest
+ docker build ./mock-data-holder-energy/Source --file ./mock-data-holder-energy/Source/Dockerfile.for-testing --tag mock-data-holder-energy:latest
# Build mock-data-holder-energy-unit-tests image
- name: Build the mock-data-holder-energy-unit-tests-energy image
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30cf582..0bb3439 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.0.0] - 2022-07-22
+### Added
+- Azure function to perform Data Recipient discovery by polling the Get Data Recipients API of the Register.
+
+### Changed
+- First version of the Mock Data Holder Energy deployed into the CDR Sandbox.
+- Updated Get Energy Concessions schema to match Consumer Data Standards 1.17.0.
+
## [0.1.1] - 2022-06-09
### Changed
- Person information in seed data.
diff --git a/Help/azurefunctions/HELP.md b/Help/azurefunctions/HELP.md
new file mode 100644
index 0000000..60ce81e
--- /dev/null
+++ b/Help/azurefunctions/HELP.md
@@ -0,0 +1,66 @@
+
Azure Functions
+
+The Mock Data Holder Energy solution contains an azure function project.
+The GetDataRecipients function is used to get the list of Data Recipients
+from the Mock Register and update the Mock Data Hoder Energy repository.
+
+
+To Run and Debug Azure Functions
+
+ The following procedures can be used to run the functions in a local development environment for evaluation of the functions.
+
+
+
+1) Start the Mock Register and the Mock Data Holder Energy solutions.
+
+
+
+2) Start the Azure Storage Emulator (Azurite):
+
+
+ using a MS Windows command prompt:
+
+
+```
+md C:\azurite
+cd "C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\Extensions\Microsoft\Azure Storage Emulator"
+azurite --silent --location c:\azurite --debug c:\azurite\debug.log
+```
+
+
+ Noting this is only required to be performed once, it will then be listening on ports - 10000, 10001 and 10002
+ when debugging is started from MS Visual Studio by selecting CDR.GetDataRecipients as the startup project
+ (by starting a debug instance using F5 or Debug > Start Debugging)
+
+
+
+ or by using a MS Windows command prompt:
+
+
+```
+navigate to .\mock-data-holder-energy\Source\CDR.GetDataRecipients
+func start --verbose
+```
+
+
3) Open the Mock Data Holder Energy in MS Visual Studio, select CDR.GetDataRecipients as the startup project.
+
+
4) Start each debug instances (F5 or Debug > Start Debugging), this will simulate the discovery of Data Recipients and the
+
+ updating of the data in the Mock Data Holder Energy repositories.
+
+
+
+ Noting the below sql scripts are used to observe the results.
+
+
+```
+SELECT * FROM [cdr-mdhe].[dbo].[LegalEntity]
+SELECT * FROM [cdr-mdhe].[dbo].[Brand]
+SELECT * FROM [cdr-mdhe].[dbo].[SoftwareProduct]
+SELECT * FROM [cdr-mdhe].[dbo].[LogEvents_DRService]
+```
+
+
To Build Azure Functions
+
+ dotnet SDK 6.0.30x or higher is required. Latest SDK can be found from the link https://microsoft.com/net
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 8f814ad..ac3c984 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
![Consumer Data Right Logo](https://raw.githubusercontent.com/ConsumerDataRight/mock-data-holder-energy/main/cdr-logo.png)
-[![Consumer Data Standards v1.16.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.16.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.16.0/#introduction)
+[![Consumer Data Standards v1.17.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.17.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards/#introduction)
[![FAPI 1.0 Advanced Profile](https://img.shields.io/badge/FAPI%201.0-orange.svg)](https://openid.net/specs/openid-financial-api-part-2-1_0.html)
[![made-with-dotnet](https://img.shields.io/badge/Made%20with-.NET-1f425Ff.svg)](https://dotnet.microsoft.com/)
[![made-with-csharp](https://img.shields.io/badge/Made%20with-C%23-1f425Ff.svg)](https://docs.microsoft.com/en-us/dotnet/csharp/)
@@ -13,7 +13,7 @@ This project includes source code, documentation and instructions for a Consumer
This repository contains a mock implementation of a Mock Data Holder Energy and is offered to help the community in the development and testing of their CDR solutions.
## Mock Data Holder Energy - Alignment
-The Mock Data Holder Energy aligns to [v1.16.0](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.16.0/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards/#introduction).
+The Mock Data Holder Energy aligns to [v1.17.0](https://consumerdatastandardsaustralia.github.io/standards/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards/#introduction).
The Mock Data Holder Energy is based on the [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) codebase. The [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) has passed v3.2 of the [Conformance Test Suite for Data Holders](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-holders). Conformance Testing specific to the Mock Data Holder Energy has not yet been completed.
The Mock Data Holder Energy is compliant with the [FAPI 1.0 Advanced Profile](https://openid.net/specs/openid-financial-api-part-2-1_0.html).
The Mock Data Holder Energy aligns to [FAPI 1.0 Migration Phase 2](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.16.0/#authentication-flows).
@@ -115,6 +115,9 @@ The following diagram outlines the high level architecture of the Mock Data Hold
[
](https://raw.githubusercontent.com/ConsumerDataRight/mock-data-holder-energy/main/mock-data-holder-energy-architecture.png)
+Get Data Recipients discovery Azure Function:
+
+[
](mock-data-holder-discovery-architecture.png)
## Mock Data Holder Energy - Components
The Mock Data Holder Energy contains the following components:
@@ -140,6 +143,9 @@ The Mock Data Holder Energy contains the following components:
- Not part of the Consumer Data Standards, but allows for the maintenance of data in the Mock Data Holder Energy repository.
- Also includes trigger points to refresh the Data Recipient, Data Recipient Status and Software Product Status from the Mock Register.
- A user interface may be added at some time in the future to provide user friendly access to the repository data.
+- Azure Function
+ - An Azure Function that can automate the continuous Get Data Recipients discovery process.
+ - To get help on the Azure Functions, see the [help guide](./Help/azurefunctions/HELP.md).
- Repository
- A SQL database containing Mock Data Holder Energy data.
diff --git a/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj b/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj
index 5692fc6..9bd7715 100644
--- a/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj
+++ b/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj
@@ -3,9 +3,9 @@
net6.0
win-x64;linux-x64
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
@@ -35,6 +35,10 @@
+
+
+
+
Always
diff --git a/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/CertificateValidator.cs b/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/CertificateValidator.cs
index d649038..9b27126 100644
--- a/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/CertificateValidator.cs
+++ b/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/CertificateValidator.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
+using CDR.DataHolder.API.Infrastructure.Exceptions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
@@ -11,7 +12,6 @@ namespace CDR.DataHolder.API.Gateway.mTLS.Certificates
///
public class CertificateValidator : ICertificateValidator
{
- private const string ROOT_CA_SUBJECT = "CN=Mock CDR CA, OU=CDR, O=ACCC, L=Canberra, S=ACT, C=AU";
private readonly ILogger _logger;
private readonly IConfiguration _config;
@@ -21,7 +21,7 @@ public CertificateValidator(ILogger logger, IConfiguration
_config = config;
}
- public bool IsValid(X509Certificate2 clientCert)
+ public void ValidateClientCertificate(X509Certificate2 clientCert)
{
_logger.LogInformation($"Validating certificate within the {nameof(CertificateValidator)}");
@@ -34,7 +34,8 @@ public bool IsValid(X509Certificate2 clientCert)
var rootCACertificate = new X509Certificate2(_config.GetValue("RootCACertificate:Path"));
var ch = new X509Chain();
ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
- ch.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
+ ch.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
+ ch.ChainPolicy.CustomTrustStore.Clear();
ch.ChainPolicy.CustomTrustStore.Add(rootCACertificate);
ch.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
@@ -44,16 +45,13 @@ public bool IsValid(X509Certificate2 clientCert)
}
catch (Exception ex)
{
- throw new ArgumentException("The certificate chain cannot be discovered from the provided client certificate.", ex);
+ throw new ClientCertificateException("The certificate chain cannot be discovered from the provided client certificate.", ex);
}
if (ch.ChainStatus.Any())
{
- _logger.LogError("The client cert could not be verified to have been issued by '{subject}'. {statusInformation}", ROOT_CA_SUBJECT, ch.ChainStatus[0].StatusInformation);
- return false;
+ throw new ClientCertificateException(ch.ChainStatus.First().StatusInformation);
}
-
- return true;
}
}
}
diff --git a/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/ICertificateValidator.cs b/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/ICertificateValidator.cs
index 88ab597..1b57d43 100644
--- a/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/ICertificateValidator.cs
+++ b/Source/CDR.DataHolder.API.Gateway.mTLS/Certificates/ICertificateValidator.cs
@@ -4,6 +4,6 @@ namespace CDR.DataHolder.API.Gateway.mTLS.Certificates
{
public interface ICertificateValidator
{
- bool IsValid(X509Certificate2 clientCert);
+ void ValidateClientCertificate(X509Certificate2 clientCert);
}
}
diff --git a/Source/CDR.DataHolder.API.Gateway.mTLS/Startup.cs b/Source/CDR.DataHolder.API.Gateway.mTLS/Startup.cs
index fd10a4e..083dda8 100644
--- a/Source/CDR.DataHolder.API.Gateway.mTLS/Startup.cs
+++ b/Source/CDR.DataHolder.API.Gateway.mTLS/Startup.cs
@@ -1,9 +1,12 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using CDR.DataHolder.API.Gateway.mTLS.Certificates;
+using CDR.DataHolder.API.Infrastructure.Exceptions;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -11,6 +14,7 @@
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Serilog;
+using static System.Net.Mime.MediaTypeNames;
namespace CDR.DataHolder.API.Gateway.mTLS
{
@@ -44,39 +48,55 @@ public void ConfigureServices(IServiceCollection services)
logger.LogInformation("OnCertificateValidated...");
var certValidator = context.HttpContext.RequestServices.GetService();
- if (!certValidator.IsValid(context.ClientCertificate))
- {
- const string failValidationMsg = "The client certificate failed to validate";
- logger.LogWarning(failValidationMsg);
- context.Fail(failValidationMsg);
- }
-
+ certValidator.ValidateClientCertificate(context.ClientCertificate);
+ context.Success();
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
- context.Fail("invalid cert");
- return Task.CompletedTask;
+ context.Fail("invalid client certificate");
+ throw context.Exception;
}
};
})
// Adding an ICertificateValidationCache results in certificate auth caching the results.
// The default implementation uses a memory cache.
.AddCertificateCache();
+ services.AddAuthorization();
services.AddOcelot();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
- if (env.IsDevelopment())
+ app.UseSerilogRequestLogging();
+
+ app.UseExceptionHandler(exceptionHandlerApp =>
{
- app.UseDeveloperExceptionPage();
- }
+ exceptionHandlerApp.Run(async context =>
+ {
+ // Try and retrieve the error from the ExceptionHandler middleware
+ var exceptionDetails = context.Features.Get();
+ var ex = exceptionDetails?.Error;
- app.UseSerilogRequestLogging();
+ if (ex is ClientCertificateException)
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ }
+ else
+ {
+ context.Response.StatusCode = StatusCodes.Status502BadGateway;
+ }
+
+ // using static System.Net.Mime.MediaTypeNames;
+ context.Response.ContentType = Text.Plain;
+ await context.Response.WriteAsync($"An error occurred handling the request: {ex?.Message}");
+ });
+ });
app.UseHttpsRedirection();
+ app.UseAuthentication();
+ app.UseAuthorization();
var pipelineConfiguration = new OcelotPipelineConfiguration
{
@@ -97,8 +117,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
await next.Invoke();
}
};
-
- app.UseAuthentication();
app.UseOcelot(pipelineConfiguration).Wait();
}
}
diff --git a/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj b/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj
index 478d9c1..bea42b3 100644
--- a/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj
+++ b/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj
@@ -3,9 +3,9 @@
net6.0
false
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj b/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj
index 84383fd..32f9b53 100644
--- a/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj
+++ b/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj
@@ -2,9 +2,9 @@
net6.0
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.API.Infrastructure/Constants.cs b/Source/CDR.DataHolder.API.Infrastructure/Constants.cs
index 18b1b63..9112485 100644
--- a/Source/CDR.DataHolder.API.Infrastructure/Constants.cs
+++ b/Source/CDR.DataHolder.API.Infrastructure/Constants.cs
@@ -14,11 +14,19 @@ public static class ApiScopes
public static class Energy
{
public const string AccountsBasicRead = "energy:accounts.basic:read";
+ public const string AccountsDetailRead = "energy:accounts.detail:read";
public const string ConcessionsRead = "energy:accounts.concessions:read";
+ public const string ServicePointsBasicRead = "energy:electricity.servicepoints.basic:read";
+ public const string ServicePointsDetailRead = "energy:electricity.servicepoints.detail:read";
+ public const string ElectricityUsageRead = "energy:electricity.usage:read";
+ public const string ElectricityDerRead = "energy:electricity.der:read";
+ public const string EnergyAccountsPaymentScheduleRead = "energy:accounts.paymentschedule:read";
+ public const string EnergyBillingRead = "energy:billing:read";
}
public static class Common
{
public const string CustomerBasicRead = "common:customer.basic:read";
+ public const string CustomerDetailRead = "common:customer.detail:read";
}
}
diff --git a/Source/CDR.DataHolder.API.Infrastructure/Exceptions/ClientCertificateException.cs b/Source/CDR.DataHolder.API.Infrastructure/Exceptions/ClientCertificateException.cs
new file mode 100644
index 0000000..cdbe35c
--- /dev/null
+++ b/Source/CDR.DataHolder.API.Infrastructure/Exceptions/ClientCertificateException.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace CDR.DataHolder.API.Infrastructure.Exceptions
+{
+ public class ClientCertificateException : Exception
+ {
+ public ClientCertificateException(string message) : base($"An error occurred validating the client certificate: {message}")
+ {
+ }
+
+ public ClientCertificateException(string message, Exception ex) : base($"An error occurred validating the client certificate: {message}", ex)
+ {
+ }
+ }
+}
diff --git a/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj b/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj
index 9f30f83..48db728 100644
--- a/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj
+++ b/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj
@@ -3,9 +3,9 @@
net6.0
win-x64;linux-x64
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj b/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj
index 6c98340..3bd403b 100644
--- a/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj
+++ b/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj
@@ -2,9 +2,9 @@
net6.0
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.Domain/Entities/EnergyAccountConcession.cs b/Source/CDR.DataHolder.Domain/Entities/EnergyAccountConcession.cs
index b3e574e..becf931 100644
--- a/Source/CDR.DataHolder.Domain/Entities/EnergyAccountConcession.cs
+++ b/Source/CDR.DataHolder.Domain/Entities/EnergyAccountConcession.cs
@@ -2,14 +2,15 @@
{
public class EnergyAccountConcession
{
+ public string Type { get; set; }
public string DisplayName { get; set; }
public string AdditionalInfo { get; set; }
public string AdditionalInfoUri { get; set; }
public string StartDate { get; set; }
public string EndDate { get; set; }
- public string DailyDiscount { get; set; }
- public string MonthlyDiscount { get; set; }
- public string YearlyDiscount { get; set; }
- public string PercentageDiscount { get; set; }
+ public string DiscountFrequency { get; set; }
+ public string Amount { get; set; }
+ public string Percentage { get; set; }
+ public string[] AppliedTo { get; set; }
}
}
\ No newline at end of file
diff --git a/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj b/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj
index 8f3e312..82181f8 100644
--- a/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj
+++ b/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj
@@ -3,9 +3,9 @@
net6.0
false
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.IdentityServer/AutoMapper/RegistrationProfile.cs b/Source/CDR.DataHolder.IdentityServer/AutoMapper/RegistrationProfile.cs
index 3635bff..24bc81c 100644
--- a/Source/CDR.DataHolder.IdentityServer/AutoMapper/RegistrationProfile.cs
+++ b/Source/CDR.DataHolder.IdentityServer/AutoMapper/RegistrationProfile.cs
@@ -8,6 +8,7 @@
using IdentityServer4.Models;
using static CDR.DataHolder.IdentityServer.CdsConstants;
using CDR.DataHolder.IdentityServer.Extensions;
+using static CDR.DataHolder.API.Infrastructure.Constants;
namespace CDR.DataHolder.IdentityServer.AutoMapper
{
diff --git a/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj b/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj
index 12b355f..81a4f98 100644
--- a/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj
+++ b/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj
@@ -3,9 +3,9 @@
net6.0
win-x64;linux-x64
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.IdentityServer/CdsConstants.cs b/Source/CDR.DataHolder.IdentityServer/CdsConstants.cs
index 8a95450..c1d1242 100644
--- a/Source/CDR.DataHolder.IdentityServer/CdsConstants.cs
+++ b/Source/CDR.DataHolder.IdentityServer/CdsConstants.cs
@@ -714,37 +714,6 @@ public static class JwtToken
public const string JwtType = "JWT";
}
- public static class ApiScopes
- {
- public static class Banking
- {
- public const string Accounts = "bank:accounts.basic:read";
- public const string Transactions = "bank:transactions:read";
- }
- }
-
- public static class StandardScopes
- {
- //
- // Summary:
- // REQUIRED. Informs the Authorization Server that the Client is making an OpenID
- // Connect request. If the openid scope value is not present, the behavior is entirely
- // unspecified.
- public const string OpenId = "openid";
- //
- // Summary:
- // OPTIONAL. This scope value requests access to the End-User's default profile
- // Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username,
- // profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
- public const string Profile = "profile";
- //
- // Summary:
- // This scope value MUST NOT be used with the OpenID Connect Implicit Client Implementer's
- // Guide 1.0. See the OpenID Connect Basic Client Implementer's Guide 1.0 (http://openid.net/specs/openid-connect-implicit-1_0.html#OpenID.Basic)
- // for its usage in that subset of OpenID Connect.
- public const string OfflineAccess = "offline_access";
- }
-
public static class ProtocolTypes
{
public const string OpenIdConnect = "oidc";
diff --git a/Source/CDR.DataHolder.IdentityServer/Configuration/InMemoryConfig.cs b/Source/CDR.DataHolder.IdentityServer/Configuration/InMemoryConfig.cs
index 395d376..f498571 100644
--- a/Source/CDR.DataHolder.IdentityServer/Configuration/InMemoryConfig.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Configuration/InMemoryConfig.cs
@@ -1,41 +1,40 @@
using System.Collections.Generic;
using IdentityModel;
using IdentityServer4.Models;
+using Microsoft.Extensions.Configuration;
namespace CDR.DataHolder.IdentityServer.Configuration
{
public static class InMemoryConfig
{
- public static IEnumerable Apis =>
+ public static IEnumerable Apis(IConfiguration config) =>
new List
{
- new ApiResource("cds-au", "Mock Data Holder (MDH) Resource API")
+ new ApiResource("cds-au", "Mock Data Holder Energy (MDHE) Resource API")
{
- // Need to add all the scopes supported by cds-au api.
- // All the scopes supported by the cds-au API.
- Scopes = new string[] {
- API.Infrastructure.Constants.CdrScopes.Registration,
- API.Infrastructure.Constants.ApiScopes.Energy.AccountsBasicRead,
- API.Infrastructure.Constants.ApiScopes.Energy.ConcessionsRead,
- API.Infrastructure.Constants.ApiScopes.Common.CustomerBasicRead,
-
- }
+ Scopes = config["ScopesSupported"].Split(',')
}
};
- public static IEnumerable ApiScopes =>
- new[]
+ public static IEnumerable ApiScopes(IConfiguration config)
+ {
+ var apiScopes = new List();
+
+ // These are the supported scopes for this data holder implementation.
+ foreach (var scope in config["ScopesSupported"].Split(','))
{
- // These are the supported scopes for this data holder implementation.
- new ApiScope(API.Infrastructure.Constants.CdrScopes.Registration, "Dynamic Client Registration (DCR)"),
- new ApiScope(API.Infrastructure.Constants.ApiScopes.Energy.AccountsBasicRead, "Basic read access to energy accounts"),
- new ApiScope(API.Infrastructure.Constants.ApiScopes.Energy.ConcessionsRead, "The scope would allow the third party to access the details of any concessions for a customer’s energy account."),
- new ApiScope(API.Infrastructure.Constants.ApiScopes.Common.CustomerBasicRead, "Basic read access to customer information"),
+ if (scope != API.Infrastructure.Constants.StandardScopes.OpenId && scope != API.Infrastructure.Constants.StandardScopes.Profile)
+ {
+ apiScopes.Add(new ApiScope(scope));
+ }
+ }
- // These are the additional scopes for CDR. These need to be here to allow a DR with more scopes than supported to authorise.
- new ApiScope(API.Infrastructure.Constants.CdrScopes.MetricsBasicRead, "Metrics data accessible ONLY to the CDR Register"),
- new ApiScope(API.Infrastructure.Constants.CdrScopes.MetadataUpdate, "Update notification accessible ONLY to the CDR Register"),
- };
+ // These are the additional scopes for CDR. These need to be here to allow a DR with more scopes than supported to authorise.
+ apiScopes.Add(new ApiScope(API.Infrastructure.Constants.CdrScopes.MetricsBasicRead, "Metrics data accessible ONLY to the CDR Register"));
+ apiScopes.Add(new ApiScope(API.Infrastructure.Constants.CdrScopes.MetadataUpdate, "Update notification accessible ONLY to the CDR Register"));
+
+ return apiScopes;
+ }
public static IEnumerable IdentityResources =>
new List
diff --git a/Source/CDR.DataHolder.IdentityServer/Program.cs b/Source/CDR.DataHolder.IdentityServer/Program.cs
index e2aa4f9..3395610 100644
--- a/Source/CDR.DataHolder.IdentityServer/Program.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Program.cs
@@ -44,8 +44,16 @@ public static int Main(string[] args)
CreateHostBuilder(args, configuration, new SerilogLoggerFactory(Log.Logger).CreateLogger()).Build().Run();
return 0;
}
- catch (Exception ex) when (ex is not OperationCanceledException && ex.GetType().Name != "StopTheHostException")
+ catch (Exception ex)
{
+ // StopTheHostException - Unhandled exception from a BackgroundService thrown out of
+ // HostBuilder.Build() on adding a migration to a EF/.NET Core 6.0 RC2 project, needs to be dealt with gracefully.
+ string type = ex.GetType().Name;
+ if (type.Equals("StopTheHostException", StringComparison.Ordinal))
+ {
+ throw;
+ }
+
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Services/ClientArrangementRevocationEndpointRequestService.cs b/Source/CDR.DataHolder.IdentityServer/Services/ClientArrangementRevocationEndpointRequestService.cs
index 803fac7..bec8424 100644
--- a/Source/CDR.DataHolder.IdentityServer/Services/ClientArrangementRevocationEndpointRequestService.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Services/ClientArrangementRevocationEndpointRequestService.cs
@@ -1,11 +1,9 @@
-using CDR.DataHolder.IdentityServer.Configuration;
-using CDR.DataHolder.IdentityServer.Interfaces;
+using CDR.DataHolder.IdentityServer.Interfaces;
using CDR.DataHolder.IdentityServer.Models;
using CDR.DataHolder.IdentityServer.Services.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Serilog.Context;
using System;
@@ -115,7 +113,7 @@ public async Task SendRevocationRequest(string cd
// Call the DR's arrangement revocation endpoint.
var httpResponse = await _clientArrangementRevocationEndpointHttpClient.PostToArrangementRevocationEndPoint(
- (await GetFormValues(cdrArrangementId, useJwt)),
+ (await GetFormValues(cdrArrangementId, brandId, revocationUri.ToString(), useJwt)),
signedBearerTokenJwt,
revocationUri);
@@ -150,13 +148,25 @@ public async Task GetSignedJwt(JwtSecurityToken jwtSecurityToken)
return $"{plaintext}.{signature}";
}
- private async Task> GetFormValues(string cdrArrangementId, bool useJwt = false)
+ private async Task> GetFormValues(
+ string cdrArrangementId,
+ string brandId,
+ string audience,
+ bool useJwt = false)
{
var formValues = new Dictionary();
if (useJwt)
{
- var jwt = new JwtSecurityToken(claims: new Claim[] { new Claim(CdrArrangementRevocationRequest.CdrArrangementJwt, cdrArrangementId) });
+ var jwt = new JwtSecurityToken(
+ issuer: brandId,
+ audience: audience,
+ expires: DateTime.UtcNow.AddMinutes(_defaultExpiryMinutes),
+ claims: new Claim[] {
+ new Claim(CdrArrangementRevocationRequest.CdrArrangementId, cdrArrangementId),
+ new Claim(JwtRegisteredClaimNames.Sub, brandId),
+ new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
+ });
formValues.Add(CdrArrangementRevocationRequest.CdrArrangementJwt, (await GetSignedJwt(jwt)));
}
else
@@ -164,6 +174,8 @@ private async Task> GetFormValues(string cdrArrangeme
formValues.Add(CdrArrangementRevocationRequest.CdrArrangementId, cdrArrangementId);
}
+ _logger.LogInformation("Arrangement revocation request using {form}:{form_value} ", formValues.First().Key, formValues.First().Value);
+
return formValues;
}
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Services/ClientService.cs b/Source/CDR.DataHolder.IdentityServer/Services/ClientService.cs
index a66e72e..615fdb6 100644
--- a/Source/CDR.DataHolder.IdentityServer/Services/ClientService.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Services/ClientService.cs
@@ -1,8 +1,13 @@
-using System.Threading.Tasks;
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Threading.Tasks;
using CDR.DataHolder.IdentityServer.Configuration;
using CDR.DataHolder.IdentityServer.Models;
using CDR.DataHolder.IdentityServer.Stores;
using IdentityServer4.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.IdentityModel.Tokens;
namespace CDR.DataHolder.IdentityServer.Services
{
@@ -10,9 +15,14 @@ public class ClientService : IClientService
{
private readonly DynamicClientStore _clientStore;
private readonly IConfigurationSettings _configurationSettings;
+ private readonly ILogger _logger;
- public ClientService(DynamicClientStore clientStore, IConfigurationSettings configurationSettings)
+ public ClientService(
+ ILogger logger,
+ DynamicClientStore clientStore,
+ IConfigurationSettings configurationSettings)
{
+ _logger = logger;
_configurationSettings = configurationSettings;
_clientStore = clientStore;
}
@@ -40,5 +50,27 @@ public async Task UpdateClient(DataRecipientClient client)
return await _clientStore.StoreClientAsync(client);
}
+
+ public async Task RefreshJwks(string clientId)
+ => await _clientStore.RefreshJwks(clientId);
+
+ public async Task EnsureKid(string clientId, string jwt, TokenValidationParameters tokenValidationParameters)
+ {
+ // Read the token without validation, to retrieve the kid from the header.
+ var handler = new JwtSecurityTokenHandler();
+ var unvalidatedToken = handler.ReadJwtToken(jwt);
+
+ // Check if the incoming jwt has a key id that is including in the client's secret keys.
+ if (unvalidatedToken.Header.Kid != null
+ && !tokenValidationParameters.IssuerSigningKeys.Any(k => k.KeyId.Equals(unvalidatedToken.Header.Kid, StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogInformation("Matching kid ({kid}) not found in client secrets, refreshing jwks from client...", unvalidatedToken.Header.Kid);
+
+ // Is there a matching key id between the clientAssertion JWT and the JWKS stored for the client?
+ // If not, reload the JWKS from the client.
+ var refreshedClient = await RefreshJwks(clientId);
+ tokenValidationParameters.IssuerSigningKeys = await refreshedClient.ClientSecrets.GetKeysAsync();
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Source/CDR.DataHolder.IdentityServer/Services/IClientService.cs b/Source/CDR.DataHolder.IdentityServer/Services/IClientService.cs
index eed3b32..a09dcdf 100644
--- a/Source/CDR.DataHolder.IdentityServer/Services/IClientService.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Services/IClientService.cs
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using CDR.DataHolder.IdentityServer.Models;
using IdentityServer4.Models;
+using Microsoft.IdentityModel.Tokens;
namespace CDR.DataHolder.IdentityServer.Services
{
@@ -15,5 +16,9 @@ public interface IClientService
Task RegisterClient(DataRecipientClient client);
Task UpdateClient(DataRecipientClient client);
+
+ Task RefreshJwks(string clientId);
+
+ Task EnsureKid(string clientId, string jwt, TokenValidationParameters tokenValidationParameters);
}
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Services/TokenResponseGenerator.cs b/Source/CDR.DataHolder.IdentityServer/Services/TokenResponseGenerator.cs
index 9864917..c3ce39d 100644
--- a/Source/CDR.DataHolder.IdentityServer/Services/TokenResponseGenerator.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Services/TokenResponseGenerator.cs
@@ -17,6 +17,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
+using static CDR.DataHolder.API.Infrastructure.Constants;
using static CDR.DataHolder.IdentityServer.CdsConstants;
namespace CDR.DataHolder.IdentityServer.Services
diff --git a/Source/CDR.DataHolder.IdentityServer/Startup.cs b/Source/CDR.DataHolder.IdentityServer/Startup.cs
index 76308eb..c1f153a 100644
--- a/Source/CDR.DataHolder.IdentityServer/Startup.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Startup.cs
@@ -132,8 +132,8 @@ public void ConfigureServices(IServiceCollection services)
options.ConfigureDbContext = b => b.UseSqlServer(migrationsConnectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddInMemoryIdentityResources(InMemoryConfig.IdentityResources)
- .AddInMemoryApiResources(InMemoryConfig.Apis)
- .AddInMemoryApiScopes(InMemoryConfig.ApiScopes)
+ .AddInMemoryApiResources(InMemoryConfig.Apis(_configuration))
+ .AddInMemoryApiScopes(InMemoryConfig.ApiScopes(_configuration))
.AddProfileService();
services.AddDbContext(options => options.UseSqlServer(_configuration.GetConnectionString(DbConstants.ConnectionStringNames.Identity.Default)));
diff --git a/Source/CDR.DataHolder.IdentityServer/Stores/ClientStore.cs b/Source/CDR.DataHolder.IdentityServer/Stores/ClientStore.cs
index bbca356..3f8b461 100644
--- a/Source/CDR.DataHolder.IdentityServer/Stores/ClientStore.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Stores/ClientStore.cs
@@ -10,6 +10,7 @@
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using static CDR.DataHolder.IdentityServer.CdsConstants;
@@ -20,19 +21,23 @@ public class ClientStore : IClientStore
{
protected readonly ILogger _logger;
protected readonly ConfigurationDbContext _configurationDbContext;
+ protected readonly IDistributedCache _cache;
- public ClientStore(ILogger logger,
- ConfigurationDbContext configurationDbContext)
+ public ClientStore(
+ ILogger logger,
+ ConfigurationDbContext configurationDbContext,
+ IDistributedCache cache)
{
_logger = logger;
_configurationDbContext = configurationDbContext;
+ _cache = cache;
}
public async Task FindClientByIdAsync(string clientId)
{
- _logger.LogInformation("DynamicClientStore FindClientByIdAsync");
+ _logger.LogInformation($"{nameof(ClientStore)}.{nameof(FindClientByIdAsync)}");
- var client = await _configurationDbContext.Clients
+ var client = await _configurationDbContext.Clients.AsNoTracking()
.Include(c => c.RedirectUris)
.Include(c => c.ClientSecrets)
.Include(c => c.AllowedGrantTypes)
@@ -46,27 +51,61 @@ public ClientStore(ILogger logger,
return null;
}
- // This implements the client key rotation
- // Get the latest JWKs from the client secrets. The keys may have been rotated from the client side, so it is always best to get the latest keys dynamically.
- // Filter only URIs as there can be other secrets such as keys.
+ return await GetClientSecrets(client, true);
+ }
+
+ public async Task RefreshJwks(string clientId)
+ {
+ _logger.LogInformation($"{nameof(ClientStore)}.{nameof(RefreshJwks)}");
+
+ var client = await _configurationDbContext.Clients.AsNoTracking()
+ .Include(c => c.RedirectUris)
+ .Include(c => c.ClientSecrets)
+ .Include(c => c.AllowedGrantTypes)
+ .Include(c => c.AllowedScopes)
+ .Include(c => c.Claims)
+ .FirstOrDefaultAsync(x => x.ClientId == clientId);
+
+ if (client == null)
+ {
+ _logger.LogError("Client {ClientId} is not found.", clientId);
+ return null;
+ }
+
+ return await GetClientSecrets(client, false);
+ }
+
+ private async Task GetClientSecrets(IdentityServer4.EntityFramework.Entities.Client client, bool checkCache = true)
+ {
+ _logger.LogInformation($"{nameof(ClientStore)}.{nameof(GetClientSecrets)}");
+ _logger.LogInformation("Client secrets: {secretCount}", client.ClientSecrets.Count);
+
var updatedClientSecrets = new List();
foreach (var clientSecret in client.ClientSecrets)
{
+ _logger.LogDebug("Client ID: {clientId}. Client secret type: {type}. Client secret value: {value}. Is url: {isUrl}", client.ClientId, clientSecret.Type, clientSecret.Value, Uri.IsWellFormedUriString(clientSecret.Value, UriKind.Absolute));
+
if (clientSecret.Type == SecretTypes.JsonWebKey && Uri.IsWellFormedUriString(clientSecret.Value, UriKind.Absolute))
{
try
{
- var jwks = await GetJwks(clientSecret.Value);
+ _logger.LogInformation("Retrieving JWKS from {secretCount}. Check cache: {checkCache}", clientSecret.Value, checkCache);
+
+ var jwks = await GetJwks(clientSecret.Value, checkCache);
updatedClientSecrets.AddRange(
jwks.Keys.Select(key => ConvertJwkToClientSecret(key)));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get JWKs from JwksUri endpoint {jwksUri}", clientSecret.Value);
+
+ // Failed to retrieve the client secret jwks, so just re-use the old one.
+ updatedClientSecrets.Add(clientSecret);
}
}
else
{
+ _logger.LogInformation("A non JWK secret found: {type}", clientSecret.Type);
updatedClientSecrets.Add(clientSecret);
}
}
@@ -89,10 +128,22 @@ private static ClientSecret ConvertJwkToClientSecret(IdentityModel.Jwk.JsonWebKe
/// Note:
/// This can be cached for a short period of time for performance enhancement
///
- private async Task GetJwks(string jwksEndpoint)
+ private async Task GetJwks(string jwksEndpoint, bool checkCache = true)
{
_logger.LogInformation($"{nameof(ClientStore)}.{nameof(GetJwks)}");
+ if (checkCache)
+ {
+ // Checking jwks is in cache.
+ var item = await _cache.GetStringAsync(jwksEndpoint);
+ if (!string.IsNullOrEmpty(item))
+ {
+ _logger.LogInformation("Cache hit: {jwksUri}", jwksEndpoint);
+ _logger.LogDebug("Cache hit contents: {item}", item);
+ return new JsonWebKeySet(item);
+ }
+ }
+
var clientHandler = new HttpClientHandler();
clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
@@ -104,6 +155,9 @@ private async Task GetJwks(string jwksEndpoint)
_logger.LogDebug("JWKS: {jwks}", jwks);
+ _logger.LogDebug("Adding {jwksUri} to cache...", jwksEndpoint);
+ _cache.SetString(jwksEndpoint, jwks);
+
return new JsonWebKeySet(jwks);
}
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Stores/DynamicClientStore.cs b/Source/CDR.DataHolder.IdentityServer/Stores/DynamicClientStore.cs
index cede7f8..ad65779 100644
--- a/Source/CDR.DataHolder.IdentityServer/Stores/DynamicClientStore.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Stores/DynamicClientStore.cs
@@ -6,6 +6,7 @@
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.Models;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using static CDR.DataHolder.IdentityServer.CdsConstants;
@@ -13,7 +14,10 @@ namespace CDR.DataHolder.IdentityServer.Stores
{
public class DynamicClientStore : ClientStore
{
- public DynamicClientStore(ILogger logger, ConfigurationDbContext configurationDbContext):base(logger, configurationDbContext)
+ public DynamicClientStore(
+ ILogger logger,
+ ConfigurationDbContext configurationDbContext,
+ IDistributedCache cache) :base(logger, configurationDbContext, cache)
{
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Validation/ClientDetailsValidator.cs b/Source/CDR.DataHolder.IdentityServer/Validation/ClientDetailsValidator.cs
index b0a2337..ec589b2 100644
--- a/Source/CDR.DataHolder.IdentityServer/Validation/ClientDetailsValidator.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Validation/ClientDetailsValidator.cs
@@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
+using System.Threading.Tasks;
using CDR.DataHolder.API.Infrastructure.Extensions;
using CDR.DataHolder.IdentityServer.Events;
using CDR.DataHolder.IdentityServer.Extensions;
using CDR.DataHolder.IdentityServer.Models;
+using CDR.DataHolder.IdentityServer.Services;
using FluentValidation;
using FluentValidation.Validators;
using IdentityServer4.Configuration;
@@ -26,17 +28,20 @@ public class ClientDetailsValidator : AbstractValidator
private readonly IEventService _eventService;
private readonly ILogger _logger;
private readonly ITokenReplayCache _tokenCache;
+ private readonly IClientService _clientService;
public ClientDetailsValidator(
IConfiguration config,
IdentityServerOptions options,
IEventService eventService,
+ IClientService clientService,
ILogger logger,
ITokenReplayCache tokenCache)
{
_config = config;
_options = options;
_eventService = eventService;
+ _clientService = clientService;
_logger = logger;
_tokenCache = tokenCache;
@@ -134,6 +139,7 @@ private bool BeValidJwt(ClientDetails clientDetails, string clientAssertion)
try
{
var handler = new JwtSecurityTokenHandler();
+ Task.Run(async () => await _clientService.EnsureKid(clientDetails.ClientId, clientAssertion, tokenValidationParameters)).Wait();
handler.ValidateToken(clientAssertion, tokenValidationParameters, out var token);
jwtToken = token as JwtSecurityToken;
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Validation/CustomJwtRequestValidator.cs b/Source/CDR.DataHolder.IdentityServer/Validation/CustomJwtRequestValidator.cs
index 952a963..626db88 100644
--- a/Source/CDR.DataHolder.IdentityServer/Validation/CustomJwtRequestValidator.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Validation/CustomJwtRequestValidator.cs
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using CDR.DataHolder.API.Infrastructure.Extensions;
using CDR.DataHolder.IdentityServer.Configuration;
+using CDR.DataHolder.IdentityServer.Services;
using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
@@ -26,15 +27,20 @@ public class CustomJwtRequestValidator
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
+ private readonly IClientService _clientService;
///
/// Initializes a new instance of the class.
/// Instantiates an instance of private_key_jwt secret validator
///
- public CustomJwtRequestValidator(IConfiguration configuration, ILogger logger)
+ public CustomJwtRequestValidator(
+ IConfiguration configuration,
+ ILogger logger,
+ IClientService clientService)
{
_configuration = configuration;
_logger = logger;
+ _clientService = clientService;
}
///
@@ -136,7 +142,7 @@ protected virtual Task> GetKeysAsync(Client client)
/// The keys
/// The client
///
- protected virtual Task ValidateJwtAsync(string jwtTokenString, IEnumerable keys, Client client)
+ protected virtual async Task ValidateJwtAsync(string jwtTokenString, IEnumerable keys, Client client)
{
var validAudiences = new List
{
@@ -195,9 +201,9 @@ protected virtual Task ValidateJwtAsync(string jwtTokenString,
};
var handler = new JwtSecurityTokenHandler();
+ await _clientService.EnsureKid(client.ClientId, jwtTokenString, tokenValidationParameters);
handler.ValidateToken(jwtTokenString, tokenValidationParameters, out var token);
-
- return Task.FromResult((JwtSecurityToken)token);
+ return (JwtSecurityToken)token;
}
///
diff --git a/Source/CDR.DataHolder.IdentityServer/Validation/PushedAuthorizationRequestValidator.cs b/Source/CDR.DataHolder.IdentityServer/Validation/PushedAuthorizationRequestValidator.cs
index 27710a1..c8649ec 100644
--- a/Source/CDR.DataHolder.IdentityServer/Validation/PushedAuthorizationRequestValidator.cs
+++ b/Source/CDR.DataHolder.IdentityServer/Validation/PushedAuthorizationRequestValidator.cs
@@ -10,6 +10,7 @@
using CDR.DataHolder.IdentityServer.Interfaces;
using CDR.DataHolder.IdentityServer.Logging;
using CDR.DataHolder.IdentityServer.Models;
+using CDR.DataHolder.IdentityServer.Services;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Configuration;
@@ -41,6 +42,7 @@ public class PushedAuthorizationRequestValidator : IPushedAuthorizationRequestVa
private readonly IPersistedGrantStore _persistedGrantStore;
private readonly ILogger _logger;
private readonly ITokenReplayCache _tokenCache;
+ private readonly IClientService _clientService;
private readonly ResponseTypeEqualityComparer
_responseTypeEqualityComparer = new ResponseTypeEqualityComparer();
@@ -49,6 +51,7 @@ public PushedAuthorizationRequestValidator(
IConfiguration config,
IdentityServerOptions options,
IClientStore clients,
+ IClientService clientService,
ICustomAuthorizeRequestValidator customValidator,
IRedirectUriValidator uriValidator,
IPersistedGrantStore persistedGrantStore,
@@ -61,6 +64,7 @@ public PushedAuthorizationRequestValidator(
_options = options;
_persistedGrantStore = persistedGrantStore;
_clients = clients;
+ _clientService = clientService;
_customValidator = customValidator;
_uriValidator = uriValidator;
_customJwtRequestValidator = customJwtRequestValidator;
@@ -777,6 +781,7 @@ private bool BeValidClientAssertion(string clientAssertion, Microsoft.IdentityMo
try
{
var handler = new JwtSecurityTokenHandler();
+ Task.Run(async () => await _clientService.EnsureKid(clientId, clientAssertion, tokenValidationParameters)).Wait();
handler.ValidateToken(clientAssertion, tokenValidationParameters, out var token);
jwtToken = token as JwtSecurityToken;
}
diff --git a/Source/CDR.DataHolder.IdentityServer/Views/Account/Login.cshtml b/Source/CDR.DataHolder.IdentityServer/Views/Account/Login.cshtml
index d2dae26..0f1d664 100644
--- a/Source/CDR.DataHolder.IdentityServer/Views/Account/Login.cshtml
+++ b/Source/CDR.DataHolder.IdentityServer/Views/Account/Login.cshtml
@@ -24,6 +24,7 @@
@string.Format("Connect to {0}", data.dataHolderName)
Please provide your Customer ID to
connect @Model.ClientName to
@data.dataHolderName
+
Customer IDs available are: mmoss
diff --git a/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj b/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj
index ec5a9af..8d152d4 100644
--- a/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj
+++ b/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj
@@ -2,9 +2,9 @@
net6.0
false
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.IntegrationTests/US28722_MDH_EnergyAPI_GetConcessions.cs b/Source/CDR.DataHolder.IntegrationTests/US28722_MDH_EnergyAPI_GetConcessions.cs
index dbaf2f0..6d77990 100644
--- a/Source/CDR.DataHolder.IntegrationTests/US28722_MDH_EnergyAPI_GetConcessions.cs
+++ b/Source/CDR.DataHolder.IntegrationTests/US28722_MDH_EnergyAPI_GetConcessions.cs
@@ -47,15 +47,16 @@ private static (string, int) GetExpectedResponse(
.Where(accountConcession => accountConcession.Account.AccountId == accountId)
.Select(accountConcession => new
{
+ type = accountConcession.Type,
displayName = accountConcession.DisplayName,
additionalInfo = accountConcession.AdditionalInfo,
additionalInfoUri = accountConcession.AdditionalInfoUri,
startDate = (accountConcession.StartDate ?? DateTime.MinValue).ToString("yyyy-MM-dd") ?? "",
endDate = (accountConcession.EndDate ?? DateTime.MinValue).ToString("yyyy-MM-dd") ?? "",
- dailyDiscount = accountConcession.DailyDiscount.ToString(),
- monthlyDiscount = accountConcession.MonthlyDiscount.ToString(),
- yearlyDiscount = accountConcession.YearlyDiscount.ToString(),
- percentageDiscount = accountConcession.PercentageDiscount.ToString(),
+ discountFrequency = accountConcession.DiscountFrequency,
+ amount = accountConcession.Amount,
+ percentage = accountConcession.Percentage,
+ appliedTo = (accountConcession.AppliedTo ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries),
})
.ToList();
diff --git a/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj b/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj
index 5d3e65f..de45d03 100644
--- a/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj
+++ b/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj
@@ -3,9 +3,9 @@
net6.0
win-x64;linux-x64
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.Manage.API/Data/seed-data.json b/Source/CDR.DataHolder.Manage.API/Data/seed-data.json
index a66d54c..508ca5c 100644
--- a/Source/CDR.DataHolder.Manage.API/Data/seed-data.json
+++ b/Source/CDR.DataHolder.Manage.API/Data/seed-data.json
@@ -52,7 +52,9 @@
"DisplayName": "Pensioner",
"StartDate": "2019-01-01",
"EndDate": "2020-01-01",
- "yearlyDiscount": "87.20"
+ "Type": "FIXED_AMOUNT",
+ "Amount": "87.20",
+ "DiscountFrequency": "P1Y"
}
]
},
@@ -279,14 +281,18 @@
"DisplayName": "Student",
"StartDate": "2020-06-01",
"EndDate": "2021-01-01",
- "DailyDiscount": "0.13"
+ "Type": "FIXED_AMOUNT",
+ "Amount": "0.13",
+ "DiscountFrequency": "P1D"
},
{
"AccountConcessionId": "f40834f4-22eb-4e53-86f2-cd48bf1a35a6",
"DisplayName": "Student Graduate",
"StartDate": "2021-01-01",
"EndDate": "2021-06-01",
- "DailyDiscount": "0.11"
+ "Type": "FIXED_AMOUNT",
+ "Amount": "0.11",
+ "DiscountFrequency": "P1D"
}
]
}
@@ -367,7 +373,9 @@
"DisplayName": "Loyalty Bonus",
"StartDate": "2020-06-01",
"EndDate": "2022-01-01",
- "percentageDiscount": "7.5"
+ "Type": "FIXED_PERCENTAGE",
+ "Percentage": "7.5",
+ "DiscountFrequency": "P1M"
}
]
}
@@ -448,14 +456,18 @@
"DisplayName": "Veteran",
"StartDate": "2019-06-01",
"EndDate": "2020-01-01",
- "percentageDiscount": "23"
+ "Type": "FIXED_PERCENTAGE",
+ "Percentage": "23",
+ "DiscountFrequency": "P1Y"
},
{
"AccountConcessionId": "0696ae92-f851-487b-8e34-50bbf9387aa9",
"DisplayName": "Pension",
"StartDate": "2019-06-01",
"EndDate": "2022-06-01",
- "dailyDiscount": "0.29"
+ "Type": "FIXED_AMOUNT",
+ "Amount": "0.29",
+ "DiscountFrequency": "P1D"
}
]
}
diff --git a/Source/CDR.DataHolder.Manage.API/Program.cs b/Source/CDR.DataHolder.Manage.API/Program.cs
index 3395042..9e6a2b4 100644
--- a/Source/CDR.DataHolder.Manage.API/Program.cs
+++ b/Source/CDR.DataHolder.Manage.API/Program.cs
@@ -45,6 +45,14 @@ public static int Main(string[] args)
}
catch (Exception ex)
{
+ // StopTheHostException - Unhandled exception from a BackgroundService thrown out of
+ // HostBuilder.Build() on adding a migration to a EF/.NET Core 6.0 RC2 project, needs to be dealt with gracefully.
+ string type = ex.GetType().Name;
+ if (type.Equals("StopTheHostException", StringComparison.Ordinal))
+ {
+ throw;
+ }
+
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
diff --git a/Source/CDR.DataHolder.Manage.API/appsettings.Development.json b/Source/CDR.DataHolder.Manage.API/appsettings.Development.json
index 18237fb..2862728 100644
--- a/Source/CDR.DataHolder.Manage.API/appsettings.Development.json
+++ b/Source/CDR.DataHolder.Manage.API/appsettings.Development.json
@@ -6,7 +6,7 @@
},
"SeedData": {
"FilePath": "Data\\seed-data.json",
- "OverwriteExistingData": false,
+ "OverwriteExistingData": true,
"OffsetDates": false,
"TimeSpan": 3
},
diff --git a/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj b/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj
index 9f30f83..48db728 100644
--- a/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj
+++ b/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj
@@ -3,9 +3,9 @@
net6.0
win-x64;linux-x64
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
diff --git a/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj b/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj
index 43e8421..6163e6c 100644
--- a/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj
+++ b/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj
@@ -2,9 +2,9 @@
net6.0
- 0.1.0
- 0.1.0
- 0.1.0
+ 1.0.0
+ 1.0.0
+ 1.0.0
@@ -27,9 +27,5 @@
-
-
-
-
diff --git a/Source/CDR.DataHolder.Repository/DataRecipientRepository.cs b/Source/CDR.DataHolder.Repository/DataRecipientRepository.cs
new file mode 100644
index 0000000..062bc11
--- /dev/null
+++ b/Source/CDR.DataHolder.Repository/DataRecipientRepository.cs
@@ -0,0 +1,259 @@
+using CDR.DataHolder.Repository.Entities;
+using CDR.DataHolder.Repository.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace CDR.DataHolder.Repository
+{
+ public class DataRecipientRepository
+ {
+ private readonly DbContextOptions _options;
+
+ public DataRecipientRepository(string connString)
+ {
+ _options = new DbContextOptionsBuilder().UseSqlServer(connString).Options;
+ }
+
+ public async Task<(IList, Exception)> GetDHDataRecipients()
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ var legalEntities = await dhDbContext.LegalEntities.AsNoTracking()
+ .Include(p => p.Brands)
+ .ThenInclude(brand => brand.SoftwareProducts)
+ .OrderBy(p => p.LegalEntityId)
+ .ToListAsync();
+
+ return (legalEntities, null);
+ }
+ }
+ catch (Exception ex)
+ {
+ return (null, ex);
+ }
+ }
+
+ public async Task InsertDataRecipient(LegalEntity regDataRecipient)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ // Insert LegalEntity entity including its child Brands and SoftwareProducts entities
+ dhDbContext.Add(regDataRecipient);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task UpdateDataRecipient(LegalEntity regDr)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ var dhDrLegalEnt = await dhDbContext.LegalEntities
+ .Include(le => le.Brands)
+ .ThenInclude(b => b.SoftwareProducts)
+ .FirstOrDefaultAsync(le => le.LegalEntityId == regDr.LegalEntityId);
+
+ if (dhDrLegalEnt != null)
+ {
+ dhDrLegalEnt.LegalEntityName = regDr.LegalEntityName;
+ dhDrLegalEnt.LogoUri = regDr.LogoUri;
+ dhDrLegalEnt.Status = regDr.Status;
+ dhDbContext.Update(dhDrLegalEnt);
+
+ foreach (var regDrBrand in regDr.Brands)
+ {
+ var dhDrBrand = dhDrLegalEnt.Brands.FirstOrDefault(x => x.BrandId == regDrBrand.BrandId);
+ if (dhDrBrand != null)
+ {
+ dhDrBrand.BrandName = regDrBrand.BrandName;
+ dhDrBrand.LogoUri = regDrBrand.LogoUri;
+ dhDrBrand.Status = regDrBrand.Status;
+ dhDbContext.Update(dhDrBrand);
+
+ foreach (var regDrSwProd in regDrBrand.SoftwareProducts)
+ {
+ var dhDrSwProd = dhDrBrand.SoftwareProducts.FirstOrDefault(x => x.SoftwareProductId == regDrSwProd.SoftwareProductId);
+ if (dhDrSwProd != null)
+ {
+ dhDrSwProd.SoftwareProductName = regDrSwProd.SoftwareProductName;
+ dhDrSwProd.SoftwareProductDescription = regDrSwProd.SoftwareProductDescription;
+ dhDrSwProd.LogoUri = regDrSwProd.LogoUri;
+ dhDrSwProd.Status = regDrSwProd.Status;
+ dhDbContext.Update(dhDrSwProd);
+ }
+ }
+ }
+ }
+ }
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task DeleteDataRecipients(IList dhDataRecipients)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ dhDbContext.RemoveRange(dhDataRecipients);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task InsertBrand(Brand regDrBrand)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ // Insert the Register Brand and any SoftwareProduct entities
+ IList swProducts = new List();
+ foreach (var swProdItem in regDrBrand.SoftwareProducts)
+ {
+ swProducts.Add(new()
+ {
+ SoftwareProductId = swProdItem.SoftwareProductId,
+ SoftwareProductName = swProdItem.SoftwareProductName,
+ SoftwareProductDescription = swProdItem.SoftwareProductDescription,
+ LogoUri = swProdItem.LogoUri,
+ Status = swProdItem.Status
+ });
+ }
+ Brand brand = new()
+ {
+ BrandId = regDrBrand.BrandId,
+ BrandName = regDrBrand.BrandName,
+ LogoUri = regDrBrand.LogoUri,
+ Status = regDrBrand.Status,
+ SoftwareProducts = swProducts,
+ LegalEntityId = regDrBrand.LegalEntityId
+ };
+
+ dhDbContext.Add(brand);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task DeleteBrands(IList dhBrands)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ dhDbContext.RemoveRange(dhBrands);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task InsertSoftwareProduct(SoftwareProduct regDrSwProd)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ // Insert the Register SoftwareProduct entity
+ SoftwareProduct swProduct = new()
+ {
+ SoftwareProductId = regDrSwProd.SoftwareProductId,
+ SoftwareProductName = regDrSwProd.SoftwareProductName,
+ SoftwareProductDescription = regDrSwProd.SoftwareProductDescription,
+ LogoUri = regDrSwProd.LogoUri,
+ Status = regDrSwProd.Status,
+ BrandId = regDrSwProd.BrandId
+ };
+
+ dhDbContext.Add(swProduct);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ public async Task DeleteSoftwareProduct(IList dhSwProds)
+ {
+ try
+ {
+ using (var dhDbContext = new DataHolderDatabaseContext(_options))
+ {
+ using (var txn = dhDbContext.Database.BeginTransaction())
+ {
+ dhDbContext.RemoveRange(dhSwProds);
+ await dhDbContext.SaveChangesAsync();
+ await txn.CommitAsync();
+ }
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/CDR.DataHolder.Repository/Entities/AccountConcession.cs b/Source/CDR.DataHolder.Repository/Entities/AccountConcession.cs
index e626a96..c527e9d 100644
--- a/Source/CDR.DataHolder.Repository/Entities/AccountConcession.cs
+++ b/Source/CDR.DataHolder.Repository/Entities/AccountConcession.cs
@@ -1,6 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
-using CDR.DataHolder.Repository.Entities;
+using System.ComponentModel.DataAnnotations.Schema;
namespace CDR.DataHolder.Repository.Entities
{
@@ -13,6 +13,8 @@ public class AccountConcession
public string AccountId { get; set; }
public virtual Account Account { get; set; }
+ [MaxLength(1000), Required]
+ public string Type { get; set; }
[Required, MaxLength(100)]
public string DisplayName { get; set; }
[MaxLength(1000)]
@@ -21,9 +23,13 @@ public class AccountConcession
public string AdditionalInfoUri { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
- public decimal? DailyDiscount { get; set; }
- public decimal? MonthlyDiscount { get; set; }
- public decimal? YearlyDiscount { get; set; }
- public decimal? PercentageDiscount { get; set; }
+ [MaxLength(1000)]
+ public string DiscountFrequency { get; set; }
+ [MaxLength(1000)]
+ public string Amount { get; set; }
+ [MaxLength(1000)]
+ public string Percentage { get; set; }
+ [MaxLength(1000)]
+ public string AppliedTo { get; set; }
}
}
\ No newline at end of file
diff --git a/Source/CDR.DataHolder.Repository/Entities/LogEventsDrService.cs b/Source/CDR.DataHolder.Repository/Entities/LogEventsDrService.cs
new file mode 100644
index 0000000..1b5601c
--- /dev/null
+++ b/Source/CDR.DataHolder.Repository/Entities/LogEventsDrService.cs
@@ -0,0 +1,37 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace CDR.DataHolder.Repository.Entities
+{
+ public class LogEventsDrService
+ {
+ [Key]
+ public int Id { get; set; }
+
+ public string Message { get; set; }
+
+ public string Level { get; set; }
+
+ public DateTime TimeStamp { get; set; }
+
+ public string Exception { get; set; }
+
+ [MaxLength(50)]
+ public string Environment { get; set; }
+
+ [MaxLength(50)]
+ public string ProcessId { get; set; }
+
+ [MaxLength(50)]
+ public string ProcessName { get; set; }
+
+ [MaxLength(50)]
+ public string ThreadId { get; set; }
+
+ [MaxLength(50)]
+ public string MethodName { get; set; }
+
+ [MaxLength(100)]
+ public string SourceContext { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Source/CDR.DataHolder.Repository/Infrastructure/DataHolderDatabaseContext.cs b/Source/CDR.DataHolder.Repository/Infrastructure/DataHolderDatabaseContext.cs
index 09feb28..dd969aa 100644
--- a/Source/CDR.DataHolder.Repository/Infrastructure/DataHolderDatabaseContext.cs
+++ b/Source/CDR.DataHolder.Repository/Infrastructure/DataHolderDatabaseContext.cs
@@ -24,6 +24,7 @@ public DataHolderDatabaseContext(DbContextOptions opt
public DbSet Brands { get; set; }
public DbSet SoftwareProducts { get; set; }
+ public DbSet LogEventsDrService { get; set; }
public DbSet LogEventsManageAPI { get; set; }
// Energy schema
diff --git a/Source/CDR.DataHolder.Repository/Infrastructure/MappingProfile.cs b/Source/CDR.DataHolder.Repository/Infrastructure/MappingProfile.cs
index 4b23366..af0155b 100644
--- a/Source/CDR.DataHolder.Repository/Infrastructure/MappingProfile.cs
+++ b/Source/CDR.DataHolder.Repository/Infrastructure/MappingProfile.cs
@@ -1,4 +1,5 @@
-using AutoMapper;
+using System;
+using AutoMapper;
using CDR.DataHolder.Repository.Entities;
using DomainEntities = CDR.DataHolder.Domain.Entities;
namespace CDR.DataHolder.Repository.Infrastructure
@@ -12,10 +13,9 @@ public MappingProfile()
CreateMap()
.ForMember(dest => dest.StartDate, source => source.MapFrom(source => source.StartDate.HasValue ? source.StartDate.Value.ToString("yyyy-MM-dd") : null))
.ForMember(dest => dest.EndDate, source => source.MapFrom(source => source.EndDate.HasValue ? source.EndDate.Value.ToString("yyyy-MM-dd") : null))
- .ForMember(dest => dest.DailyDiscount, source => source.MapFrom(source => source.DailyDiscount.HasValue ? source.DailyDiscount.Value.ToString("F2") : null))
- .ForMember(dest => dest.MonthlyDiscount, source => source.MapFrom(source => source.MonthlyDiscount.HasValue ? source.MonthlyDiscount.Value.ToString("F2") : null))
- .ForMember(dest => dest.YearlyDiscount, source => source.MapFrom(source => source.YearlyDiscount.HasValue ? source.YearlyDiscount.Value.ToString("F2") : null))
- .ForMember(dest => dest.PercentageDiscount, source => source.MapFrom(source => source.PercentageDiscount.HasValue ? source.PercentageDiscount.Value.ToString(): null))
+ .ForMember(dest => dest.Amount, source => source.MapFrom(source => string.IsNullOrEmpty(source.Amount) ? null : decimal.Parse(source.Amount).ToString("F2")))
+ .ForMember(dest => dest.Percentage, source => source.MapFrom(source => string.IsNullOrEmpty(source.Percentage) ? null : decimal.Parse(source.Percentage).ToString("F2")))
+ .ForMember(dest => dest.AppliedTo, source => source.MapFrom(source => string.IsNullOrEmpty(source.AppliedTo) ? Array.Empty() : source.AppliedTo.Split(',', StringSplitOptions.RemoveEmptyEntries)))
.ReverseMap();
CreateMap()
.ReverseMap();
diff --git a/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.Designer.cs b/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.Designer.cs
new file mode 100644
index 0000000..dbf2022
--- /dev/null
+++ b/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.Designer.cs
@@ -0,0 +1,812 @@
+//
+using System;
+using CDR.DataHolder.Repository.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CDR.DataHolder.Repository.Migrations
+{
+ [DbContext(typeof(DataHolderDatabaseContext))]
+ [Migration("20220504053142_LogEventsDrService")]
+ partial class LogEventsDrService
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Account", b =>
+ {
+ b.Property("AccountId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountNumber")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("CustomerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DisplayName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("AccountId");
+
+ b.HasIndex("CustomerId");
+
+ b.ToTable("Account", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountConcession", b =>
+ {
+ b.Property("AccountConcessionId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountId")
+ .IsRequired()
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("AdditionalInfoUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("DailyDiscount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime2");
+
+ b.Property("MonthlyDiscount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("PercentageDiscount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("StartDate")
+ .HasColumnType("datetime2");
+
+ b.Property("YearlyDiscount")
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("AccountConcessionId");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("AccountConcession", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountPlan", b =>
+ {
+ b.Property("AccountPlanId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountId")
+ .IsRequired()
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Nickname")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PlanId")
+ .IsRequired()
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("AccountPlanId");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("PlanId");
+
+ b.ToTable("AccountPlan", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Brand", b =>
+ {
+ b.Property("BrandId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("BrandName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LegalEntityId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LogoUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(25)
+ .HasColumnType("nvarchar(25)");
+
+ b.HasKey("BrandId");
+
+ b.HasIndex("LegalEntityId");
+
+ b.ToTable("Brand", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Customer", b =>
+ {
+ b.Property("CustomerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CustomerUType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LoginId")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("OrganisationId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PersonId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("CustomerId");
+
+ b.HasIndex("OrganisationId")
+ .IsUnique()
+ .HasFilter("[OrganisationId] IS NOT NULL");
+
+ b.HasIndex("PersonId")
+ .IsUnique()
+ .HasFilter("[PersonId] IS NOT NULL");
+
+ b.ToTable("Customer", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.LegalEntity", b =>
+ {
+ b.Property("LegalEntityId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LegalEntityName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LogoUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(25)
+ .HasColumnType("nvarchar(25)");
+
+ b.HasKey("LegalEntityId");
+
+ b.ToTable("LegalEntity", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.LogEventsDrService", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Environment")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Exception")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Level")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Message")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MethodName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProcessId")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProcessName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("SourceContext")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ThreadId")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("TimeStamp")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("LogEventsDrService", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.LogEventsManageAPI", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Environment")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Exception")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Level")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Message")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MethodName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProcessId")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ProcessName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("SourceContext")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ThreadId")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("TimeStamp")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("LogEventsManageAPI", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Organisation", b =>
+ {
+ b.Property("OrganisationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Abn")
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Acn")
+ .HasMaxLength(9)
+ .HasColumnType("nvarchar(9)");
+
+ b.Property("AgentFirstName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AgentLastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AgentRole")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("BusinessName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("EstablishmentDate")
+ .HasColumnType("datetime2");
+
+ b.Property("IndustryCode")
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("IndustryCodeVersion")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsAcnCRegistered")
+ .HasColumnType("bit");
+
+ b.Property("LastUpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("LegalName")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("OrganisationType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RegisteredCountry")
+ .HasMaxLength(3)
+ .HasColumnType("nvarchar(3)");
+
+ b.Property("ShortName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("OrganisationId");
+
+ b.ToTable("Organisation", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Person", b =>
+ {
+ b.Property("PersonId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("FirstName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastUpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("MiddleNames")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("OccupationCode")
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("OccupationCodeVersion")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Prefix")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Suffix")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("PersonId");
+
+ b.ToTable("Person", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Plan", b =>
+ {
+ b.Property("PlanId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ApplicationUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Brand")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("BrandName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CustomerType")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("DisplayName")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("EffectiveFrom")
+ .HasColumnType("datetime2");
+
+ b.Property("EffectiveTo")
+ .HasColumnType("datetime2");
+
+ b.Property("FuelType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastUpdated")
+ .HasColumnType("datetime2");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("PlanId");
+
+ b.ToTable("Plan", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.PlanOverview", b =>
+ {
+ b.Property("PlanOverviewId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountPlanId")
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("DisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime2");
+
+ b.Property("StartDate")
+ .HasColumnType("datetime2");
+
+ b.HasKey("PlanOverviewId");
+
+ b.HasIndex("AccountPlanId")
+ .IsUnique()
+ .HasFilter("[AccountPlanId] IS NOT NULL");
+
+ b.ToTable("PlanOverview", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.ServicePoint", b =>
+ {
+ b.Property("ServicePointId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountPlanId")
+ .IsRequired()
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("IsGenerator")
+ .HasColumnType("bit");
+
+ b.Property("JurisdictionCode")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastUpdateDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("NationalMeteringId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ServicePointClassification")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ServicePointStatus")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ValidFromDate")
+ .HasColumnType("datetime2");
+
+ b.HasKey("ServicePointId");
+
+ b.HasIndex("AccountPlanId");
+
+ b.ToTable("ServicePoint", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.SoftwareProduct", b =>
+ {
+ b.Property("SoftwareProductId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("BrandId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LogoUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("SoftwareProductDescription")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("SoftwareProductName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(25)
+ .HasColumnType("nvarchar(25)");
+
+ b.HasKey("SoftwareProductId");
+
+ b.HasIndex("BrandId");
+
+ b.ToTable("SoftwareProduct", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Transaction", b =>
+ {
+ b.Property("TransactionId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccountId")
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Amount")
+ .HasPrecision(16, 2)
+ .HasColumnType("decimal(16,2)");
+
+ b.Property("ApcaNumber")
+ .HasMaxLength(6)
+ .HasColumnType("nvarchar(6)");
+
+ b.Property("BillerCode")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("BillerName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Crn")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Currency")
+ .HasMaxLength(3)
+ .HasColumnType("nvarchar(3)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ExecutionDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("MerchantCategoryCode")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("MerchantName")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("PostingDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Reference")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Status")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TransactionType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ValueDateTime")
+ .HasColumnType("datetime2");
+
+ b.HasKey("TransactionId");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("Transaction");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Account", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Customer", "Customer")
+ .WithMany("Accounts")
+ .HasForeignKey("CustomerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountConcession", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Account", "Account")
+ .WithMany("AccountConcessions")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountPlan", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Account", "Account")
+ .WithMany("AccountPlans")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("CDR.DataHolder.Repository.Entities.Plan", "Plan")
+ .WithMany()
+ .HasForeignKey("PlanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+
+ b.Navigation("Plan");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Brand", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.LegalEntity", "LegalEntity")
+ .WithMany("Brands")
+ .HasForeignKey("LegalEntityId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LegalEntity");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Customer", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Organisation", "Organisation")
+ .WithOne("Customer")
+ .HasForeignKey("CDR.DataHolder.Repository.Entities.Customer", "OrganisationId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("CDR.DataHolder.Repository.Entities.Person", "Person")
+ .WithOne("Customer")
+ .HasForeignKey("CDR.DataHolder.Repository.Entities.Customer", "PersonId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Organisation");
+
+ b.Navigation("Person");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.PlanOverview", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.AccountPlan", "AccountPlan")
+ .WithOne("PlanOverview")
+ .HasForeignKey("CDR.DataHolder.Repository.Entities.PlanOverview", "AccountPlanId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("AccountPlan");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.ServicePoint", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.AccountPlan", "AccountPlan")
+ .WithMany("ServicePoints")
+ .HasForeignKey("AccountPlanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AccountPlan");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.SoftwareProduct", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Brand", "Brand")
+ .WithMany("SoftwareProducts")
+ .HasForeignKey("BrandId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Brand");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Transaction", b =>
+ {
+ b.HasOne("CDR.DataHolder.Repository.Entities.Account", "Account")
+ .WithMany()
+ .HasForeignKey("AccountId");
+
+ b.Navigation("Account");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Account", b =>
+ {
+ b.Navigation("AccountConcessions");
+
+ b.Navigation("AccountPlans");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountPlan", b =>
+ {
+ b.Navigation("PlanOverview");
+
+ b.Navigation("ServicePoints");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Brand", b =>
+ {
+ b.Navigation("SoftwareProducts");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Customer", b =>
+ {
+ b.Navigation("Accounts");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.LegalEntity", b =>
+ {
+ b.Navigation("Brands");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Organisation", b =>
+ {
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Person", b =>
+ {
+ b.Navigation("Customer");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.cs b/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.cs
new file mode 100644
index 0000000..41fe5ee
--- /dev/null
+++ b/Source/CDR.DataHolder.Repository/Migrations/20220504053142_LogEventsDrService.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CDR.DataHolder.Repository.Migrations
+{
+ public partial class LogEventsDrService : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "LogEventsDrService",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false).Annotation("SqlServer:Identity", "1, 1"),
+ Message = table.Column(type: "nvarchar(max)", nullable: true),
+ Level = table.Column(type: "nvarchar(max)", nullable: true),
+ TimeStamp = table.Column(type: "datetime2", nullable: false),
+ Exception = table.Column(type: "nvarchar(max)", nullable: true),
+ Environment = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ ProcessId = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ ProcessName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ ThreadId = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ MethodName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true),
+ SourceContext = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_LogEventsDrService", x => x.Id);
+ });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "LogEventsDrService");
+ }
+ }
+}
diff --git a/Source/CDR.DataHolder.Repository/Migrations/20220630000851_UpdateConcessionSchema.Designer.cs b/Source/CDR.DataHolder.Repository/Migrations/20220630000851_UpdateConcessionSchema.Designer.cs
new file mode 100644
index 0000000..78707e0
--- /dev/null
+++ b/Source/CDR.DataHolder.Repository/Migrations/20220630000851_UpdateConcessionSchema.Designer.cs
@@ -0,0 +1,821 @@
+//
+using System;
+using CDR.DataHolder.Repository.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CDR.DataHolder.Repository.Migrations
+{
+ [DbContext(typeof(DataHolderDatabaseContext))]
+ [Migration("20220630000851_UpdateConcessionSchema")]
+ partial class UpdateConcessionSchema
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.Account", b =>
+ {
+ b.Property("AccountId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountNumber")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("CustomerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DisplayName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("AccountId");
+
+ b.HasIndex("CustomerId");
+
+ b.ToTable("Account", (string)null);
+ });
+
+ modelBuilder.Entity("CDR.DataHolder.Repository.Entities.AccountConcession", b =>
+ {
+ b.Property("AccountConcessionId")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AccountId")
+ .IsRequired()
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("AdditionalInfoUri")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Amount")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("AppliedTo")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("DiscountFrequency")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("EndDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Percentage")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property