From 9b09f92d177c8dada5195baa2c0ab2d446b6e885 Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Sun, 22 Sep 2024 01:05:36 +0100 Subject: [PATCH 01/30] #dp-542 adding all screens for quick wins and also multiline views added --- .../FormElementMultiLineInputModelTest.cs | 82 + .../Models/DynamicForms.cs | 3 +- .../Exclusions/DeclaringExclusions.cshtml.cs | 2 +- .../Pages/Forms/DynamicFormsPage.cshtml.cs | 7 +- .../Forms/FormElementMultiLineInputModel.cs | 33 + .../Pages/Forms/_FormElementDateInput.cshtml | 11 +- .../Forms/_FormElementMultiLineInput.cshtml | 32 + .../Pages/Forms/_FormElementTextInput.cshtml | 12 +- .../Pages/Forms/_FormElementYesNoInput.cshtml | 4 +- .../Forms/FormQuestion.cs | 3 +- ...240919135601_ExclusionFormData.Designer.cs | 1809 +++++++++++++++++ .../20240919135601_ExclusionFormData.cs | 69 + 12 files changed, 2052 insertions(+), 15 deletions(-) create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementMultiLineInputModelTest.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementMultiLineInputModel.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementMultiLineInputModelTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementMultiLineInputModelTest.cs new file mode 100644 index 000000000..5dbff7d2e --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Forms/FormElementMultiLineInputModelTest.cs @@ -0,0 +1,82 @@ +using CO.CDP.OrganisationApp.Models; +using CO.CDP.OrganisationApp.Pages.Forms; +using FluentAssertions; +using System.ComponentModel.DataAnnotations; + + +namespace CO.CDP.OrganisationApp.Tests.Pages.Forms; +public class FormElementMultiLineInputModelTest +{ + private readonly FormElementMultiLineInputModel _model; + + public FormElementMultiLineInputModelTest() + { + _model = new FormElementMultiLineInputModel(); + } + + [Theory] + [InlineData(null, null)] + [InlineData(" ", null)] + [InlineData("Test value", "Test value")] + public void GetAnswer_GetsExpectedFormAnswer(string? input, string? expected) + { + _model.TextInput = input; + + var answer = _model.GetAnswer(); + + if (expected == null) + { + answer.Should().BeNull(); + } + else + { + answer.Should().NotBeNull(); + answer!.TextValue.Should().Be(expected); + } + } + + + [Theory] + [InlineData(null, null)] + [InlineData("Test value", "Test value")] + public void SetAnswer_SetsExpectedTextInput(string? input, string? expected) + { + var answer = new FormAnswer { TextValue = input }; + + _model.SetAnswer(answer); + + if (expected == null) + { + _model.TextInput.Should().BeNull(); + } + else + { + _model.TextInput.Should().NotBeNull(); + _model.TextInput!.Should().Be(expected); + } + } + + [Theory] + [InlineData(true, null, "All information is required on this page")] + [InlineData(true, " ", "All information is required on this page")] + [InlineData(false, null, null)] + [InlineData(false, "Some value", null)] + public void Validate_ReturnsExpectedResults(bool isRequired, string? textInput, string? expectedErrorMessage) + { + _model.IsRequired = isRequired; + _model.CurrentFormQuestionType = FormQuestionType.MultiLine; + _model.TextInput = textInput; + + var validationResults = _model.Validate(new ValidationContext(_model)).ToList(); + + if (expectedErrorMessage != null) + { + validationResults.Should().ContainSingle(); + validationResults.First().ErrorMessage.Should().Be(expectedErrorMessage); + } + else + { + validationResults.Should().BeEmpty(); + } + } +} \ 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 14837f475..0785731fe 100644 --- a/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs +++ b/Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs @@ -77,5 +77,6 @@ public enum FormQuestionType CheckYourAnswers, Date, CheckBox, - Address + Address, + MultiLine } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Exclusions/DeclaringExclusions.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Exclusions/DeclaringExclusions.cshtml.cs index 18e49bbb8..2119d8440 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Exclusions/DeclaringExclusions.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Exclusions/DeclaringExclusions.cshtml.cs @@ -30,7 +30,7 @@ public IActionResult OnPost() if (YesNoInput == true) { - return RedirectToPage("", new { OrganisationId, FormId, SectionId }); + return RedirectToPage("../Forms/DynamicFormsPage", new { OrganisationId, FormId, SectionId }); } formsEngine.SaveUpdateAnswers(FormId, SectionId, OrganisationId, new FormQuestionAnswerState() { FurtherQuestionsExempted = true }); diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/DynamicFormsPage.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/DynamicFormsPage.cshtml.cs index f3524cee0..c63ee8948 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/DynamicFormsPage.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/DynamicFormsPage.cshtml.cs @@ -35,6 +35,8 @@ public class DynamicFormsPageModel( [BindProperty] public FormElementTextInputModel? TextInputModel { get; set; } [BindProperty] + public FormElementMultiLineInputModel? MultiLineInputModel { get; set; } + [BindProperty] public FormElementYesNoInputModel? YesNoInputModel { get; set; } [BindProperty] public FormElementAddressModel? AddressModel { get; set; } @@ -147,6 +149,7 @@ public async Task> GetAnswers() string answerString = question.Type switch { FormQuestionType.Text => answer.Answer?.TextValue ?? "", + FormQuestionType.MultiLine => answer.Answer?.TextValue ?? "", FormQuestionType.CheckBox => answer.Answer?.BoolValue == true ? question.Options.Choices?.FirstOrDefault() ?? "" : "", FormQuestionType.FileUpload => answer.Answer?.TextValue ?? "", FormQuestionType.YesOrNo => answer.Answer?.BoolValue.HasValue == true ? (answer.Answer.BoolValue == true ? "yes" : "no") : "", @@ -219,7 +222,8 @@ public bool PreviousQuestionHasNonUKAddressAnswer() { FormQuestionType.Date, "_FormElementDateInput" }, { FormQuestionType.Text, "_FormElementTextInput" }, { FormQuestionType.CheckBox, "_FormElementCheckBoxInput" }, - { FormQuestionType.Address, "_FormElementAddress" } + { FormQuestionType.Address, "_FormElementAddress" }, + { FormQuestionType.MultiLine, "_FormElementMultiLineInput" }, }; if (formQuestionPartials.TryGetValue(question.Type, out var partialView)) @@ -239,6 +243,7 @@ public bool PreviousQuestionHasNonUKAddressAnswer() { FormQuestionType.NoInput => NoInputModel ?? new FormElementNoInputModel(), FormQuestionType.Text => TextInputModel ?? new FormElementTextInputModel(), + FormQuestionType.MultiLine => MultiLineInputModel ?? new FormElementMultiLineInputModel(), FormQuestionType.FileUpload => FileUploadModel ?? new FormElementFileUploadModel(), FormQuestionType.YesOrNo => YesNoInputModel ?? new FormElementYesNoInputModel(), FormQuestionType.Date => DateInputModel ?? new FormElementDateInputModel(), diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementMultiLineInputModel.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementMultiLineInputModel.cs new file mode 100644 index 000000000..9dfe01918 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementMultiLineInputModel.cs @@ -0,0 +1,33 @@ +using CO.CDP.OrganisationApp.Models; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; + +namespace CO.CDP.OrganisationApp.Pages.Forms; + +public class FormElementMultiLineInputModel : FormElementModel, IValidatableObject +{ + [BindProperty] + public string? TextInput { get; set; } + + public override FormAnswer? GetAnswer() + { + return string.IsNullOrWhiteSpace(TextInput) ? null : new FormAnswer { TextValue = TextInput }; + } + + public override void SetAnswer(FormAnswer? answer) + { + if (answer?.TextValue != null) + { + TextInput = answer.TextValue; + } + } + + public IEnumerable Validate(ValidationContext validationContext) + { + + if (IsRequired == true && string.IsNullOrWhiteSpace(TextInput)) + { + yield return new ValidationResult("All information is required on this page", new[] { nameof(TextInput) }); + } + } +} diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementDateInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementDateInput.cshtml index 1356bca61..2bfa6c995 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementDateInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementDateInput.cshtml @@ -19,9 +19,14 @@

@Model.Heading

} -
- For example, 05 04 2022 -
+ @if (!string.IsNullOrWhiteSpace(Model.Caption)) + { + @Model.Caption + } else { +
+ For example, 05 04 2022 +
+ } @if (dayHasError) {

diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml new file mode 100644 index 000000000..f70fb8462 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -0,0 +1,32 @@ +@model FormElementMultiLineInputModel + +@{ + var hasError = ((TagBuilder)Html.ValidationMessageFor(m => m.TextInput)).HasInnerHtml; +} + +

+ + @if (!string.IsNullOrWhiteSpace(Model.Heading)) + { + + } + @if (!string.IsNullOrWhiteSpace(Model.Caption)) + { + @Model.Caption + } + + @if (!string.IsNullOrWhiteSpace(Model.Description)) + { + @Html.Raw(Model.Description) + } + + @if (hasError) + { +

+ Error: + @Html.ValidationMessageFor(m => m.TextInput) +

+ } + + +
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml index eeb9d2e3d..34c755070 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml @@ -5,15 +5,15 @@ }
- @if (!string.IsNullOrWhiteSpace(Model.Caption)) - { - @Model.Caption - } - + @if (!string.IsNullOrWhiteSpace(Model.Heading)) { } + @if (!string.IsNullOrWhiteSpace(Model.Caption)) + { + @Model.Caption + } @if (!string.IsNullOrWhiteSpace(Model.Description)) { @@ -28,5 +28,5 @@

} - +
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementYesNoInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementYesNoInput.cshtml index 9699573cf..9122f079a 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementYesNoInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementYesNoInput.cshtml @@ -5,10 +5,10 @@
- + @if (!string.IsNullOrWhiteSpace(Model.Heading)) { -

@Model.Heading

+

@Model.Heading

}
diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs index ce41b6b7b..679e196ec 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs @@ -31,7 +31,8 @@ public enum FormQuestionType CheckYourAnswers, Date, CheckBox, - Address + Address, + MultiLine } public record FormQuestionOptions diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs new file mode 100644 index 000000000..ec14d2fc8 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs @@ -0,0 +1,1809 @@ +// +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("20240919135601_ExclusionFormData")] + partial class ExclusionFormData + { + /// + 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("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("Key") + .IsUnique() + .HasDatabaseName("ix_authentication_keys_key"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_authentication_keys_organisation_id"); + + 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("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("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("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("Configuration") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("configuration"); + + 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("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("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("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("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("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.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("CompletedQualification") + .HasColumnType("boolean") + .HasColumnName("completed_qualification"); + + b1.Property("CompletedRegAddress") + .HasColumnType("boolean") + .HasColumnName("completed_reg_address"); + + b1.Property("CompletedTradeAssurance") + .HasColumnType("boolean") + .HasColumnName("completed_trade_assurance"); + + 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.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Qualification", "Qualifications", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_qualifications"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_qualifications_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_qualifications_supplier_information_organisation_id"); + + b2.ToTable("qualifications", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_qualifications_supplier_information_supplier_information_or"); + }); + + b1.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+TradeAssurance", "TradeAssurances", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reference_number"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_trade_assurances"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_trade_assurances_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_trade_assurances_supplier_information_organisation_id"); + + b2.ToTable("trade_assurances", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_trade_assurances_supplier_information_supplier_information_"); + }); + + b1.Navigation("LegalForm"); + + b1.Navigation("Qualifications"); + + b1.Navigation("TradeAssurances"); + }); + + b.Navigation("Addresses"); + + b.Navigation("BuyerInfo"); + + b.Navigation("ContactPoints"); + + b.Navigation("Identifiers"); + + 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/20240919135601_ExclusionFormData.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs new file mode 100644 index 000000000..6d7d4903f --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs @@ -0,0 +1,69 @@ +using CO.CDP.OrganisationInformation.Persistence.Forms; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations; + +/// +public partial class ExclusionFormData : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($@" + DO $$ + DECLARE + sectionId INT; + previousQuestionId INT; + BEGIN + SELECT id INTO sectionId FROM form_sections WHERE guid = '8a75cb04-fe29-45ae-90f9-168832dbea48'; + + INSERT INTO form_questions (guid, section_id, type, is_required, title, description, options) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.CheckYourAnswers}, TRUE, 'Check your answers', NULL, '{{}}') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Date}, previousQuestionId, TRUE, 'Have the circumstances that led to the exclusion ended?', '
For example, a court decision for environmental misconduct led your organisation or the connected person to stop harming the environment.
', '{{}}', 'Enter the date the circumstances ended, For example, 05 04 2022 , 'Date circumstances ended') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.FileUpload}, previousQuestionId, TRUE, 'Do you have a supporting document to upload?', '
A decision from a public authority that was the basis for the offence. For example, documentation from the police, HMRC or the court.
', '{{}}', 'Upload a file, You can upload most file types including: PDF, scans, mobile phone photos, Word, Excel and PowerPoint, multimedia and ZIP files that are smaller than 25MB.', 'Supporting document') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'How the exclusion is being managed', '
  • have done to prove it was taken seriously - for example, paid a fine or compensation
  • have done to stop the circumstances that caused it from happening again - for example, taking steps like changing staff or management or putting procedures or training in place
  • are doing to monitor the steps that were taken - for example, regular meetings
', '{{}}','You must tell us what you or the person who was subject to the event:' , 'Exclusion being managed') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'Describe the exclusion in more detail', NULL, '{{}}', 'Give us your explanation of the event. For example, any background information you can give about what happened or what caused the exclusion.', 'Exclusion in detail') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Text}, previousQuestionId, TRUE, 'Enter the email address?', NULL, '{{}}', 'Where the contracting authority can contact someone about the exclusion', 'Contact email') + RETURNING id INTO previousQuestionId; + + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.YesOrNo}, previousQuestionId, TRUE, 'Did this exclusion happen in the UK?', NULL, '{{}}', NULL, 'UK exclusion') + RETURNING id INTO previousQuestionId; + + END $$; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($@" + DO $$ + DECLARE + sectionId INT; + BEGIN + SELECT id INTO sectionId FROM form_sections WHERE guid = '8a75cb04-fe29-45ae-90f9-168832dbea48'; + + DELETE FROM form_questions WHERE section_id = sectionId; + END $$; + "); + } + +} From f904fc84e2bc88580d3269b3e1f2571b79b21fa0 Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Mon, 23 Sep 2024 10:55:28 +0100 Subject: [PATCH 02/30] #dp-542 updated exclusions test --- .../Pages/Exclusions/DeclaringExclusionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Exclusions/DeclaringExclusionsTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Exclusions/DeclaringExclusionsTests.cs index 4d382c7f0..57f1eaeb9 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Exclusions/DeclaringExclusionsTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/Exclusions/DeclaringExclusionsTests.cs @@ -57,7 +57,7 @@ public void OnPost_ShouldRedirectToCorrectPage_WhenYesOptionIsSelected() It.IsAny(), It.IsAny() ), Times.Never); - redirectResult!.PageName.Should().Be(""); + redirectResult!.PageName.Should().Be("../Forms/DynamicFormsPage"); redirectResult.RouteValues.Should().ContainKey("OrganisationId").WhoseValue.Should().Be(organisationId); redirectResult.RouteValues.Should().ContainKey("FormId").WhoseValue.Should().Be(formId); redirectResult.RouteValues.Should().ContainKey("SectionId").WhoseValue.Should().Be(sectionId); From 045159d323180b702fdf31bf4bb9fe482a8a85c1 Mon Sep 17 00:00:00 2001 From: Andy Mantell <134642+andymantell@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:16:58 +0100 Subject: [PATCH 03/30] Applying access control to the organisation overview change links --- .../Organisation/OrganisationEmail.cshtml.cs | 3 ++ .../Organisation/OrganisationName.cshtml.cs | 4 ++- .../Organisation/OrganisationOverview.cshtml | 32 ++++++++++++------- .../OrganisationOverview.cshtml.cs | 3 ++ .../OrganisationRegisteredAddress.cshtml.cs | 2 ++ 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationEmail.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationEmail.cshtml.cs index 3d5af9dce..19df3dc68 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationEmail.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationEmail.cshtml.cs @@ -1,6 +1,8 @@ using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; using CO.CDP.OrganisationApp.Models; using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.ComponentModel; @@ -8,6 +10,7 @@ namespace CO.CDP.OrganisationApp.Pages.Organisation; +[Authorize(Policy = OrgScopeRequirement.Editor)] public class OrganisationEmailModel(IOrganisationClient organisationClient) : PageModel { [BindProperty] diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationName.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationName.cshtml.cs index d9086b1bd..b5c002472 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationName.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationName.cshtml.cs @@ -1,5 +1,7 @@ using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.ComponentModel; @@ -7,7 +9,7 @@ namespace CO.CDP.OrganisationApp.Pages.Organisation; - +[Authorize(Policy = OrgScopeRequirement.Editor)] public class OrganisationNameModel(IOrganisationClient organisationClient) : PageModel { [BindProperty] diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml index a7e2c1e4a..170d0ac9c 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml @@ -25,9 +25,11 @@

@organisationDetails.Name

-
- Change address -
+ +
+ Change address +
+
@@ -49,9 +51,11 @@ }

-
- Add address -
+ +
+ Add address +
+
@@ -61,9 +65,11 @@

@organisationDetails.ContactPoint.Email

-
- Change address -
+ +
+ Change address +
+
@if (registeredAddress != null) @@ -82,9 +88,11 @@

@registeredAddress.PostalCode

@registeredAddress.CountryName

-
- Change address -
+ +
+ Change address +
+
} diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml.cs index d306a4e22..cbb34136a 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml.cs @@ -1,10 +1,13 @@ using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using OrganisationWebApiClient = CO.CDP.Organisation.WebApiClient; namespace CO.CDP.OrganisationApp.Pages; +[Authorize(Policy = OrgScopeRequirement.Viewer)] public class OrganisationOverviewModel(IOrganisationClient organisationClient) : PageModel { public OrganisationWebApiClient.Organisation? OrganisationDetails { get; set; } diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationRegisteredAddress.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationRegisteredAddress.cshtml.cs index 0fb0055d8..b87185ee0 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationRegisteredAddress.cshtml.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationRegisteredAddress.cshtml.cs @@ -3,12 +3,14 @@ using CO.CDP.OrganisationApp.Models; using CO.CDP.OrganisationApp.Pages.Shared; using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using AddressType = CO.CDP.Organisation.WebApiClient.AddressType; namespace CO.CDP.OrganisationApp.Pages.Organisation; +[Authorize(Policy = OrgScopeRequirement.Editor)] public class OrganisationRegisteredAddressModel(IOrganisationClient organisationClient) : PageModel { From cb79666d52bc53dbd2a83612e8622fa234271fa8 Mon Sep 17 00:00:00 2001 From: Andy Mantell <134642+andymantell@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:26:41 +0100 Subject: [PATCH 04/30] Fix authorization tests now that Viewer checks have been added to org overview page --- Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs index 7600deb75..4028cf1b1 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs @@ -169,7 +169,7 @@ public async Task TestAuthorizationIsUnsuccessful_WhenUserIsNotAllowedToAccessRe [Fact] public async Task TestCanSeeUsersLinkOnOrganisationPage_WhenUserIsAllowedToAccessResourceAsAdminUser() { - var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Admin ]); + var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Admin, OrganisationPersonScopes.Viewer ]); var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}"); @@ -186,7 +186,7 @@ public async Task TestCanSeeUsersLinkOnOrganisationPage_WhenUserIsAllowedToAcces [Fact] public async Task TestCannotSeeUsersLinkOnOrganisationPage_WhenUserIsNotAllowedToAccessResourceAsEditorUser() { - var _httpClient = BuildHttpClient([]); + var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Viewer ]); var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}"); From d6b53ff2074be1a317e693cb948d294942d87644 Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Mon, 23 Sep 2024 11:27:49 +0100 Subject: [PATCH 05/30] #dp-542 migration script updated --- .../Migrations/20240919135601_ExclusionFormData.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs index 6d7d4903f..3c657e750 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs @@ -22,31 +22,31 @@ protected override void Up(MigrationBuilder migrationBuilder) INSERT INTO form_questions (guid, section_id, type, is_required, title, description, options) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.CheckYourAnswers}, TRUE, 'Check your answers', NULL, '{{}}') RETURNING id INTO previousQuestionId; - + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Date}, previousQuestionId, TRUE, 'Have the circumstances that led to the exclusion ended?', '
For example, a court decision for environmental misconduct led your organisation or the connected person to stop harming the environment.
', '{{}}', 'Enter the date the circumstances ended, For example, 05 04 2022 , 'Date circumstances ended') + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Date}, previousQuestionId, TRUE, 'Have the circumstances that led to the exclusion ended?', '
For example, a court decision for environmental misconduct led your organisation or the connected person to stop harming the environment.
', '{{}}', 'Enter the date the circumstances ended, For example, 05 04 2022' , 'Date circumstances ended') RETURNING id INTO previousQuestionId; INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.FileUpload}, previousQuestionId, TRUE, 'Do you have a supporting document to upload?', '
A decision from a public authority that was the basis for the offence. For example, documentation from the police, HMRC or the court.
', '{{}}', 'Upload a file, You can upload most file types including: PDF, scans, mobile phone photos, Word, Excel and PowerPoint, multimedia and ZIP files that are smaller than 25MB.', 'Supporting document') RETURNING id INTO previousQuestionId; - + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'How the exclusion is being managed', '
  • have done to prove it was taken seriously - for example, paid a fine or compensation
  • have done to stop the circumstances that caused it from happening again - for example, taking steps like changing staff or management or putting procedures or training in place
  • are doing to monitor the steps that were taken - for example, regular meetings
', '{{}}','You must tell us what you or the person who was subject to the event:' , 'Exclusion being managed') RETURNING id INTO previousQuestionId; - + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'Describe the exclusion in more detail', NULL, '{{}}', 'Give us your explanation of the event. For example, any background information you can give about what happened or what caused the exclusion.', 'Exclusion in detail') RETURNING id INTO previousQuestionId; - + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Text}, previousQuestionId, TRUE, 'Enter the email address?', NULL, '{{}}', 'Where the contracting authority can contact someone about the exclusion', 'Contact email') RETURNING id INTO previousQuestionId; - + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.YesOrNo}, previousQuestionId, TRUE, 'Did this exclusion happen in the UK?', NULL, '{{}}', NULL, 'UK exclusion') RETURNING id INTO previousQuestionId; - + END $$; "); } From 073c7045d941b5fbe239fcf71c1b60e25051dd00 Mon Sep 17 00:00:00 2001 From: Ali Bahman Date: Mon, 23 Sep 2024 11:48:58 +0100 Subject: [PATCH 06/30] DP-630 Grant access to more users from CGI (#643) --- terragrunt/components/terragrunt.hcl | 7 +++++-- terragrunt/modules/core-iam/locals.tf | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/terragrunt/components/terragrunt.hcl b/terragrunt/components/terragrunt.hcl index 50b7a0c2e..be365e24c 100644 --- a/terragrunt/components/terragrunt.hcl +++ b/terragrunt/components/terragrunt.hcl @@ -322,14 +322,17 @@ locals { allowed_ips = [ "212.139.19.138", #GOACO "94.174.71.0/24", # Ali Bahman - "82.38.3.0/24", # Dorian Stefan + "82.38.3.0/24", # Dorian Stefan ] user_arns = [ "arn:aws:iam::525593800265:user/ali.bahman@goaco.com", "arn:aws:iam::525593800265:user/dorian.stefan@goaco.com", ] external_user_arns = [ - "arn:aws:iam::495599741725:user/gdavies" + "arn:aws:iam::464141439926:user/jamesmoss-cgi", # James Moss from CGI + "arn:aws:iam::495599741725:user/gdavies", # Gavin Davis from CGI + "arn:aws:iam::571600860189:user/nchamdal", # Naresh Chamdal from CGI + "arn:aws:iam::717279707340:user/Chrisr-CGI", # Chris Rooney from CGI ] } diff --git a/terragrunt/modules/core-iam/locals.tf b/terragrunt/modules/core-iam/locals.tf index 663fb98e9..106014e42 100644 --- a/terragrunt/modules/core-iam/locals.tf +++ b/terragrunt/modules/core-iam/locals.tf @@ -5,6 +5,6 @@ locals { pipeline_iam_name = "${local.name_prefix}-${var.environment}-ci-pipeline" use_codestar_connection = var.environment != "orchestrator" - pen_testing_user_arns = var.environment == "staging" ? concat(var.pen_testing_user_arns, var.pen_testing_external_user_arns) : var.pen_testing_user_arns + pen_testing_user_arns = contains(["staging", "orchestrator"], var.environment) ? concat(var.pen_testing_user_arns, var.pen_testing_external_user_arns) : var.pen_testing_user_arns pen_testing_allowed_ips = var.environment == "staging" ? [] : var.pen_testing_allowed_ips } From 45f83c263e4deb62735a564ccfa82d29428e785c Mon Sep 17 00:00:00 2001 From: Dharmendra Verma <64859911+dharmverma@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:24:52 +0100 Subject: [PATCH 07/30] Part 3: Data Sharing API endpoints authorization (#641) * Part 3: Data Sharing API endpoints authorization * Fixed db error * Refactor test --- ...anisationScopeAuthorizationHandlerTests.cs | 45 +--- .../ClaimServiceTests.cs | 53 ++--- .../OrganisationScopeAuthorizationHandler.cs | 9 +- .../CO.CDP.Authentication/ClaimService.cs | 14 +- Libraries/CO.CDP.Authentication/Extensions.cs | 2 +- .../CO.CDP.Authentication/IClaimService.cs | 2 +- .../Api/DataSharingTests.cs | 207 ++++++++++++++++++ .../CO.CDP.DataSharing.WebApi.Tests.csproj | 1 + .../PdfGenerator/PdfGeneratorTests.cs | 14 +- .../UseCase/GenerateShareCodeUseCaseTest.cs | 24 +- .../UseCase/GetSharedDataPdfUseCaseTests.cs | 37 +++- .../Api/DataSharing.cs | 22 +- .../DataService/DataMappingFactory.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Model/Exceptions.cs | 1 + .../Model/SharedSupplierInformation.cs | 1 + .../UseCase/GenerateShareCodeUseCase.cs | 4 +- .../UseCase/GetSharedDataPdfUseCase.cs | 15 +- .../Api/BuyerInformationEndpointsTests.cs | 8 +- .../Api/ConnectedEntityEndpointsTests.cs | 21 +- .../Api/OrganisationEndpointsTests.cs | 21 +- .../Api/OrganisationLookupEndpointsTests.cs | 8 +- .../Api/PersonInvitesEndpointsTests.cs | 17 +- .../Api/PersonsEndpointsTests.cs | 13 +- .../Api/SupplierInformationEndpointsTests.cs | 13 +- .../DatabaseOrganisationRepository.cs | 8 + .../IOrganisationRepository.cs | 2 + Services/CO.CDP.Tenant.WebApi/Program.cs | 7 +- .../TestAuthorizationWebApplicationFactory.cs | 24 +- 29 files changed, 382 insertions(+), 213 deletions(-) create mode 100644 Services/CO.CDP.DataSharing.WebApi.Tests/Api/DataSharingTests.cs diff --git a/Libraries/CO.CDP.Authentication.Tests/Authorization/OrganisationScopeAuthorizationHandlerTests.cs b/Libraries/CO.CDP.Authentication.Tests/Authorization/OrganisationScopeAuthorizationHandlerTests.cs index 917a52810..6d8efe582 100644 --- a/Libraries/CO.CDP.Authentication.Tests/Authorization/OrganisationScopeAuthorizationHandlerTests.cs +++ b/Libraries/CO.CDP.Authentication.Tests/Authorization/OrganisationScopeAuthorizationHandlerTests.cs @@ -13,15 +13,13 @@ namespace CO.CDP.Authentication.Tests.Authorization; public class OrganisationScopeAuthorizationHandlerTests { - private readonly Mock _mockHttpContextAccessor; - private readonly Mock _mockTenantRepository; + private readonly Mock _mockHttpContextAccessor = new(); + private readonly Mock _mockOrganisationRepository = new(); private readonly OrganisationScopeAuthorizationHandler _handler; public OrganisationScopeAuthorizationHandlerTests() { - _mockHttpContextAccessor = new(); - _mockTenantRepository = new(); - _handler = new OrganisationScopeAuthorizationHandler(_mockHttpContextAccessor.Object, _mockTenantRepository.Object); + _handler = new OrganisationScopeAuthorizationHandler(_mockHttpContextAccessor.Object, _mockOrganisationRepository.Object); } [Fact] @@ -71,8 +69,8 @@ public async Task HandleRequirementAsync_MatchExpectedResult(bool organisationSc var context = CreateAuthorizationHandlerContext(userUrn, organisationScopeExists ? ["Admin"] : ["Scope1"], location); MockHttpContext(location, string.Format(urlFormat, organisationId)); - _mockTenantRepository.Setup(x => x.LookupTenant(userUrn)) - .ReturnsAsync(GetTenantLookup(userUrn, organisationId)); + _mockOrganisationRepository.Setup(x => x.FindOrganisationPerson(organisationId, userUrn)) + .ReturnsAsync(GetOrganisationPerson()); await _handler.HandleAsync(context); @@ -87,8 +85,8 @@ public async Task FetchOrganisationIdAsync_Should_Return_Correct_OrganisationId_ var context = CreateAuthorizationHandlerContext(userUrn, ["Admin"], OrganisationIdLocation.Body); MockHttpContext(OrganisationIdLocation.Body, organisationId: organisationId); - _mockTenantRepository.Setup(x => x.LookupTenant(userUrn)) - .ReturnsAsync(GetTenantLookup(userUrn, organisationId)); + _mockOrganisationRepository.Setup(x => x.FindOrganisationPerson(organisationId, userUrn)) + .ReturnsAsync(GetOrganisationPerson()); await _handler.HandleAsync(context); @@ -116,33 +114,14 @@ private void MockHttpContext(OrganisationIdLocation location, string? url = null _mockHttpContextAccessor.Setup(x => x.HttpContext).Returns(mockHttpContext); } - private static TenantLookup GetTenantLookup(string userUrn, Guid organisationId) + private static OrganisationPerson GetOrganisationPerson() { - var tenantLookup = new TenantLookup + return new OrganisationPerson { - User = new TenantLookup.PersonUser - { - Email = "test@test.com", - Name = "Test person", - Urn = userUrn - }, - Tenants = [ - new TenantLookup.Tenant - { - Id = Guid.NewGuid(), - Name = $"Test Tenant", - Organisations = [ - new TenantLookup.Organisation - { - Id = organisationId, - Name = $"Test Org", - Roles = [OrganisationInformation.PartyRole.Buyer], - Scopes = ["Admin"] - }] - }] + Organisation = Mock.Of(), + Person = Mock.Of(), + Scopes = ["Admin"] }; - - return tenantLookup; } private static AuthorizationHandlerContext CreateAuthorizationHandlerContext(string userUrn, string[] scopes, OrganisationIdLocation orgIdLoc) diff --git a/Libraries/CO.CDP.Authentication.Tests/ClaimServiceTests.cs b/Libraries/CO.CDP.Authentication.Tests/ClaimServiceTests.cs index 6b4f62989..9fc4f3a6b 100644 --- a/Libraries/CO.CDP.Authentication.Tests/ClaimServiceTests.cs +++ b/Libraries/CO.CDP.Authentication.Tests/ClaimServiceTests.cs @@ -9,12 +9,7 @@ namespace CO.CDP.Authentication.Tests; public class ClaimServiceTests { - private readonly Mock mockTenantRepo; - - public ClaimServiceTests() - { - mockTenantRepo = new(); - } + private readonly Mock mockOrgRepo = new(); [Fact] public void GetUserUrn_ShouldReturnUrn_WhenUserHasSubClaim() @@ -22,7 +17,7 @@ public void GetUserUrn_ShouldReturnUrn_WhenUserHasSubClaim() var userUrn = "urn:fdc:gov.uk:2022:rynbwxUssDAcmU38U5gxd7dBfu9N7KFP9_nqDuZ66Hg"; var httpContextAccessor = GivenHttpContextWith([new(ClaimType.Subject, userUrn)]); - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); + var claimService = new ClaimService(httpContextAccessor.Object, mockOrgRepo.Object); var result = claimService.GetUserUrn(); result.Should().Be(userUrn); @@ -33,7 +28,7 @@ public void GetUserUrn_ShouldReturnNull_WhenUserHasNoSubClaim() { var httpContextAccessor = GivenHttpContextWith([]); - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); + var claimService = new ClaimService(httpContextAccessor.Object, mockOrgRepo.Object); var result = claimService.GetUserUrn(); result.Should().BeNull(); @@ -44,7 +39,7 @@ public async Task HaveAccessToOrganisation_ShouldReturnFalse_WhenUserHasNoSubCla { var httpContextAccessor = GivenHttpContextWith([]); - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); + var claimService = new ClaimService(httpContextAccessor.Object, mockOrgRepo.Object); var result = await claimService.HaveAccessToOrganisation(Guid.NewGuid()); result.Should().BeFalse(); @@ -53,33 +48,15 @@ public async Task HaveAccessToOrganisation_ShouldReturnFalse_WhenUserHasNoSubCla [Fact] public async Task HaveAccessToOrganisation_ShouldReturnFalse_WhenUserHasNoTenant() { + var organisationId = Guid.NewGuid(); var userUrn = "urn:fdc:gov.uk:2022:rynbwxUssDAcmU38U5gxd7dBfu9N7KFP9_nqDuZ66Hg"; var httpContextAccessor = GivenHttpContextWith([new(ClaimType.Subject, userUrn)]); - mockTenantRepo.Setup(m => m.LookupTenant(userUrn)) - .ReturnsAsync((TenantLookup?)default); - - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); - var result = await claimService.HaveAccessToOrganisation(Guid.NewGuid()); + mockOrgRepo.Setup(m => m.FindOrganisationPerson(organisationId, userUrn)) + .ReturnsAsync((OrganisationPerson?)default); - result.Should().BeFalse(); - } - - [Fact] - public async Task HaveAccessToOrganisation_ShouldReturnFalse_WhenDoesNotHaveAccessToOrganisation() - { - var userUrn = "urn:fdc:gov.uk:2022:rynbwxUssDAcmU38U5gxd7dBfu9N7KFP9_nqDuZ66Hg"; - var httpContextAccessor = GivenHttpContextWith([new(ClaimType.Subject, userUrn)]); - - mockTenantRepo.Setup(m => m.LookupTenant(userUrn)) - .ReturnsAsync(new TenantLookup - { - User = new TenantLookup.PersonUser { Name = "Test", Email = "test@test", Urn = userUrn }, - Tenants = [] - }); - - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); - var result = await claimService.HaveAccessToOrganisation(new Guid("57dcf48c-8910-4108-9cf1-c2935488a085")); + var claimService = new ClaimService(httpContextAccessor.Object, mockOrgRepo.Object); + var result = await claimService.HaveAccessToOrganisation(organisationId); result.Should().BeFalse(); } @@ -91,15 +68,15 @@ public async Task HaveAccessToOrganisation_ShouldReturnTrue_WhenDoesHaveAccessTo var userUrn = "urn:fdc:gov.uk:2022:rynbwxUssDAcmU38U5gxd7dBfu9N7KFP9_nqDuZ66Hg"; var httpContextAccessor = GivenHttpContextWith([new(ClaimType.Subject, userUrn)]); - mockTenantRepo.Setup(m => m.LookupTenant(userUrn)) - .ReturnsAsync(new TenantLookup + mockOrgRepo.Setup(m => m.FindOrganisationPerson(organisationId, userUrn)) + .ReturnsAsync(new OrganisationPerson { - User = new TenantLookup.PersonUser { Name = "Test", Email = "test@test", Urn = userUrn }, - Tenants = [new TenantLookup.Tenant { Id = Guid.NewGuid(), Name = "Ten", - Organisations = [new TenantLookup.Organisation { Id = organisationId, Name = "org", Roles = [], Scopes = [] }] }] + Organisation = Mock.Of(), + Person = Mock.Of(), + Scopes = ["Admin"] }); - var claimService = new ClaimService(httpContextAccessor.Object, mockTenantRepo.Object); + var claimService = new ClaimService(httpContextAccessor.Object, mockOrgRepo.Object); var result = await claimService.HaveAccessToOrganisation(organisationId); result.Should().BeTrue(); diff --git a/Libraries/CO.CDP.Authentication/Authorization/OrganisationScopeAuthorizationHandler.cs b/Libraries/CO.CDP.Authentication/Authorization/OrganisationScopeAuthorizationHandler.cs index 959541fc1..1427a4f67 100644 --- a/Libraries/CO.CDP.Authentication/Authorization/OrganisationScopeAuthorizationHandler.cs +++ b/Libraries/CO.CDP.Authentication/Authorization/OrganisationScopeAuthorizationHandler.cs @@ -12,7 +12,7 @@ namespace CO.CDP.Authentication.Authorization; public class OrganisationScopeAuthorizationHandler( IHttpContextAccessor httpContextAccessor, - ITenantRepository tenantRepository) + IOrganisationRepository organisationRepository) : AuthorizationHandler { private const string RegexGuid = @"[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"; @@ -39,9 +39,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext if (!string.IsNullOrWhiteSpace(organisationId) && Guid.TryParse(organisationId, out var organisationGuid)) { - var lookup = await tenantRepository.LookupTenant(userUrn); - List orgScopes = lookup?.Tenants?.SelectMany(t => t.Organisations)? - .FirstOrDefault(o => o.Id == organisationGuid)?.Scopes ?? []; + var orgPerson = await organisationRepository.FindOrganisationPerson(organisationGuid, userUrn); + List orgScopes = orgPerson?.Scopes ?? []; if (requirement.Scopes.Intersect(orgScopes).Any()) { @@ -71,7 +70,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext break; case OrganisationIdLocation.Body: - if (currentRequest.Body == null || currentRequest.ContentType != Application.Json) return null; + if (currentRequest.Body == null || currentRequest.ContentType?.Contains(Application.Json) == false) return null; if (!currentRequest.Body.CanSeek) currentRequest.EnableBuffering(); currentRequest.Body.Position = 0; diff --git a/Libraries/CO.CDP.Authentication/ClaimService.cs b/Libraries/CO.CDP.Authentication/ClaimService.cs index 97303bda6..65318ed39 100644 --- a/Libraries/CO.CDP.Authentication/ClaimService.cs +++ b/Libraries/CO.CDP.Authentication/ClaimService.cs @@ -6,22 +6,24 @@ namespace CO.CDP.Authentication; public class ClaimService( IHttpContextAccessor httpContextAccessor, - ITenantRepository tenantRepository) : IClaimService + IOrganisationRepository organisationRepository) : IClaimService { public string? GetUserUrn() { return httpContextAccessor.HttpContext?.User?.FindFirst(ClaimType.Subject)?.Value; } - public async Task HaveAccessToOrganisation(Guid oragnisationId) + public async Task HaveAccessToOrganisation(Guid organisationId, string[]? scopes = null) { var userUrn = GetUserUrn(); - if (string.IsNullOrEmpty(userUrn)) return false; + if (string.IsNullOrWhiteSpace(userUrn)) return false; - var tenantlookup = await tenantRepository.LookupTenant(userUrn); - if (tenantlookup == null) return false; + var organisationPerson = await organisationRepository.FindOrganisationPerson(organisationId, userUrn); + if (organisationPerson == null) return false; - return tenantlookup.Tenants.SelectMany(t => t.Organisations).Any(o => o.Id == oragnisationId); + if (scopes == null) return true; + + return organisationPerson.Scopes.Intersect(scopes).Any(); } public Guid? GetOrganisationId() diff --git a/Libraries/CO.CDP.Authentication/Extensions.cs b/Libraries/CO.CDP.Authentication/Extensions.cs index 103355d7a..f1b0bfc0c 100644 --- a/Libraries/CO.CDP.Authentication/Extensions.cs +++ b/Libraries/CO.CDP.Authentication/Extensions.cs @@ -73,7 +73,7 @@ public static IServiceCollection AddApiKeyAuthenticationServices(this IServiceCo public static IServiceCollection AddOrganisationAuthorization(this IServiceCollection services) { - services.TryAddScoped(); + services.TryAddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); diff --git a/Libraries/CO.CDP.Authentication/IClaimService.cs b/Libraries/CO.CDP.Authentication/IClaimService.cs index 4f66ec9de..5ddf5bce0 100644 --- a/Libraries/CO.CDP.Authentication/IClaimService.cs +++ b/Libraries/CO.CDP.Authentication/IClaimService.cs @@ -5,5 +5,5 @@ public interface IClaimService Guid? GetOrganisationId(); - Task HaveAccessToOrganisation(Guid oragnisationId); + Task HaveAccessToOrganisation(Guid organisationId, string[] scopes); } \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/Api/DataSharingTests.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/Api/DataSharingTests.cs new file mode 100644 index 000000000..61e10b5d6 --- /dev/null +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/Api/DataSharingTests.cs @@ -0,0 +1,207 @@ +using CO.CDP.DataSharing.WebApi.Model; +using CO.CDP.DataSharing.WebApi.UseCase; +using CO.CDP.TestKit.Mvc; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System.Net; +using System.Net.Http.Json; +using static CO.CDP.Authentication.Constants; +using static System.Net.HttpStatusCode; + +namespace CO.CDP.DataSharing.WebApi.Tests.Api; + +public class DataSharingTests +{ + private readonly Mock> _getSharedDataUseCase = new(); + private readonly Mock> _getSharedDataPdfUseCase = new(); + private readonly Mock> _generateShareCodeUseCase = new(); + private readonly Mock> _getShareCodeVerifyUseCase = new(); + private readonly Mock?>> _getShareCodesUseCase = new(); + private readonly Mock> _getShareCodeDetailsUseCase = new(); + + [Theory] + [InlineData(OK, Channel.OrganisationKey)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OneLogin)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetSharedData_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var shareCode = "valid-share-code"; + + _getSharedDataUseCase.Setup(uc => uc.Execute(shareCode)) + .ReturnsAsync(GetSupplierInfo()); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _getSharedDataUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/share/data/{shareCode}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(OK, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetSharedDataPdf_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var shareCode = "valid-share-code"; + + _getSharedDataPdfUseCase.Setup(uc => uc.Execute(shareCode)).ReturnsAsync(new byte[1]); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _getSharedDataPdfUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/share/data/{shareCode}/pdf"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Responder)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task CreateSharedData_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var organisationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var shareRequest = new ShareRequest { FormId = formId, OrganisationId = organisationId }; + + _generateShareCodeUseCase.Setup(uc => uc.Execute(shareRequest)) + .ReturnsAsync(new ShareReceipt { FormId = formId, ShareCode = "new_code" }); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, + services => services.AddScoped(_ => _generateShareCodeUseCase.Object)); + + var response = await factory.CreateClient().PostAsJsonAsync("/share/data", shareRequest); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(OK, Channel.OrganisationKey)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OneLogin)] + [InlineData(Forbidden, "unknown_channel")] + public async Task VerifySharedData_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var shareCode = "new_code"; + var formVersionId = "v.0"; + var shareRequest = new ShareVerificationRequest { ShareCode = shareCode, FormVersionId = formVersionId }; + + _getShareCodeVerifyUseCase.Setup(uc => uc.Execute(shareRequest)) + .ReturnsAsync(new ShareVerificationReceipt { ShareCode = shareCode, FormVersionId = formVersionId, IsLatest = true }); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _getShareCodeVerifyUseCase.Object)); + + var response = await factory.CreateClient().PostAsJsonAsync("/share/data/verify", shareRequest); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Responder)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetShareCodeList_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var organisationId = Guid.NewGuid(); + + _getShareCodesUseCase.Setup(uc => uc.Execute(organisationId)) + .ReturnsAsync([new SharedConsent()]); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, + services => services.AddScoped(_ => _getShareCodesUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/share/organisations/{organisationId}/codes"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Responder)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetShareCodeDetails_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var organisationId = Guid.NewGuid(); + var shareCode = "new_code"; + var command = (organisationId, shareCode); + + _getShareCodeDetailsUseCase.Setup(uc => uc.Execute(command)) + .ReturnsAsync(new SharedConsentDetails { ShareCode = shareCode }); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, + services => services.AddScoped(_ => _getShareCodeDetailsUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/share/organisations/{organisationId}/codes/{shareCode}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + private static SupplierInformation GetSupplierInfo() + { + return new SupplierInformation + { + Id = Guid.NewGuid(), + Name = "si", + AssociatedPersons = [], + AdditionalParties = [], + AdditionalEntities = [], + Identifier = new OrganisationInformation.Identifier { Scheme = "fake", LegalName = "test" }, + AdditionalIdentifiers = [], + Address = new OrganisationInformation.Address + { + StreetAddress = "1 st", + Locality = "very local", + PostalCode = "WS1", + Country = "GB", + CountryName = "UK", + Type = OrganisationInformation.AddressType.Registered + }, + ContactPoint = new OrganisationInformation.ContactPoint(), + Roles = [], + Details = new Details(), + SupplierInformationData = new SupplierInformationData + { + Form = new Form + { + FormId = Guid.NewGuid(), + Name = "f1", + FormVersionId = "v.0", + OrganisationId = Guid.NewGuid(), + IsRequired = false, + ShareCode = "new_code", + SubmissionState = FormSubmissionState.Submitted, + SubmittedAt = DateTime.Now + }, + Questions = [], + AnswerSets = [] + } + }; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/CO.CDP.DataSharing.WebApi.Tests.csproj b/Services/CO.CDP.DataSharing.WebApi.Tests/CO.CDP.DataSharing.WebApi.Tests.csproj index 7f49cd8c2..4c8e715fd 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/CO.CDP.DataSharing.WebApi.Tests.csproj +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/CO.CDP.DataSharing.WebApi.Tests.csproj @@ -23,6 +23,7 @@ all + diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/PdfGenerator/PdfGeneratorTests.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/PdfGenerator/PdfGeneratorTests.cs index 98dcec9df..6d1965e4b 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/PdfGenerator/PdfGeneratorTests.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/PdfGenerator/PdfGeneratorTests.cs @@ -1,6 +1,6 @@ -using CO.CDP.DataSharing.WebApi; -using CO.CDP.DataSharing.WebApi.Tests; -using DataSharing.Tests; +using FluentAssertions; + +namespace CO.CDP.DataSharing.WebApi.Tests.Pdf; public class PdfGeneratorTests { @@ -14,16 +14,16 @@ public PdfGeneratorTests() [Fact] public void GenerateBasicInformationPdf_ShouldGeneratePdfWithAllInformation() { - - var supplierInformation = new CO.CDP.DataSharing.WebApi.Model.SharedSupplierInformation + var supplierInformation = new Model.SharedSupplierInformation { + OrganisationId = Guid.NewGuid(), BasicInformation = DataSharingFactory.CreateMockBasicInformation(), ConnectedPersonInformation = DataSharingFactory.CreateMockConnectedPersonInformation(), }; var pdfBytes = _pdfGenerator.GenerateBasicInformationPdf(supplierInformation); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 0); + pdfBytes.Should().NotBeNull(); + pdfBytes.Length.Should().BeGreaterThan(0); } } \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GenerateShareCodeUseCaseTest.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GenerateShareCodeUseCaseTest.cs index 21db76035..174c747cb 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GenerateShareCodeUseCaseTest.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GenerateShareCodeUseCaseTest.cs @@ -4,17 +4,15 @@ using CO.CDP.OrganisationInformation.Persistence; using FluentAssertions; using Moq; -using CO.CDP.Authentication; namespace CO.CDP.DataSharing.WebApi.Tests.UseCase; public class GenerateShareCodeUseCaseTest(AutoMapperFixture mapperFixture) : IClassFixture { - private readonly Mock _claimService = new(); private readonly Mock _organisationRepository = new(); private readonly Mock _formRepository = new(); private readonly Mock _shareCodeRepository = new(); - private GenerateShareCodeUseCase UseCase => new(_claimService.Object, _organisationRepository.Object, _formRepository.Object, _shareCodeRepository.Object, mapperFixture.Mapper); + private GenerateShareCodeUseCase UseCase => new(_organisationRepository.Object, _formRepository.Object, _shareCodeRepository.Object, mapperFixture.Mapper); [Fact] public async Task ThrowsInvalidOrganisationRequestedException_WhenShareCodeRequestedForNonExistentOrganisation() @@ -31,24 +29,6 @@ public async Task ThrowsInvalidOrganisationRequestedException_WhenShareCodeReque await shareReceipt.Should().ThrowAsync(); } - [Fact] - public async Task ThrowsInvalidOrganisationRequestedException_WhenShareCodeRequestedForNotAuthorisedOrganisation() - { - var organisationId = 2; - var organisationGuid = Guid.NewGuid(); - var formId = (Guid)default; - - var shareRequest = EntityFactory.GetShareRequest(organisationGuid: organisationGuid, formId: formId); - var sharedConsent = EntityFactory.GetSharedConsent(organisationId: organisationId, organisationGuid: organisationGuid, formId: formId); - - _claimService.Setup(x => x.HaveAccessToOrganisation(organisationGuid)).ReturnsAsync(false); - _organisationRepository.Setup(x => x.Find(organisationGuid)).ReturnsAsync(sharedConsent.Organisation); - - var shareReceipt = async () => await UseCase.Execute(shareRequest); - - await shareReceipt.Should().ThrowAsync(); - } - [Fact] public async Task ThrowsSharedConsentNotFoundException_WhenNoSharedConsentOrNoneInValidStateFound() { @@ -60,7 +40,6 @@ public async Task ThrowsSharedConsentNotFoundException_WhenNoSharedConsentOrNone var shareRequest = EntityFactory.GetShareRequest(organisationGuid: organisationGuid, formId: formId); var sharedConsent = EntityFactory.GetSharedConsent(organisationId: organisationId, organisationGuid: organisationGuid, formId: formId); - _claimService.Setup(x => x.HaveAccessToOrganisation(organisationGuid)).ReturnsAsync(true); _organisationRepository.Setup(x => x.Find(organisationGuid)).ReturnsAsync(sharedConsent.Organisation); _shareCodeRepository.Setup(r => r.GetSharedConsentDraftAsync(shareRequest.FormId, shareRequest.OrganisationId)).ReturnsAsync((OrganisationInformation.Persistence.Forms.SharedConsent?)null); @@ -80,7 +59,6 @@ public async Task ReturnsRelevantShareReceipt_WhenAuthorisedAndShareRequestForVa var shareRequest = EntityFactory.GetShareRequest(organisationGuid: organisationGuid, formId: formId); var sharedConsent = EntityFactory.GetSharedConsent(organisationId: organisationId, organisationGuid: organisationGuid, formId: formId); - _claimService.Setup(x => x.HaveAccessToOrganisation(organisationGuid)).ReturnsAsync(true); _organisationRepository.Setup(x => x.Find(organisationGuid)).ReturnsAsync(sharedConsent.Organisation); _shareCodeRepository.Setup(r => r.GetSharedConsentDraftAsync(shareRequest.FormId, shareRequest.OrganisationId)).ReturnsAsync(sharedConsent); diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GetSharedDataPdfUseCaseTests.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GetSharedDataPdfUseCaseTests.cs index 68ea91f79..e23aeb132 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GetSharedDataPdfUseCaseTests.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/UseCase/GetSharedDataPdfUseCaseTests.cs @@ -1,3 +1,4 @@ +using CO.CDP.Authentication; using CO.CDP.DataSharing.WebApi; using CO.CDP.DataSharing.WebApi.DataService; using CO.CDP.DataSharing.WebApi.Model; @@ -5,6 +6,7 @@ using CO.CDP.DataSharing.WebApi.UseCase; using FluentAssertions; using Moq; +using static CO.CDP.Authentication.Constants; namespace DataSharing.Tests.UseCase; @@ -12,15 +14,19 @@ public class GetSharedDataPdfUseCaseTests { private readonly Mock _dataService = new(); private readonly Mock _pdfGenerator = new(); + private readonly Mock _claimService = new(); + private string[] requiredClaims = [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer]; - private GetSharedDataPdfUseCase UseCase => new(_pdfGenerator.Object, _dataService.Object); + private GetSharedDataPdfUseCase UseCase => new(_pdfGenerator.Object, _dataService.Object, _claimService.Object); [Fact] public async Task Execute_ShouldReturnPdfBytes_WhenShareCodeExists() { + var organisationId = Guid.NewGuid(); var sharecode = "valid-sharecode"; var sharedSupplierInformation = new SharedSupplierInformation { + OrganisationId = organisationId, BasicInformation = DataSharingFactory.CreateMockBasicInformation(), ConnectedPersonInformation = DataSharingFactory.CreateMockConnectedPersonInformation() }; @@ -32,6 +38,8 @@ public async Task Execute_ShouldReturnPdfBytes_WhenShareCodeExists() _pdfGenerator.Setup(generator => generator.GenerateBasicInformationPdf(sharedSupplierInformation)) .Returns(pdfBytes); + _claimService.Setup(cs => cs.HaveAccessToOrganisation(organisationId, requiredClaims)) + .ReturnsAsync(true); var result = await UseCase.Execute(sharecode); @@ -41,10 +49,12 @@ public async Task Execute_ShouldReturnPdfBytes_WhenShareCodeExists() [Fact] public async Task Execute_ShouldCallDataServiceAndPdfGenerator_WhenShareCodeExists() { + var organisationId = Guid.NewGuid(); var sharecode = "valid-sharecode"; var sharedSupplierInformation = new SharedSupplierInformation { + OrganisationId = organisationId, BasicInformation = DataSharingFactory.CreateMockBasicInformation(), ConnectedPersonInformation = DataSharingFactory.CreateMockConnectedPersonInformation() }; @@ -56,9 +66,34 @@ public async Task Execute_ShouldCallDataServiceAndPdfGenerator_WhenShareCodeExis _pdfGenerator.Setup(generator => generator.GenerateBasicInformationPdf(sharedSupplierInformation)) .Returns(pdfBytes); + _claimService.Setup(cs => cs.HaveAccessToOrganisation(organisationId, requiredClaims)) + .ReturnsAsync(true); var result = await UseCase.Execute(sharecode); result.Should().BeEquivalentTo(pdfBytes); } + + [Fact] + public async Task Execute_WhenDoesNotHaveAccessToOrganisation_ThrowsUserUnauthorizedException() + { + var organisationId = Guid.NewGuid(); + var sharecode = "valid-sharecode"; + string[] invalidscope = [OrganisationPersonScope.Responder]; + + _dataService.Setup(service => service.GetSharedSupplierInformationAsync(sharecode)) + .ReturnsAsync(new SharedSupplierInformation + { + OrganisationId = organisationId, + BasicInformation = DataSharingFactory.CreateMockBasicInformation(), + ConnectedPersonInformation = DataSharingFactory.CreateMockConnectedPersonInformation() + }); + + _claimService.Setup(cs => cs.HaveAccessToOrganisation(organisationId, invalidscope)) + .ReturnsAsync(true); + + var act = async () => await UseCase.Execute(sharecode); + + await act.Should().ThrowAsync(); + } } \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi/Api/DataSharing.cs b/Services/CO.CDP.DataSharing.WebApi/Api/DataSharing.cs index abcbbb17a..fc9b304a2 100644 --- a/Services/CO.CDP.DataSharing.WebApi/Api/DataSharing.cs +++ b/Services/CO.CDP.DataSharing.WebApi/Api/DataSharing.cs @@ -11,6 +11,7 @@ using Microsoft.OpenApi.Models; using System.Net.Mime; using System.Reflection; +using static CO.CDP.Authentication.Constants; namespace CO.CDP.DataSharing.WebApi.Api; @@ -63,7 +64,10 @@ await useCase.Execute(sharecode) }); app.MapPost("/share/data", - [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor], + OrganisationIdLocation.Body)] async (ShareRequest shareRequest, IUseCase useCase) => await useCase.Execute(shareRequest) .AndThen(shareReceipt => Results.Ok(shareReceipt))) @@ -104,10 +108,15 @@ await useCase.Execute(request) return operation; }); - app.MapGet("/share/organisations/{organisationId}/codes", async (Guid organisationId, - IUseCase?> useCase) => await useCase.Execute(organisationId) + app.MapGet("/share/organisations/{organisationId}/codes", + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer], + OrganisationIdLocation.Path)] + async (Guid organisationId, + IUseCase?> useCase) => await useCase.Execute(organisationId) .AndThen(sharedCodes => sharedCodes != null ? Results.Ok(sharedCodes) : Results.NotFound())) - .Produces>(StatusCodes.Status200OK, "application/json") + .Produces>(StatusCodes.Status200OK, "application/json") .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError) @@ -124,7 +133,10 @@ await useCase.Execute(request) }); app.MapGet("/share/organisations/{organisationId}/codes/{sharecode}", - [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer], + OrganisationIdLocation.Path)] async (Guid organisationId, string shareCode, IUseCase<(Guid, string), SharedConsentDetails?> useCase) => await useCase.Execute((organisationId, shareCode)) .AndThen(sharedCodeDetails => sharedCodeDetails != null ? Results.Ok(sharedCodeDetails) : Results.NotFound())) diff --git a/Services/CO.CDP.DataSharing.WebApi/DataService/DataMappingFactory.cs b/Services/CO.CDP.DataSharing.WebApi/DataService/DataMappingFactory.cs index 2cb384e08..6909ddc4a 100644 --- a/Services/CO.CDP.DataSharing.WebApi/DataService/DataMappingFactory.cs +++ b/Services/CO.CDP.DataSharing.WebApi/DataService/DataMappingFactory.cs @@ -15,6 +15,7 @@ public static SharedSupplierInformation MapToSharedSupplierInformation(SharedCon { return new SharedSupplierInformation { + OrganisationId = sharedConsent.Organisation.Guid, BasicInformation = MapToBasicInformation(sharedConsent.Organisation), ConnectedPersonInformation = MapToConnectedPersonInformation(sharedConsent.Organisation.Guid, connectedEntityRepository) }; diff --git a/Services/CO.CDP.DataSharing.WebApi/Extensions/ServiceCollectionExtensions.cs b/Services/CO.CDP.DataSharing.WebApi/Extensions/ServiceCollectionExtensions.cs index 7036af7df..4b06b50d8 100644 --- a/Services/CO.CDP.DataSharing.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Services/CO.CDP.DataSharing.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ public static class ServiceCollectionExtensions { private static readonly Dictionary ExceptionMap = new() { + { typeof(UserUnauthorizedException), (StatusCodes.Status403Forbidden, "UNAUTHORIZED") }, { typeof(InvalidOrganisationRequestedException), (StatusCodes.Status403Forbidden, "INVALID_ORGANISATION_REQUESTED") }, { typeof(ShareCodeNotFoundException), (StatusCodes.Status404NotFound, Constants.ShareCodeNotFoundExceptionCode) } }; diff --git a/Services/CO.CDP.DataSharing.WebApi/Model/Exceptions.cs b/Services/CO.CDP.DataSharing.WebApi/Model/Exceptions.cs index 4fae1f223..459b25689 100644 --- a/Services/CO.CDP.DataSharing.WebApi/Model/Exceptions.cs +++ b/Services/CO.CDP.DataSharing.WebApi/Model/Exceptions.cs @@ -1,5 +1,6 @@ namespace CO.CDP.DataSharing.WebApi.Model; +public class UserUnauthorizedException : Exception; public class InvalidOrganisationRequestedException(string message, Exception? cause = null) : Exception(message, cause); public class ShareCodeNotFoundException(string message, Exception? cause = null) : Exception(message, cause); public class SupplierInformationNotFoundException(string message, Exception? cause = null) : Exception(message, cause); \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi/Model/SharedSupplierInformation.cs b/Services/CO.CDP.DataSharing.WebApi/Model/SharedSupplierInformation.cs index 4d99a3376..41daed729 100644 --- a/Services/CO.CDP.DataSharing.WebApi/Model/SharedSupplierInformation.cs +++ b/Services/CO.CDP.DataSharing.WebApi/Model/SharedSupplierInformation.cs @@ -2,6 +2,7 @@ namespace CO.CDP.DataSharing.WebApi.Model; public record SharedSupplierInformation { + public required Guid OrganisationId { get; init; } public required BasicInformation BasicInformation { get; init; } public required Task> ConnectedPersonInformation { get; init; } } \ No newline at end of file diff --git a/Services/CO.CDP.DataSharing.WebApi/UseCase/GenerateShareCodeUseCase.cs b/Services/CO.CDP.DataSharing.WebApi/UseCase/GenerateShareCodeUseCase.cs index 11cc3aa0c..9df6f4ddb 100644 --- a/Services/CO.CDP.DataSharing.WebApi/UseCase/GenerateShareCodeUseCase.cs +++ b/Services/CO.CDP.DataSharing.WebApi/UseCase/GenerateShareCodeUseCase.cs @@ -1,5 +1,4 @@ using AutoMapper; -using CO.CDP.Authentication; using CO.CDP.DataSharing.WebApi.Extensions; using CO.CDP.DataSharing.WebApi.Model; using CO.CDP.OrganisationInformation.Persistence; @@ -8,7 +7,6 @@ namespace CO.CDP.DataSharing.WebApi.UseCase; public class GenerateShareCodeUseCase( - IClaimService claimService, IOrganisationRepository organisationRepository, IFormRepository formRepository, IShareCodeRepository shareCodeRepository, @@ -18,7 +16,7 @@ public class GenerateShareCodeUseCase( public async Task Execute(ShareRequest shareRequest) { var org = await organisationRepository.Find(shareRequest.OrganisationId); - if (org == null || await claimService.HaveAccessToOrganisation(shareRequest.OrganisationId) == false) + if (org == null) { throw new InvalidOrganisationRequestedException("Invalid Organisation requested."); } diff --git a/Services/CO.CDP.DataSharing.WebApi/UseCase/GetSharedDataPdfUseCase.cs b/Services/CO.CDP.DataSharing.WebApi/UseCase/GetSharedDataPdfUseCase.cs index 8200ce372..029dea558 100644 --- a/Services/CO.CDP.DataSharing.WebApi/UseCase/GetSharedDataPdfUseCase.cs +++ b/Services/CO.CDP.DataSharing.WebApi/UseCase/GetSharedDataPdfUseCase.cs @@ -1,14 +1,27 @@ +using CO.CDP.Authentication; using CO.CDP.DataSharing.WebApi.DataService; +using CO.CDP.DataSharing.WebApi.Model; +using static CO.CDP.Authentication.Constants; namespace CO.CDP.DataSharing.WebApi.UseCase; -public class GetSharedDataPdfUseCase(IPdfGenerator pdfGenerator, IDataService dataService) +public class GetSharedDataPdfUseCase( + IPdfGenerator pdfGenerator, + IDataService dataService, + IClaimService claimService) : IUseCase { public async Task Execute(string sharecode) { var sharedSupplierInfo = await dataService.GetSharedSupplierInformationAsync(sharecode); + if (!await claimService.HaveAccessToOrganisation( + sharedSupplierInfo.OrganisationId, + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer])) + { + throw new UserUnauthorizedException(); + } + var pdfBytes = pdfGenerator.GenerateBasicInformationPdf(sharedSupplierInfo); return pdfBytes; diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/BuyerInformationEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/BuyerInformationEndpointsTests.cs index b9503c3ab..d3a2fe946 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/BuyerInformationEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/BuyerInformationEndpointsTests.cs @@ -6,7 +6,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -24,7 +23,8 @@ public class BuyerInformationEndpointsTests [InlineData(Forbidden, Channel.ServiceKey)] [InlineData(Forbidden, Channel.OrganisationKey)] [InlineData(Forbidden, "unknown_channel")] - public async Task UpdateBuyerInformation_Authorization_ReturnsExpectedStatusCode(HttpStatusCode expectedStatusCode, string channel, string? scope = null) + public async Task UpdateBuyerInformation_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) { var organisationId = Guid.NewGuid(); var updateBuyerInformation = new UpdateBuyerInformation { Type = BuyerInformationUpdateType.BuyerOrganisationType, BuyerInformation = new() }; @@ -33,9 +33,7 @@ public async Task UpdateBuyerInformation_Authorization_ReturnsExpectedStatusCode _updateBuyerInformationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updateBuyerInformationUseCase.Object)); var response = await factory.CreateClient().PatchAsJsonAsync($"/organisations/{organisationId}/buyer-information", updateBuyerInformation); diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/ConnectedEntityEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/ConnectedEntityEndpointsTests.cs index ee19a7868..0e972149e 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/ConnectedEntityEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/ConnectedEntityEndpointsTests.cs @@ -6,7 +6,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -36,9 +35,7 @@ public async Task GetConnectedEntities_Authorization_ReturnsExpectedStatusCode( _getConnectedEntitiesUseCase.Setup(uc => uc.Execute(organisationId)).ReturnsAsync([]); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getConnectedEntitiesUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}/connected-entities"); @@ -65,9 +62,7 @@ public async Task GetConnectedEntity_Authorization_ReturnsExpectedStatusCode( .ReturnsAsync(new ConnectedEntity { EntityType = ConnectedEntityType.Organisation }); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getConnectedEntityUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}/connected-entities/{connectedEntityId}"); @@ -93,9 +88,7 @@ public async Task CreateConnectedEntity_Authorization_ReturnsExpectedStatusCode( _registerConnectedEntityUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _registerConnectedEntityUseCase.Object)); var response = await factory.CreateClient().PostAsJsonAsync($"/organisations/{organisationId}/connected-entities", updateConnectedEntity); @@ -122,9 +115,7 @@ public async Task UpdateConnectedEntity_Authorization_ReturnsExpectedStatusCode( _updateConnectedEntityUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updateConnectedEntityUseCase.Object)); var response = await factory.CreateClient().PutAsJsonAsync($"/organisations/{organisationId}/connected-entities/{connectedEntityId}", updateConnectedEntity); @@ -151,9 +142,7 @@ public async Task DeleteConnectedEntity_Authorization_ReturnsExpectedStatusCode( _deleteConnectedEntityUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _deleteConnectedEntityUseCase.Object)); var response = await factory.CreateClient().SendAsync( diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationEndpointsTests.cs index d842e63a3..8efe9aeb7 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationEndpointsTests.cs @@ -9,7 +9,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -124,13 +123,13 @@ public async Task UpdateOrganisation_InvalidOrganisationId_ReturnsUnprocessableE [InlineData(Forbidden, Channel.ServiceKey)] [InlineData(Forbidden, Channel.OrganisationKey)] [InlineData(Forbidden, "unknown_channel")] - public async Task CreateOrganisation_Authorization_ReturnsExpectedStatusCode(HttpStatusCode expectedStatusCode, string channel) + public async Task CreateOrganisation_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) { var command = GivenRegisterOrganisationCommand(); SetupRegisterOrganisationUseCaseMock(command); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - serviceCollection: services => services.AddScoped(_ => _registerOrganisationUseCase.Object)); + channel, serviceCollection: s => s.AddScoped(_ => _registerOrganisationUseCase.Object)); var httpClient = factory.CreateClient(); var response = await httpClient.PostAsJsonAsync("/organisations", command); @@ -146,7 +145,8 @@ [new Claim(ClaimType.Channel, channel)], [InlineData(Forbidden, Channel.OrganisationKey)] [InlineData(Forbidden, "unknown_channel")] [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Responder)] - public async Task GetOrganisation_Authorization_ReturnsExpectedStatusCode(HttpStatusCode expectedStatusCode, string channel, string? scope = null) + public async Task GetOrganisation_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) { var organisationId = Guid.NewGuid(); @@ -154,9 +154,7 @@ public async Task GetOrganisation_Authorization_ReturnsExpectedStatusCode(HttpSt .ReturnsAsync(GivenOrganisation(organisationId)); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getOrganisationUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}"); @@ -172,7 +170,8 @@ [new Claim(ClaimType.Channel, channel)], [InlineData(Forbidden, Channel.ServiceKey)] [InlineData(Forbidden, Channel.OrganisationKey)] [InlineData(Forbidden, "unknown_channel")] - public async Task UpdateOrganisation_Authorization_ReturnsExpectedStatusCode(HttpStatusCode expectedStatusCode, string channel, string? scope = null) + public async Task UpdateOrganisation_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) { var organisationId = Guid.NewGuid(); var updateOrganisation = new UpdateOrganisation { Type = OrganisationUpdateType.AdditionalIdentifiers, Organisation = new() }; @@ -181,9 +180,7 @@ public async Task UpdateOrganisation_Authorization_ReturnsExpectedStatusCode(Htt _updatesOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updatesOrganisationUseCase.Object)); var response = await factory.CreateClient().PatchAsJsonAsync($"/organisations/{organisationId}", updateOrganisation); diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationLookupEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationLookupEndpointsTests.cs index 16694dcab..65a017559 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationLookupEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/OrganisationLookupEndpointsTests.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using System.Net; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -27,8 +26,7 @@ public async Task MyOrganisation_Authorization_ReturnsExpectedStatusCode(HttpSta .ReturnsAsync(OrganisationEndpointsTests.GivenOrganisation(Guid.NewGuid())); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - serviceCollection: services => services.AddScoped(_ => _getMyOrganisationUseCase.Object)); + channel, serviceCollection: s => s.AddScoped(_ => _getMyOrganisationUseCase.Object)); var httpClient = factory.CreateClient(); var response = await httpClient.GetAsync("/organisation/me"); @@ -49,8 +47,8 @@ public async Task LookupOrganisation_Authorization_ReturnsExpectedStatusCode(Htt .ReturnsAsync(OrganisationEndpointsTests.GivenOrganisation(Guid.NewGuid())); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - serviceCollection: services => services.AddScoped(_ => _lookupOrganisationUseCase.Object)); + channel, + serviceCollection: s => s.AddScoped(_ => _lookupOrganisationUseCase.Object)); var httpClient = factory.CreateClient(); var response = await httpClient.GetAsync($"/organisation/lookup?name={name}"); diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonInvitesEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonInvitesEndpointsTests.cs index 3039e04ba..175f41706 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonInvitesEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonInvitesEndpointsTests.cs @@ -7,7 +7,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -36,9 +35,7 @@ public async Task GetOrganisationPersonInvites_Authorization_ReturnsExpectedStat _getPersonInvitesUseCase.Setup(uc => uc.Execute(organisationId)).ReturnsAsync([]); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getPersonInvitesUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}/invites"); @@ -70,9 +67,7 @@ public async Task CreatePersonInvite_Authorization_ReturnsExpectedStatusCode( _invitePersonToOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(Mock.Of()); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _invitePersonToOrganisationUseCase.Object)); var response = await factory.CreateClient().PostAsJsonAsync($"/organisations/{organisationId}/invites", invitePersonToOrganisation); @@ -99,9 +94,7 @@ public async Task UpdatePersonInvite_Authorization_ReturnsExpectedStatusCode( _updateInvitedPersonToOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updateInvitedPersonToOrganisationUseCase.Object)); var response = await factory.CreateClient().PatchAsJsonAsync($"/organisations/{organisationId}/invites/{personInviteId}", updateInvitedPersonToOrganisation); @@ -127,9 +120,7 @@ public async Task RemovePersonInviteFromOrganisation_Authorization_ReturnsExpect _removePersonInviteFromOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _removePersonInviteFromOrganisationUseCase.Object)); var response = await factory.CreateClient().DeleteAsync($"/organisations/{organisationId}/invites/{personInviteId}"); diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonsEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonsEndpointsTests.cs index 7c1d55d93..77c697b57 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonsEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/PersonsEndpointsTests.cs @@ -6,7 +6,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; @@ -34,9 +33,7 @@ public async Task GetOrganisationPersons_Authorization_ReturnsExpectedStatusCode _getPersonsUseCase.Setup(uc => uc.Execute(organisationId)).ReturnsAsync([]); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getPersonsUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}/persons"); @@ -63,9 +60,7 @@ public async Task UpdateOrganisationPerson_Authorization_ReturnsExpectedStatusCo _updatePersonToOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updatePersonToOrganisationUseCase.Object)); var response = await factory.CreateClient().PatchAsJsonAsync($"/organisations/{organisationId}/persons/{persoinId}", updatePersonToOrganisation); @@ -91,9 +86,7 @@ public async Task RemovePersonFromOrganisation_Authorization_ReturnsExpectedStat _removePersonFromOrganisationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _removePersonFromOrganisationUseCase.Object)); var response = await factory.CreateClient().SendAsync( diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Api/SupplierInformationEndpointsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Api/SupplierInformationEndpointsTests.cs index 6ef6256b6..9284f0575 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Api/SupplierInformationEndpointsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Api/SupplierInformationEndpointsTests.cs @@ -7,7 +7,6 @@ using Moq; using System.Net; using System.Net.Http.Json; -using System.Security.Claims; using System.Text; using System.Text.Json; using static CO.CDP.Authentication.Constants; @@ -145,9 +144,7 @@ public async Task GetOrganisationSupplierInformation_Authorization_ReturnsExpect .ReturnsAsync(new SupplierInformation { OrganisationName = "FakeOrg" }); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _getSupplierInformationUseCase.Object)); var response = await factory.CreateClient().GetAsync($"/organisations/{organisationId}/supplier-information"); @@ -177,9 +174,7 @@ public async Task UpdateSupplierInformation_Authorization_ReturnsExpectedStatusC _updatesSupplierInformationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _updatesSupplierInformationUseCase.Object)); var response = await factory.CreateClient().PatchAsJsonAsync($"/organisations/{organisationId}/supplier-information", updateSupplierInformation); @@ -205,9 +200,7 @@ public async Task DeleteSupplierInformation_Authorization_ReturnsExpectedStatusC _deleteSupplierInformationUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); var factory = new TestAuthorizationWebApplicationFactory( - [new Claim(ClaimType.Channel, channel)], - organisationId, - scope, + channel, organisationId, scope, services => services.AddScoped(_ => _deleteSupplierInformationUseCase.Object)); var response = await factory.CreateClient().SendAsync( diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseOrganisationRepository.cs b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseOrganisationRepository.cs index 384a500c5..feaa92b14 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseOrganisationRepository.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseOrganisationRepository.cs @@ -31,6 +31,13 @@ public void Dispose() return await context.Set().FirstOrDefaultAsync(o => o.Organisation.Guid == organisationId && o.Person.Guid == personId); } + public async Task FindOrganisationPerson(Guid organisationId, string userUrn) + { + return await context.Set() + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Organisation.Guid == organisationId && o.Person.UserUrn == userUrn); + } + public async Task> FindByUserUrn(string userUrn) { var person = await context.Persons @@ -40,6 +47,7 @@ public async Task> FindByUserUrn(string userUrn) .FirstOrDefaultAsync(p => p.UserUrn == userUrn); return person?.Organisations ?? []; } + public async Task FindByIdentifier(string scheme, string identifierId) { return await context.Organisations diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/IOrganisationRepository.cs b/Services/CO.CDP.OrganisationInformation.Persistence/IOrganisationRepository.cs index ea65af4a0..c4dd354a6 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/IOrganisationRepository.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/IOrganisationRepository.cs @@ -10,6 +10,8 @@ public interface IOrganisationRepository : IDisposable public Task FindOrganisationPerson(Guid organisationId, Guid personId); + public Task FindOrganisationPerson(Guid organisationId, string userUrn); + public Task FindByName(string name); public Task> FindByUserUrn(string userUrn); diff --git a/Services/CO.CDP.Tenant.WebApi/Program.cs b/Services/CO.CDP.Tenant.WebApi/Program.cs index fc9616bc7..3c6f70c92 100644 --- a/Services/CO.CDP.Tenant.WebApi/Program.cs +++ b/Services/CO.CDP.Tenant.WebApi/Program.cs @@ -38,6 +38,7 @@ builder.Services.AddTenantProblemDetails(); builder.Services.AddJwtBearerAndApiKeyAuthentication(builder.Configuration, builder.Environment); +builder.Services.AddOrganisationAuthorization(); if (Assembly.GetEntryAssembly().IsRunAs("CO.CDP.Tenant.WebApi")) { @@ -50,12 +51,6 @@ builder.Services.AddHealthChecks() .AddNpgSql(ConnectionStringHelper.GetConnectionString(builder.Configuration, "OrganisationInformationDatabase")); - - builder.Services.AddOrganisationAuthorization(); -} -else -{ - builder.Services.AddAuthorization(); } var app = builder.Build(); diff --git a/TestKit/CO.CDP.TestKit.Mvc/TestAuthorizationWebApplicationFactory.cs b/TestKit/CO.CDP.TestKit.Mvc/TestAuthorizationWebApplicationFactory.cs index 622738c80..71d10c2ca 100644 --- a/TestKit/CO.CDP.TestKit.Mvc/TestAuthorizationWebApplicationFactory.cs +++ b/TestKit/CO.CDP.TestKit.Mvc/TestAuthorizationWebApplicationFactory.cs @@ -12,7 +12,7 @@ namespace CO.CDP.TestKit.Mvc; public class TestAuthorizationWebApplicationFactory( - Claim[] claimFeed, + string channel, Guid? organisationId = null, string? assignedOrganisationScopes = null, Action? serviceCollection = null) @@ -25,20 +25,20 @@ protected override IHost CreateHost(IHostBuilder builder) builder.ConfigureServices(services => { services.AddTransient(sp => new AuthorizationPolicyEvaluator( - ActivatorUtilities.CreateInstance(sp), claimFeed, assignedOrganisationScopes)); + ActivatorUtilities.CreateInstance(sp), channel, assignedOrganisationScopes)); - if (assignedOrganisationScopes != null) + if (assignedOrganisationScopes != null && organisationId != null) { - Mock mockDatabaseTenantRepo = new(); - mockDatabaseTenantRepo.Setup(r => r.LookupTenant("urn:fake_user")) - .ReturnsAsync(new TenantLookup + Mock mockDatabaseOrgRepo = new(); + mockDatabaseOrgRepo.Setup(r => r.FindOrganisationPerson(organisationId.Value, "urn:fake_user")) + .ReturnsAsync(new OrganisationPerson { - User = new TenantLookup.PersonUser { Name = "Test", Email = "test@test", Urn = "urn:fake_user" }, - Tenants = [new TenantLookup.Tenant { Id = Guid.NewGuid(), Name = "Ten", - Organisations = [new TenantLookup.Organisation { Id = organisationId ?? Guid.NewGuid(), Name = "org", Roles = [], Scopes = [assignedOrganisationScopes] }] }] + Person = Mock.Of(), + Organisation = Mock.Of(), + Scopes = [assignedOrganisationScopes] }); - services.AddTransient(sc => mockDatabaseTenantRepo.Object); + services.AddTransient(sc => mockDatabaseOrgRepo.Object); } }); @@ -46,14 +46,14 @@ protected override IHost CreateHost(IHostBuilder builder) } } -public class AuthorizationPolicyEvaluator(PolicyEvaluator innerEvaluator, Claim[] claimFeed, string? assignedOrganisationScopes) : IPolicyEvaluator +public class AuthorizationPolicyEvaluator(PolicyEvaluator innerEvaluator, string? channel, string? assignedOrganisationScopes) : IPolicyEvaluator { const string JwtBearerOrApiKeyScheme = "JwtBearer_Or_ApiKey"; public async Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) { var claimsIdentity = new ClaimsIdentity(JwtBearerOrApiKeyScheme); - if (claimFeed.Length > 0) claimsIdentity.AddClaims(claimFeed); + if (!string.IsNullOrWhiteSpace(channel)) claimsIdentity.AddClaims([new Claim("channel", channel)]); if (assignedOrganisationScopes != null) claimsIdentity.AddClaim(new Claim("sub", "urn:fake_user")); return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), From d2a8968d5759b639a3bb6871b39299569366aa12 Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Thu, 19 Sep 2024 16:56:07 +0100 Subject: [PATCH 08/30] DP-613: Add "Name" field to the FormQuestion entity --- .../EntityFactory.cs | 6 + .../GetFormSectionQuestionsUseCaseTest.cs | 2 + .../DatabaseFormRepositoryTest.cs | 4 + .../DatabaseShareCodeRepositoryTest.cs | 1 + .../Factories/SharedConsentFactory.cs | 1 + .../Forms/FormQuestion.cs | 1 + ...19151457_AddNameToFormQuestion.Designer.cs | 1814 +++++++++++++++++ .../20240919151457_AddNameToFormQuestion.cs | 65 + ...nisationInformationContextModelSnapshot.cs | 5 + 9 files changed, 1899 insertions(+) create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.Designer.cs create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs index 8a9d371b6..d177ccfb0 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs @@ -271,6 +271,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section01", Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = PersistenceForms.FormQuestionType.NoInput, @@ -287,6 +288,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section02", Title = "Were your accounts audited?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.YesOrNo, @@ -303,6 +305,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section02", Title = "Upload your accounts", Description = "Upload your most recent 2 financial years. If you do not have 2, upload your most recent financial year.", Type = PersistenceForms.FormQuestionType.FileUpload, @@ -319,6 +322,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 3, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section03", Title = "What is the financial year end date for the information you uploaded?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Date, @@ -335,6 +339,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 4, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section04", Title = "Check your answers", Type = PersistenceForms.FormQuestionType.CheckYourAnswers, Description = String.Empty, @@ -351,6 +356,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 5, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section05", Title = "Enter your postal address", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Address, diff --git a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs index 30bfece7d..334a3c83e 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs @@ -67,6 +67,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section01", Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.NoInput, @@ -81,6 +82,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", + Name = "_Section01", Title = "Were your accounts audited?.", Description = "", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.YesOrNo, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs index 928d17261..c3e510bce 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs @@ -104,6 +104,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, + Name = "_Section01", Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -118,6 +119,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, + Name = "_Section02", Title = "Question 2", Caption = "Question Caption", Description = "Question 2 desc", @@ -385,6 +387,7 @@ public async Task GetFormAnswerSetAsync_WhenFormAnswerSetsExists_ReturnsFormAnsw { Guid = questionId, Section = section, + Name = "_Section01", Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -491,6 +494,7 @@ public async Task GetQuestionsAsync_WhenOptionsAreSimple_ReturnsCorrectOptions() { Guid = questionId, Section = section, + Name = "_Section01", Title = "Question with Simple Options", Caption = "Question Caption", Description = "This is a test question with simple options.", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs index a334dd678..579a4528d 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs @@ -267,6 +267,7 @@ private static FormQuestion GivenYesOrNoQuestion(FormSection section) Section = section, Type = FormQuestionType.YesOrNo, IsRequired = true, + Name = "_Section01", Title = "Yes or no?", Description = "Please answer.", NextQuestion = null, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs index 12ac9f461..d7a08ba7e 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs @@ -87,6 +87,7 @@ public static FormQuestion GivenFormQuestion( var question = new FormQuestion { Guid = questionId ?? Guid.NewGuid(), + Name = "_Section01", Title = "Were your accounts audited?", Caption = "", Description = "Please answer.", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs index ce41b6b7b..785f25431 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs @@ -11,6 +11,7 @@ public class FormQuestion : IEntityDate public required FormSection Section { get; set; } public required FormQuestionType Type { get; set; } public required bool IsRequired { get; set; } = true; + public required string Name { get; set; } public required string Title { get; set; } public required string? Description { get; set; } = null; public required string? Caption { get; set; } = null; diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.Designer.cs new file mode 100644 index 000000000..3c8fa9cb6 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.Designer.cs @@ -0,0 +1,1814 @@ +// +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("20240919151457_AddNameToFormQuestion")] + partial class AddNameToFormQuestion + { + /// + 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("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("Key") + .IsUnique() + .HasDatabaseName("ix_authentication_keys_key"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_authentication_keys_organisation_id"); + + 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("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("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("Configuration") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("configuration"); + + 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("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("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("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("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("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.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("CompletedQualification") + .HasColumnType("boolean") + .HasColumnName("completed_qualification"); + + b1.Property("CompletedRegAddress") + .HasColumnType("boolean") + .HasColumnName("completed_reg_address"); + + b1.Property("CompletedTradeAssurance") + .HasColumnType("boolean") + .HasColumnName("completed_trade_assurance"); + + 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.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Qualification", "Qualifications", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_qualifications"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_qualifications_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_qualifications_supplier_information_organisation_id"); + + b2.ToTable("qualifications", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_qualifications_supplier_information_supplier_information_or"); + }); + + b1.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+TradeAssurance", "TradeAssurances", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reference_number"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_trade_assurances"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_trade_assurances_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_trade_assurances_supplier_information_organisation_id"); + + b2.ToTable("trade_assurances", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_trade_assurances_supplier_information_supplier_information_"); + }); + + b1.Navigation("LegalForm"); + + b1.Navigation("Qualifications"); + + b1.Navigation("TradeAssurances"); + }); + + b.Navigation("Addresses"); + + b.Navigation("BuyerInfo"); + + b.Navigation("ContactPoints"); + + b.Navigation("Identifiers"); + + 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/20240919151457_AddNameToFormQuestion.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs new file mode 100644 index 000000000..1710255df --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + /// + public partial class AddNameToFormQuestion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "name", + table: "form_questions", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_FinancialInformation01' WHERE title = 'What is the financial year end date for the information you uploaded?'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_FinancialInformation02' WHERE title = 'Upload your accounts'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_FinancialInformation03' WHERE title = 'Were your accounts audited?'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_FinancialInformation04' WHERE title = 'The financial information you will need.'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation01' WHERE title = 'Enter your postal address'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation02' WHERE title = 'Enter your email address'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation03' WHERE title = 'Enter your job title'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation04' WHERE title = 'Enter your name'" + ); + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation05' WHERE title = 'Declaration'" + ); + + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_FinancialInformation05' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Financial Information') AND title = 'Check your answers'" + ); + + migrationBuilder.Sql( + "UPDATE form_questions SET name = '_ShareMyInformation06' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Share my information') AND title = 'Check your answers'" + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "name", + table: "form_questions"); + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs index e0a382649..4e93cc567 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs @@ -431,6 +431,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("is_required"); + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + b.Property("NextQuestionAlternativeId") .HasColumnType("integer") .HasColumnName("next_question_alternative_id"); From c82fc53e14d086a90092d58b686901844fa0368d Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Fri, 20 Sep 2024 13:37:33 +0100 Subject: [PATCH 09/30] DP-613: Update migration to add index on name --- .../Forms/FormQuestion.cs | 2 + .../20240919151457_AddNameToFormQuestion.cs | 45 +++++++++---------- ...nisationInformationContextModelSnapshot.cs | 4 ++ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs index 785f25431..7c3f3a4c4 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormQuestion.cs @@ -1,7 +1,9 @@ using CO.CDP.EntityFrameworkCore.Timestamps; +using Microsoft.EntityFrameworkCore; namespace CO.CDP.OrganisationInformation.Persistence.Forms; +[Index(nameof(Name), IsUnique = true)] public class FormQuestion : IEntityDate { public int Id { get; set; } diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs index 1710255df..6aa46c4cf 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919151457_AddNameToFormQuestion.cs @@ -18,45 +18,42 @@ protected override void Up(MigrationBuilder migrationBuilder) defaultValue: ""); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_FinancialInformation01' WHERE title = 'What is the financial year end date for the information you uploaded?'" - ); + "UPDATE form_questions SET name = '_FinancialInformation01' WHERE title = 'What is the financial year end date for the information you uploaded?'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_FinancialInformation02' WHERE title = 'Upload your accounts'" - ); + "UPDATE form_questions SET name = '_FinancialInformation02' WHERE title = 'Upload your accounts'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_FinancialInformation03' WHERE title = 'Were your accounts audited?'" - ); + "UPDATE form_questions SET name = '_FinancialInformation03' WHERE title = 'Were your accounts audited?'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_FinancialInformation04' WHERE title = 'The financial information you will need.'" - ); + "UPDATE form_questions SET name = '_FinancialInformation04' WHERE title = 'The financial information you will need.'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation01' WHERE title = 'Enter your postal address'" - ); + "UPDATE form_questions SET name = '_ShareMyInformation01' WHERE title = 'Enter your postal address'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation02' WHERE title = 'Enter your email address'" - ); + "UPDATE form_questions SET name = '_ShareMyInformation02' WHERE title = 'Enter your email address'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation03' WHERE title = 'Enter your job title'" - ); + "UPDATE form_questions SET name = '_ShareMyInformation03' WHERE title = 'Enter your job title'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation04' WHERE title = 'Enter your name'" - ); + "UPDATE form_questions SET name = '_ShareMyInformation04' WHERE title = 'Enter your name'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation05' WHERE title = 'Declaration'" - ); - + "UPDATE form_questions SET name = '_ShareMyInformation05' WHERE title = 'Declaration'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_FinancialInformation05' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Financial Information') AND title = 'Check your answers'" - ); - + "UPDATE form_questions SET name = '_FinancialInformation05' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Financial Information') AND title = 'Check your answers'"); migrationBuilder.Sql( - "UPDATE form_questions SET name = '_ShareMyInformation06' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Share my information') AND title = 'Check your answers'" - ); + "UPDATE form_questions SET name = '_ShareMyInformation06' WHERE section_id = (SELECT id FROM form_sections WHERE title = 'Share my information') AND title = 'Check your answers'"); + + migrationBuilder.CreateIndex( + name: "ix_form_questions_name", + table: "form_questions", + column: "name", + unique: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropIndex( + name: "ix_form_questions_name", + table: "form_questions"); + migrationBuilder.DropColumn( name: "name", table: "form_questions"); diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs index 4e93cc567..81c1168d9 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs @@ -475,6 +475,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); From af67f0f8ba3bd73aa5e8b40c9dfee74614151193 Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Mon, 23 Sep 2024 10:17:07 +0100 Subject: [PATCH 10/30] DP-613: Make Name field unique on FormQuestion --- .../DatabaseFormRepositoryTest.cs | 11 +++++++---- .../DatabaseShareCodeRepositoryTest.cs | 3 ++- .../Factories/SharedConsentFactory.cs | 4 +++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs index c3e510bce..831a45657 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs @@ -8,6 +8,9 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseFormRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { + + private static int NextQuestionNumber = 1; + [Fact] public async Task GetFormSummaryAsync_WhenFormDoesNotExists_ReturnsEmptyCollection() { @@ -104,7 +107,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section01", + Name = "_Section0" + (NextQuestionNumber++), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -119,7 +122,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section02", + Name = "_Section0" + (NextQuestionNumber++), Title = "Question 2", Caption = "Question Caption", Description = "Question 2 desc", @@ -387,7 +390,7 @@ public async Task GetFormAnswerSetAsync_WhenFormAnswerSetsExists_ReturnsFormAnsw { Guid = questionId, Section = section, - Name = "_Section01", + Name = "_Section0" + (NextQuestionNumber++), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -494,7 +497,7 @@ public async Task GetQuestionsAsync_WhenOptionsAreSimple_ReturnsCorrectOptions() { Guid = questionId, Section = section, - Name = "_Section01", + Name = "_Section0" + (NextQuestionNumber++), Title = "Question with Simple Options", Caption = "Question Caption", Description = "This is a test question with simple options.", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs index 579a4528d..a0594492d 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs @@ -6,6 +6,7 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseShareCodeRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { + private static int NextQuestionNumber = 1; [Fact] public async Task GetSharedConsentDraftAsync_WhenSharedConsentDoesNotExist_ReturnsNull() @@ -267,7 +268,7 @@ private static FormQuestion GivenYesOrNoQuestion(FormSection section) Section = section, Type = FormQuestionType.YesOrNo, IsRequired = true, - Name = "_Section01", + Name = "_Section0" + (NextQuestionNumber++), Title = "Yes or no?", Description = "Please answer.", NextQuestion = null, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs index d7a08ba7e..9ae8c736a 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs @@ -7,6 +7,8 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests.Factories; public static class SharedConsentFactory { + private static int NextQuestionNumber = 1; + public static SharedConsent GivenSharedConsent( Organisation? organisation = null, Form? form = null, @@ -87,7 +89,7 @@ public static FormQuestion GivenFormQuestion( var question = new FormQuestion { Guid = questionId ?? Guid.NewGuid(), - Name = "_Section01", + Name = "_Section0" + (NextQuestionNumber++), Title = "Were your accounts audited?", Caption = "", Description = "Please answer.", From 23c2131536244f231d5e653933f8b8559a209db5 Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Mon, 23 Sep 2024 11:06:24 +0100 Subject: [PATCH 11/30] DP-613: Make new question number counter thread safe --- .../DatabaseFormRepositoryTest.cs | 16 +++++++++++----- .../DatabaseShareCodeRepositoryTest.cs | 11 +++++++++-- .../Factories/SharedConsentFactory.cs | 11 +++++++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs index 831a45657..b21893b9e 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs @@ -8,8 +8,14 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseFormRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { + private static int nextQuestionNumber = 0; - private static int NextQuestionNumber = 1; + private static int getQuestionNumber() + { + Interlocked.Increment(ref nextQuestionNumber); + + return nextQuestionNumber; + } [Fact] public async Task GetFormSummaryAsync_WhenFormDoesNotExists_ReturnsEmptyCollection() @@ -107,7 +113,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -122,7 +128,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Question 2", Caption = "Question Caption", Description = "Question 2 desc", @@ -390,7 +396,7 @@ public async Task GetFormAnswerSetAsync_WhenFormAnswerSetsExists_ReturnsFormAnsw { Guid = questionId, Section = section, - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -497,7 +503,7 @@ public async Task GetQuestionsAsync_WhenOptionsAreSimple_ReturnsCorrectOptions() { Guid = questionId, Section = section, - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Question with Simple Options", Caption = "Question Caption", Description = "This is a test question with simple options.", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs index a0594492d..b23207556 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs @@ -6,7 +6,14 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseShareCodeRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { - private static int NextQuestionNumber = 1; + private static int nextQuestionNumber = 0; + + private static int getQuestionNumber() + { + Interlocked.Increment(ref nextQuestionNumber); + + return nextQuestionNumber; + } [Fact] public async Task GetSharedConsentDraftAsync_WhenSharedConsentDoesNotExist_ReturnsNull() @@ -268,7 +275,7 @@ private static FormQuestion GivenYesOrNoQuestion(FormSection section) Section = section, Type = FormQuestionType.YesOrNo, IsRequired = true, - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Yes or no?", Description = "Please answer.", NextQuestion = null, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs index 9ae8c736a..0f5b08873 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs @@ -7,7 +7,14 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests.Factories; public static class SharedConsentFactory { - private static int NextQuestionNumber = 1; + private static int nextQuestionNumber = 0; + + private static int getQuestionNumber() + { + Interlocked.Increment(ref nextQuestionNumber); + + return nextQuestionNumber; + } public static SharedConsent GivenSharedConsent( Organisation? organisation = null, @@ -89,7 +96,7 @@ public static FormQuestion GivenFormQuestion( var question = new FormQuestion { Guid = questionId ?? Guid.NewGuid(), - Name = "_Section0" + (NextQuestionNumber++), + Name = "_Section0" + getQuestionNumber(), Title = "Were your accounts audited?", Caption = "", Description = "Please answer.", From 6483e596222a19d85d2b86fadee3439ef6a46fd3 Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Mon, 23 Sep 2024 11:22:01 +0100 Subject: [PATCH 12/30] DP-613: Add thread safe incremental section number --- .../EntityFactory.cs | 21 +++++++++++++------ .../GetFormSectionQuestionsUseCaseTest.cs | 13 ++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs index d177ccfb0..435f6c9f4 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs @@ -11,6 +11,15 @@ namespace CO.CDP.DataSharing.WebApi.Tests; internal static class EntityFactory { + private static int nextQuestionNumber = 0; + + private static int getQuestionNumber() + { + Interlocked.Increment(ref nextQuestionNumber); + + return nextQuestionNumber; + } + internal static ShareRequest GetShareRequest(Guid organisationGuid, Guid formId) { return new ShareRequest @@ -271,7 +280,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section01", + Name = "_Section0" + getQuestionNumber(), Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = PersistenceForms.FormQuestionType.NoInput, @@ -288,7 +297,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section02", + Name = "_Section0" + getQuestionNumber(), Title = "Were your accounts audited?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.YesOrNo, @@ -305,7 +314,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section02", + Name = "_Section0" + getQuestionNumber(), Title = "Upload your accounts", Description = "Upload your most recent 2 financial years. If you do not have 2, upload your most recent financial year.", Type = PersistenceForms.FormQuestionType.FileUpload, @@ -322,7 +331,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 3, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section03", + Name = "_Section0" + getQuestionNumber(), Title = "What is the financial year end date for the information you uploaded?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Date, @@ -339,7 +348,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 4, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section04", + Name = "_Section0" + getQuestionNumber(), Title = "Check your answers", Type = PersistenceForms.FormQuestionType.CheckYourAnswers, Description = String.Empty, @@ -356,7 +365,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 5, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section05", + Name = "_Section0" + getQuestionNumber(), Title = "Enter your postal address", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Address, diff --git a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs index 334a3c83e..a3d3b0843 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs @@ -12,6 +12,15 @@ public class GetFormSectionQuestionsUseCaseTest(AutoMapperFixture mapperFixture) private readonly Mock _repository = new(); private GetFormSectionQuestionsUseCase UseCase => new(_repository.Object, mapperFixture.Mapper); + private static int nextQuestionNumber = 0; + + private static int getQuestionNumber() + { + Interlocked.Increment(ref nextQuestionNumber); + + return nextQuestionNumber; + } + [Fact] public async Task ItReturnsNullIfNoSectionIsFound() { @@ -67,7 +76,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section01", + Name = "_Section0" + getQuestionNumber(), Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.NoInput, @@ -82,7 +91,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section01", + Name = "_Section0" + getQuestionNumber(), Title = "Were your accounts audited?.", Description = "", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.YesOrNo, From 305ad1a8c9de336896db4bd6951647efc9a074fa Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Mon, 23 Sep 2024 11:38:00 +0100 Subject: [PATCH 13/30] Follow the coding standard convention --- .../DatabaseFormRepositoryTest.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs index b21893b9e..417224ee0 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseFormRepositoryTest.cs @@ -8,13 +8,13 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseFormRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { - private static int nextQuestionNumber = 0; + private static int _nextQuestionNumber = 100; - private static int getQuestionNumber() + private static int GetQuestionNumber() { - Interlocked.Increment(ref nextQuestionNumber); + Interlocked.Increment(ref _nextQuestionNumber); - return nextQuestionNumber; + return _nextQuestionNumber; } [Fact] @@ -113,7 +113,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -128,7 +128,7 @@ public async Task GetQuestionsAsync_WhenSectionExists_ReturnsQuestions() { Guid = Guid.NewGuid(), Section = section, - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Question 2", Caption = "Question Caption", Description = "Question 2 desc", @@ -381,7 +381,7 @@ public async Task GetFormAnswerSetAsync_WhenFormAnswerSetDoesNotExist_ReturnsNul [Fact] public async Task GetFormAnswerSetAsync_WhenFormAnswerSetsExists_ReturnsFormAnswerSet() { - using var context = postgreSql.OrganisationInformationContext(); + await using var context = postgreSql.OrganisationInformationContext(); var repository = FormRepository(context); var formId = Guid.NewGuid(); @@ -396,7 +396,7 @@ public async Task GetFormAnswerSetAsync_WhenFormAnswerSetsExists_ReturnsFormAnsw { Guid = questionId, Section = section, - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Question 1", Caption = "Question Caption", Description = "Question 1 desc", @@ -503,7 +503,7 @@ public async Task GetQuestionsAsync_WhenOptionsAreSimple_ReturnsCorrectOptions() { Guid = questionId, Section = section, - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Question with Simple Options", Caption = "Question Caption", Description = "This is a test question with simple options.", From 2e5d2bfb1c2bf1c866935e24cc8a41532695a981 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Mon, 23 Sep 2024 11:39:03 +0100 Subject: [PATCH 14/30] Follow the coding standard convention --- .../EntityFactory.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs index 435f6c9f4..f78cef183 100644 --- a/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs +++ b/Services/CO.CDP.DataSharing.WebApi.Tests/EntityFactory.cs @@ -11,13 +11,13 @@ namespace CO.CDP.DataSharing.WebApi.Tests; internal static class EntityFactory { - private static int nextQuestionNumber = 0; + private static int _nextQuestionNumber; - private static int getQuestionNumber() + private static int GetQuestionNumber() { - Interlocked.Increment(ref nextQuestionNumber); + Interlocked.Increment(ref _nextQuestionNumber); - return nextQuestionNumber; + return _nextQuestionNumber; } internal static ShareRequest GetShareRequest(Guid organisationGuid, Guid formId) @@ -280,7 +280,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = PersistenceForms.FormQuestionType.NoInput, @@ -297,7 +297,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Were your accounts audited?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.YesOrNo, @@ -314,7 +314,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Upload your accounts", Description = "Upload your most recent 2 financial years. If you do not have 2, upload your most recent financial year.", Type = PersistenceForms.FormQuestionType.FileUpload, @@ -331,7 +331,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 3, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "What is the financial year end date for the information you uploaded?", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Date, @@ -348,7 +348,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 4, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Check your answers", Type = PersistenceForms.FormQuestionType.CheckYourAnswers, Description = String.Empty, @@ -365,7 +365,7 @@ private static PersistenceForms.FormSection GivenSection(Guid sectionId, Persist Id = 5, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Enter your postal address", Description = String.Empty, Type = PersistenceForms.FormQuestionType.Address, From 6aaf5b39ad30882e0d2e60989aa96acccd14ec81 Mon Sep 17 00:00:00 2001 From: Marek Matulka Date: Mon, 23 Sep 2024 11:54:50 +0100 Subject: [PATCH 15/30] DP-613: Clean up to match the selected coding convention --- .../UseCase/GetFormSectionQuestionsUseCaseTest.cs | 12 ++++++------ .../DatabaseShareCodeRepositoryTest.cs | 10 +++++----- .../Factories/SharedConsentFactory.cs | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs index a3d3b0843..7fcf7a5a9 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionQuestionsUseCaseTest.cs @@ -12,13 +12,13 @@ public class GetFormSectionQuestionsUseCaseTest(AutoMapperFixture mapperFixture) private readonly Mock _repository = new(); private GetFormSectionQuestionsUseCase UseCase => new(_repository.Object, mapperFixture.Mapper); - private static int nextQuestionNumber = 0; + private static int _nextQuestionNumber; - private static int getQuestionNumber() + private static int GetQuestionNumber() { - Interlocked.Increment(ref nextQuestionNumber); + Interlocked.Increment(ref _nextQuestionNumber); - return nextQuestionNumber; + return _nextQuestionNumber; } [Fact] @@ -76,7 +76,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 1, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "The financial information you will need.", Description = "You will need to upload accounts or statements for your 2 most recent financial years. If you do not have 2 years, you can upload your most recent financial year. You will need to enter the financial year end date for the information you upload.", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.NoInput, @@ -91,7 +91,7 @@ public async Task ItReturnsTheSectionWithQuestions() Id = 2, Guid = Guid.NewGuid(), Caption = "Page caption", - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Were your accounts audited?.", Description = "", Type = CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestionType.YesOrNo, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs index b23207556..d0e5ac45f 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseShareCodeRepositoryTest.cs @@ -6,13 +6,13 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests; public class DatabaseShareCodeRepositoryTest(PostgreSqlFixture postgreSql) : IClassFixture { - private static int nextQuestionNumber = 0; + private static int _nextQuestionNumber; - private static int getQuestionNumber() + private static int GetQuestionNumber() { - Interlocked.Increment(ref nextQuestionNumber); + Interlocked.Increment(ref _nextQuestionNumber); - return nextQuestionNumber; + return _nextQuestionNumber; } [Fact] @@ -275,7 +275,7 @@ private static FormQuestion GivenYesOrNoQuestion(FormSection section) Section = section, Type = FormQuestionType.YesOrNo, IsRequired = true, - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Yes or no?", Description = "Please answer.", NextQuestion = null, diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs index 0f5b08873..e0b8c4534 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/Factories/SharedConsentFactory.cs @@ -7,13 +7,13 @@ namespace CO.CDP.OrganisationInformation.Persistence.Tests.Factories; public static class SharedConsentFactory { - private static int nextQuestionNumber = 0; + private static int _nextQuestionNumber; - private static int getQuestionNumber() + private static int GetQuestionNumber() { - Interlocked.Increment(ref nextQuestionNumber); + Interlocked.Increment(ref _nextQuestionNumber); - return nextQuestionNumber; + return _nextQuestionNumber; } public static SharedConsent GivenSharedConsent( @@ -96,7 +96,7 @@ public static FormQuestion GivenFormQuestion( var question = new FormQuestion { Guid = questionId ?? Guid.NewGuid(), - Name = "_Section0" + getQuestionNumber(), + Name = "_Section0" + GetQuestionNumber(), Title = "Were your accounts audited?", Caption = "", Description = "Please answer.", From 8708d0791503eba5408af0148e9b5b6a808017d4 Mon Sep 17 00:00:00 2001 From: dpatel017 Date: Mon, 23 Sep 2024 13:30:32 +0100 Subject: [PATCH 16/30] Api key management (#615) * start and create api keys -screen * Added revoked column * Added migration for new column * Added repository method to get list of Api keys * Added model * Added use case to get Api keys based on Organisation id * Added use case to create new api key * Added test to register authentication key * Added Register Authentication key endpoint in api * updated return type of endpoints * Added Patch endpoint to revoke authentication key Added repo method Added test for repo & usecase * Added revoke unctionality Api key listing unit test * APIKey Name already exist check while saving * Added authorize policy for editor role * PR changes for api endpoint * Added new column and made revoved non-nullable * Fix migration * Added auth key revoke consideration * used RevokedOn instead of UpdatedOn --------- Co-authored-by: Dharm --- .../ApiKeyManagement/CreateApiKeyTest.cs | 123 ++ .../ApiKeyManagement/ManageApiKeyTest.cs | 63 + .../ApiKeyManagement/NewApiKeyDetailsTest.cs | 24 + .../ApiKeyManagement/RevokeApiKeyTest.cs | 67 + .../Constants/ErrorCodes.cs | 1 + .../Constants/ErrorMessagesList.cs | 2 + .../ApiKeyManagement/CreateApiKey.cshtml | 46 + .../ApiKeyManagement/CreateApiKey.cshtml.cs | 70 + .../ApiKeyManagement/ManageApiKey.cshtml | 70 + .../ApiKeyManagement/ManageApiKey.cshtml.cs | 36 + .../ApiKeyManagement/NewApiKeyDetails.cshtml | 48 + .../NewApiKeyDetails.cshtml.cs | 22 + .../ApiKeyManagement/RevokeApiKey.cshtml | 29 + .../ApiKeyManagement/RevokeApiKey.cshtml.cs | 34 + .../Organisation/OrganisationOverview.cshtml | 17 +- .../OrganisationClientExtensions.cs | 12 + .../WebApiClients/OrganisationExtensions.cs | 5 + .../ServiceCollectionExtensionsTests.cs | 12 + .../GetAuthenticationKeyUseCaseTest.cs | 62 + .../RegisterAuthenticationKeyUseCaseTest.cs | 119 ++ .../RevokeAuthenticationKeyUseCaseTest.cs | 123 ++ .../Api/Organisation.cs | 79 + .../AutoMapper/WebApiToPersistenceProfile.cs | 2 + .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Model/Command.cs | 20 +- .../Model/Exceptions.cs | 5 +- .../CO.CDP.Organisation.WebApi/Program.cs | 9 + .../UseCase/GetAuthenticationKeyUseCase.cs | 14 + .../RegisterAuthenticationKeyUseCase.cs | 26 + .../UseCase/RevokeAuthenticationKeyUseCase.cs | 32 + ...DatabaseAuthenticationKeyRepositoryTest.cs | 43 + .../AuthenticationKey.cs | 3 +- .../DatabaseAuthenticationKeyRepository.cs | 40 +- .../IAuthenticationKeyRepository.cs | 9 +- ...3101141_AlterAuthenticationKey.Designer.cs | 1841 +++++++++++++++++ .../20240923101141_AlterAuthenticationKey.cs | 61 + ...nisationInformationContextModelSnapshot.cs | 18 +- .../OrganisationInformationContext.cs | 1 + 38 files changed, 3175 insertions(+), 15 deletions(-) create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/CreateApiKeyTest.cs create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/ManageApiKeyTest.cs create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/NewApiKeyDetailsTest.cs create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/RevokeApiKeyTest.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml create mode 100644 Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml.cs create mode 100644 Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetAuthenticationKeyUseCaseTest.cs create mode 100644 Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RegisterAuthenticationKeyUseCaseTest.cs create mode 100644 Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RevokeAuthenticationKeyUseCaseTest.cs create mode 100644 Services/CO.CDP.Organisation.WebApi/UseCase/GetAuthenticationKeyUseCase.cs create mode 100644 Services/CO.CDP.Organisation.WebApi/UseCase/RegisterAuthenticationKeyUseCase.cs create mode 100644 Services/CO.CDP.Organisation.WebApi/UseCase/RevokeAuthenticationKeyUseCase.cs create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.Designer.cs create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.cs diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/CreateApiKeyTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/CreateApiKeyTest.cs new file mode 100644 index 000000000..fbd69b0fd --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/CreateApiKeyTest.cs @@ -0,0 +1,123 @@ +using Amazon.S3; +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; +using CO.CDP.OrganisationApp.Pages.ApiKeyManagement; +using FluentAssertions; +using FluentAssertions.Equivalency; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Moq; +using System.Net; +using ProblemDetails = CO.CDP.Organisation.WebApiClient.ProblemDetails; + +namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement; + +public class CreateApiKeyTest +{ + private readonly Mock _mockOrganisationClient = new(); + private readonly Guid _organisationId = Guid.NewGuid(); + private readonly CreateApiKeyModel _model; + public CreateApiKeyTest() + { + _model = new CreateApiKeyModel(_mockOrganisationClient.Object); + } + + [Fact] + public async Task OnPost_WhenModelStateIsInvalid_ShouldReturnPage() + { + _model.ModelState.AddModelError("key", "error"); + + var result = await _model.OnPost(); + + result.Should().BeOfType(); + } + + [Fact] + public async Task OnPost_WhenApiExceptionOccurs_ShouldRedirectToPageNotFound() + { + _mockOrganisationClient.Setup(client => client.CreateAuthenticationKeyAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null)); + + _model.Id = Guid.NewGuid(); + _model.ApiKeyName = "TestApiKey"; + + var result = await _model.OnPost(); + + result.Should().BeOfType() + .Which.Url.Should().Be("/page-not-found"); + } + + [Fact] + public async Task OnPost_ShouldRedirectToManageApiKey() + { + _model.Id = Guid.NewGuid(); + + _mockOrganisationClient + .Setup(c => c.CreateAuthenticationKeyAsync(_organisationId, DummyApiKeyEntity())); + + var result = await _model.OnPost(); + + var redirectToPageResult = result.Should().BeOfType().Subject; + + _mockOrganisationClient.Verify(c => c.CreateAuthenticationKeyAsync(It.IsAny(), It.IsAny()), Times.Once()); + + redirectToPageResult.PageName.Should().Be("NewApiKeyDetails"); + } + + [Fact] + public async Task OnPost_ShouldReturnPage_WhenCreateApiKeyModelStateIsInvalid() + { + var model = new CreateApiKeyModel(_mockOrganisationClient.Object); + + model.ModelState.AddModelError("ApiKeyName", "Enter the api key name"); + + var result = await model.OnPost(); + + result.Should().BeOfType(); + } + + [Theory] + [InlineData(ErrorCodes.APIKEY_NAME_ALREADY_EXISTS, ErrorMessagesList.DuplicateApiKeyName, StatusCodes.Status400BadRequest)] + public async Task OnPost_AddsModelError(string errorCode, string expectedErrorMessage, int statusCode) + { + var problemDetails = new ProblemDetails( + title: errorCode, + detail: ErrorMessagesList.DuplicateApiKeyName, + status: statusCode, + instance: null, + type: null + ) + { + AdditionalProperties = + { + { "code", "APIKEY_NAME_ALREADY_EXISTS" } + } + }; + var aex = new ApiException( + "Duplicate Api key name", + statusCode, + "Bad Request", + null, + problemDetails ?? new ProblemDetails("Detail", "Instance", statusCode, "Problem title", "Problem type"), + null + ); + + _mockOrganisationClient.Setup(client => client.CreateAuthenticationKeyAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(aex); + + _model.Id = Guid.NewGuid(); + _model.ApiKeyName = "TestApiKey"; + + var result = await _model.OnPost(); + + _model.ModelState[string.Empty].As().Errors + .Should().Contain(e => e.ErrorMessage == expectedErrorMessage); + } + + private RegisterAuthenticationKey DummyApiKeyEntity() + { + return new RegisterAuthenticationKey(key: "_key", name: "name", organisationId: _organisationId); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/ManageApiKeyTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/ManageApiKeyTest.cs new file mode 100644 index 000000000..6a6d8fd9f --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/ManageApiKeyTest.cs @@ -0,0 +1,63 @@ +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Pages.ApiKeyManagement; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Moq; +using System.Net; + +namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement; + +public class ManageApiKeyTest +{ + private readonly Mock _mockOrganisationClient = new(); + private readonly ManageApiKeyModel _model; + + public ManageApiKeyTest() + { + _model = new ManageApiKeyModel(_mockOrganisationClient.Object); + _model.Id = Guid.NewGuid(); + } + + [Fact] + public async Task OnGet_WhenAuthenticationKeysAreRetrieved_ShouldReturnPage() + { + var authenticationKeys = new List { + new AuthenticationKey(createdOn: DateTimeOffset.UtcNow.AddDays(-1), name: "TestKey1", revoked: false, revokedOn: DateTimeOffset.UtcNow) + }; + + _mockOrganisationClient + .Setup(client => client.GetAuthenticationKeysAsync(It.IsAny())) + .ReturnsAsync(authenticationKeys); + + var result = await _model.OnGet(); + + result.Should().BeOfType(); + + _model.AuthenticationKeys.Should().HaveCount(1); + _model.AuthenticationKeys.FirstOrDefault()!.Name.Should().Be("TestKey1"); + } + + [Fact] + public async Task OnGet_WhenApiExceptionOccurs_ShouldRedirectToPageNotFound() + { + _mockOrganisationClient.Setup(client => client.GetAuthenticationKeysAsync(It.IsAny())) + .ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null)); + + var result = await _model.OnGet(); + + result.Should().BeOfType() + .Which.Url.Should().Be("/page-not-found"); + } + + [Fact] + public void OnPost_ShouldRedirectToCreateApiKeyPage() + { + var result = _model.OnPost(); + + result.Should().BeOfType() + .Which.PageName.Should().Be("CreateApiKey"); + + result.As().RouteValues!["Id"].Should().Be(_model.Id); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/NewApiKeyDetailsTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/NewApiKeyDetailsTest.cs new file mode 100644 index 000000000..89d0f03ef --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/NewApiKeyDetailsTest.cs @@ -0,0 +1,24 @@ +using CO.CDP.OrganisationApp.Pages.ApiKeyManagement; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement; + +public class NewApiKeyDetailsTest +{ + [Fact] + public void OnGet_ShouldReturnPageResult() + { + var pageModel = new NewApiKeyDetailsModel + { + Id = Guid.NewGuid(), + ApiKey = "test-api-key" + }; + + var result = pageModel.OnGet(); + + result.Should().BeOfType(); + pageModel.Id.Should().NotBeEmpty(); + pageModel.ApiKey.Should().Be("test-api-key"); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/RevokeApiKeyTest.cs b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/RevokeApiKeyTest.cs new file mode 100644 index 000000000..208821e8f --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/Pages/ApiKeyManagement/RevokeApiKeyTest.cs @@ -0,0 +1,67 @@ +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Pages.ApiKeyManagement; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Moq; +using System.Net; + +namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement; + +public class RevokeApiKeyTest +{ + private readonly Mock _mockOrganisationClient; + private readonly RevokeApiKeyModel _pageModel; + + public RevokeApiKeyTest() + { + _mockOrganisationClient = new Mock(); + _pageModel = new RevokeApiKeyModel(_mockOrganisationClient.Object) + { + Id = Guid.NewGuid(), + ApiKeyName = "TestApiKey" + }; + } + + [Fact] + public async Task OnPost_ValidModelState_ShouldRedirectToManageApiKeyPage() + { + _mockOrganisationClient + .Setup(client => client.RevokeAuthenticationKeyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var result = await _pageModel.OnPost(); + + result.Should().BeOfType() + .Which.PageName.Should().Be("ManageApiKey"); + result.As().RouteValues!["Id"].Should().Be(_pageModel.Id); + + _mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(_pageModel.Id, _pageModel.ApiKeyName), Times.Once); + } + + [Fact] + public async Task OnPost_InvalidModelState_ShouldReturnPageResult() + { + _pageModel.ModelState.AddModelError("ApiKeyName", "ApiKeyName is required"); + + var result = await _pageModel.OnPost(); + + result.Should().BeOfType(); + + _mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task OnPost_ApiException404_ShouldRedirectToPageNotFound() + { + _mockOrganisationClient.Setup(client => client.RevokeAuthenticationKeyAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null)); + + var result = await _pageModel.OnPost(); + + result.Should().BeOfType() + .Which.Url.Should().Be("/page-not-found"); + + _mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(_pageModel.Id, _pageModel.ApiKeyName), Times.Once); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Constants/ErrorCodes.cs b/Frontend/CO.CDP.OrganisationApp/Constants/ErrorCodes.cs index fab7d22fd..87578aa3f 100644 --- a/Frontend/CO.CDP.OrganisationApp/Constants/ErrorCodes.cs +++ b/Frontend/CO.CDP.OrganisationApp/Constants/ErrorCodes.cs @@ -18,4 +18,5 @@ public static class ErrorCodes public const string BUYER_INFO_NOT_EXISTS = "BUYER_INFO_NOT_EXISTS"; public const string UNKNOWN_BUYER_INFORMATION_UPDATE_TYPE = "UNKNOWN_BUYER_INFORMATION_UPDATE_TYPE"; public const string PERSON_INVITE_ALREADY_CLAIMED = "PERSON_INVITE_ALREADY_CLAIMED"; + public const string APIKEY_NAME_ALREADY_EXISTS = "APIKEY_NAME_ALREADY_EXISTS"; } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Constants/ErrorMessagesList.cs b/Frontend/CO.CDP.OrganisationApp/Constants/ErrorMessagesList.cs index 28023f781..c0064dd81 100644 --- a/Frontend/CO.CDP.OrganisationApp/Constants/ErrorMessagesList.cs +++ b/Frontend/CO.CDP.OrganisationApp/Constants/ErrorMessagesList.cs @@ -20,4 +20,6 @@ public static class ErrorMessagesList public const string BuyerInfoNotExists = "The buyer information for requested organisation not exist."; public const string UnknownBuyerInformationUpdateType = "The requested buyer information update type is unknown."; + public const string DuplicateApiKeyName = "An API key Name with this name already exists. Please try again."; + } \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml new file mode 100644 index 000000000..b24c254b7 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml @@ -0,0 +1,46 @@ +@page "/organisation/{id}/manage-api-key/create" +@model CreateApiKeyModel + +@{ + var apiKeyNameHasError = ((TagBuilder)Html.ValidationMessageFor(m => m.ApiKeyName)).HasInnerHtml; +} +Back + +
+
+
+ +
+
+

+ +

+
+ For security we only show the API key once on the confirmation page. Add a name for reference. For example, the name of the eSender it's for. +
+
+ @if (apiKeyNameHasError) + { +

+ Error: + @Html.ValidationMessageFor(m => m.ApiKeyName) +

+ } + + +
+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml.cs new file mode 100644 index 000000000..5d9c979bd --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml.cs @@ -0,0 +1,70 @@ +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; +using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; +using OrganisationWebApiClient = CO.CDP.Organisation.WebApiClient; + +namespace CO.CDP.OrganisationApp.Pages.ApiKeyManagement; + +[Authorize(Policy = OrgScopeRequirement.Editor)] +public class CreateApiKeyModel(IOrganisationClient organisationClient) : PageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + [Required(ErrorMessage = "Enter the api key name")] + public string? ApiKeyName { get; set; } + + public async Task OnPost() + { + try + { + if (!ModelState.IsValid) return Page(); + + var apiKey = Guid.NewGuid().ToString(); + + var registerApiKey = new RegisterAuthenticationKey( + key: apiKey, + name: ApiKeyName, + organisationId: Id); + + await organisationClient.CreateAuthenticationKey(Id, registerApiKey); + + return RedirectToPage("NewApiKeyDetails", new { Id, apiKey }); + } + catch (ApiException aex) + { + MapApiExceptions(aex); + return Page(); + } + catch (ApiException ex) when (ex.StatusCode == 404) + { + return Redirect("/page-not-found"); + } + } + + private void MapApiExceptions(ApiException aex) + { + var code = ExtractErrorCode(aex); + + if (!string.IsNullOrEmpty(code)) + { + ModelState.AddModelError(string.Empty, code switch + { + ErrorCodes.APIKEY_NAME_ALREADY_EXISTS => ErrorMessagesList.DuplicateApiKeyName, + _ => ErrorMessagesList.UnexpectedError + }); + } + } + + private static string? ExtractErrorCode(ApiException aex) + { + return aex.Result.AdditionalProperties.TryGetValue("code", out var code) && code is string codeString + ? codeString + : null; + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml new file mode 100644 index 000000000..900a7ebcb --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml @@ -0,0 +1,70 @@ +@page "/organisation/{id}/manage-api-keys" +@model ManageApiKeyModel + +@{ + var apiKeys = Model.AuthenticationKeys; + var count = apiKeys.Count(); +} +Back + +
+
+
+
+ @if (count == 0) + { +

Manage API keys

+ +

+ API keys that you create will be listed here. +

+ } + else + { +

+ You have @count API @(count == 1 ? "key" : "keys") +

+

+ View your active and cancelled API keys. You can cancel any that are no longer needed. +

+
+
+
+
Name
+
Date created
+
Date cancelled
+
+ Actions +
+
+ @foreach (var ak in apiKeys) + { +
+
@ak.Name
+
@ak.CreatedOn.ToString("d")
+
@(ak.Revoked == true ? ak.RevokedOn?.ToString("d") : "")
+
+ @if (ak.Revoked == true) + { + Revoked + } + else + { + Revoke + } +
+
+ } +
+
+ + } +
+ +
+
+
+
+
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml.cs new file mode 100644 index 000000000..6f0461219 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/ManageApiKey.cshtml.cs @@ -0,0 +1,36 @@ +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; +using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace CO.CDP.OrganisationApp.Pages.ApiKeyManagement; + +[Authorize(Policy = OrgScopeRequirement.Editor)] +public class ManageApiKeyModel(IOrganisationClient organisationClient) : PageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + public ICollection AuthenticationKeys { get; set; } = []; + + public async Task OnGet() + { + try + { + AuthenticationKeys = await organisationClient.GetAuthenticationKeys(Id); + + return Page(); + } + catch (ApiException ex) when (ex.StatusCode == 404) + { + return Redirect("/page-not-found"); + } + } + + public IActionResult OnPost() + { + return RedirectToPage("CreateApiKey", new { Id }); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml new file mode 100644 index 000000000..a4eff2986 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml @@ -0,0 +1,48 @@ +@page "/organisation/{id}/manage-api-key/{apikey}/details" +@model NewApiKeyDetailsModel + +
+
+
+ + +
+
+
+

API key created

+
+ Your API key
+ @Model.ApiKey +
+
+
+ +
+
+
+ Copy the API key and keep it safe. +
+

+ This is the only time we show you the API key. If you lose it, you’ll need to cancel it and create a new one. +

+ +

+ Back to your API keys +

+
+
+
+
+
+
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml.cs new file mode 100644 index 000000000..ce935cd6f --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/NewApiKeyDetails.cshtml.cs @@ -0,0 +1,22 @@ +using CO.CDP.OrganisationApp.Constants; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using static CO.CDP.OrganisationApp.Pages.ApiKeyManagement.CreateApiKeyModel; + +namespace CO.CDP.OrganisationApp.Pages.ApiKeyManagement; + +[Authorize(Policy = OrgScopeRequirement.Editor)] +public class NewApiKeyDetailsModel() : PageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty(SupportsGet = true)] + public string? ApiKey { get; set; } + + public IActionResult OnGet() + { + return Page(); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml new file mode 100644 index 000000000..afbb8dfa6 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml @@ -0,0 +1,29 @@ +@page "/organisation/{id}/manage-api-key/{apiKeyName}/revoke-api-key" +@model RevokeApiKeyModel + +@{ + var backLink = $"/organisation/{@Model.Id}/manage-api-keys"; +} +Back + +
+
+
+
+

Cancel API key

+

+ Cancelling this API key will stop the sharing of information between this service and the eSender that's using it. +

+

+ You cannot re-activate a cancelled API key. You'll need to create a new one to share information again. +

+
+ + Back +
+
+
+
+
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml.cs b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml.cs new file mode 100644 index 000000000..665cad8e9 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/RevokeApiKey.cshtml.cs @@ -0,0 +1,34 @@ +using CO.CDP.Organisation.WebApiClient; +using CO.CDP.OrganisationApp.Constants; +using CO.CDP.OrganisationApp.WebApiClients; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace CO.CDP.OrganisationApp.Pages.ApiKeyManagement; + +[Authorize(Policy = OrgScopeRequirement.Editor)] +public class RevokeApiKeyModel(IOrganisationClient organisationClient) : PageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty(SupportsGet = true)] + public required string ApiKeyName { get; set; } + + public async Task OnPost() + { + try + { + if (!ModelState.IsValid) return Page(); + + await organisationClient.RevokeAuthenticationKey(Id, ApiKeyName); + + return RedirectToPage("ManageApiKey", new { Id }); + } + catch (ApiException ex) when (ex.StatusCode == 404) + { + return Redirect("/page-not-found"); + } + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml index a7e2c1e4a..2154334fe 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml @@ -86,7 +86,7 @@ Change address - } + } @if (organisationDetails.IsTenderer()) @@ -95,12 +95,25 @@ Supplier information } + else if (organisationDetails.IsBuyer()) + { + +

+ Manage API keys +

+ +
+

An application programming interface (API) key is a code that allows different pieces of software to share information.

+

You need to create an API key and share it with the e-Sender you use to publish procurement notices on the Find a Tender service.

+
+
+ } - +
Back to all organisations diff --git a/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationClientExtensions.cs b/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationClientExtensions.cs index 947c13a47..a55dc1217 100644 --- a/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationClientExtensions.cs +++ b/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationClientExtensions.cs @@ -172,6 +172,18 @@ internal static Task UpdateSupplierCompletedConnectedPerson(this IOrganisationCl new UpdateSupplierInformation( type: SupplierInformationUpdateType.CompletedConnectedPerson, supplierInformation: new SupplierInfo(supplierType: null, operationTypes: null, tradeAssurance: null, legalForm: null, qualification: null))); + + internal static async Task RevokeAuthenticationKey(this IOrganisationClient organisationClient, + Guid organisationId, string authenticationKeyName) + => await organisationClient.RevokeAuthenticationKeyAsync(organisationId, authenticationKeyName); + + internal static async Task CreateAuthenticationKey(this IOrganisationClient organisationClient, + Guid organisationId, RegisterAuthenticationKey? registerAuthenticationKey) + => await organisationClient.CreateAuthenticationKeyAsync(organisationId, registerAuthenticationKey); + + internal static async Task> GetAuthenticationKeys(this IOrganisationClient organisationClient, + Guid organisationId) + => await organisationClient.GetAuthenticationKeysAsync(organisationId); } public class ComposedOrganisation diff --git a/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationExtensions.cs b/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationExtensions.cs index 55cc03317..8ee67b965 100644 --- a/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationExtensions.cs +++ b/Frontend/CO.CDP.OrganisationApp/WebApiClients/OrganisationExtensions.cs @@ -8,4 +8,9 @@ public static bool IsTenderer(this Organisation.WebApiClient.Organisation organi { return organisation.Roles.Contains(PartyRole.Tenderer); } + + public static bool IsBuyer(this Organisation.WebApiClient.Organisation organisation) + { + return organisation.Roles.Contains(PartyRole.Buyer); + } } \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/Services/CO.CDP.Organisation.WebApi.Tests/Extensions/ServiceCollectionExtensionsTests.cs index 28b431f6a..e3d8bd3c0 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; using static CO.CDP.Organisation.WebApi.UseCase.RegisterOrganisationUseCase.RegisterOrganisationException; +using static CO.CDP.OrganisationInformation.Persistence.IAuthenticationKeyRepository.AuthenticationKeyRepositoryException; using static CO.CDP.OrganisationInformation.Persistence.IOrganisationRepository.OrganisationRepositoryException; namespace CO.CDP.Organisation.WebApi.Tests.Extensions; @@ -61,6 +62,16 @@ public void MapException_Should_Return_BadRequest_For_InvalidQueryException() Assert.Equal("ISSUE_WITH_QUERY_PARAMETERS", result.error); } + [Fact] + public void MapException_Should_Return_NotFound_For_DuplicateApiKeyNameException() + { + var exception = new DuplicateAuthenticationKeyNameException("Duplicate Api key name"); + var result = ServiceCollectionExtensions.MapException(exception); + + Assert.Equal(StatusCodes.Status400BadRequest, result.status); + Assert.Equal("APIKEY_NAME_ALREADY_EXISTS", result.error); + } + [Fact] public void ErrorCodes_ShouldReturn_ListOfStatusesMappedToErrorCodes() { @@ -70,6 +81,7 @@ public void ErrorCodes_ShouldReturn_ListOfStatusesMappedToErrorCodes() result["400"].Should().Contain("ORGANISATION_ALREADY_EXISTS"); result["400"].Should().Contain("INVALID_BUYER_INFORMATION_UPDATE_ENTITY"); result["400"].Should().Contain("INVALID_SUPPLIER_INFORMATION_UPDATE_ENTITY"); + result["400"].Should().Contain("APIKEY_NAME_ALREADY_EXISTS"); result.Should().ContainKey("404"); result["404"].Should().Contain("PERSON_DOES_NOT_EXIST"); diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetAuthenticationKeyUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetAuthenticationKeyUseCaseTest.cs new file mode 100644 index 000000000..9d4d1f9da --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetAuthenticationKeyUseCaseTest.cs @@ -0,0 +1,62 @@ +using CO.CDP.Organisation.WebApi.Tests.AutoMapper; +using CO.CDP.Organisation.WebApi.UseCase; +using CO.CDP.OrganisationInformation.Persistence; +using FluentAssertions; +using Moq; + +namespace CO.CDP.Organisation.WebApi.Tests.UseCase; +public class GetAuthenticationKeyUseCaseTest(AutoMapperFixture mapperFixture) : IClassFixture +{ + private readonly Mock _repository = new(); + private GetAuthenticationKeyUseCase UseCase => new(_repository.Object, mapperFixture.Mapper); + + [Fact] + public async Task ItReturnsEmptyIfNoAuthenticationKeyIsFound() + { + var organisationId = Guid.NewGuid(); + + var found = await UseCase.Execute(organisationId); + + found.Should().BeEmpty(); + } + + [Fact] + public async Task ItReturnsTheFoundAuthenticationKeys() + { + var organisationId = Guid.NewGuid(); + + var org = FakeOrganisation(organisationId, 1); + + var persistenceAuthorityKeys = new List + { + new AuthenticationKey { Key = "k1", Name = "key-1", OrganisationId = 1, Organisation = org }, + new AuthenticationKey { Key = "k2", Name = "key-2", OrganisationId = 1, Organisation = org } + }; + + _repository.Setup(r => r.GetAuthenticationKeys(organisationId)).ReturnsAsync(persistenceAuthorityKeys); + + var found = await UseCase.Execute(organisationId); + found.Should().HaveCountGreaterThan(1); + } + + private static OrganisationInformation.Persistence.Organisation FakeOrganisation(Guid Orgid, int id) + { + OrganisationInformation.Persistence.Organisation org = new() + { + Guid = Orgid, + Id = 1, + Name = "FakeOrg", + Tenant = new Tenant + { + Guid = Guid.NewGuid(), + Name = "Tenant 101" + }, + ContactPoints = [new OrganisationInformation.Persistence.Organisation.ContactPoint { Email = "contact@test.org" }] + }; + + org.SupplierInfo = new OrganisationInformation.Persistence.Organisation.SupplierInformation { CompletedRegAddress = true }; + + + return org; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RegisterAuthenticationKeyUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RegisterAuthenticationKeyUseCaseTest.cs new file mode 100644 index 000000000..ff904ce6b --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RegisterAuthenticationKeyUseCaseTest.cs @@ -0,0 +1,119 @@ +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.Organisation.WebApi.UseCase; +using CO.CDP.OrganisationInformation; +using CO.CDP.OrganisationInformation.Persistence; +using FluentAssertions; +using Moq; +using Persistence = CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.Tests.UseCase; + +public class RegisterAuthenticationKeyUseCaseTest() +{ + private readonly Mock _organisationRepo = new(); + private readonly Mock _keyRepo = new(); + private RegisterAuthenticationKeyUseCase UseCase => new(_keyRepo.Object, _organisationRepo.Object); + + [Fact] + public async Task Execute_ShouldThrowUnknownOrganisationException_WhenOrganisationNotFound() + { + var organisationId = Guid.NewGuid(); + Persistence.Organisation? organisation = null; + var registerAuthenticationKey = GivenRegisterAuthenticationKey(organisationId); + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(organisation); + + Func act = async () => await UseCase.Execute((organisationId, registerAuthenticationKey)); + + await act.Should() + .ThrowAsync() + .WithMessage($"Unknown organisation {organisationId}."); + } + + [Fact] + public async Task ItReturnsTheRegisteredAuthenticationKey() + { + var organisationId = Guid.NewGuid(); + + var command = (organisationId, GivenRegisterAuthenticationKey(organisationId)); + + var org = GivenOrganisationExist(organisationId); + + _organisationRepo.Setup(x => x.Save(org)); + + var result = await UseCase.Execute(command); + + var expectedAuthenticationKey = GivenRegisterAuthenticationKey(organisationId); + + _keyRepo.Verify(repo => repo.Save(It.IsAny()), Times.Once); + + result.Should().BeTrue(); + } + + [Fact] + public async Task ItSavesNewAuthenticationKeyInTheRepository() + { + var organisationId = Guid.NewGuid(); + var registerAuthenticationKey = GivenRegisterAuthenticationKey(organisationId); + var org = GivenOrganisationExist(organisationId); + + var command = (organisationId, registerAuthenticationKey); + + Persistence.AuthenticationKey? persistanceAuthenticationKey = null; + + _organisationRepo.Setup(x => x.Save(org)); + + _keyRepo + .Setup(x => x.Save(It.IsAny())) + .Callback(b => persistanceAuthenticationKey = b); + + var result = await UseCase.Execute(command); + + _keyRepo.Verify(e => e.Save(It.IsAny()), Times.Once); + + persistanceAuthenticationKey.Should().NotBeNull(); + persistanceAuthenticationKey.As().OrganisationId.Should().Be(1); + persistanceAuthenticationKey.As().Key.Should().Be("Key1"); + } + + private Persistence.Organisation GivenOrganisationExist(Guid organisationId) + { + var org = new Persistence.Organisation + { + Id = 1, + Name = "TheOrganisation", + Guid = organisationId, + Addresses = [new Persistence.Organisation.OrganisationAddress + { + Type = AddressType.Registered, + Address = new Persistence.Address + { + StreetAddress = "1234 Example St", + Locality = "Example City", + Region = "Test Region", + PostalCode = "12345", + CountryName = "Exampleland", + Country = "AB" + } + }], + Tenant = It.IsAny(), + SupplierInfo = new Persistence.Organisation.SupplierInformation() + }; + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(org); + + return org; + } + + private static RegisterAuthenticationKey GivenRegisterAuthenticationKey(Guid organisationId) + { + return new RegisterAuthenticationKey + { + Key = "Key1", + Name = "KeyName1", + OrganisationId = organisationId + }; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RevokeAuthenticationKeyUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RevokeAuthenticationKeyUseCaseTest.cs new file mode 100644 index 000000000..40eb74904 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/RevokeAuthenticationKeyUseCaseTest.cs @@ -0,0 +1,123 @@ +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.Organisation.WebApi.UseCase; +using CO.CDP.OrganisationInformation; +using CO.CDP.OrganisationInformation.Persistence; +using FluentAssertions; +using Moq; +using Persistence = CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.Tests.UseCase; + +public class RevokeAuthenticationKeyUseCaseTest() +{ + private readonly Mock _organisationRepo = new(); + private readonly Mock _keyRepo = new(); + private RevokeAuthenticationKeyUseCase UseCase => new(_organisationRepo.Object, _keyRepo.Object); + private const string _authKeyName = "keyname1"; + + [Fact] + public async Task Execute_ShouldThrowUnknownOrganisationException_WhenOrganisationNotFound() + { + var organisationId = Guid.NewGuid(); + Persistence.Organisation? organisation = null; + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(organisation); + + Func act = async () => await UseCase.Execute((organisationId, _authKeyName)); + + await act.Should() + .ThrowAsync() + .WithMessage($"Unknown organisation {organisationId}."); + } + + [Fact] + public async Task Execute_ShouldThrowEmptyAuthenticationKeyNameException_WhenNameIsNullOrEmpty() + { + var organisationId = Guid.NewGuid(); + + var org = GivenOrganisationExist(organisationId); + + _organisationRepo.Setup(x => x.Save(org)); + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(org); + + Func act = async () => await UseCase.Execute((organisationId, "")); + + await act.Should() + .ThrowAsync() + .WithMessage($"Empty Name of Revoke AuthenticationKey for organisation {organisationId}."); + } + + [Fact] + public async Task Execute_ShouldThrowUnknownAuthenticationKeyException_WhenKeyNotFound() + { + var organisationId = Guid.NewGuid(); + Persistence.AuthenticationKey? authorisationKey = null; + + var org = GivenOrganisationExist(organisationId); + + _organisationRepo.Setup(x => x.Save(org)); + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(org); + + _keyRepo.Setup(k => k.FindByKeyNameAndOrganisationId("n", organisationId)) + .ReturnsAsync(authorisationKey); + + Func act = async () => await UseCase.Execute((organisationId, _authKeyName)); + + await act.Should() + .ThrowAsync() + .WithMessage($"Unknown Authentication Key - name {_authKeyName} for organisation {organisationId}."); + } + + [Fact] + public async Task Execute_ShouldRevokeAuthenticationKey_WhenValidCommandIsProvided() + { + var organisationId = Guid.NewGuid(); + var command = (organisationId, _authKeyName); + var organisation = GivenOrganisationExist(organisationId); + var authKey = new Persistence.AuthenticationKey { Key = "k1", Name = _authKeyName, Revoked = false }; + + _organisationRepo.Setup(repo => repo.Find(organisationId)).ReturnsAsync(organisation); + _keyRepo.Setup(repo => repo.FindByKeyNameAndOrganisationId(_authKeyName, organisationId)).ReturnsAsync(authKey); + + var result = await UseCase.Execute(command); + + result.Should().BeTrue(); + authKey.Revoked.Should().BeTrue(); + _keyRepo.Verify(repo => repo.Save(authKey), Times.Once); + } + + private Persistence.Organisation GivenOrganisationExist(Guid organisationId) + { + var org = new Persistence.Organisation + { + Id = 1, + Name = "TheOrganisation", + Guid = organisationId, + Addresses = [new Persistence.Organisation.OrganisationAddress + { + Type = AddressType.Registered, + Address = new Persistence.Address + { + StreetAddress = "1234 Example St", + Locality = "Example City", + Region = "Test Region", + PostalCode = "12345", + CountryName = "Exampleland", + Country = "AB" + } + }], + Tenant = It.IsAny(), + SupplierInfo = new Persistence.Organisation.SupplierInformation() + }; + + _organisationRepo.Setup(repo => repo.Find(organisationId)) + .ReturnsAsync(org); + + return org; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs b/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs index 336bade8f..533e7ca37 100644 --- a/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs +++ b/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs @@ -645,6 +645,85 @@ await useCase.Execute((organisationId, personInviteId)) return app; } + + public static RouteGroupBuilder UseManageApiKeyEndpoints(this RouteGroupBuilder app) + { + app.MapGet("/{organisationId}/api-keys", + async (Guid organisationId, IUseCase> useCase) => + await useCase.Execute(organisationId) + .AndThen(entities => Results.Ok(entities))) + .Produces>(StatusCodes.Status200OK, "application/json") + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status422UnprocessableEntity) + .Produces(StatusCodes.Status500InternalServerError) + .WithOpenApi(operation => + { + operation.OperationId = "GetAuthenticationKeys"; + operation.Description = "Get authentication keys by Organisation ID."; + operation.Summary = "Get authentication keys information by Organisation ID."; + operation.Responses["200"].Description = "Authentication keys details."; + operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; + operation.Responses["404"].Description = "authentication keys information not found."; + operation.Responses["422"].Description = "Unprocessable entity."; + operation.Responses["500"].Description = "Internal server error."; + return operation; + }); + + app.MapPost("/{organisationId}/api-keys", + async (Guid organisationId, RegisterAuthenticationKey registerAuthenticationKey, + IUseCase<(Guid, RegisterAuthenticationKey), bool> useCase) => + + await useCase.Execute((organisationId, registerAuthenticationKey)) + .AndThen(_ => Results.Created()) + ) + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status422UnprocessableEntity) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithOpenApi(operation => + { + operation.OperationId = "CreateAuthenticationKey"; + operation.Description = "Create a new authentication key."; + operation.Summary = "Create a new authentication key."; + operation.Responses["201"].Description = "Authentication key created successfully."; + operation.Responses["400"].Description = "Bad request."; + operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; + operation.Responses["404"].Description = "Authentication failed."; + operation.Responses["422"].Description = "Unprocessable entity."; + operation.Responses["500"].Description = "Internal server error."; + return operation; + }); + + app.MapPatch("/{organisationId}/api-keys/revoke", + async (Guid organisationId, string keyName, + IUseCase<(Guid, string), bool> useCase) => + await useCase.Execute((organisationId, keyName)) + .AndThen(_ => Results.NoContent())) + .Produces(StatusCodes.Status204NoContent) + .ProducesProblem(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status422UnprocessableEntity) + .ProducesProblem(StatusCodes.Status500InternalServerError) + .WithOpenApi(operation => + { + operation.OperationId = "RevokeAuthenticationKey"; + operation.Description = "Revoke Authentication key."; + operation.Summary = "Revoke Authentication key."; + operation.Responses["204"].Description = "Authentication key revoked successfully."; + operation.Responses["400"].Description = "Bad request."; + operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; + operation.Responses["404"].Description = "Authentication failed."; + operation.Responses["422"].Description = "Unprocessable entity."; + operation.Responses["500"].Description = "Internal server error."; + return operation; + }); + + return app; + } } public static class ApiExtensions diff --git a/Services/CO.CDP.Organisation.WebApi/AutoMapper/WebApiToPersistenceProfile.cs b/Services/CO.CDP.Organisation.WebApi/AutoMapper/WebApiToPersistenceProfile.cs index 179d48d14..b96236118 100644 --- a/Services/CO.CDP.Organisation.WebApi/AutoMapper/WebApiToPersistenceProfile.cs +++ b/Services/CO.CDP.Organisation.WebApi/AutoMapper/WebApiToPersistenceProfile.cs @@ -116,6 +116,8 @@ public WebApiToPersistenceProfile() CreateMap() .ForMember(m => m.Id, o => o.MapFrom(m => m.Guid)); + CreateMap(); + ConnectedEntityMapping(); OrganisationEventsMapping(); } diff --git a/Services/CO.CDP.Organisation.WebApi/Extensions/ServiceCollectionExtensions.cs b/Services/CO.CDP.Organisation.WebApi/Extensions/ServiceCollectionExtensions.cs index 535c66f8f..41f1205c1 100644 --- a/Services/CO.CDP.Organisation.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Services/CO.CDP.Organisation.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using CO.CDP.Organisation.WebApi.Model; using static CO.CDP.Organisation.WebApi.UseCase.RegisterOrganisationUseCase.RegisterOrganisationException; +using static CO.CDP.OrganisationInformation.Persistence.IAuthenticationKeyRepository.AuthenticationKeyRepositoryException; using static CO.CDP.OrganisationInformation.Persistence.IOrganisationRepository.OrganisationRepositoryException; namespace CO.CDP.Organisation.WebApi.Extensions; @@ -20,6 +21,7 @@ public static class ServiceCollectionExtensions { typeof(InvalidUpdateBuyerInformationCommand), (StatusCodes.Status400BadRequest, "INVALID_BUYER_INFORMATION_UPDATE_ENTITY") }, { typeof(InvalidUpdateSupplierInformationCommand), (StatusCodes.Status400BadRequest, "INVALID_SUPPLIER_INFORMATION_UPDATE_ENTITY") }, { typeof(InvalidQueryException), (StatusCodes.Status400BadRequest, "ISSUE_WITH_QUERY_PARAMETERS") }, + { typeof(DuplicateAuthenticationKeyNameException), (StatusCodes.Status400BadRequest, "APIKEY_NAME_ALREADY_EXISTS") }, }; public static IServiceCollection AddOrganisationProblemDetails(this IServiceCollection services) diff --git a/Services/CO.CDP.Organisation.WebApi/Model/Command.cs b/Services/CO.CDP.Organisation.WebApi/Model/Command.cs index 5b3bb595d..8cd9906ba 100644 --- a/Services/CO.CDP.Organisation.WebApi/Model/Command.cs +++ b/Services/CO.CDP.Organisation.WebApi/Model/Command.cs @@ -56,11 +56,11 @@ public record UpdateOrganisation public enum OrganisationUpdateType { AdditionalIdentifiers, - ContactPoint, + ContactPoint, RemoveIdentifier, Address, OrganisationName, - OrganisationEmail, + OrganisationEmail, RegisteredAddress } @@ -336,12 +336,26 @@ public bool TryGetName(out string name) } } +public record RegisterAuthenticationKey +{ + public required string Name { get; set; } + public required string Key { get; set; } + public Guid OrganisationId { get; set; } +} + +public record AuthenticationKey +{ + public required string Name { get; set; } + public bool? Revoked { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public DateTimeOffset? RevokedOn { get; set; } +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SupplierInformationDeleteType { TradeAssurance, Qualification - } public static class MappingExtensions diff --git a/Services/CO.CDP.Organisation.WebApi/Model/Exceptions.cs b/Services/CO.CDP.Organisation.WebApi/Model/Exceptions.cs index 8cbbfc0ec..01b02ca80 100644 --- a/Services/CO.CDP.Organisation.WebApi/Model/Exceptions.cs +++ b/Services/CO.CDP.Organisation.WebApi/Model/Exceptions.cs @@ -13,4 +13,7 @@ public class InvalidUpdateConnectedEntityCommand(string message, Exception? caus public class UnknownConnectedEntityException(string message, Exception? cause = null) : Exception(message, cause); -public class MissingOrganisationIdException(string message, Exception? cause = null) : Exception(message, cause); \ No newline at end of file +public class MissingOrganisationIdException(string message, Exception? cause = null) : Exception(message, cause); + +public class EmptyAuthenticationKeyNameException(string message, Exception? cause = null) : Exception(message, cause); +public class UnknownAuthenticationKeyException(string message, Exception? cause = null) : Exception(message, cause); \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/Program.cs b/Services/CO.CDP.Organisation.WebApi/Program.cs index 567c499b0..77f696e16 100644 --- a/Services/CO.CDP.Organisation.WebApi/Program.cs +++ b/Services/CO.CDP.Organisation.WebApi/Program.cs @@ -56,6 +56,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped, AssignIdentifierUseCase>(); builder.Services.AddScoped, RegisterOrganisationUseCase>(); builder.Services.AddScoped, GetOrganisationUseCase>(); @@ -80,6 +81,10 @@ builder.Services.AddScoped>, GetPersonInvitesUseCase>(); builder.Services.AddScoped, RemovePersonInviteFromOrganisationUseCase>(); builder.Services.AddGovUKNotifyApiClient(builder.Configuration); +builder.Services.AddScoped>, GetAuthenticationKeyUseCase>(); +builder.Services.AddScoped, RegisterAuthenticationKeyUseCase>(); +builder.Services.AddScoped, RevokeAuthenticationKeyUseCase>(); + builder.Services.AddOrganisationProblemDetails(); builder.Services.AddJwtBearerAndApiKeyAuthentication(builder.Configuration, builder.Environment); @@ -138,5 +143,9 @@ .UsePersonsEndpoints() .WithTags("Organisation - Persons"); +app.MapGroup("/organisations") + .UseManageApiKeyEndpoints() + .WithTags("Organisation - Manage Api Keys"); + app.Run(); public abstract partial class Program; \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/GetAuthenticationKeyUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/GetAuthenticationKeyUseCase.cs new file mode 100644 index 000000000..3e7d59320 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/GetAuthenticationKeyUseCase.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using CO.CDP.Functional; +using CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.UseCase; +public class GetAuthenticationKeyUseCase(IAuthenticationKeyRepository authenticationKeyRepository, IMapper mapper) + : IUseCase> +{ + public async Task> Execute(Guid organisationId) + { + return await authenticationKeyRepository.GetAuthenticationKeys(organisationId) + .AndThen(mapper.Map>); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/RegisterAuthenticationKeyUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/RegisterAuthenticationKeyUseCase.cs new file mode 100644 index 000000000..6d3f70e30 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/RegisterAuthenticationKeyUseCase.cs @@ -0,0 +1,26 @@ +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.OrganisationInformation.Persistence; +namespace CO.CDP.Organisation.WebApi.UseCase; + +public class RegisterAuthenticationKeyUseCase( + IAuthenticationKeyRepository keyRepository, + IOrganisationRepository organisationRepository) + : IUseCase<(Guid organisationId, RegisterAuthenticationKey authKey), bool> +{ + public async Task Execute((Guid organisationId, RegisterAuthenticationKey authKey) command) + { + var authenticationKey = command.authKey; + + var organisation = await organisationRepository.Find(command.organisationId) + ?? throw new UnknownOrganisationException($"Unknown organisation {command.organisationId}."); + + await keyRepository.Save(new OrganisationInformation.Persistence.AuthenticationKey + { + Key = authenticationKey.Key, + Name = authenticationKey.Name, + OrganisationId = organisation.Id, + }); + + return await Task.FromResult(true); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/RevokeAuthenticationKeyUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/RevokeAuthenticationKeyUseCase.cs new file mode 100644 index 000000000..807f2d276 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/RevokeAuthenticationKeyUseCase.cs @@ -0,0 +1,32 @@ +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.UseCase; + +public class RevokeAuthenticationKeyUseCase( + IOrganisationRepository organisationRepository, + IAuthenticationKeyRepository keyRepository + ) + : IUseCase<(Guid organisationId, string keyName), bool> +{ + public async Task Execute((Guid organisationId, string keyName) command) + { + var revokeAuthKeyName = command.keyName; + + _ = await organisationRepository.Find(command.organisationId) + ?? throw new UnknownOrganisationException($"Unknown organisation {command.organisationId}."); + + if (string.IsNullOrEmpty(revokeAuthKeyName)) + throw new EmptyAuthenticationKeyNameException($"Empty Name of Revoke AuthenticationKey for organisation {command.organisationId}."); + + var authorisationKey = await keyRepository.FindByKeyNameAndOrganisationId(revokeAuthKeyName, command.organisationId) + ?? throw new UnknownAuthenticationKeyException($"Unknown Authentication Key - name {revokeAuthKeyName} for organisation {command.organisationId}."); + + authorisationKey.Revoked = true; + authorisationKey.RevokedOn = DateTimeOffset.UtcNow; + + await keyRepository.Save(authorisationKey); + + return await Task.FromResult(true); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseAuthenticationKeyRepositoryTest.cs b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseAuthenticationKeyRepositoryTest.cs index f586b935e..adaf3d229 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseAuthenticationKeyRepositoryTest.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence.Tests/DatabaseAuthenticationKeyRepositoryTest.cs @@ -43,6 +43,49 @@ public async Task ItUpdatesAnExistingAuthenticationKey() found.As().OrganisationId.Should().Be(authenticationKey.Organisation.Id); } + [Fact] + public async Task ItFindsSavedAuthenticationKeys() + { + using var repository = AuthenticationKeyRepository(); + var organisation = GivenOrganisation(); + var authenticationKey = GivenAuthenticationKey(key: Guid.NewGuid().ToString(), organisation: organisation); + await repository.Save(authenticationKey); + + var found = await repository.GetAuthenticationKeys(organisation.Guid); + found.As>().Should().NotBeEmpty(); + found.As>().Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task ItFindsSavedAuthenticationKeyByNameKeyAndOrganisationId() + { + using var repository = AuthenticationKeyRepository(); + var organisation = GivenOrganisation(); + var key = Guid.NewGuid().ToString(); + var authenticationKey = GivenAuthenticationKey(key: key, organisation: organisation); + await repository.Save(authenticationKey); + + var found = await repository.FindByKeyNameAndOrganisationId("fts", organisation.Guid); + found.As().Should().NotBeNull(); + found.As().Key.Should().BeSameAs(key); + } + + [Fact] + public void ItRejectsTwoApiKeyWithTheSameName() + { + using var repository = AuthenticationKeyRepository(); + var organisation = GivenOrganisation(); + var key = Guid.NewGuid().ToString(); + var authenticationKey1 = GivenAuthenticationKey(key: key, organisation: organisation); + var authenticationKey2 = GivenAuthenticationKey(key: key, organisation: organisation); + + repository.Save(authenticationKey1); + + repository.Invoking(r => r.Save(authenticationKey2)) + .Should().ThrowAsync() + .WithMessage($"Authentication Key with name `fts` already exists."); + } + private static AuthenticationKey GivenAuthenticationKey( string name = "fts", string key = "api-key", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/AuthenticationKey.cs b/Services/CO.CDP.OrganisationInformation.Persistence/AuthenticationKey.cs index ea14b5568..cc47e32cf 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/AuthenticationKey.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/AuthenticationKey.cs @@ -4,7 +4,6 @@ namespace CO.CDP.OrganisationInformation.Persistence; -[Index(nameof(Key), IsUnique = true)] public class AuthenticationKey : IEntityDate { public int Id { get; set; } @@ -13,8 +12,10 @@ public class AuthenticationKey : IEntityDate [ForeignKey(nameof(Organisation))] public int? OrganisationId { get; set; } + public bool Revoked { get; set; } public Organisation? Organisation { get; set; } public List Scopes { get; set; } = []; + public DateTimeOffset? RevokedOn { get; set; } public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset UpdatedOn { get; set; } } \ No newline at end of file diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseAuthenticationKeyRepository.cs b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseAuthenticationKeyRepository.cs index 72fd3e2ce..6fe31b7c7 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseAuthenticationKeyRepository.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseAuthenticationKeyRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace CO.CDP.OrganisationInformation.Persistence; @@ -6,17 +7,50 @@ public class DatabaseAuthenticationKeyRepository(OrganisationInformationContext { public async Task Find(string key) { - return await context.AuthenticationKeys.Include(a => a.Organisation).FirstOrDefaultAsync(t => t.Key == key); + return await context.AuthenticationKeys.Include(a => a.Organisation).FirstOrDefaultAsync(t => t.Key == key && t.Revoked == false); } public async Task Save(AuthenticationKey authenticationKey) { - context.Update(authenticationKey); - await context.SaveChangesAsync(); + try + { + context.Update(authenticationKey); + await context.SaveChangesAsync(); + } + catch (DbUpdateException cause) + { + HandleDbUpdateException(authenticationKey, cause); + } } public void Dispose() { context.Dispose(); } + + public async Task> GetAuthenticationKeys(Guid organisationId) + { + return await context.AuthenticationKeys + .Include(a => a.Organisation) + .Where(t => t.Organisation!.Guid == organisationId) + .ToArrayAsync(); + } + public async Task FindByKeyNameAndOrganisationId(string name, Guid organisationId) + { + return await context.AuthenticationKeys + .Include(a => a.Organisation) + .Where(t => t.Organisation!.Guid == organisationId) + .FirstOrDefaultAsync(t => t.Name == name); + } + + private static void HandleDbUpdateException(AuthenticationKey authenticationKey, DbUpdateException cause) + { + switch (cause.InnerException) + { + case { } e when e.ContainsDuplicateKey("_authentication_keys_name_organisation_id"): + throw new IAuthenticationKeyRepository.AuthenticationKeyRepositoryException.DuplicateAuthenticationKeyNameException( + $"Authentication Key with name `{authenticationKey.Name}` already exists.", cause); + default: throw cause; + } + } } \ No newline at end of file diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/IAuthenticationKeyRepository.cs b/Services/CO.CDP.OrganisationInformation.Persistence/IAuthenticationKeyRepository.cs index 2bd38547c..115a81297 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/IAuthenticationKeyRepository.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/IAuthenticationKeyRepository.cs @@ -3,6 +3,13 @@ namespace CO.CDP.OrganisationInformation.Persistence; public interface IAuthenticationKeyRepository : IDisposable { public Task Save(AuthenticationKey authenticationKey); - public Task Find(string key); + public Task> GetAuthenticationKeys(Guid organisationId); + + public Task FindByKeyNameAndOrganisationId(string name, Guid organisationId); + public class AuthenticationKeyRepositoryException(string message, Exception? cause = null) : Exception(message, cause) + { + public class DuplicateAuthenticationKeyNameException(string message, Exception? cause = null) + : AuthenticationKeyRepositoryException(message, cause); + } } \ No newline at end of file diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.Designer.cs new file mode 100644 index 000000000..e62803251 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.Designer.cs @@ -0,0 +1,1841 @@ +// +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("20240923101141_AlterAuthenticationKey")] + partial class AlterAuthenticationKey + { + /// + 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("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("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("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("Configuration") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("configuration"); + + 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("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("ApprovedById") + .HasColumnType("integer") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedComment") + .HasColumnType("text") + .HasColumnName("approved_comment"); + + 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("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("ApprovedById") + .HasDatabaseName("ix_organisations_approved_by_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_organisations_guid"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_organisations_name"); + + 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("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", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById") + .HasConstraintName("fk_organisations_persons_approved_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("CompletedQualification") + .HasColumnType("boolean") + .HasColumnName("completed_qualification"); + + b1.Property("CompletedRegAddress") + .HasColumnType("boolean") + .HasColumnName("completed_reg_address"); + + b1.Property("CompletedTradeAssurance") + .HasColumnType("boolean") + .HasColumnName("completed_trade_assurance"); + + 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.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Qualification", "Qualifications", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_qualifications"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_qualifications_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_qualifications_supplier_information_organisation_id"); + + b2.ToTable("qualifications", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_qualifications_supplier_information_supplier_information_or"); + }); + + b1.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+TradeAssurance", "TradeAssurances", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reference_number"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_trade_assurances"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_trade_assurances_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_trade_assurances_supplier_information_organisation_id"); + + b2.ToTable("trade_assurances", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_trade_assurances_supplier_information_supplier_information_"); + }); + + b1.Navigation("LegalForm"); + + b1.Navigation("Qualifications"); + + b1.Navigation("TradeAssurances"); + }); + + b.Navigation("Addresses"); + + b.Navigation("ApprovedBy"); + + b.Navigation("BuyerInfo"); + + b.Navigation("ContactPoints"); + + b.Navigation("Identifiers"); + + 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/20240923101141_AlterAuthenticationKey.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.cs new file mode 100644 index 000000000..685fb5123 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923101141_AlterAuthenticationKey.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + /// + public partial class AlterAuthenticationKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_authentication_keys_key", + table: "authentication_keys"); + + migrationBuilder.AddColumn( + name: "revoked", + table: "authentication_keys", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "revoked_on", + table: "authentication_keys", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_authentication_keys_name_organisation_id", + table: "authentication_keys", + columns: new[] { "name", "organisation_id" }, + unique: true) + .Annotation("Npgsql:NullsDistinct", false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_authentication_keys_name_organisation_id", + table: "authentication_keys"); + + migrationBuilder.DropColumn( + name: "revoked", + table: "authentication_keys"); + + migrationBuilder.DropColumn( + name: "revoked_on", + table: "authentication_keys"); + + migrationBuilder.CreateIndex( + name: "ix_authentication_keys_key", + table: "authentication_keys", + column: "key", + unique: true); + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs index 81c1168d9..2fffaa954 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs @@ -113,6 +113,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -127,13 +135,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_authentication_keys"); - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_authentication_keys_key"); - 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); }); diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs index ceb611918..f92160afc 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs @@ -144,6 +144,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(e => { + e.HasIndex(e => new { e.Name, e.OrganisationId }).IsUnique().AreNullsDistinct(false); e.Property(e => e.Scopes).HasJsonColumn([], PropertyBuilderExtensions.ListComparer()); }); From dd22f303819b682ec6d258ba19d3f33955dad6b8 Mon Sep 17 00:00:00 2001 From: Dharm Date: Mon, 23 Sep 2024 13:33:17 +0100 Subject: [PATCH 17/30] Exclusion update --- .../Pages/Supplier/SupplierInformationSummary.cshtml | 5 +++-- .../Api/GetFormSectionstEndpointTest.cs | 3 ++- .../UseCase/GetFormSectionsUseCaseTest.cs | 3 ++- .../CO.CDP.Forms.WebApi/Model/FormSectionResponse.cs | 1 + .../DatabaseFormRepository.cs | 10 +++++++--- .../Forms/FormSectionSummary.cs | 1 + 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Supplier/SupplierInformationSummary.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Supplier/SupplierInformationSummary.cshtml index d750f7096..63702adca 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Supplier/SupplierInformationSummary.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Supplier/SupplierInformationSummary.cshtml @@ -86,11 +86,12 @@ redirectLink += "/share-codes-list-view"; requiredScope = OrgScopeRequirement.Editor; } - else if (exclusionsSection) + else if (exclusionsSection && section.AnswerSetCount == 0) { redirectLink += "/add-exclusion-declaration"; + requiredScope = OrgScopeRequirement.Editor; } - else if (section.AnswerSetCount > 0 && section.AllowsMultipleAnswerSets == true) + else if (section.AnswerSetCount > 0 && section.AllowsMultipleAnswerSets == true && section.AnswerSetWithFurtherQuestionExemptedExists == false) { redirectLink += "/add-another-answer-set"; } diff --git a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs index 7c07e9617..4a243758d 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs @@ -46,7 +46,8 @@ public async Task GetFormSections_WhenFormIdExists_ShouldReturnOk() AllowsMultipleAnswerSets = true, AnswerSetCount = 1, SectionId = Guid.NewGuid(), - SectionName = "TestSection" + SectionName = "TestSection", + AnswerSetWithFurtherQuestionExemptedExists = false }] }; diff --git a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionsUseCaseTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionsUseCaseTest.cs index 5d22a90da..c8a327b29 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionsUseCaseTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/UseCase/GetFormSectionsUseCaseTest.cs @@ -40,7 +40,8 @@ public async Task Execute_ShouldReturnFormSectionResponse_WhenFormSummariesAreFo AnswerSetCount = 1, Type = FormSectionType.Standard, SectionId = sectionId, - SectionName = "TestSection" + SectionName = "TestSection", + AnswerSetWithFurtherQuestionExemptedExists = false }]; _repository.Setup(repo => repo.GetFormSummaryAsync(formId, organisationId)) diff --git a/Services/CO.CDP.Forms.WebApi/Model/FormSectionResponse.cs b/Services/CO.CDP.Forms.WebApi/Model/FormSectionResponse.cs index b8150e03b..8eb3345cd 100644 --- a/Services/CO.CDP.Forms.WebApi/Model/FormSectionResponse.cs +++ b/Services/CO.CDP.Forms.WebApi/Model/FormSectionResponse.cs @@ -12,4 +12,5 @@ public record FormSectionSummary public required FormSectionType Type { get; init; } public required bool AllowsMultipleAnswerSets { get; init; } public required int AnswerSetCount { get; init; } + public required bool AnswerSetWithFurtherQuestionExemptedExists { get; init; } } \ No newline at end of file diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseFormRepository.cs b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseFormRepository.cs index 8ef03fa05..1d9d142d6 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseFormRepository.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/DatabaseFormRepository.cs @@ -13,6 +13,8 @@ public void Dispose() private struct FormSectionGroupSelection { public int? FormId { get; set; } + + public bool? FurtherQuestionsExempted { get; set; } } public async Task> GetFormSummaryAsync(Guid formId, Guid organisationId) @@ -21,7 +23,7 @@ public async Task> GetFormSummaryAsync(Guid form join fas in context.FormAnswerSets on sc.Id equals fas.SharedConsentId join o in context.Organisations on sc.OrganisationId equals o.Id where o.Guid == organisationId && fas.Deleted == false - select new { sc.FormId, fas.SectionId, SharedConsentId = sc.Id }; + select new { sc.FormId, fas.SectionId, fas.FurtherQuestionsExempted, SharedConsentId = sc.Id }; var currentSharedConsent = await context.SharedConsents .OrderByDescending(x => x.CreatedOn) @@ -37,14 +39,16 @@ join fss in context.Set() on f.Id equals fss.FormId join subQuery in answersQuery on new { FormId = f.Id, SectionId = fss.Id } equals new { subQuery.FormId, subQuery.SectionId } into answers from answer in answers.DefaultIfEmpty() where f.Guid == formId - group new FormSectionGroupSelection { FormId = answer.FormId } by new { fss.Guid, fss.Title, fss.Type, fss.AllowsMultipleAnswerSets } into g + group new FormSectionGroupSelection { FormId = answer.FormId, FurtherQuestionsExempted = answer.FurtherQuestionsExempted } + by new { fss.Guid, fss.Title, fss.Type, fss.AllowsMultipleAnswerSets } into g select new FormSectionSummary { SectionId = g.Key.Guid, SectionName = g.Key.Title, Type = g.Key.Type, AllowsMultipleAnswerSets = g.Key.AllowsMultipleAnswerSets, - AnswerSetCount = g.Count(a => a.FormId != null) + AnswerSetCount = g.Count(a => a.FormId != null), + AnswerSetWithFurtherQuestionExemptedExists = g.Any(a => a.FurtherQuestionsExempted == true) }; return await query.ToListAsync(); diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormSectionSummary.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormSectionSummary.cs index fbc3c0cd8..7202ddda5 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormSectionSummary.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Forms/FormSectionSummary.cs @@ -7,4 +7,5 @@ public record FormSectionSummary public required FormSectionType Type { get; init; } public required bool AllowsMultipleAnswerSets { get; init; } public required int AnswerSetCount { get; init; } + public required bool AnswerSetWithFurtherQuestionExemptedExists { get; init; } } \ No newline at end of file From 87b3de3087724c01fbf34759df58dd7b0291eec9 Mon Sep 17 00:00:00 2001 From: Ali Bahman Date: Mon, 23 Sep 2024 15:43:02 +0100 Subject: [PATCH 18/30] DP-632 Include Direct URLs for all services in the API's landing page (#646) --- terragrunt/modules/api-gateway/locals.tf | 1 + .../modules/api-gateway/templates/landing-page.html.tftpl | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/terragrunt/modules/api-gateway/locals.tf b/terragrunt/modules/api-gateway/locals.tf index 487253c80..8e419a66d 100644 --- a/terragrunt/modules/api-gateway/locals.tf +++ b/terragrunt/modules/api-gateway/locals.tf @@ -15,6 +15,7 @@ locals { { name = service.name, url = "https://${aws_api_gateway_domain_name.ecs_api.domain_name}/${service.name}/swagger/index.html" + direct_url = "https://${service.name}.${var.public_hosted_zone_fqdn}/swagger/index.html" } ] diff --git a/terragrunt/modules/api-gateway/templates/landing-page.html.tftpl b/terragrunt/modules/api-gateway/templates/landing-page.html.tftpl index 5e42a34e3..db73daf37 100644 --- a/terragrunt/modules/api-gateway/templates/landing-page.html.tftpl +++ b/terragrunt/modules/api-gateway/templates/landing-page.html.tftpl @@ -30,6 +30,13 @@

API Endpoints: V${service_version}

+

ECS Direct URLs:

+ +

API Gateway URLs: (not in use)

    %{ for endpoint in endpoints }
  • ${endpoint.name}
  • From 2affe74e66c5f5638d41003c359396bc1e6c9d8f Mon Sep 17 00:00:00 2001 From: Dharm Date: Mon, 23 Sep 2024 17:32:43 +0100 Subject: [PATCH 19/30] Updated migration --- ...40923162413_ExclusionFormData.Designer.cs} | 33 +++++++++++- ...cs => 20240923162413_ExclusionFormData.cs} | 53 +++++++++---------- 2 files changed, 58 insertions(+), 28 deletions(-) rename Services/CO.CDP.OrganisationInformation.Persistence/Migrations/{20240919135601_ExclusionFormData.Designer.cs => 20240923162413_ExclusionFormData.Designer.cs} (98%) rename Services/CO.CDP.OrganisationInformation.Persistence/Migrations/{20240919135601_ExclusionFormData.cs => 20240923162413_ExclusionFormData.cs} (81%) diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.Designer.cs similarity index 98% rename from Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs rename to Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.Designer.cs index ec14d2fc8..0695c796c 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.Designer.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.Designer.cs @@ -13,7 +13,7 @@ namespace CO.CDP.OrganisationInformation.Persistence.Migrations { [DbContext(typeof(OrganisationInformationContext))] - [Migration("20240919135601_ExclusionFormData")] + [Migration("20240923162413_ExclusionFormData")] partial class ExclusionFormData { /// @@ -434,6 +434,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("is_required"); + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + b.Property("NextQuestionAlternativeId") .HasColumnType("integer") .HasColumnName("next_question_alternative_id"); @@ -473,6 +478,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) 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"); @@ -618,6 +627,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ApprovedById") + .HasColumnType("integer") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedComment") + .HasColumnType("text") + .HasColumnName("approved_comment"); + + b.Property("ApprovedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("approved_on"); + b.Property("CreatedOn") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -651,6 +672,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_organisations"); + b.HasIndex("ApprovedById") + .HasDatabaseName("ix_organisations_approved_by_id"); + b.HasIndex("Guid") .IsUnique() .HasDatabaseName("ix_organisations_guid"); @@ -1242,6 +1266,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById") + .HasConstraintName("fk_organisations_persons_approved_by_id"); + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Tenant", "Tenant") .WithMany("Organisations") .HasForeignKey("TenantId") @@ -1701,6 +1730,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Addresses"); + b.Navigation("ApprovedBy"); + b.Navigation("BuyerInfo"); b.Navigation("ContactPoints"); diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.cs similarity index 81% rename from Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs rename to Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.cs index 3c657e750..ecf0e64a0 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240919135601_ExclusionFormData.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240923162413_ExclusionFormData.cs @@ -3,15 +3,15 @@ #nullable disable -namespace CO.CDP.OrganisationInformation.Persistence.Migrations; - -/// -public partial class ExclusionFormData : Migration +namespace CO.CDP.OrganisationInformation.Persistence.Migrations { /// - protected override void Up(MigrationBuilder migrationBuilder) + public partial class ExclusionFormData : Migration { - migrationBuilder.Sql($@" + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($@" DO $$ DECLARE sectionId INT; @@ -19,42 +19,41 @@ protected override void Up(MigrationBuilder migrationBuilder) BEGIN SELECT id INTO sectionId FROM form_sections WHERE guid = '8a75cb04-fe29-45ae-90f9-168832dbea48'; - INSERT INTO form_questions (guid, section_id, type, is_required, title, description, options) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.CheckYourAnswers}, TRUE, 'Check your answers', NULL, '{{}}') + INSERT INTO form_questions (guid, section_id, type, is_required, title, description, options, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.CheckYourAnswers}, TRUE, 'Check your answers', NULL, '{{}}', '_Exclusion01') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Date}, previousQuestionId, TRUE, 'Have the circumstances that led to the exclusion ended?', '
    For example, a court decision for environmental misconduct led your organisation or the connected person to stop harming the environment.
    ', '{{}}', 'Enter the date the circumstances ended, For example, 05 04 2022' , 'Date circumstances ended') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Date}, previousQuestionId, TRUE, 'Have the circumstances that led to the exclusion ended?', '
    For example, a court decision for environmental misconduct led your organisation or the connected person to stop harming the environment.
    ', '{{}}', 'Enter the date the circumstances ended, For example, 05 04 2022' , 'Date circumstances ended', '_Exclusion02') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.FileUpload}, previousQuestionId, TRUE, 'Do you have a supporting document to upload?', '
    A decision from a public authority that was the basis for the offence. For example, documentation from the police, HMRC or the court.
    ', '{{}}', 'Upload a file, You can upload most file types including: PDF, scans, mobile phone photos, Word, Excel and PowerPoint, multimedia and ZIP files that are smaller than 25MB.', 'Supporting document') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.FileUpload}, previousQuestionId, TRUE, 'Do you have a supporting document to upload?', '
    A decision from a public authority that was the basis for the offence. For example, documentation from the police, HMRC or the court.
    ', '{{}}', 'Upload a file, You can upload most file types including: PDF, scans, mobile phone photos, Word, Excel and PowerPoint, multimedia and ZIP files that are smaller than 25MB.', 'Supporting document', '_Exclusion03') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'How the exclusion is being managed', '
    • have done to prove it was taken seriously - for example, paid a fine or compensation
    • have done to stop the circumstances that caused it from happening again - for example, taking steps like changing staff or management or putting procedures or training in place
    • are doing to monitor the steps that were taken - for example, regular meetings
    ', '{{}}','You must tell us what you or the person who was subject to the event:' , 'Exclusion being managed') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'How the exclusion is being managed', '
    • have done to prove it was taken seriously - for example, paid a fine or compensation
    • have done to stop the circumstances that caused it from happening again - for example, taking steps like changing staff or management or putting procedures or training in place
    • are doing to monitor the steps that were taken - for example, regular meetings
    ', '{{}}','You must tell us what you or the person who was subject to the event:' , 'Exclusion being managed', '_Exclusion04') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'Describe the exclusion in more detail', NULL, '{{}}', 'Give us your explanation of the event. For example, any background information you can give about what happened or what caused the exclusion.', 'Exclusion in detail') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.MultiLine}, previousQuestionId, TRUE, 'Describe the exclusion in more detail', NULL, '{{}}', 'Give us your explanation of the event. For example, any background information you can give about what happened or what caused the exclusion.', 'Exclusion in detail', '_Exclusion05') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Text}, previousQuestionId, TRUE, 'Enter the email address?', NULL, '{{}}', 'Where the contracting authority can contact someone about the exclusion', 'Contact email') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.Text}, previousQuestionId, TRUE, 'Enter the email address?', NULL, '{{}}', 'Where the contracting authority can contact someone about the exclusion', 'Contact email', '_Exclusion06') RETURNING id INTO previousQuestionId; - INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title) - VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.YesOrNo}, previousQuestionId, TRUE, 'Did this exclusion happen in the UK?', NULL, '{{}}', NULL, 'UK exclusion') + INSERT INTO form_questions (guid, section_id, type, next_question_id, is_required, title, description, options, caption, summary_title, name) + VALUES ('{Guid.NewGuid()}', sectionId, {(int)FormQuestionType.YesOrNo}, previousQuestionId, TRUE, 'Did this exclusion happen in the UK?', NULL, '{{}}', NULL, 'UK exclusion', '_Exclusion07') RETURNING id INTO previousQuestionId; - END $$; "); - } + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql($@" + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql($@" DO $$ DECLARE sectionId INT; @@ -64,6 +63,6 @@ protected override void Down(MigrationBuilder migrationBuilder) DELETE FROM form_questions WHERE section_id = sectionId; END $$; "); + } } - } From 6e6f47488704f11fba6599fd11f724a7a83a263b Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Tue, 24 Sep 2024 09:13:47 +0100 Subject: [PATCH 20/30] #dp-542 updated headings tags --- .../Pages/Forms/_FormElementMultiLineInput.cshtml | 2 +- .../Pages/Forms/_FormElementTextInput.cshtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml index f70fb8462..3ac7f6127 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - + } @if (!string.IsNullOrWhiteSpace(Model.Caption)) { diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml index 34c755070..219622f8b 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - + } @if (!string.IsNullOrWhiteSpace(Model.Caption)) { From 9b50d49c30f409d2c8460f02dfb679694528f748 Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Tue, 24 Sep 2024 09:26:53 +0100 Subject: [PATCH 21/30] #dp-542 updated ids for input boxes --- .../Pages/Forms/_FormElementMultiLineInput.cshtml | 4 ++-- .../Pages/Forms/_FormElementTextInput.cshtml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml index 3ac7f6127..d3e4aacee 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - + } @if (!string.IsNullOrWhiteSpace(Model.Caption)) { @@ -28,5 +28,5 @@

    } - +
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml index 219622f8b..0cc07fcc6 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - + } @if (!string.IsNullOrWhiteSpace(Model.Caption)) { @@ -28,5 +28,5 @@

} - +
\ No newline at end of file From 644cb8d680e582309e8f7f4828894e48ff0edbac Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Tue, 24 Sep 2024 09:36:56 +0100 Subject: [PATCH 22/30] #dp-542 accessibility updates --- .../Pages/Forms/_FormElementMultiLineInput.cshtml | 2 +- .../Pages/Forms/_FormElementTextInput.cshtml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml index d3e4aacee..767ed1a7c 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -28,5 +28,5 @@

} - +
\ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml index 0cc07fcc6..1fd7d4e4e 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - +

@Model.Heading

} @if (!string.IsNullOrWhiteSpace(Model.Caption)) { @@ -28,5 +28,5 @@

} - + \ No newline at end of file From b28dd66eefccb9831139260fa83f4cec55eb0c59 Mon Sep 17 00:00:00 2001 From: Jawwad Baig Date: Tue, 24 Sep 2024 09:37:56 +0100 Subject: [PATCH 23/30] #dp-542 updated label to h1 --- .../Pages/Forms/_FormElementMultiLineInput.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml index 767ed1a7c..e73e80820 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -8,7 +8,7 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { - +

@Model.Heading

} @if (!string.IsNullOrWhiteSpace(Model.Caption)) { From e16ccd96b7475adeeae370a6c8d89c93ed5282e9 Mon Sep 17 00:00:00 2001 From: JBaigGoaco <159906815+JBaigGoaco@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:45:46 +0100 Subject: [PATCH 24/30] Update Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml Co-authored-by: Andy Mantell <134642+andymantell@users.noreply.github.com> --- .../Pages/Forms/_FormElementTextInput.cshtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml index 1fd7d4e4e..92bcb90d5 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementTextInput.cshtml @@ -8,7 +8,9 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { -

@Model.Heading

+

+ +

} @if (!string.IsNullOrWhiteSpace(Model.Caption)) { From fd2ed86f1d86e41250ecdda0d02780d94b75bd07 Mon Sep 17 00:00:00 2001 From: JBaigGoaco <159906815+JBaigGoaco@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:45:51 +0100 Subject: [PATCH 25/30] Update Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml Co-authored-by: Andy Mantell <134642+andymantell@users.noreply.github.com> --- .../Pages/Forms/_FormElementMultiLineInput.cshtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml index e73e80820..878ba0596 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/_FormElementMultiLineInput.cshtml @@ -8,7 +8,9 @@ @if (!string.IsNullOrWhiteSpace(Model.Heading)) { -

@Model.Heading

+

+ +

} @if (!string.IsNullOrWhiteSpace(Model.Caption)) { From 87ff2ba0a7101336a74206adfb4b7831218e2b96 Mon Sep 17 00:00:00 2001 From: Dharmendra Verma <64859911+dharmverma@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:49:25 +0100 Subject: [PATCH 26/30] Part4: Person, Tenant, Forms, Entity Verification API endpoints authorization (#644) * Part4: Person, Tenant, Forms, Entity Verification API endpoints authorization * Fix failing test --- Libraries/CO.CDP.Authentication/Extensions.cs | 6 ++ .../Api/GetIdentifiersTests.cs | 35 +++++++++++ .../TestAuthorizationWebApplicationFactory.cs | 49 +++++++++++++++ .../Api/PponEndpointExtensions.cs | 4 +- .../Api/DeleteFormAnswerSetEndpointTest.cs | 25 ++++++++ .../Api/GetFormSectionQuestionsTest.cs | 30 ++++++++- .../Api/GetFormSectionstEndpointTest.cs | 25 ++++++++ .../Api/PutFormSectionAnswersTest.cs | 45 ++++++++++++++ Services/CO.CDP.Forms.WebApi/Api/Forms.cs | 25 ++++++-- .../Api/GetPersonTest.cs | 62 +++++++++++++++++++ Services/CO.CDP.Person.WebApi/Api/Person.cs | 11 ++-- .../UseCase/GetPersonUseCase.cs | 4 +- .../Api/TenantLookupTest.cs | 28 +++++++-- Services/CO.CDP.Tenant.WebApi/Api/Tenant.cs | 5 +- 14 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 Services/CO.CDP.EntityVerification.Tests/Api/GetIdentifiersTests.cs create mode 100644 Services/CO.CDP.EntityVerification.Tests/Api/TestAuthorizationWebApplicationFactory.cs create mode 100644 Services/CO.CDP.Forms.WebApi.Tests/Api/PutFormSectionAnswersTest.cs create mode 100644 Services/CO.CDP.Person.WebApi.Tests/Api/GetPersonTest.cs diff --git a/Libraries/CO.CDP.Authentication/Extensions.cs b/Libraries/CO.CDP.Authentication/Extensions.cs index f1b0bfc0c..4bc4fc72b 100644 --- a/Libraries/CO.CDP.Authentication/Extensions.cs +++ b/Libraries/CO.CDP.Authentication/Extensions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; +using static CO.CDP.Authentication.Constants; namespace CO.CDP.Authentication; @@ -86,6 +87,11 @@ public static AuthorizationBuilder AddEntityVerificationAuthorization(this IServ { return services .AddAuthorizationBuilder() + .AddPolicy("OneLoginPolicy", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim(ClaimType.Channel, Channel.OneLogin); + }) .SetFallbackPolicy( new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) diff --git a/Services/CO.CDP.EntityVerification.Tests/Api/GetIdentifiersTests.cs b/Services/CO.CDP.EntityVerification.Tests/Api/GetIdentifiersTests.cs new file mode 100644 index 000000000..fb34ce2df --- /dev/null +++ b/Services/CO.CDP.EntityVerification.Tests/Api/GetIdentifiersTests.cs @@ -0,0 +1,35 @@ +using CO.CDP.EntityVerification.Model; +using CO.CDP.EntityVerification.UseCase; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System.Net; +using static CO.CDP.Authentication.Constants; +using static System.Net.HttpStatusCode; + +namespace CO.CDP.EntityVerification.Tests.Api; + +public class GetIdentifiersTests +{ + private readonly Mock>> _lookupIdentifierUseCase = new(); + + [Theory] + [InlineData(OK, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetIdentifiers_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var identifier = "test_identifier"; + _lookupIdentifierUseCase.Setup(useCase => useCase.Execute(It.IsAny())) + .ReturnsAsync([new Identifier { Scheme = "SIC", Id = "01230", LegalName = "Acme Ltd" }]); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _lookupIdentifierUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/identifiers/{identifier}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.EntityVerification.Tests/Api/TestAuthorizationWebApplicationFactory.cs b/Services/CO.CDP.EntityVerification.Tests/Api/TestAuthorizationWebApplicationFactory.cs new file mode 100644 index 000000000..873c9c738 --- /dev/null +++ b/Services/CO.CDP.EntityVerification.Tests/Api/TestAuthorizationWebApplicationFactory.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Security.Claims; + +namespace CO.CDP.EntityVerification.Tests.Api; + +public class TestAuthorizationWebApplicationFactory( + string channel, + Action? serviceCollection = null) + : WebApplicationFactory where TProgram : class +{ + protected override IHost CreateHost(IHostBuilder builder) + { + if (serviceCollection != null) builder.ConfigureServices(serviceCollection); + + builder.ConfigureServices(services => + { + services.AddTransient(sp => new AuthorizationPolicyEvaluator( + ActivatorUtilities.CreateInstance(sp), channel)); + }); + + return base.CreateHost(builder); + } +} + +public class AuthorizationPolicyEvaluator(PolicyEvaluator innerEvaluator, string? channel) : IPolicyEvaluator +{ + const string JwtBearerScheme = "Bearer"; + + public async Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) + { + var claimsIdentity = new ClaimsIdentity(JwtBearerScheme); + if (!string.IsNullOrWhiteSpace(channel)) claimsIdentity.AddClaims([new Claim("channel", channel)]); + + return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), + new AuthenticationProperties(), JwtBearerScheme))); + } + + public Task AuthorizeAsync( + AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource) + { + return innerEvaluator.AuthorizeAsync(policy, authenticationResult, context, resource); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.EntityVerification/Api/PponEndpointExtensions.cs b/Services/CO.CDP.EntityVerification/Api/PponEndpointExtensions.cs index f5b0de893..dc52fa8c8 100644 --- a/Services/CO.CDP.EntityVerification/Api/PponEndpointExtensions.cs +++ b/Services/CO.CDP.EntityVerification/Api/PponEndpointExtensions.cs @@ -4,6 +4,7 @@ using CO.CDP.Swashbuckle.Filter; using CO.CDP.Swashbuckle.Security; using DotSwashbuckle.AspNetCore.SwaggerGen; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; @@ -14,7 +15,8 @@ public static class PponEndpointExtensions public static void UsePponEndpoints(this WebApplication app) { app.MapGet("/identifiers/{identifier}", - async (string identifier, IUseCase> useCase) => + [Authorize(Policy = "OneLoginPolicy")] + async (string identifier, IUseCase> useCase) => await useCase.Execute(new LookupIdentifierQuery(identifier)) .AndThen(identifier => identifier.Any() ? Results.Ok(identifier) : Results.NotFound())) .Produces>(StatusCodes.Status200OK, "application/json") diff --git a/Services/CO.CDP.Forms.WebApi.Tests/Api/DeleteFormAnswerSetEndpointTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/Api/DeleteFormAnswerSetEndpointTest.cs index 12d7c48ef..7b9dd0f67 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/Api/DeleteFormAnswerSetEndpointTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/Api/DeleteFormAnswerSetEndpointTest.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Hosting; using Moq; using System.Net; +using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; namespace CO.CDP.Forms.WebApi.Tests.Api; @@ -36,4 +37,28 @@ public async Task DeleteSupplierInformation_TestCases(bool useCaseResult, HttpSt response.StatusCode.Should().Be(expectedStatusCode); } + + [Theory] + [InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var answerSetId = Guid.NewGuid(); + var organisationId = Guid.NewGuid(); + var command = (organisationId, answerSetId); + + _deleteAnswerSetUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _deleteAnswerSetUseCase.Object)); + + var response = await factory.CreateClient().DeleteAsync($"/forms/answer-sets/{answerSetId}?organisation-id={organisationId}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } } \ No newline at end of file diff --git a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionQuestionsTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionQuestionsTest.cs index a4396b4a4..70251abcc 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionQuestionsTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionQuestionsTest.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; +using System.Net; +using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; namespace CO.CDP.Forms.WebApi.Tests.Api; @@ -19,7 +21,7 @@ public GetFormSectionQuestionsTest() TestWebApplicationFactory factory = new(builder => { builder.ConfigureServices(services => - services.AddScoped>(_ => _getFormSectionQuestionsUseCase.Object) + services.AddScoped(_ => _getFormSectionQuestionsUseCase.Object) ); }); _httpClient = factory.CreateClient(); @@ -59,6 +61,32 @@ public async Task ItFindsTheFormSectionWithQuestions() await response.Should().HaveContent(sectionQuestionsResponse); } + [Theory] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var formId = Guid.NewGuid(); + var sectionId = Guid.NewGuid(); + var organisationId = Guid.NewGuid(); + var command = (formId, sectionId, organisationId); + + _getFormSectionQuestionsUseCase.Setup(uc => uc.Execute(command)) + .ReturnsAsync(new SectionQuestionsResponse()); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _getFormSectionQuestionsUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/forms/{formId}/sections/{sectionId}/questions?organisation-id={organisationId}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + private static SectionQuestionsResponse GivenSectionQuestionsResponse() { var question1 = Guid.NewGuid(); diff --git a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs index 7c07e9617..68c1edbf3 100644 --- a/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs +++ b/Services/CO.CDP.Forms.WebApi.Tests/Api/GetFormSectionstEndpointTest.cs @@ -5,7 +5,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; +using System.Net; using System.Net.Http.Json; +using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; namespace CO.CDP.Forms.WebApi.Tests.Api; @@ -58,4 +60,27 @@ public async Task GetFormSections_WhenFormIdExists_ShouldReturnOk() response.Should().BeEquivalentTo(formSections); } + + [Theory] + [InlineData(OK, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetFormSections_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var formId = Guid.NewGuid(); + var organisationId = Guid.NewGuid(); + var command = (formId, organisationId); + + _useCase.Setup(uc => uc.Execute(command)) + .ReturnsAsync(new FormSectionResponse { FormSections = [] }); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _useCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/forms/{formId}/sections?organisation-id={organisationId}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } } \ No newline at end of file diff --git a/Services/CO.CDP.Forms.WebApi.Tests/Api/PutFormSectionAnswersTest.cs b/Services/CO.CDP.Forms.WebApi.Tests/Api/PutFormSectionAnswersTest.cs new file mode 100644 index 000000000..61e8bc9da --- /dev/null +++ b/Services/CO.CDP.Forms.WebApi.Tests/Api/PutFormSectionAnswersTest.cs @@ -0,0 +1,45 @@ +using CO.CDP.Forms.WebApi.Model; +using CO.CDP.Forms.WebApi.UseCase; +using CO.CDP.TestKit.Mvc; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System.Net; +using System.Net.Http.Json; +using static CO.CDP.Authentication.Constants; +using static System.Net.HttpStatusCode; + +namespace CO.CDP.Forms.WebApi.Tests.Api; + +public class PutFormSectionAnswersTest +{ + private readonly Mock> _updateFormSectionAnswersUseCase = new(); + + [Theory] + [InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Admin)] + [InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Editor)] + [InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel, string? scope = null) + { + var formId = Guid.NewGuid(); + var sectionId = Guid.NewGuid(); + var answerSetId = Guid.NewGuid(); + var organisationId = Guid.NewGuid(); + var updateFormSectionAnswers = new UpdateFormSectionAnswers(); + var command = (formId, sectionId, answerSetId, organisationId, updateFormSectionAnswers); + + _updateFormSectionAnswersUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _updateFormSectionAnswersUseCase.Object)); + + var response = await factory.CreateClient().PutAsJsonAsync( + $"/forms/{formId}/sections/{sectionId}/answers/{answerSetId}?organisation-id={organisationId}", updateFormSectionAnswers); + + response.StatusCode.Should().Be(expectedStatusCode); + } +} diff --git a/Services/CO.CDP.Forms.WebApi/Api/Forms.cs b/Services/CO.CDP.Forms.WebApi/Api/Forms.cs index c686a5a4f..9b57339d2 100644 --- a/Services/CO.CDP.Forms.WebApi/Api/Forms.cs +++ b/Services/CO.CDP.Forms.WebApi/Api/Forms.cs @@ -1,3 +1,4 @@ +using CO.CDP.Authentication.Authorization; using CO.CDP.Forms.WebApi.Model; using CO.CDP.Forms.WebApi.UseCase; using CO.CDP.Functional; @@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; using System.Reflection; +using static CO.CDP.Authentication.Constants; namespace CO.CDP.Forms.WebApi.Api; @@ -17,7 +19,8 @@ public static class EndpointExtensions public static void UseFormsEndpoints(this WebApplication app) { app.MapGet("/forms/{formId}/sections", - async (Guid formId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), FormSectionResponse?> useCase) => + [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + async (Guid formId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), FormSectionResponse?> useCase) => await useCase.Execute((formId, organisationId)) .AndThen(res => res != null ? Results.Ok(res) : Results.NotFound())) .Produces(StatusCodes.Status200OK, "application/json") @@ -37,7 +40,11 @@ await useCase.Execute((formId, organisationId)) }); app.MapGet("/forms/{formId}/sections/{sectionId}/questions", - async (Guid formId, Guid sectionId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid, Guid), SectionQuestionsResponse?> useCase) => + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer], + OrganisationIdLocation.QueryString)] + async (Guid formId, Guid sectionId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid, Guid), SectionQuestionsResponse?> useCase) => await useCase.Execute((formId, sectionId, organisationId)) .AndThen(sectionQuestions => sectionQuestions != null ? Results.Ok(sectionQuestions) : Results.NotFound())) .Produces(StatusCodes.Status200OK, "application/json") @@ -56,8 +63,12 @@ await useCase.Execute((formId, sectionId, organisationId)) return operation; }); - app.MapPut("/forms/{formId}/sections/{sectionId}/answers/{answerSetId}", async ( - Guid formId, Guid sectionId, Guid answerSetId, + app.MapPut("/forms/{formId}/sections/{sectionId}/answers/{answerSetId}", + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor], + OrganisationIdLocation.QueryString)] + async (Guid formId, Guid sectionId, Guid answerSetId, [FromQuery(Name = "organisation-id")] Guid organisationId, [FromBody] UpdateFormSectionAnswers updateFormSectionAnswers, IUseCase<(Guid formId, Guid sectionId, Guid answerSetId, Guid organisationId, UpdateFormSectionAnswers updateFormSectionAnswers), bool> updateFormSectionAnswersUseCase) => @@ -82,7 +93,11 @@ await useCase.Execute((formId, sectionId, organisationId)) }); app.MapDelete("/forms/answer-sets/{answerSetId}", - async (Guid answerSetId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), bool> useCase) => + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin], + [OrganisationPersonScope.Admin, OrganisationPersonScope.Editor], + OrganisationIdLocation.QueryString)] + async (Guid answerSetId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), bool> useCase) => await useCase.Execute((organisationId, answerSetId)) .AndThen(success => success ? Results.NoContent() : Results.NotFound())) .Produces(StatusCodes.Status204NoContent) diff --git a/Services/CO.CDP.Person.WebApi.Tests/Api/GetPersonTest.cs b/Services/CO.CDP.Person.WebApi.Tests/Api/GetPersonTest.cs new file mode 100644 index 000000000..dc2b4f319 --- /dev/null +++ b/Services/CO.CDP.Person.WebApi.Tests/Api/GetPersonTest.cs @@ -0,0 +1,62 @@ +using CO.CDP.OrganisationInformation.Persistence; +using CO.CDP.Person.WebApi.Model; +using CO.CDP.Person.WebApi.UseCase; +using CO.CDP.TestKit.Mvc; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System.Net; +using System.Net.Http.Json; +using static CO.CDP.Authentication.Constants; +using static System.Net.HttpStatusCode; + +namespace CO.CDP.Person.WebApi.Tests.Api; + +public class GetPersonTest +{ + private readonly Mock> _getPersonUseCase = new(); + private readonly Mock> _claimPersonInviteUseCase = new(); + + [Theory] + [InlineData(OK, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetSharedData_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var personId = Guid.NewGuid(); + _getPersonUseCase.Setup(useCase => useCase.Execute(personId)) + .ReturnsAsync(new Model.Person { Id = personId, FirstName = "fn", LastName = "ln", Email = "fn.ln@test" }); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _getPersonUseCase.Object)); + + var response = await factory.CreateClient().GetAsync($"/persons/{personId}"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [InlineData(NoContent, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task ClaimPersonInvite_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + var personId = Guid.NewGuid(); + var claimPersonInvite = new ClaimPersonInvite { PersonInviteId = Guid.NewGuid() }; + var command = (personId, claimPersonInvite); + + _claimPersonInviteUseCase.Setup(useCase => useCase.Execute(command)) + .ReturnsAsync(Mock.Of()); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _claimPersonInviteUseCase.Object)); + + var response = await factory.CreateClient().PostAsJsonAsync($"/persons/{personId}/claim-person-invite", claimPersonInvite); + + response.StatusCode.Should().Be(expectedStatusCode); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Person.WebApi/Api/Person.cs b/Services/CO.CDP.Person.WebApi/Api/Person.cs index eb020c63d..30e6dd150 100644 --- a/Services/CO.CDP.Person.WebApi/Api/Person.cs +++ b/Services/CO.CDP.Person.WebApi/Api/Person.cs @@ -1,3 +1,4 @@ +using CO.CDP.Authentication.Authorization; using CO.CDP.Functional; using CO.CDP.OrganisationInformation.Persistence; using CO.CDP.Person.WebApi.Model; @@ -49,7 +50,9 @@ await useCase.Execute(command) return operation; }); - app.MapGet("/persons/{personId}", async (Guid personId, IUseCase useCase) => + app.MapGet("/persons/{personId}", + [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + async (Guid personId, IUseCase useCase) => await useCase.Execute(personId) .AndThen(person => person != null ? Results.Ok(person) : Results.NotFound())) .Produces(200, "application/json") @@ -65,11 +68,11 @@ await useCase.Execute(personId) }); app.MapPost("/persons/{personId}/claim-person-invite", - async (Guid personId, ClaimPersonInvite command, IUseCase<(Guid, ClaimPersonInvite), PersonInvite> useCase) => + [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + async (Guid personId, ClaimPersonInvite command, IUseCase<(Guid, ClaimPersonInvite), PersonInvite> useCase) => await useCase.Execute((personId, command)) .AndThen(_ => Results.NoContent()) ) - .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status204NoContent) .ProducesProblem(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) @@ -81,7 +84,6 @@ await useCase.Execute((personId, command)) operation.OperationId = "ClaimPersonInvite"; operation.Description = "Claims a person invite."; operation.Summary = "Claims a person invite."; - operation.Responses["200"].Description = "Person invite claimed successfully."; operation.Responses["204"].Description = "Person invite claimed successfully."; operation.Responses["400"].Description = "Bad request."; operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; @@ -114,6 +116,7 @@ await useCase.Execute((personId, command)) operation.Responses["200"].Description = "Person updated."; return operation; }); + app.MapDelete("/persons/{personId}", (Guid personId) => { _persons.Remove(personId); diff --git a/Services/CO.CDP.Person.WebApi/UseCase/GetPersonUseCase.cs b/Services/CO.CDP.Person.WebApi/UseCase/GetPersonUseCase.cs index adb1686c3..934dcde36 100644 --- a/Services/CO.CDP.Person.WebApi/UseCase/GetPersonUseCase.cs +++ b/Services/CO.CDP.Person.WebApi/UseCase/GetPersonUseCase.cs @@ -6,9 +6,9 @@ namespace CO.CDP.Person.WebApi.UseCase; public class GetPersonUseCase(IPersonRepository personRepository, IMapper mapper) : IUseCase { - public async Task Execute(Guid tenantId) + public async Task Execute(Guid personId) { - return await personRepository.Find(tenantId) + return await personRepository.Find(personId) .AndThen(mapper.Map); } } \ No newline at end of file diff --git a/Services/CO.CDP.Tenant.WebApi.Tests/Api/TenantLookupTest.cs b/Services/CO.CDP.Tenant.WebApi.Tests/Api/TenantLookupTest.cs index b70cd39b0..5a08477c1 100644 --- a/Services/CO.CDP.Tenant.WebApi.Tests/Api/TenantLookupTest.cs +++ b/Services/CO.CDP.Tenant.WebApi.Tests/Api/TenantLookupTest.cs @@ -5,7 +5,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; +using System.Net; using System.Net.Http.Json; +using static CO.CDP.Authentication.Constants; using static System.Net.HttpStatusCode; namespace CO.CDP.Tenant.WebApi.Tests.Api; @@ -29,9 +31,8 @@ public TenantLookupTest() [Fact] public async Task IfNoTenantIsFound_ReturnsNotFound() { - _getTenantUseCase.Setup(useCase => useCase.Execute()) - .Returns(Task.FromResult(null)); + .ReturnsAsync((TenantLookup?)null); var response = await _httpClient.GetAsync($"/tenant/lookup"); @@ -44,14 +45,33 @@ public async Task IfTenantIsFound_ReturnsTenant() var lookup = GivenTenantLookup(); _getTenantUseCase.Setup(useCase => useCase.Execute()) - .Returns(Task.FromResult(lookup)); + .ReturnsAsync(lookup); - var response = await _httpClient.GetAsync($"/tenant/lookup"); + var response = await _httpClient.GetAsync("/tenant/lookup"); response.Should().HaveStatusCode(OK); (await response.Content.ReadFromJsonAsync()).Should().BeEquivalentTo(lookup); } + [Theory] + [InlineData(OK, Channel.OneLogin)] + [InlineData(Forbidden, Channel.ServiceKey)] + [InlineData(Forbidden, Channel.OrganisationKey)] + [InlineData(Forbidden, "unknown_channel")] + public async Task GetSharedData_Authorization_ReturnsExpectedStatusCode( + HttpStatusCode expectedStatusCode, string channel) + { + _getTenantUseCase.Setup(useCase => useCase.Execute()) + .ReturnsAsync(GivenTenantLookup()); + + var factory = new TestAuthorizationWebApplicationFactory( + channel, serviceCollection: s => s.AddScoped(_ => _getTenantUseCase.Object)); + + var response = await factory.CreateClient().GetAsync("/tenant/lookup"); + + response.StatusCode.Should().Be(expectedStatusCode); + } + private static TenantLookup GivenTenantLookup() { return new TenantLookup diff --git a/Services/CO.CDP.Tenant.WebApi/Api/Tenant.cs b/Services/CO.CDP.Tenant.WebApi/Api/Tenant.cs index 49c1b4f39..dc7c9682b 100644 --- a/Services/CO.CDP.Tenant.WebApi/Api/Tenant.cs +++ b/Services/CO.CDP.Tenant.WebApi/Api/Tenant.cs @@ -1,3 +1,4 @@ +using CO.CDP.Authentication.Authorization; using CO.CDP.Functional; using CO.CDP.OrganisationInformation; using CO.CDP.Swashbuckle.Filter; @@ -39,6 +40,7 @@ await useCase.Execute(command) operation.Responses["500"].Description = "Internal server error."; return operation; }); + app.MapGet("/tenants/{tenantId}", async (Guid tenantId, IUseCase useCase) => await useCase.Execute(tenantId) .AndThen(tenant => tenant != null ? Results.Ok(tenant) : Results.NotFound())) @@ -64,7 +66,8 @@ public static void UseTenantLookupEndpoints(this WebApplication app) var openApiTags = new List { new() { Name = "Tenant Lookup" } }; app.MapGet("/tenant/lookup", - async (IUseCase useCase) => + [OrganisationAuthorize([AuthenticationChannel.OneLogin])] + async (IUseCase useCase) => await useCase.Execute() .AndThen(tenant => tenant != null ? Results.Ok(tenant) : Results.NotFound())) .Produces(StatusCodes.Status200OK, "application/json") From e46a76742f6d2468d448c63cb3aaf9187755c259 Mon Sep 17 00:00:00 2001 From: dpatel017 Date: Tue, 24 Sep 2024 10:04:44 +0100 Subject: [PATCH 27/30] back link fix (#648) --- .../Pages/ApiKeyManagement/CreateApiKey.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml index b24c254b7..76dce99b0 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/ApiKeyManagement/CreateApiKey.cshtml @@ -4,7 +4,7 @@ @{ var apiKeyNameHasError = ((TagBuilder)Html.ValidationMessageFor(m => m.ApiKeyName)).HasInnerHtml; } -Back +Back
From 65213e0114aba93bfa6a663e61e80fef44df4295 Mon Sep 17 00:00:00 2001 From: Ali Bahman Date: Tue, 24 Sep 2024 10:46:01 +0100 Subject: [PATCH 28/30] DP-630 Lift IP restrictions when assuming Orchestrator's pen-testing IAM role (#647) --- terragrunt/modules/core-iam/locals.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terragrunt/modules/core-iam/locals.tf b/terragrunt/modules/core-iam/locals.tf index 106014e42..1b72b665c 100644 --- a/terragrunt/modules/core-iam/locals.tf +++ b/terragrunt/modules/core-iam/locals.tf @@ -6,5 +6,5 @@ locals { use_codestar_connection = var.environment != "orchestrator" pen_testing_user_arns = contains(["staging", "orchestrator"], var.environment) ? concat(var.pen_testing_user_arns, var.pen_testing_external_user_arns) : var.pen_testing_user_arns - pen_testing_allowed_ips = var.environment == "staging" ? [] : var.pen_testing_allowed_ips + pen_testing_allowed_ips = contains(["staging", "orchestrator"], var.environment) ? [] : var.pen_testing_allowed_ips } From afa8392eff78f84e5aff6a322738d853cd73cb73 Mon Sep 17 00:00:00 2001 From: Andy Mantell <134642+andymantell@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:20:21 +0100 Subject: [PATCH 29/30] Apply missing access control to supplier information link on org overview page --- .../Pages/Organisation/OrganisationOverview.cshtml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml index aa90e704c..4036f96ac 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Organisation/OrganisationOverview.cshtml @@ -99,9 +99,11 @@ @if (organisationDetails.IsTenderer()) { - + + + } else if (organisationDetails.IsBuyer()) { From 5dfed793230a6df02c3f90779d8190d49342e6ce Mon Sep 17 00:00:00 2001 From: Andy Mantell <134642+andymantell@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:21:06 +0100 Subject: [PATCH 30/30] Tweak access control so that admin users can do anything within an org --- .../AuthorizationTests.cs | 23 ++++++++++++++++--- .../Authorization/OrganisationScopeHandler.cs | 9 +++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs index 4028cf1b1..8d4d1d63c 100644 --- a/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs +++ b/Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs @@ -30,7 +30,7 @@ public HttpClient BuildHttpClient(List userScopes) "Org name", [ Tenant.WebApiClient.PartyRole.Supplier, - Tenant.WebApiClient.PartyRole.ProcuringEntity + Tenant.WebApiClient.PartyRole.Tenderer ], userScopes, new Uri("http://foo") @@ -78,7 +78,7 @@ [ organisation ] testOrganisationId, new Identifier("asd", "asd", "asd", new Uri("http://whatever")), "Org name", - [ CO.CDP.Organisation.WebApiClient.PartyRole.Supplier, CO.CDP.Organisation.WebApiClient.PartyRole.ProcuringEntity ] + [ CO.CDP.Organisation.WebApiClient.PartyRole.Supplier, CO.CDP.Organisation.WebApiClient.PartyRole.Tenderer ] ) ); @@ -186,7 +186,7 @@ public async Task TestCanSeeUsersLinkOnOrganisationPage_WhenUserIsAllowedToAcces [Fact] public async Task TestCannotSeeUsersLinkOnOrganisationPage_WhenUserIsNotAllowedToAccessResourceAsEditorUser() { - var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Viewer ]); + var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Editor ]); var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}"); @@ -201,4 +201,21 @@ public async Task TestCannotSeeUsersLinkOnOrganisationPage_WhenUserIsNotAllowedT responseBody.Should().NotContain($"href=\"/organisation/{testOrganisationId}/users/user-summary\">Users"); responseBody.Should().NotContain("Users"); } + + [Fact] + public async Task TestCanSeeLinkToSupplierInformation_WhenUserIsAdmin() + { + var _httpClient = BuildHttpClient([OrganisationPersonScopes.Admin]); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}"); + + var response = await _httpClient.SendAsync(request); + + var responseBody = await response.Content.ReadAsStringAsync(); + + responseBody.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + responseBody.Should().Contain("Supplier information"); + } } diff --git a/Frontend/CO.CDP.OrganisationApp/Authorization/OrganisationScopeHandler.cs b/Frontend/CO.CDP.OrganisationApp/Authorization/OrganisationScopeHandler.cs index 78bd3dde7..26ca3e61b 100644 --- a/Frontend/CO.CDP.OrganisationApp/Authorization/OrganisationScopeHandler.cs +++ b/Frontend/CO.CDP.OrganisationApp/Authorization/OrganisationScopeHandler.cs @@ -35,13 +35,20 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var scopes = await _userInfo.GetOrganisationUserScopes(); + // Admin role can do anything within this organisation + if (scopes.Contains(OrganisationPersonScopes.Admin)) + { + context.Succeed(requirement); + return; + } + if (scopes.Contains(requirement.Scope)) { context.Succeed(requirement); return; } - // Editor role implies viewer permissions also + // Editor role implies viewer permissions if (requirement.Scope == OrganisationPersonScopes.Viewer && scopes.Contains(OrganisationPersonScopes.Editor)) { context.Succeed(requirement);