diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs index 378f5ad2e..90e5b15a5 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs @@ -89,7 +89,7 @@ private static WebApiClient.SectionQuestionsResponse CreateApiSectionQuestionsRe ); } - private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid sectionId, Guid questionId, Guid nextQuestionId, string? choiceProviderStrategy = null, List? options = null) + private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid sectionId, Guid questionId, Guid nextQuestionId, string? choiceProviderStrategy = null, Dictionary? options = null) { return new SectionQuestionsResponse { @@ -108,8 +108,9 @@ private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid NextQuestion = nextQuestionId, Options = new FormQuestionOptions { - Choices = options == null ? new List { "Option1" } : options, - ChoiceProviderStrategy = choiceProviderStrategy + Choices = options == null ? new Dictionary() { { "Option1", "Option1" } } : options, + ChoiceProviderStrategy = choiceProviderStrategy, + ChoiceAnswerFieldName = "OptionValue", } } } @@ -254,12 +255,19 @@ public async Task GetFormSectionAsync_ShouldFetchChoicesFromCustomChoiceProvider var questionId = Guid.NewGuid(); var nextQuestionId = Guid.NewGuid(); var apiResponse = CreateApiSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy"); - var expectedResponse = CreateModelSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy", ["User's current organisation", "Connected person", "Connected organisation"]); + var expectedResponse = CreateModelSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy"); + + expectedResponse.Questions[0].Options.Choices = new Dictionary() { + { $@"{{""id"":""{organisationId}"",""type"":""organisation""}}", "User's current organisation" }, + { "{\"id\":\"e4bdd7ef-8200-4257-9892-b16f43d1803e\",\"type\":\"connected-entity\"}", "Connected person" }, + { "{\"id\":\"4c8dccba-df39-4997-814b-7599ed9b5bed\",\"type\":\"connected-entity\"}", "Connected organisation" } }; + + expectedResponse.Questions[0].Options.ChoiceAnswerFieldName = "JsonValue"; _organisationClientMock.Setup(c => c.GetConnectedEntitiesAsync(It.IsAny())) .ReturnsAsync([ - new ConnectedEntityLookup(new Guid(), ConnectedEntityType.Individual, "Connected person", new Uri("http://whatever")), - new ConnectedEntityLookup(new Guid(), ConnectedEntityType.Organisation, "Connected organisation", new Uri("http://whatever")) + new ConnectedEntityLookup(new Guid("e4bdd7ef-8200-4257-9892-b16f43d1803e"), ConnectedEntityType.Individual, "Connected person", new Uri("http://whatever")), + new ConnectedEntityLookup(new Guid("4c8dccba-df39-4997-814b-7599ed9b5bed"), ConnectedEntityType.Organisation, "Connected organisation", new Uri("http://whatever")) ]); _organisationClientMock.Setup(c => c.GetOrganisationAsync(organisationId)) .ReturnsAsync(new Organisation.WebApiClient.Organisation([], [],null, null, organisationId, null, "User's current organisation", [])); diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementSingleChoiceModelTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementSingleChoiceModelTest.cs index 94eed3317..47a1caecd 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementSingleChoiceModelTest.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementSingleChoiceModelTest.cs @@ -12,7 +12,7 @@ public class FormElementSingleChoiceModelTest public FormElementSingleChoiceModelTest() { _model = new FormElementSingleChoiceModel(); - _model.Options = new FormQuestionOptions() { Choices = ["Option 1", "Option 2", "Option 3"] } ; + _model.Options = new FormQuestionOptions() { ChoiceAnswerFieldName = "OptionValue", Choices = new Dictionary() {{ "Option 1", "Option 1" }, { "Option 2", "Option 2" }, { "Option 3", "Option 3" }}}; } [Theory] diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsAnswerSetSummaryModelTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsAnswerSetSummaryModelTest.cs index f07b8fb82..b683ac84d 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsAnswerSetSummaryModelTest.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsAnswerSetSummaryModelTest.cs @@ -1,5 +1,6 @@ using CO.CDP.Forms.WebApiClient; using CO.CDP.OrganisationApp.Pages.Forms; +using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -12,12 +13,14 @@ public class FormsAnswerSetSummaryModelTest private readonly Mock _formsClientMock; private readonly Mock _formsEngineMock; private readonly Mock _tempDataServiceMock; + private readonly Mock _choiceProviderService; private readonly FormsAnswerSetSummaryModel _model; private readonly Guid AnswerSetId = Guid.NewGuid(); public FormsAnswerSetSummaryModelTest() { _tempDataServiceMock = new Mock(); + _choiceProviderService = new Mock(); _formsEngineMock = new(); _formsClientMock = new Mock(); @@ -42,7 +45,7 @@ public FormsAnswerSetSummaryModelTest() furtherQuestionsExempted : false)] )); - _model = new FormsAnswerSetSummaryModel(_formsClientMock.Object, _formsEngineMock.Object, _tempDataServiceMock.Object) + _model = new FormsAnswerSetSummaryModel(_formsClientMock.Object, _formsEngineMock.Object, _tempDataServiceMock.Object, _choiceProviderService.Object) { OrganisationId = Guid.NewGuid(), FormId = Guid.NewGuid(), diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsQuestionPageModelTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsQuestionPageModelTest.cs index b9748dea0..6c3f6a557 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsQuestionPageModelTest.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormsQuestionPageModelTest.cs @@ -26,10 +26,7 @@ public FormsQuestionPageModelTest() { Questions = [new FormQuestion { Id = TextQuestionId, Type = FormQuestionType.Text, SummaryTitle = "Sample Question" }] }; - - _formsEngineMock.Setup(f => f.ExecuteChoiceProviderStrategy(It.IsAny())) - .ReturnsAsync(["Choices"]); - + _formsEngineMock.Setup(f => f.GetFormSectionAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(form); _tempDataServiceMock = new Mock(); @@ -38,7 +35,7 @@ public FormsQuestionPageModelTest() _tempDataServiceMock.Setup(t => t.PeekOrDefault(It.IsAny())) .Returns(new FormQuestionAnswerState()); _tempDataServiceMock.Setup(t => t.Remove(It.IsAny())); - _pageModel = new FormsQuestionPageModel(_formsEngineMock.Object, _tempDataServiceMock.Object, _fileHostManagerMock.Object); + _pageModel = new FormsQuestionPageModel(_formsEngineMock.Object, _tempDataServiceMock.Object, _fileHostManagerMock.Object, _choiceProviderServiceMock.Object); _pageModel.OrganisationId = Guid.NewGuid(); _pageModel.FormId = Guid.NewGuid(); diff --git a/Frontend/CO.CDP.OrganisationApp/FormsEngine.cs b/Frontend/CO.CDP.OrganisationApp/FormsEngine.cs index 71021d588..08d034981 100644 --- a/Frontend/CO.CDP.OrganisationApp/FormsEngine.cs +++ b/Frontend/CO.CDP.OrganisationApp/FormsEngine.cs @@ -34,22 +34,27 @@ public async Task GetFormSectionAsync(Guid organisatio Title = response.Section.Title, AllowsMultipleAnswerSets = response.Section.AllowsMultipleAnswerSets }, - Questions = (await Task.WhenAll(response.Questions.Select(async q => new Models.FormQuestion - { - Id = q.Id, - Title = q.Title, - Description = q.Description, - Caption = q.Caption, - SummaryTitle = q.SummaryTitle, - Type = (Models.FormQuestionType)q.Type, - IsRequired = q.IsRequired, - NextQuestion = q.NextQuestion, - NextQuestionAlternative = q.NextQuestionAlternative, - Options = new Models.FormQuestionOptions + Questions = (await Task.WhenAll(response.Questions.Select(async q => { + IChoiceProviderStrategy choiceProviderStrategy = choiceProviderService.GetStrategy(q.Options.ChoiceProviderStrategy); + + return new Models.FormQuestion { - Choices = await ExecuteChoiceProviderStrategy(q.Options), - ChoiceProviderStrategy = q.Options.ChoiceProviderStrategy - } + Id = q.Id, + Title = q.Title, + Description = q.Description, + Caption = q.Caption, + SummaryTitle = q.SummaryTitle, + Type = (Models.FormQuestionType)q.Type, + IsRequired = q.IsRequired, + NextQuestion = q.NextQuestion, + NextQuestionAlternative = q.NextQuestionAlternative, + Options = new Models.FormQuestionOptions + { + Choices = await choiceProviderStrategy.Execute(q.Options), + ChoiceProviderStrategy = q.Options.ChoiceProviderStrategy, + ChoiceAnswerFieldName = choiceProviderStrategy.AnswerFieldName + } + }; }))).ToList() }; @@ -57,12 +62,6 @@ public async Task GetFormSectionAsync(Guid organisatio return sectionQuestionsResponse; } - public async Task?> ExecuteChoiceProviderStrategy(Forms.WebApiClient.FormQuestionOptions options) - { - IChoiceProviderStrategy strategy = choiceProviderService.GetStrategy(options.ChoiceProviderStrategy); - return await strategy.Execute(options); - } - public async Task GetNextQuestion(Guid organisationId, Guid formId, Guid sectionId, Guid currentQuestionId) { var section = await GetFormSectionAsync(organisationId, formId, sectionId); @@ -118,7 +117,8 @@ public async Task SaveUpdateAnswers(Guid formId, Guid sectionId, Guid organisati textValue: a.Answer?.TextValue, optionValue: a.Answer?.OptionValue, questionId: a.QuestionId, - addressValue: MapAddress(a.Answer?.AddressValue) + addressValue: MapAddress(a.Answer?.AddressValue), + jsonValue: a.Answer?.JsonValue )).ToArray(), furtherQuestionsExempted: answerSet.FurtherQuestionsExempted ); diff --git a/Frontend/CO.CDP.OrganisationApp/IFormsEngine.cs b/Frontend/CO.CDP.OrganisationApp/IFormsEngine.cs index 60892dff7..26e217567 100644 --- a/Frontend/CO.CDP.OrganisationApp/IFormsEngine.cs +++ b/Frontend/CO.CDP.OrganisationApp/IFormsEngine.cs @@ -14,5 +14,4 @@ public interface IFormsEngine Task SaveUpdateAnswers(Guid formId, Guid sectionId, Guid organisationId, FormQuestionAnswerState answerSet); Task CreateShareCodeAsync(Guid formId, Guid organisationId); - Task?> ExecuteChoiceProviderStrategy(Forms.WebApiClient.FormQuestionOptions options); } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs b/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs index 27a6ed227..69844714b 100644 --- a/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs +++ b/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs @@ -36,8 +36,9 @@ public class FormQuestion public class FormQuestionOptions { - public List? Choices { get; set; } + public Dictionary? Choices { get; set; } public string? ChoiceProviderStrategy { get; set; } + public string? ChoiceAnswerFieldName { get; set; } } public class FormQuestionAnswerState @@ -64,6 +65,7 @@ public class FormAnswer public string? TextValue { get; init; } public string? OptionValue { get; init; } public Address? AddressValue { get; init; } + public string? JsonValue { get; init; } } public enum FormQuestionType diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ChoiceProviderService.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ChoiceProviderService.cs index a035bf36c..27cb04e1d 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ChoiceProviderService.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ChoiceProviderService.cs @@ -1,7 +1,7 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; public class ChoiceProviderService(IServiceProvider serviceProvider) : IChoiceProviderService { - public IChoiceProviderStrategy GetStrategy(string strategyType) + public IChoiceProviderStrategy GetStrategy(string? strategyType) { strategyType ??= "DefaultChoiceProviderStrategy"; return serviceProvider.GetKeyedService(strategyType)!; diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/DefaultChoiceProviderStrategy.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/DefaultChoiceProviderStrategy.cs index 213e3b1c0..c588edd37 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/DefaultChoiceProviderStrategy.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/DefaultChoiceProviderStrategy.cs @@ -1,10 +1,26 @@ -namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; using CO.CDP.Forms.WebApiClient; +namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; public class DefaultChoiceProviderStrategy() : IChoiceProviderStrategy { - public async Task?> Execute(FormQuestionOptions options) + public string AnswerFieldName { get; } = "OptionValue"; + public async Task?> Execute(FormQuestionOptions options) + { + return await Task.FromResult(options.Choices.ToDictionary(c => c.Title, c => c.Title)); + } + + public async Task RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer) + { + return await RenderOption(answer?.OptionValue); + } + + public async Task RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer) + { + return await RenderOption(answer?.OptionValue); + } + + private async Task RenderOption(string? optionValue) { - return await Task.FromResult(options.Choices.Select(c => c.Title).ToList()); + return await Task.FromResult(optionValue); } } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ExclusionAppliesToChoiceProviderStrategy.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ExclusionAppliesToChoiceProviderStrategy.cs index 74cafda23..1ffcef16d 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ExclusionAppliesToChoiceProviderStrategy.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/ExclusionAppliesToChoiceProviderStrategy.cs @@ -1,24 +1,84 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; using CO.CDP.Forms.WebApiClient; using CO.CDP.Organisation.WebApiClient; -using CO.CDP.Tenant.WebApiClient; +using System.Text.Json; public class ExclusionAppliesToChoiceProviderStrategy(IUserInfoService userInfoService, IOrganisationClient organisationClient) : IChoiceProviderStrategy { - public async Task?> Execute(FormQuestionOptions options) + public string AnswerFieldName { get; } = "JsonValue"; + + public async Task?> Execute(FormQuestionOptions options) { + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + var organisationId = userInfoService.GetOrganisationId(); if (organisationId != null) { var connectedEntities = await organisationClient.GetConnectedEntitiesAsync((Guid)organisationId); - List returnList = [ - (await organisationClient.GetOrganisationAsync((Guid)organisationId)).Name - ]; - returnList.AddRange(connectedEntities.Select(entity => entity.Name)); + var organisation = await organisationClient.GetOrganisationAsync((Guid)organisationId); + + var result = new Dictionary(); + + result[JsonSerializer.Serialize(new { id = organisation.Id, type = "organisation" }, jsonSerializerOptions)] = organisation.Name; - return returnList; + foreach (var entity in connectedEntities) + { + result[JsonSerializer.Serialize(new { id = entity.EntityId, type = "connected-entity" }, jsonSerializerOptions)] = entity.Name; + } + + + return result; + } + + return null; + } + + public async Task RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer) + { + return await RenderOption(answer?.JsonValue); + } + + public async Task RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer) + { + return await RenderOption(answer?.JsonValue); + } + + private async Task RenderOption(string? jsonValue) + { + if (jsonValue != null) + { + var jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + ExclusionAppliesToChoiceProviderStrategyAnswer? answerValues = JsonSerializer.Deserialize(jsonValue, jsonSerializerOptions); + + switch (answerValues?.Type) + { + case "organisation": + var organisation = await organisationClient.GetOrganisationAsync(answerValues.Id); + return organisation.Name; + + case "connected-entity": + var organisationId = userInfoService.GetOrganisationId(); + if (organisationId != null) + { + var connectedEntities = await organisationClient.GetConnectedEntitiesAsync((Guid)organisationId); + var entity = connectedEntities.FirstOrDefault(e => e.EntityId == answerValues.Id); + return entity?.Name; + } + + break; + } } return null; } } + +public class ExclusionAppliesToChoiceProviderStrategyAnswer() +{ + required public Guid Id { get; set; } + required public string Type { get; set; } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderService.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderService.cs index b50acbe46..0b8589ad6 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderService.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderService.cs @@ -2,5 +2,5 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; public interface IChoiceProviderService { - IChoiceProviderStrategy GetStrategy(string strategyType); + IChoiceProviderStrategy GetStrategy(string? strategyType); } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderStrategy.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderStrategy.cs index 23bc1f7f6..15236ab74 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderStrategy.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/ChoiceProviderStrategies/IChoiceProviderStrategy.cs @@ -4,5 +4,8 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; public interface IChoiceProviderStrategy { - Task?> Execute(FormQuestionOptions options); + public string AnswerFieldName { get; } + Task?> Execute(FormQuestionOptions options); + public Task RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer); + public Task RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer); } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementSingleChoiceModel.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementSingleChoiceModel.cs index 72587697e..ed9b73937 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementSingleChoiceModel.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementSingleChoiceModel.cs @@ -1,7 +1,6 @@ using CO.CDP.OrganisationApp.Models; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; namespace CO.CDP.OrganisationApp.Pages.Forms; @@ -15,10 +14,25 @@ public class FormElementSingleChoiceModel : FormElementModel, IValidatableObject if( SelectedOption != null && Options?.Choices != null - && Options.Choices.Contains(SelectedOption) + && Options.Choices.ContainsKey(SelectedOption) ) { - return new FormAnswer { OptionValue = SelectedOption }; + FormAnswer formAnswer; + + switch (Options.ChoiceAnswerFieldName) + { + case nameof(FormAnswer.OptionValue): + formAnswer = new FormAnswer { OptionValue = SelectedOption }; + break; + case nameof(FormAnswer.JsonValue): + formAnswer = new FormAnswer { JsonValue = SelectedOption }; + break; + default: + throw new InvalidOperationException($"Unsupported field: {Options.ChoiceAnswerFieldName}"); + } + + return formAnswer; + } return null; @@ -26,9 +40,23 @@ public class FormElementSingleChoiceModel : FormElementModel, IValidatableObject public override void SetAnswer(FormAnswer? answer) { - if (answer?.OptionValue != null && Options?.Choices != null && Options.Choices.Contains(answer.OptionValue)) + string? value; + + switch (Options?.ChoiceAnswerFieldName) + { + case nameof(FormAnswer.OptionValue): + value = answer?.OptionValue; + break; + case nameof(FormAnswer.JsonValue): + value = answer?.JsonValue; + break; + default: + throw new InvalidOperationException($"Unsupported field: {Options?.ChoiceAnswerFieldName}"); + } + + if (value != null && Options?.Choices != null && Options.Choices.ContainsKey(value)) { - SelectedOption = answer.OptionValue; + SelectedOption = value; } } @@ -41,8 +69,9 @@ public IEnumerable Validate(ValidationContext validationContex yield break; } - if(Options?.Choices == null || (SelectedOption != null && !Options.Choices.Contains(SelectedOption))) + if(Options?.Choices == null || (SelectedOption != null && !Options.Choices.ContainsKey(SelectedOption))) { + // Note that any JSON responses received from the client *must* be present in the Choices list, and this check ensures that this is the case. yield return new ValidationResult("Invalid option selected", new[] { nameof(SelectedOption) }); yield break; } diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsAnswerSetSummary.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsAnswerSetSummary.cshtml.cs index e94168885..a08d4156c 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsAnswerSetSummary.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsAnswerSetSummary.cshtml.cs @@ -1,5 +1,6 @@ using CO.CDP.Forms.WebApiClient; using CO.CDP.OrganisationApp.Constants; +using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -10,7 +11,8 @@ namespace CO.CDP.OrganisationApp.Pages.Forms; public class FormsAnswerSetSummaryModel( IFormsClient formsClient, IFormsEngine formsEngine, - ITempDataService tempDataService) : PageModel + ITempDataService tempDataService, + IChoiceProviderService choiceProviderService) : PageModel { [BindProperty(SupportsGet = true)] public Guid OrganisationId { get; set; } @@ -108,7 +110,8 @@ public async Task OnGetChange([FromQuery(Name = "answer-set-id")] OptionValue = a.OptionValue, StartValue = a.StartValue, TextValue = a.TextValue, - AddressValue = MapAddress(a.AddressValue) + AddressValue = MapAddress(a.AddressValue), + JsonValue = a.JsonValue, } }).ToList() }; @@ -154,7 +157,7 @@ private async Task InitAndVerifyPage() } else { - FormAnswerSets = GetAnswers(form); + FormAnswerSets = await GetAnswers(form); Heading = form.Section.Configuration.SingularSummaryHeading; if (FormAnswerSets.Count != 1 && form.Section.Configuration.PluralSummaryHeadingFormat != null) @@ -165,7 +168,7 @@ private async Task InitAndVerifyPage() return true; } - private static List<(Guid answerSetId, IEnumerable answers)> GetAnswers(SectionQuestionsResponse form) + private async Task answers)>> GetAnswers(SectionQuestionsResponse form) { List<(Guid answerSetId, IEnumerable answers)> summaryList = []; @@ -177,15 +180,16 @@ private async Task InitAndVerifyPage() var question = form.Questions.FirstOrDefault(q => q.Id == answer.QuestionId); if (question != null && question.Type != FormQuestionType.NoInput && question.Type != FormQuestionType.CheckYourAnswers) { + var choiceProviderStrategy = choiceProviderService.GetStrategy(question.Options.ChoiceProviderStrategy); var answerString = question.Type switch { FormQuestionType.Text => answer.TextValue ?? "", FormQuestionType.FileUpload => answer.TextValue ?? "", FormQuestionType.YesOrNo => answer.BoolValue.HasValue ? (answer.BoolValue == true ? "Yes" : "No") : "", - FormQuestionType.SingleChoice => answer.OptionValue ?? "", FormQuestionType.Date => answer.DateValue.HasValue ? answer.DateValue.Value.ToString("dd/MM/yyyy") : "", FormQuestionType.CheckBox => answer.BoolValue.HasValue ? question.Options.Choices?.FirstOrDefault()?.Title ?? "" : "", FormQuestionType.Address => answer.AddressValue != null ? $"{answer.AddressValue.StreetAddress}, {answer.AddressValue.Locality}, {answer.AddressValue.PostalCode}, {answer.AddressValue.CountryName}" : "", + FormQuestionType.SingleChoice => await choiceProviderStrategy.RenderOption(answer), FormQuestionType.MultiLine => answer.TextValue ?? "", FormQuestionType.Url => answer.TextValue ?? "", _ => "" diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsQuestionPage.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsQuestionPage.cshtml.cs index eadc6b360..1926314a4 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsQuestionPage.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormsQuestionPage.cshtml.cs @@ -1,6 +1,7 @@ using CO.CDP.AwsServices; using CO.CDP.OrganisationApp.Constants; using CO.CDP.OrganisationApp.Models; +using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -12,7 +13,8 @@ namespace CO.CDP.OrganisationApp.Pages.Forms; public class FormsQuestionPageModel( IFormsEngine formsEngine, ITempDataService tempDataService, - IFileHostManager fileHostManager) : PageModel + IFileHostManager fileHostManager, + IChoiceProviderService choiceProviderService) : PageModel { [BindProperty(SupportsGet = true)] public Guid OrganisationId { get; set; } @@ -153,14 +155,15 @@ public async Task> GetAnswers() var question = form?.Questions.FirstOrDefault(q => q.Id == answer.QuestionId); if (question != null && question.Type != FormQuestionType.NoInput && question.Type != FormQuestionType.CheckYourAnswers) { + var choiceProviderStrategy = choiceProviderService.GetStrategy(question.Options.ChoiceProviderStrategy); string answerString = question.Type switch { FormQuestionType.Text => answer.Answer?.TextValue ?? "", FormQuestionType.FileUpload => answer.Answer?.TextValue ?? "", FormQuestionType.YesOrNo => answer.Answer?.BoolValue.HasValue == true ? (answer.Answer.BoolValue == true ? "Yes" : "No") : "", - FormQuestionType.SingleChoice => answer.Answer?.OptionValue ?? "", + FormQuestionType.SingleChoice => await choiceProviderStrategy.RenderOption(answer.Answer) ?? "", FormQuestionType.Date => answer.Answer?.DateValue.HasValue == true ? answer.Answer.DateValue.Value.ToString("dd/MM/yyyy") : "", - FormQuestionType.CheckBox => answer.Answer?.BoolValue == true ? question.Options.Choices?.FirstOrDefault() ?? "" : "", + FormQuestionType.CheckBox => answer.Answer?.BoolValue == true ? question?.Options?.Choices?.Values.FirstOrDefault() ?? "" : "", FormQuestionType.Address => answer.Answer?.AddressValue != null ? answer.Answer.AddressValue.ToHtmlString() : "", FormQuestionType.MultiLine => answer.Answer?.TextValue ?? "", FormQuestionType.Url => answer.Answer?.TextValue ?? "", diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementSingleChoice.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementSingleChoice.cshtml index f5cbf9a53..3fccfa204 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementSingleChoice.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementSingleChoice.cshtml @@ -1,4 +1,5 @@ @model FormElementSingleChoiceModel +@using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies @{ var hasError = ((TagBuilder)Html.ValidationMessageFor(m => m.SelectedOption)).HasInnerHtml; @@ -49,13 +50,13 @@ @{ var index = 0; if (Model.Options?.Choices != null ) { - foreach(var value in Model.Options.Choices) + foreach(var choice in Model.Options.Choices) { var id = index == 0 ? "SelectedOption" : $"SelectedOption_{index}";
- - + +
index++; diff --git a/Services/CO.CDP.Forms.WebApi/Model/FormAnswer.cs b/Services/CO.CDP.Forms.WebApi/Model/FormAnswer.cs index 5cf1040a8..0d30e617e 100644 --- a/Services/CO.CDP.Forms.WebApi/Model/FormAnswer.cs +++ b/Services/CO.CDP.Forms.WebApi/Model/FormAnswer.cs @@ -12,6 +12,7 @@ public record FormAnswer public string? TextValue { get; init; } public string? OptionValue { get; init; } public FormAddress? AddressValue { get; init; } + public string? JsonValue { get; init; } } public record FormAddress diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormAnswer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormAnswer.cs index 0d1914192..268c29741 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormAnswer.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormAnswer.cs @@ -48,6 +48,7 @@ public class FormAnswer : IEntityDate public DateTime? EndValue { get; set; } public string? TextValue { get; set; } public string? OptionValue { get; set; } + public string? JsonValue { get; set; } public FormAddress? AddressValue { get; set; } public Guid? CreatedFrom { get; init; } public DateTimeOffset CreatedOn { get; set; } diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.Designer.cs new file mode 100644 index 000000000..7b3f67262 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.Designer.cs @@ -0,0 +1,1736 @@ +// +using System; +using System.Collections.Generic; +using CO.CDP.OrganisationInformation.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + [DbContext(typeof(OrganisationInformationContext))] + [Migration("20241001080722_AddJsonValueToFormAnswer")] + partial class AddJsonValueToFormAnswer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_entity_type", new[] { "organisation", "individual", "trust_or_trustee" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_organisation_category", new[] { "registered_company", "director_or_the_same_responsibilities", "parent_or_subsidiary_company", "a_company_your_organisation_has_taken_over", "any_other_organisation_with_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_person_category", new[] { "person_with_significant_control", "director_or_individual_with_the_same_responsibilities", "any_other_individual_with_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_person_type", new[] { "individual", "trust_or_trustee" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_condition", new[] { "none", "owns_shares", "has_voting_rights", "can_appoint_or_remove_directors", "has_other_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Country") + .IsRequired() + .HasColumnType("text") + .HasColumnName("country"); + + b.Property("CountryName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("country_name"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Locality") + .IsRequired() + .HasColumnType("text") + .HasColumnName("locality"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("postal_code"); + + b.Property("Region") + .HasColumnType("text") + .HasColumnName("region"); + + b.Property("StreetAddress") + .IsRequired() + .HasColumnType("text") + .HasColumnName("street_address"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.ToTable("addresses", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.AuthenticationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("Revoked") + .HasColumnType("boolean") + .HasColumnName("revoked"); + + b.Property("RevokedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_on"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_authentication_keys"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_authentication_keys_organisation_id"); + + b.HasIndex("Name", "OrganisationId") + .IsUnique() + .HasDatabaseName("ix_authentication_keys_name_organisation_id"); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Name", "OrganisationId"), false); + + b.ToTable("authentication_keys", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompanyHouseNumber") + .HasColumnType("text") + .HasColumnName("company_house_number"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("EntityType") + .HasColumnType("integer") + .HasColumnName("entity_type"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("HasCompnayHouseNumber") + .HasColumnType("boolean") + .HasColumnName("has_compnay_house_number"); + + b.Property("OverseasCompanyNumber") + .HasColumnType("text") + .HasColumnName("overseas_company_number"); + + b.Property("RegisterName") + .HasColumnType("text") + .HasColumnName("register_name"); + + b.Property("RegisteredDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("registered_date"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.Property("SupplierOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_organisation_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_connected_entities"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_connected_entities_guid"); + + b.HasIndex("SupplierOrganisationId") + .HasDatabaseName("ix_connected_entities_supplier_organisation_id"); + + b.ToTable("connected_entities", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.Form", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasColumnName("is_required"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Scope") + .HasColumnType("integer") + .HasColumnName("scope"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_forms"); + + b.ToTable("forms", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressValue") + .HasColumnType("jsonb") + .HasColumnName("address_value"); + + b.Property("BoolValue") + .HasColumnType("boolean") + .HasColumnName("bool_value"); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DateValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_value"); + + b.Property("EndValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_value"); + + b.Property("FormAnswerSetId") + .HasColumnType("integer") + .HasColumnName("form_answer_set_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("JsonValue") + .HasColumnType("jsonb") + .HasColumnName("json_value"); + + b.Property("NumericValue") + .HasColumnType("double precision") + .HasColumnName("numeric_value"); + + b.Property("OptionValue") + .HasColumnType("text") + .HasColumnName("option_value"); + + b.Property("QuestionId") + .HasColumnType("integer") + .HasColumnName("question_id"); + + b.Property("StartValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_value"); + + b.Property("TextValue") + .HasColumnType("text") + .HasColumnName("text_value"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_answers"); + + b.HasIndex("FormAnswerSetId") + .HasDatabaseName("ix_form_answers_form_answer_set_id"); + + b.HasIndex("QuestionId") + .HasDatabaseName("ix_form_answers_question_id"); + + b.ToTable("form_answers", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Deleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("deleted"); + + b.Property("FurtherQuestionsExempted") + .HasColumnType("boolean") + .HasColumnName("further_questions_exempted"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("section_id"); + + b.Property("SharedConsentId") + .HasColumnType("integer") + .HasColumnName("shared_consent_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_answer_sets"); + + b.HasIndex("SectionId") + .HasDatabaseName("ix_form_answer_sets_section_id"); + + b.HasIndex("SharedConsentId") + .HasDatabaseName("ix_form_answer_sets_shared_consent_id"); + + b.ToTable("form_answer_sets", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("text") + .HasColumnName("caption"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasColumnName("is_required"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NextQuestionAlternativeId") + .HasColumnType("integer") + .HasColumnName("next_question_alternative_id"); + + b.Property("NextQuestionId") + .HasColumnType("integer") + .HasColumnName("next_question_id"); + + b.Property("Options") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("section_id"); + + b.Property("SummaryTitle") + .HasColumnType("text") + .HasColumnName("summary_title"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_questions"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_form_questions_name"); + + b.HasIndex("NextQuestionAlternativeId") + .HasDatabaseName("ix_form_questions_next_question_alternative_id"); + + b.HasIndex("NextQuestionId") + .HasDatabaseName("ix_form_questions_next_question_id"); + + b.HasIndex("SectionId") + .HasDatabaseName("ix_form_questions_section_id"); + + b.ToTable("form_questions", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowsMultipleAnswerSets") + .HasColumnType("boolean") + .HasColumnName("allows_multiple_answer_sets"); + + b.Property("CheckFurtherQuestionsExempted") + .HasColumnType("boolean") + .HasColumnName("check_further_questions_exempted"); + + b.Property("Configuration") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("configuration"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DisplayOrder") + .HasColumnType("integer") + .HasColumnName("display_order"); + + b.Property("FormId") + .HasColumnType("integer") + .HasColumnName("form_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_sections"); + + b.HasIndex("FormId") + .HasDatabaseName("ix_form_sections_form_id"); + + b.ToTable("form_sections", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FormId") + .HasColumnType("integer") + .HasColumnName("form_id"); + + b.Property("FormVersionId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("form_version_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("ShareCode") + .HasColumnType("text") + .HasColumnName("share_code"); + + b.Property("SubmissionState") + .HasColumnType("integer") + .HasColumnName("submission_state"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted_at"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_shared_consents"); + + b.HasIndex("FormId") + .HasDatabaseName("ix_shared_consents_form_id"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_shared_consents_organisation_id"); + + b.ToTable("shared_consents", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("approved_on"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ReviewComment") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)") + .HasColumnName("review_comment"); + + b.Property("ReviewedById") + .HasColumnType("integer") + .HasColumnName("reviewed_by_id"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("roles"); + + b.Property("TenantId") + .HasColumnType("integer") + .HasColumnName("tenant_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_organisations"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_organisations_guid"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_organisations_name"); + + b.HasIndex("ReviewedById") + .HasDatabaseName("ix_organisations_reviewed_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_organisations_tenant_id"); + + b.ToTable("organisations", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.OrganisationPerson", b => + { + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("OrganisationId", "PersonId") + .HasName("pk_organisation_person"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_organisation_person_person_id"); + + b.ToTable("organisation_person", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("Phone") + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserUrn") + .HasColumnType("text") + .HasColumnName("user_urn"); + + b.HasKey("Id") + .HasName("pk_persons"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_persons_email"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_persons_guid"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.PersonInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("InviteSentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("invite_sent_on"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_person_invites"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_person_invites_guid"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_person_invites_organisation_id"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_invites_person_id"); + + b.ToTable("person_invites", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("Revoked") + .HasColumnType("boolean") + .HasColumnName("revoked"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("token_hash"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_refresh_tokens_token_hash"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_tenants_guid"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_tenants_name"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.TenantPerson", b => + { + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property("TenantId") + .HasColumnType("integer") + .HasColumnName("tenant_id"); + + b.HasKey("PersonId", "TenantId") + .HasName("pk_tenant_person"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_tenant_person_tenant_id"); + + b.ToTable("tenant_person", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.AuthenticationKey", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_authentication_keys_organisations_organisation_id"); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "SupplierOrganisation") + .WithMany() + .HasForeignKey("SupplierOrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_connected_entities_organisations_supplier_organisation_id"); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedEntityAddress", "Addresses", b1 => + { + b1.Property("ConnectedEntityId") + .HasColumnType("integer") + .HasColumnName("connected_entity_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AddressId") + .HasColumnType("integer") + .HasColumnName("address_id"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b1.HasKey("ConnectedEntityId", "Id") + .HasName("pk_connected_entity_address"); + + b1.HasIndex("AddressId") + .HasDatabaseName("ix_connected_entity_address_address_id"); + + b1.ToTable("connected_entity_address", (string)null); + + b1.HasOne("CO.CDP.OrganisationInformation.Persistence.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_connected_entity_address_address_address_id"); + + b1.WithOwner() + .HasForeignKey("ConnectedEntityId") + .HasConstraintName("fk_connected_entity_address_connected_entities_connected_entit"); + + b1.Navigation("Address"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedIndividualTrust", "IndividualOrTrust", b1 => + { + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("connected_individual_trust_id"); + + b1.Property("Category") + .HasColumnType("integer") + .HasColumnName("category"); + + b1.Property("ConnectedType") + .HasColumnType("integer") + .HasColumnName("connected_type"); + + b1.Property("ControlCondition") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("control_condition"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("DateOfBirth") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_of_birth"); + + b1.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b1.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b1.Property("Nationality") + .HasColumnType("text") + .HasColumnName("nationality"); + + b1.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b1.Property("ResidentCountry") + .HasColumnType("text") + .HasColumnName("resident_country"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("Id") + .HasName("pk_connected_individual_trust"); + + b1.ToTable("connected_individual_trust", (string)null); + + b1.WithOwner() + .HasForeignKey("Id") + .HasConstraintName("fk_connected_individual_trust_connected_entities_connected_ind"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedOrganisation", "Organisation", b1 => + { + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("connected_organisation_id"); + + b1.Property("Category") + .HasColumnType("integer") + .HasColumnName("category"); + + b1.Property("ControlCondition") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("control_condition"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("InsolvencyDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("insolvency_date"); + + b1.Property("LawRegistered") + .HasColumnType("text") + .HasColumnName("law_registered"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b1.Property("OrganisationId") + .HasColumnType("uuid") + .HasColumnName("organisation_id"); + + b1.Property("RegisteredLegalForm") + .HasColumnType("text") + .HasColumnName("registered_legal_form"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("Id") + .HasName("pk_connected_organisation"); + + b1.ToTable("connected_organisation", (string)null); + + b1.WithOwner() + .HasForeignKey("Id") + .HasConstraintName("fk_connected_organisation_connected_entities_connected_organis"); + }); + + b.Navigation("Addresses"); + + b.Navigation("IndividualOrTrust"); + + b.Navigation("Organisation"); + + b.Navigation("SupplierOrganisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswer", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", "FormAnswerSet") + .WithMany("Answers") + .HasForeignKey("FormAnswerSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answers_form_answer_sets_form_answer_set_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "Question") + .WithMany() + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answers_form_questions_question_id"); + + b.Navigation("FormAnswerSet"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answer_sets_form_section_section_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", "SharedConsent") + .WithMany("AnswerSets") + .HasForeignKey("SharedConsentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answer_sets_shared_consents_shared_consent_id"); + + b.Navigation("Section"); + + b.Navigation("SharedConsent"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "NextQuestionAlternative") + .WithMany() + .HasForeignKey("NextQuestionAlternativeId") + .HasConstraintName("fk_form_questions_form_questions_next_question_alternative_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "NextQuestion") + .WithMany() + .HasForeignKey("NextQuestionId") + .HasConstraintName("fk_form_questions_form_questions_next_question_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", "Section") + .WithMany("Questions") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_questions_form_sections_section_id"); + + b.Navigation("NextQuestion"); + + b.Navigation("NextQuestionAlternative"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.Form", "Form") + .WithMany("Sections") + .HasForeignKey("FormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_sections_forms_form_id"); + + b.Navigation("Form"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.Form", "Form") + .WithMany() + .HasForeignKey("FormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shared_consents_forms_form_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shared_consents_organisations_organisation_id"); + + b.Navigation("Form"); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "ReviewedBy") + .WithMany() + .HasForeignKey("ReviewedById") + .HasConstraintName("fk_organisations_persons_reviewed_by_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Tenant", "Tenant") + .WithMany("Organisations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisations_tenants_tenant_id"); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+BuyerInformation", "BuyerInfo", b1 => + { + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("BuyerType") + .HasColumnType("text") + .HasColumnName("buyer_type"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("DevolvedRegulations") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("devolved_regulations"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("OrganisationId") + .HasName("pk_buyer_information"); + + b1.ToTable("buyer_information", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_buyer_information_organisations_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+ContactPoint", "ContactPoints", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b1.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Telephone") + .HasColumnType("text") + .HasColumnName("telephone"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Url") + .HasColumnType("text") + .HasColumnName("url"); + + b1.HasKey("Id") + .HasName("pk_contact_points"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_contact_points_organisation_id"); + + b1.ToTable("contact_points", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_contact_points_organisations_organisation_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Identifier", "Identifiers", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("IdentifierId") + .HasColumnType("text") + .HasColumnName("identifier_id"); + + b1.Property("LegalName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("legal_name"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Primary") + .HasColumnType("boolean") + .HasColumnName("primary"); + + b1.Property("Scheme") + .IsRequired() + .HasColumnType("text") + .HasColumnName("scheme"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Uri") + .HasColumnType("text") + .HasColumnName("uri"); + + b1.HasKey("Id") + .HasName("pk_identifiers"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_identifiers_organisation_id"); + + b1.HasIndex("IdentifierId", "Scheme") + .IsUnique() + .HasDatabaseName("ix_identifiers_identifier_id_scheme"); + + b1.ToTable("identifiers", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_identifiers_organisations_organisation_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+OrganisationAddress", "Addresses", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AddressId") + .HasColumnType("integer") + .HasColumnName("address_id"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b1.HasKey("Id") + .HasName("pk_organisation_address"); + + b1.HasIndex("AddressId") + .HasDatabaseName("ix_organisation_address_address_id"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_organisation_address_organisation_id"); + + b1.ToTable("organisation_address", (string)null); + + b1.HasOne("CO.CDP.OrganisationInformation.Persistence.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_address_address_address_id"); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_organisation_address_organisations_organisation_id"); + + b1.Navigation("Address"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+SupplierInformation", "SupplierInfo", b1 => + { + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("CompletedConnectedPerson") + .HasColumnType("boolean") + .HasColumnName("completed_connected_person"); + + b1.Property("CompletedEmailAddress") + .HasColumnType("boolean") + .HasColumnName("completed_email_address"); + + b1.Property("CompletedLegalForm") + .HasColumnType("boolean") + .HasColumnName("completed_legal_form"); + + b1.Property("CompletedOperationType") + .HasColumnType("boolean") + .HasColumnName("completed_operation_type"); + + b1.Property("CompletedPostalAddress") + .HasColumnType("boolean") + .HasColumnName("completed_postal_address"); + + b1.Property("CompletedRegAddress") + .HasColumnType("boolean") + .HasColumnName("completed_reg_address"); + + b1.Property("CompletedVat") + .HasColumnType("boolean") + .HasColumnName("completed_vat"); + + b1.Property("CompletedWebsiteAddress") + .HasColumnType("boolean") + .HasColumnName("completed_website_address"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("OperationTypes") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("operation_types"); + + b1.Property("SupplierType") + .HasColumnType("integer") + .HasColumnName("supplier_type"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("OrganisationId") + .HasName("pk_supplier_information"); + + b1.ToTable("supplier_information", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_supplier_information_organisations_id"); + + b1.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+LegalForm", "LegalForm", b2 => + { + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("LawRegistered") + .IsRequired() + .HasColumnType("text") + .HasColumnName("law_registered"); + + b2.Property("RegisteredLegalForm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("registered_legal_form"); + + b2.Property("RegisteredUnderAct2006") + .HasColumnType("boolean") + .HasColumnName("registered_under_act2006"); + + b2.Property("RegistrationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("registration_date"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("SupplierInformationOrganisationId") + .HasName("pk_legal_forms"); + + b2.ToTable("legal_forms", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_legal_forms_supplier_information_id"); + }); + + b1.Navigation("LegalForm"); + }); + + b.Navigation("Addresses"); + + b.Navigation("BuyerInfo"); + + b.Navigation("ContactPoints"); + + b.Navigation("Identifiers"); + + b.Navigation("ReviewedBy"); + + b.Navigation("SupplierInfo"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.OrganisationPerson", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany("OrganisationPersons") + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_person_organisations_organisation_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "Person") + .WithMany("PersonOrganisations") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_person_persons_person_id"); + + b.Navigation("Organisation"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.PersonInvite", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_person_invites_organisations_organisation_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .HasConstraintName("fk_person_invites_persons_person_id"); + + b.Navigation("Organisation"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.TenantPerson", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_person_persons_person_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_person_tenants_tenant_id"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.Form", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.Navigation("Questions"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.Navigation("AnswerSets"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.Navigation("OrganisationPersons"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Person", b => + { + b.Navigation("PersonOrganisations"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Tenant", b => + { + b.Navigation("Organisations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.cs new file mode 100644 index 000000000..8272f08c7 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20241001080722_AddJsonValueToFormAnswer.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + /// + public partial class AddJsonValueToFormAnswer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "json_value", + table: "form_answers", + type: "jsonb", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "json_value", + table: "form_answers"); + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs index 2381f889a..276ba05f3 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs @@ -360,6 +360,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("guid"); + b.Property("JsonValue") + .HasColumnType("jsonb") + .HasColumnName("json_value"); + b.Property("NumericValue") .HasColumnType("double precision") .HasColumnName("numeric_value"); diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs index 79ce4f804..641fd8dc5 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs @@ -196,6 +196,9 @@ private static void OnFormModelCreating(ModelBuilder modelBuilder) e.Property(p => p.AddressValue) .HasJsonColumn(PropertyBuilderExtensions.RecordComparer(), JsonOptions.SerializerOptions); + + e.Property(fa => fa.JsonValue) + .HasColumnType("jsonb"); }); } diff --git a/docs/development/01-development-environment.adoc b/docs/development/01-development-environment.adoc index ba5dbf42d..00f49cd5e 100644 --- a/docs/development/01-development-environment.adoc +++ b/docs/development/01-development-environment.adoc @@ -58,6 +58,29 @@ for the `organisation-app` service: OneLogin__PrivateKey: "-----BEGIN RSA PRIVATE KEY-----SECRET KEY-----END RSA PRIVATE KEY-----" ---- +=== Organisation WebApi + +The `Organisation.WebApi` requires the following secrets / environment variables: + +* `GOVUKNotify:ApiKey` / `GOVUKNotify__ApiKey` + +These can be set as secrets with the following dotnet commands: + +[source,bash] +---- +dotnet user-secrets set --project Services/CO.CDP.Organisation.WebApi/CO.CDP.Organisation.WebApi.csproj GOVUKNotify:ApiKey "123456" +---- + +For Docker, the following environment variables need to be updated in `compose.override.yml` +for the `organisation` service: + +[source,yaml] +---- + organisation: + environment: + GOVUKNotify__ApiKey: "123456" +---- + === Authority API The `Authority` API depends on the following secrets / environment variables: