diff --git a/.gitignore b/.gitignore index e7682d20..bc9acb7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,5 @@ node_modules mnt note **/Properties/launchSettings.json -UITest secrets .terraform \ No newline at end of file diff --git a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature index 2fcbe922..62688ae9 100644 --- a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature +++ b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature @@ -11,14 +11,27 @@ When I navigate to the delisting request feature #Input Form Then I should be presented with an input form that includes fields for the listing URL -And I should be presented with a field to select which platform to send the request to +And I Should be Presented with an Input form that Lists requests Initiated By +And I should be presented with a field to select which platform to send the request to And I should see an optional field for adding a LG staff user email address to be copied on the email +#Initiated By + +When Selecting the LG for Initiated By + +Then The system should present a list of available LG options to populate the field + +#ListingIDField + +When Entering the listing ID "0" + +Then The system should validate the ID is a number + #ListingURLField -When Entering the listing URL "ListingURL" +When Entering the listing URL "http://listingURL.com" Then The system should validate the URL format and ensure it is a valid link to the property listing diff --git a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature.cs b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature.cs index ae5069b0..07c61e4a 100644 --- a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature.cs +++ b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingRequestWithoutADSSListing.feature.cs @@ -113,66 +113,81 @@ public virtual void SendDelistingRequestWithoutADSSListing() "", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden #line 14 +testRunner.And("I Should be Presented with an Input form that Lists requests Initiated By", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 16 testRunner.And("I should be presented with a field to select which platform to send the request t" + "o", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden -#line 17 +#line 18 testRunner.And("I should see an optional field for adding a LG staff user email address to be cop" + "ied on the email", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden -#line 21 -testRunner.When("Entering the listing URL \"ListingURL\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 22 +testRunner.When("Selecting the LG for Initiated By", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 24 +testRunner.Then("The system should present a list of available LG options to populate the field", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 28 +testRunner.When("Entering the listing ID \"0\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 30 +testRunner.Then("The system should validate the ID is a number", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 23 +#line 34 +testRunner.When("Entering the listing URL \"http://listingURL.com\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 36 testRunner.Then("The system should validate the URL format and ensure it is a valid link to the pr" + "operty listing", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 26 +#line 39 testRunner.When("selecting the platform", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 28 +#line 41 testRunner.Then("the system should present a list of available platform options to populate the fi" + "eld", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 32 +#line 45 testRunner.When("all required fields are entered", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 34 +#line 47 testRunner.Then("I click the review button", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 36 +#line 49 testRunner.Then("I see a template delisting request message that will be sent to both the platform" + "", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 40 +#line 53 testRunner.When("I submit the form with valid information", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 42 +#line 55 testRunner.Then("the system should send the delisting request message to the platform email addres" + "ses associated with the selected platform", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 46 +#line 59 testRunner.When("successful submission", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 48 +#line 61 testRunner.Then("I should receive a confirmation message indicating that the delisting request has" + " been sent", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 50 +#line 63 testRunner.Then("I should be copied on the email", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 53 +#line 66 testRunner.When("the delisting request is submitted", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 55 +#line 68 testRunner.Then("the platform and host should receive email notifications containing the delisting" + " request and instructions for compliance", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 59 +#line 72 testRunner.When("there are issues with the submission, such as invalid email addresses or a missin" + "g URL", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 61 +#line 74 testRunner.Then("the system should provide clear error messages guiding me on how to correct the i" + "ssues", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden diff --git a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature index a720ba9f..a607a2b2 100644 --- a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature +++ b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature @@ -15,6 +15,8 @@ And I should be presented with a field to select which platform to send the warn And I should be presented with a dropdown menu to select reason for delisting +And I should see an optional field for Listing ID + And I should see an optional field for adding a LG staff user email address to be copied on the email #ListingURLField diff --git a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature.cs b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature.cs index 35c1dd56..7dd77c19 100644 --- a/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature.cs +++ b/Test/UITest/SpecFlowProjectBDD/Features/SendDelistingWarningWithoutADSSListing.feature.cs @@ -120,73 +120,76 @@ public virtual void SendDelistingWarningWithoutADSSListing() testRunner.And("I should be presented with a dropdown menu to select reason for delisting", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden #line 18 +testRunner.And("I should see an optional field for Listing ID", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 20 testRunner.And("I should see an optional field for adding a LG staff user email address to be cop" + "ied on the email", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden -#line 22 +#line 24 testRunner.When("Entering the listing URL \"ListingURL\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 24 +#line 26 testRunner.Then("The system should validate the URL format and ensure it is a valid link to the pr" + "operty listing", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 27 +#line 29 testRunner.When("selecting the platform", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 29 +#line 31 testRunner.Then("the system should present a list of available platform options to populate the fi" + "eld", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 32 +#line 34 testRunner.When("entering the optional host email address", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 34 +#line 36 testRunner.Then("the system should validate the email format", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 38 +#line 40 testRunner.When("I select a reason for delisting", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 40 +#line 42 testRunner.Then("the system should present a list of reasons for requesting delisting: No business" + " licence provided, invalid business licence number, expired business licence, or" + " suspended business license", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 44 +#line 46 testRunner.When("all required fields are entered", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 46 +#line 48 testRunner.Then("I see a template delisting warning message that will be sent to both the platform" + " and host", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 50 +#line 52 testRunner.When("I submit the form with valid information", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 52 +#line 54 testRunner.Then("the system should send the delisting warning message to the provided platform and" + " host email addresses", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 56 +#line 58 testRunner.When("successful submission", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 58 +#line 60 testRunner.Then("I should receive a confirmation message indicating that the delisting warning has" + " been sent", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 60 +#line 62 testRunner.Then("I should be copied on the email", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 63 +#line 65 testRunner.When("the delisting warning is submitted", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 65 +#line 67 testRunner.Then("the platform and host should receive email notifications containing the delisting" + " warning and instructions for compliance", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden -#line 69 +#line 71 testRunner.When("there are issues with the submission, such as invalid email addresses or a missin" + "g URL", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden -#line 71 +#line 73 testRunner.Then("the system should provide clear error messages guiding me on how to correct the i" + "ssues", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden diff --git a/Test/UITest/SpecFlowProjectBDD/Hooks/SeleniumSpecFlowHooks.cs b/Test/UITest/SpecFlowProjectBDD/Hooks/SeleniumSpecFlowHooks.cs index 829245f8..411e3be1 100644 --- a/Test/UITest/SpecFlowProjectBDD/Hooks/SeleniumSpecFlowHooks.cs +++ b/Test/UITest/SpecFlowProjectBDD/Hooks/SeleniumSpecFlowHooks.cs @@ -1,11 +1,4 @@ using BoDi; -using OpenQA.Selenium; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; using UITest.TestDriver; namespace SpecFlowProjectBDD.Hooks @@ -24,6 +17,7 @@ public SeleniumSpecFlowHooks(IObjectContainer container) public void SetupDrivers() { SeleniumDriver webDriver = new SeleniumDriver(SeleniumDriver.DRIVERTYPE.CHROME); + webDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5); _Container.RegisterInstanceAs(webDriver); } diff --git a/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingRequestWithoutADSSListingStepDefinitions.cs b/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingRequestWithoutADSSListingStepDefinitions.cs index b7f3b8a4..aa9807db 100644 --- a/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingRequestWithoutADSSListingStepDefinitions.cs +++ b/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingRequestWithoutADSSListingStepDefinitions.cs @@ -1,21 +1,8 @@ -using Models; -using NUnit.Framework; -using System; - - -using Microsoft.VisualStudio.TestPlatform.Utilities; -using Models; -using NUnit.Framework; +using NUnit.Framework; using UITest.PageObjects; using UITest.TestDriver; -using UITest.TestEngine; -using System.Reflection; -using System.ComponentModel.DataAnnotations; -using System.Security.Policy; -using System.Collections.Generic; using Configuration; using TestFrameWork.Models; -using System.Reflection.Metadata; namespace SpecFlowProjectBDD.StepDefinitions { @@ -25,6 +12,7 @@ public sealed class SendDelistingRequestWithoutADSSListingStepDefinitions { HomePage _HomePage; DelistingRequestPage _DelistingRequestPage; + TakeDownRequestPage _TakeDownRequestPage; PathFinderPage _PathFinderPage; IDRLoginPage _IDRLoginPage; string _TestUserName; @@ -38,6 +26,7 @@ public SendDelistingRequestWithoutADSSListingStepDefinitions(SeleniumDriver Driv _driver = Driver; _HomePage = new HomePage(_driver); _DelistingRequestPage = new DelistingRequestPage(_driver); + _TakeDownRequestPage = new TakeDownRequestPage(_driver); _PathFinderPage = new PathFinderPage(_driver); _IDRLoginPage = new IDRLoginPage(_driver); AppSettings appSettings = new AppSettings(); @@ -65,15 +54,21 @@ public void GivenIAmAauthenticatedLGStaffMemberUser() [When("I navigate to the delisting request feature")] public void WhenINavigateToTheDelistingRequestFeature() { - _driver.Url = "http://127.0.0.1:4200/delisting-request"; - _driver.Navigate(); - + //_driver.Url = "http://127.0.0.1:4200/delisting-request"; + //_driver.Navigate(); } //Input Form [Then("I should be presented with an input form that includes fields for the listing URL")] public void IshouldBePresentedWithAnInputFormThatIncludesFields() { + + } + + [Then("I Should be Presented with an Input form that Lists requests Initiated By")] + public void IShouldBePresentedWithAnInputFormThatListsRequestsInitiatedBy() + { + } [Then("I should be presented with a field to select which platform to send the request to")] @@ -82,11 +77,6 @@ public void IShouldBePresentedWithAFieldToSelectWhichPlatformToSendTheRequestTo( _DelistingRequestPage.PlatformReceipientDropdown.Click(); _DelistingRequestPage.PlatformReceipientDropdown.Click(); } - - //[Then("I should be presented with a dropdown menu to select reason for delisting")] - //public void IShouldBePresentedWithADropdownMenuToSelectReasonForDelisting() - //{ - //} [Then("I should see an optional field for adding a LG staff user email address to be copied on the email")] public void IShouldSeeAnOptionalFieldForAddingALGStaffUserEmailAddressToBeCopiedOnTheEmail() @@ -94,11 +84,23 @@ public void IShouldSeeAnOptionalFieldForAddingALGStaffUserEmailAddressToBeCopied _DelistingRequestPage.AdditionalCCsTextBox.EnterText("foo@foo.com"); } + //ListingID + [When(@"Entering the listing ID ""(.*)""")] + public void WhenEnteringTheListingID(String ID) + { + _DelistingRequestPage.ListingIDNumberTextBox.EnterText(ID); + } + + [Then("The system should validate the ID is a number")] + public void TheSystemShouldValidateTheIDFormat() + { + } + //ListingURLField [When(@"Entering the listing URL ""(.*)""")] public void WhenEnteringTheListingURL(String URL) { - _DelistingRequestPage.ListingUrlTextBox.EnterText("http://listingUrl.com"); + _DelistingRequestPage.ListingUrlTextBox.EnterText(URL); } [Then("The system should validate the URL format and ensure it is a valid link to the property listing")] @@ -106,11 +108,27 @@ public void TheSystemShouldValidateTheURLFormat() { } + //RequestInitiaitedByField + [When("Selecting the LG for Initiated By")] + public void SelectingTheLGForInitiatedBy() + { + _DelistingRequestPage.RequestInitiatedByDropDown.Click(); + _DelistingRequestPage.RequestInitiatedByDropDown.ExecuteJavaScript(@"document.querySelector(""#lgId_0"").click()"); + //Assert.IsTrue(_DelistingRequestPage.PlatformReceipientDropdown.Text.Contains("AIRBNB")); + } + + [Then("The system should present a list of available LG options to populate the field")] + public void TheSystemShouldPresentAListOfLGOptions() + { + } + //PlatformField [When("selecting the platform")] public void WhenSelectingThePlatform() { - + _DelistingRequestPage.PlatformReceipientDropdown.Click(); + _DelistingRequestPage.PlatformReceipientDropdown.ExecuteJavaScript(@"document.querySelector(""#platformId_0"").click()"); + Assert.IsTrue( _DelistingRequestPage.PlatformReceipientDropdown.Text.ToUpper().Contains("AIRBNB")); } [Then("the system should present a list of available platform options to populate the field")] @@ -120,7 +138,7 @@ public void TheSystemShouldPresentAListOfAvailablePlatformOption() //DelistingRequestMessage [When("all required fields are entered")] - public void WhenALlRequiredFieldsAreEntered() + public void WhenAllRequiredFieldsAreEntered() { } @@ -138,12 +156,14 @@ public void ThenISeeATemplateDelistingRequestMessage() //SendDelistingRequest [When("I submit the form with valid information")] public void WhenISubmitTheFormWithValidInformation() - { + { + _TakeDownRequestPage.SubmitButton.Click(); } [Then("the system should send the delisting request message to the platform email addresses associated with the selected platform")] public void ThenTheSystemShouldSendTheDelistingRequestMessage() - { + { + _DelistingRequestPage.ReturnHomeButton.Click(); } //ConfirmationMessage diff --git a/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingWarningWithoutADSSListingStepDefinitions.cs b/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingWarningWithoutADSSListingStepDefinitions.cs index d0de112e..90dbc72b 100644 --- a/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingWarningWithoutADSSListingStepDefinitions.cs +++ b/Test/UITest/SpecFlowProjectBDD/StepDefinitions/SendDelistingWarningWithoutADSSListingStepDefinitions.cs @@ -1,18 +1,8 @@ -using Models; -using NUnit.Framework; -using System; - - -using Microsoft.VisualStudio.TestPlatform.Utilities; -using Models; -using NUnit.Framework; +using NUnit.Framework; using UITest.PageObjects; using UITest.TestDriver; -using UITest.TestEngine; -using System.Reflection; -using System.ComponentModel.DataAnnotations; -using System.Security.Policy; -using System.Collections.Generic; +using TestFrameWork.Models; +using Configuration; namespace SpecFlowProjectBDD.StepDefinitions { @@ -21,28 +11,53 @@ namespace SpecFlowProjectBDD.StepDefinitions public sealed class SendDelistingWarningWithoutADSSListingStepDefinitions { HomePage _HomePage; + + IDriver _Driver; + DelistingWarningPage _DelistingWarningPage; - IDriver _driver; + PathFinderPage _PathFinderPage; + IDRLoginPage _IDRLoginPage; + NoticeOfTakeDownPage _NoticeOfTakeDownPage; + + string _TestUserName; + string _TestPassword; + - public SendDelistingWarningWithoutADSSListingStepDefinitions(SeleniumDriver Driver) { - _driver = Driver; - _HomePage = new HomePage(_driver); - _DelistingWarningPage = new DelistingWarningPage(_driver); + _Driver = Driver; + _HomePage = new HomePage(_Driver); + _DelistingWarningPage = new DelistingWarningPage(_Driver); + _NoticeOfTakeDownPage = new NoticeOfTakeDownPage(_Driver); + _PathFinderPage = new PathFinderPage(_Driver); + _IDRLoginPage = new IDRLoginPage(_Driver); + + AppSettings appSettings = new AppSettings(); + _TestUserName = appSettings.GetValue("TestUserName") ?? string.Empty; + _TestPassword = appSettings.GetValue("TestPassword") ?? string.Empty; } //User Authentication [Given("I am an authenticated LG staff member")] public void GivenIAmAauthenticatedLGStaffMemberUser() { + _Driver.Url = "http://127.0.0.1:4200/compliance-notice"; + _Driver.Navigate(); + + _PathFinderPage.IDRButton.Click(); + + _IDRLoginPage.UserNameTextBox.EnterText(_TestUserName); + _IDRLoginPage.PasswordTextBox.EnterText(_TestPassword); + + _IDRLoginPage.ContinueButton.Click(); } [When("I navigate to the delisting warning feature")] public void WhenINavigateToTheDelistingWarningFeature() { - + //_Driver.Url = "http://127.0.0.1:4200/compliance-notice"; + //_Driver.Navigate(); } //Input Form @@ -54,22 +69,35 @@ public void IshouldBePresentedWithAnInputFormThatIncludesFields() [Then("I should be presented with a field to select which platform to send the warning to")] public void IShouldBePresentedWithAFieldToSelectWhichPlatformToSendTheWarningTo() { + _DelistingWarningPage.PlatformReceipientDropdown.Click(); + _DelistingWarningPage.PlatformReceipientDropdown.Click(); } [Then("I should be presented with a dropdown menu to select reason for delisting")] public void IShouldBePresentedWithADropdownMenuToSelectReasonForDelisting() { + _DelistingWarningPage.ReasonDropdown.Click(); + _DelistingWarningPage.ReasonDropdown.Click(); + } + + [Then("I should see an optional field for Listing ID")] + public void IIhouldDeeAnOptionalFieldForListingID() + { + //add listing ID + _DelistingWarningPage.ListingIDNumberTextBox.EnterText("0"); } [Then("I should see an optional field for adding a LG staff user email address to be copied on the email")] public void IShouldSeeAnOptionalFieldForAddingALGStaffUserEmailAddressToBeCopiedOnTheEmail() { + _DelistingWarningPage.LocalGovEmailTextBox.EnterText("Local@gov.com"); } //ListingURLField [When(@"Entering the listing URL ""(.*)""")] public void WhenEnteringTheListingURL(String URL) { + _DelistingWarningPage.ListingUrlTextBox.EnterText("http://listingUrl.com"); } [Then("The system should validate the URL format and ensure it is a valid link to the property listing")] @@ -81,19 +109,23 @@ public void TheSystemShouldValidateTheURLFormat() [When("selecting the platform")] public void WhenSelectingThePlatform() { + _DelistingWarningPage.PlatformReceipientDropdown.Click(); } [Then("the system should present a list of available platform options to populate the field")] public void TheSystemShouldPresentAListOfAvailablePlatformOption() { + _DelistingWarningPage.PlatformReceipientDropdown.ExecuteJavaScript(@"document.querySelector(""#platformId_0"").click()"); + Assert.IsTrue(_DelistingWarningPage.PlatformReceipientDropdown.Text.ToUpper().Contains("AIRBNB")); } //HostEmailAddressField [When("entering the optional host email address")] - public void WhenENteringTheOptionalHostEmailAddress() - { + public void WhenEnteringTheOptionalHostEmailAddress() + { + _DelistingWarningPage.HostEmailAddressTextBox.EnterText("host@foo.com"); } @@ -104,49 +136,81 @@ public void ThenTheSystemShouldValidateTheEmailFormat() //ReasonForDelisting [When("I select a reason for delisting")] - public void WhenISelectAReasonForDelisting() { } + public void WhenISelectAReasonForDelisting() + { + _DelistingWarningPage.ReasonDropdown.Click(); + _DelistingWarningPage.ReasonDropdown.ExecuteJavaScript(@"document.querySelector(""#reasonId_0"").click()"); + } [Then("the system should present a list of reasons for requesting delisting: No business licence provided, invalid business licence number, expired business licence, or suspended business license")] - public void ThenTheSystemShouldPresentAListOfReasonsForRequestingDelisting() { } - + public void ThenTheSystemShouldPresentAListOfReasonsForRequestingDelisting() + { + + } //DelistingWarningMessage [When("all required fields are entered")] - public void WhenALlRequiredFieldsAreEntered() { } + public void WhenALlRequiredFieldsAreEnteredandIClickTheReviewButton() + { + _DelistingWarningPage.ReviewButton.Click(); + } [Then("I see a template delisting warning message that will be sent to both the platform and host")] - public void ThenISeeATemplateDelistingWarningMessage() { } + public void ThenISeeATemplateDelistingWarningMessage() + { + } //SendDelistingRequest [When("I submit the form with valid information")] - public void WhenISubmitTheFormWithValidInformation() { } + public void WhenISubmitTheFormWithValidInformation() + { + + _NoticeOfTakeDownPage.CommentsTextBox.EnterText("get a business license"); + _NoticeOfTakeDownPage.SubmitButton.Click(); + } [Then("the system should send the delisting warning message to the provided platform and host email addresses")] - public void ThenTheSystemShouldSendTheDelistingWarningMessage() { } + public void ThenTheSystemShouldSendTheDelistingWarningMessage() + { + } //ConfirmationMessage [When("successful submission")] - public void WhenSuccessfulSubmission() { } + public void WhenSuccessfulSubmission() + { + } [Then("I should receive a confirmation message indicating that the delisting warning has been sent")] - public void ThenIShouldReceiveAConfirmationMessage() { } + public void ThenIShouldReceiveAConfirmationMessage() + { + } [Then("I should be copied on the email")] - public void ThenIShouldBeCopiedOnTheEmail() { } + public void ThenIShouldBeCopiedOnTheEmail() + { + } //NotificationToPlatformAndHost [When("the delisting warning is submitted")] - public void WhenTheDelistingWarningIsSubmitted() { } + public void WhenTheDelistingWarningIsSubmitted() + { + } [Then("the platform and host should receive email notifications containing the delisting warning and instructions for compliance")] - public void ThenThePlatformAndHostShouldReceiveEmailNotifications() { } + public void ThenThePlatformAndHostShouldReceiveEmailNotifications() + { + } //FrontEndErrorHandling [When("there are issues with the submission, such as invalid email addresses or a missing URL")] - public void WhenThereAreIssuesWithTheSubmission() { } + public void WhenThereAreIssuesWithTheSubmission() + { + } [Then("the system should provide clear error messages guiding me on how to correct the issues")] - public void ThenTheSystemShouldProvideClearErrorMessages() { } + public void ThenTheSystemShouldProvideClearErrorMessages() + { + } } } diff --git a/Test/UITest/TestFrameWork/Models/DelistingRequestModel.cs b/Test/UITest/TestFrameWork/Models/DelistingRequestModel.cs index f8cb0c82..d37eb01b 100644 --- a/Test/UITest/TestFrameWork/Models/DelistingRequestModel.cs +++ b/Test/UITest/TestFrameWork/Models/DelistingRequestModel.cs @@ -1,15 +1,16 @@ -using OpenQA.Selenium.DevTools.V118.DOM; - -namespace UITest.Models +namespace UITest.Models { public class DelistingRequestModel { - public static string RequestInitiatedByDropDown { get => ""; } public static string PlaformRecepientDropDown { get => "platformId"; } - public static string ListingIDNumberTextBox { get => ""; } + + public static string RequestInitiatedByDropDown { get => "lgId"; } + public static string ListingIDNumberTextBox { get => "listingId"; } public static string ListingUrlTextBox { get => "listingUrl"; } public static string SendCopyCheckbox { get => "sendCopy"; } - public static string AdditionalCCsTextBox { get => "#ccList > div > ul > li > input[type=text]"; } + public static string AdditionalCCsTextBox { get => "ccList"; } public static string ReviewButton { get => "form-preview-btn"; } + + public static string ReturnHomeButton { get => "return-home-btn"; } } } diff --git a/Test/UITest/TestFrameWork/Models/DelistingWarningModel.cs b/Test/UITest/TestFrameWork/Models/DelistingWarningModel.cs index ad50f934..0138d6d4 100644 --- a/Test/UITest/TestFrameWork/Models/DelistingWarningModel.cs +++ b/Test/UITest/TestFrameWork/Models/DelistingWarningModel.cs @@ -4,24 +4,28 @@ public class DelistingWarningModel { public static string PlatformReceipientDropDown { get => "platformId"; } - public static string ListingIDNumberTextBox { get => ""; } + public static string ListingIDNumberTextBox { get => "listingId"; } public static string ListingUrlTextBox { get => "listingUrl"; } public static string HostEmailAddressTextBox { get => "hostEmail"; } + //public static string AlternativeNoticeSentCheckbox { get => "sentAlternativelyInput"; } + + public static string AlternativeNoticeSentCheckbox { get => "#sentAlternatively > div > div.p-checkbox-box"; } + public static string ReasonDropdown { get => "reasonId"; } public static string SendCopyCheckbox { get => "sendCopy"; } public static string AdditionalCCsTextBox { get => "ccList"; } - public static string LocalGovEmailTextBox { get => ""; } + public static string LocalGovEmailTextBox { get => "LgContactEmail"; } - public static string LocalGovPhoneTextBox { get => ""; } + public static string LocalGovPhoneTextBox { get => "LgContactPhone"; } - public static string LocalGovUrlTextBox { get => ""; } + public static string LocalGovUrlTextBox { get => "StrBylawUrl"; } - public static string ReviewButton { get => "Review"; } + public static string ReviewButton { get => "form-preview-btn"; } } } diff --git a/Test/UITest/TestFrameWork/Models/NoticeOfTakeDownModel.cs b/Test/UITest/TestFrameWork/Models/NoticeOfTakeDownModel.cs index d0efad79..39236970 100644 --- a/Test/UITest/TestFrameWork/Models/NoticeOfTakeDownModel.cs +++ b/Test/UITest/TestFrameWork/Models/NoticeOfTakeDownModel.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TestFrameWork.Models +namespace TestFrameWork.Models { public class NoticeOfTakeDownModel { diff --git a/Test/UITest/TestFrameWork/Models/TakeDownRequestModel.cs b/Test/UITest/TestFrameWork/Models/TakeDownRequestModel.cs index e312405a..40634896 100644 --- a/Test/UITest/TestFrameWork/Models/TakeDownRequestModel.cs +++ b/Test/UITest/TestFrameWork/Models/TakeDownRequestModel.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TestFrameWork.Models +namespace TestFrameWork.Models { public class TakeDownRequestModel { diff --git a/Test/UITest/TestFrameWork/PageObjects/DelistingRequestPage.cs b/Test/UITest/TestFrameWork/PageObjects/DelistingRequestPage.cs index 190792c7..b14e4b8a 100644 --- a/Test/UITest/TestFrameWork/PageObjects/DelistingRequestPage.cs +++ b/Test/UITest/TestFrameWork/PageObjects/DelistingRequestPage.cs @@ -7,34 +7,41 @@ namespace UITest.PageObjects { public class DelistingRequestPage { - private DropDownList _RequestInitiatedByDropDown; + //private DropDownList _RequestInitiatedByDropDown; private DropDownList _PlatformReceipientDropdown; + private DropDownList _RequestInitiatedByDropDown; private TextBox _ListingIDNumberTextBox; private CheckBox _SendCopyCheckbox; private TextBox _ListingUrlTextBox; private TextBox _AdditionalCCsTextBox; private Button _ReviewButton; + private Button _ReturnHomeButton; private IDriver _Driver; - public DropDownList PlatformReceipientDropdown { get => _PlatformReceipientDropdown; } - public TextBox ListingIDNumberTextBox { get => _ListingIDNumberTextBox; } - public TextBox ListingUrlTextBox { get => _ListingUrlTextBox; } + public DropDownList PlatformReceipientDropdown { get => _PlatformReceipientDropdown; } + public DropDownList RequestInitiatedByDropDown { get => _RequestInitiatedByDropDown; } + public TextBox ListingIDNumberTextBox { get => _ListingIDNumberTextBox; } + public TextBox ListingUrlTextBox { get => _ListingUrlTextBox; } public TextBox AdditionalCCsTextBox { get => _AdditionalCCsTextBox; } - public Button ReviewButton { get => _ReviewButton; } - public DropDownList RequestInitiatedByDropDown { get => _RequestInitiatedByDropDown; set => _RequestInitiatedByDropDown = value; } + //public DropDownList RequestInitiatedByDropDown { get => _RequestInitiatedByDropDown; } public CheckBox SendCopyCheckbox { get => _SendCopyCheckbox; } + public Button ReviewButton { get => _ReviewButton; } + + public Button ReturnHomeButton { get => _ReturnHomeButton; } public DelistingRequestPage(IDriver Driver) { _Driver = Driver; - _RequestInitiatedByDropDown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingRequestModel.RequestInitiatedByDropDown); + //_RequestInitiatedByDropDown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingRequestModel.RequestInitiatedByDropDown); _PlatformReceipientDropdown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingRequestModel.PlaformRecepientDropDown); + _RequestInitiatedByDropDown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingRequestModel.RequestInitiatedByDropDown); _ListingIDNumberTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingRequestModel.ListingIDNumberTextBox); _SendCopyCheckbox = new CheckBox(Driver, Enums.FINDBY.ID, DelistingRequestModel.SendCopyCheckbox); _ListingUrlTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingRequestModel.ListingUrlTextBox); - _AdditionalCCsTextBox = new TextBox(Driver, Enums.FINDBY.CSSSELECTOR, DelistingRequestModel.AdditionalCCsTextBox); + _AdditionalCCsTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingRequestModel.AdditionalCCsTextBox); _ReviewButton = new Button(Driver, Enums.FINDBY.ID, DelistingRequestModel.ReviewButton); + _ReturnHomeButton = new Button(Driver, Enums.FINDBY.ID, DelistingRequestModel.ReturnHomeButton); } } diff --git a/Test/UITest/TestFrameWork/PageObjects/DelistingWarningPage.cs b/Test/UITest/TestFrameWork/PageObjects/DelistingWarningPage.cs index 492a12e4..adf2eeeb 100644 --- a/Test/UITest/TestFrameWork/PageObjects/DelistingWarningPage.cs +++ b/Test/UITest/TestFrameWork/PageObjects/DelistingWarningPage.cs @@ -1,4 +1,5 @@ -using UITest.Models; +using OpenQA.Selenium.DevTools.V118.CSS; +using UITest.Models; using UITest.SeleniumObjects; using UITest.TestDriver; using UITest.TestObjectFramework; @@ -11,6 +12,7 @@ public class DelistingWarningPage private TextBox _ListingIDNumberTextBox; private TextBox _ListingUrlTextBox; private TextBox _HostEmailAddressTextBox; + private CheckBox _AlternativeNoticeSentCheckbox; private DropDownList _ReasonDropdown; private CheckBox _SendCopyCheckbox; private TextBox _AdditionalCCsTextBox; @@ -25,6 +27,8 @@ public class DelistingWarningPage public TextBox ListingIDNumberTextBox { get => _ListingIDNumberTextBox; } public TextBox ListingUrlTextBox { get => _ListingUrlTextBox; } public TextBox HostEmailAddressTextBox { get => _HostEmailAddressTextBox; } + + public CheckBox AlternativeNoticeSentCheckbox { get => _AlternativeNoticeSentCheckbox; } public DropDownList ReasonDropdown { get => _ReasonDropdown; } public CheckBox SendCopyCheckbox { get => _SendCopyCheckbox; } @@ -35,15 +39,15 @@ public class DelistingWarningPage public TextBox LocalGovUrlTextBox { get => _LocalGovUrlTextBox; } public Button ReviewButton { get => _ReviewButton; } - public DelistingWarningPage(IDriver Driver) { _Driver = Driver; _PlatformReceipientDropdown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingWarningModel.PlatformReceipientDropDown); _ListingIDNumberTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.ListingIDNumberTextBox); - _ListingUrlTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.ListingIDNumberTextBox); + _ListingUrlTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.ListingUrlTextBox); _HostEmailAddressTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.HostEmailAddressTextBox); + _AlternativeNoticeSentCheckbox = new CheckBox(Driver, Enums.FINDBY.CSSSELECTOR, DelistingWarningModel.AlternativeNoticeSentCheckbox); _ReasonDropdown = new DropDownList(Driver, Enums.FINDBY.ID, DelistingWarningModel.ReasonDropdown); _SendCopyCheckbox = new CheckBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.SendCopyCheckbox); _AdditionalCCsTextBox = new TextBox(Driver, Enums.FINDBY.ID, DelistingWarningModel.AdditionalCCsTextBox); diff --git a/Test/UITest/TestFrameWork/PageObjects/TakeDownRequestPage.cs b/Test/UITest/TestFrameWork/PageObjects/TakeDownRequestPage.cs index c0f49f6c..a9efc85b 100644 --- a/Test/UITest/TestFrameWork/PageObjects/TakeDownRequestPage.cs +++ b/Test/UITest/TestFrameWork/PageObjects/TakeDownRequestPage.cs @@ -1,11 +1,10 @@ using TestFrameWork.Models; -using UITest.Models; using UITest.SeleniumObjects; using UITest.TestDriver; namespace UITest.PageObjects { - public class TakeDownRequest + public class TakeDownRequestPage { private TextBox _CommentsTextBox; private Button _SubmitButton; @@ -17,7 +16,7 @@ public class TakeDownRequest public Button SubmitButton { get => _SubmitButton; } public Button CancelButton { get => _CancelButton; } - public TakeDownRequest(IDriver Driver) + public TakeDownRequestPage(IDriver Driver) { _Driver = Driver; _CommentsTextBox = new TextBox(Driver, Enums.FINDBY.ID, TakeDownRequestModel.CommentsTextBox); diff --git a/Test/UITest/TestFrameWork/SeleniumObjects/UIElement.cs b/Test/UITest/TestFrameWork/SeleniumObjects/UIElement.cs index ab68486b..85538158 100644 --- a/Test/UITest/TestFrameWork/SeleniumObjects/UIElement.cs +++ b/Test/UITest/TestFrameWork/SeleniumObjects/UIElement.cs @@ -16,7 +16,7 @@ public string Text { get { - throw new NotImplementedException(); + return (Element.Text); } } diff --git a/Test/UITest/XUnitTests/HousrSTRE2E.cs b/Test/UITest/XUnitTests/HousrSTRE2E.cs new file mode 100644 index 00000000..ac4a9d58 --- /dev/null +++ b/Test/UITest/XUnitTests/HousrSTRE2E.cs @@ -0,0 +1,186 @@ +using TestFrameWork.Models; +using UITest.PageObjects; +using UITest.TestDriver; +using Configuration; + +using Xunit.Abstractions; + +namespace XUnitTests +{ + + public class HousrSTRE2E + { + private ITestOutputHelper _Output; + private HomePage _HomePage; + private DelistingRequestPage _DelistingRequestPage; + private DelistingWarningPage _DelistingWarningPage; + private NoticeOfTakeDownPage _NoticeOfTakeDownPage; + private TakeDownRequestPage _TakeDownRequestPage; + private PathFinderPage _PathFinderPage; + private IDRLoginPage _IDRLoginPage; + private string _TestUserName; + private string _TestPassword; + private IDriver _Driver; + private string _FeURL = "http://127.0.0.1:4200"; + + public HousrSTRE2E(ITestOutputHelper output) + { + this._Output = output; + _Driver = new SeleniumDriver(SeleniumDriver.DRIVERTYPE.CHROME); + _HomePage = new HomePage(_Driver); + _DelistingRequestPage = new DelistingRequestPage(_Driver); + _DelistingWarningPage = new DelistingWarningPage(_Driver); + _NoticeOfTakeDownPage = new NoticeOfTakeDownPage(_Driver); + _TakeDownRequestPage = new TakeDownRequestPage(_Driver); + _PathFinderPage = new PathFinderPage(_Driver); + _IDRLoginPage = new IDRLoginPage(_Driver); + AppSettings appSettings = new AppSettings(); + _TestUserName = appSettings.GetValue("TestUserName") ?? string.Empty; + _TestPassword = appSettings.GetValue("TestPassword") ?? string.Empty; + } + + [Fact] + public void TestLoginAndRequestDelisting() + { + + try + { + _Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5); + _Driver.Url = _FeURL+ "/delisting-request"; + _Driver.Navigate(); + + _PathFinderPage.IDRButton.WaitFor(); + _PathFinderPage.IDRButton.Click(); + + + _IDRLoginPage.UserNameTextBox.WaitFor(); + _IDRLoginPage.UserNameTextBox.EnterText(_TestUserName); + + _IDRLoginPage.PasswordTextBox.WaitFor(); + _IDRLoginPage.PasswordTextBox.EnterText(_TestPassword); + + _IDRLoginPage.ContinueButton.WaitFor(); + _IDRLoginPage.ContinueButton.Click(); + + + //Click to populate dropdown values + _DelistingRequestPage.PlatformReceipientDropdown.Click(); + + Assert.Contains("SELECT A PLATFORM", _DelistingRequestPage.PlatformReceipientDropdown.Text.ToUpper()); + + _DelistingRequestPage.ListingIDNumberTextBox.EnterText("1"); + + //Click to deselect + _DelistingRequestPage.PlatformReceipientDropdown.Click(); + + //Test with Valid email address + _DelistingRequestPage.AdditionalCCsTextBox.EnterText("foo@foo.com"); + + + + //Test with Invalid email address + //_DelistingRequestPage.AdditionalCCsTextBox.EnterText("foo@@joe"); + + _DelistingRequestPage.ListingUrlTextBox.EnterText("http://listingUrl.com"); + + _DelistingRequestPage.RequestInitiatedByDropDown.Click(); + _DelistingRequestPage.RequestInitiatedByDropDown.ExecuteJavaScript(@"document.querySelector(""#lgId_0"").click()"); + + _DelistingRequestPage.PlatformReceipientDropdown.Click(); + _DelistingRequestPage.PlatformReceipientDropdown.ExecuteJavaScript(@"document.querySelector(""#platformId_0"").click()"); + Assert.Contains("AIRBNB", _DelistingRequestPage.PlatformReceipientDropdown.Text.ToUpper()); + + _DelistingRequestPage.ReviewButton.Click(); + + _TakeDownRequestPage.SubmitButton.Click(); + + _DelistingRequestPage.ReturnHomeButton.Click(); + + + } + finally + { + _Driver.Close(); + } + } + + [Fact] + public void TestLoginAndSendWarningDelisting() + { + + try + { + _Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5); + _Driver.Url = _FeURL + "/compliance-notice"; + _Driver.Navigate(); + + _PathFinderPage.IDRButton.WaitFor(); + _PathFinderPage.IDRButton.Click(); + + + _IDRLoginPage.UserNameTextBox.WaitFor(); + _IDRLoginPage.UserNameTextBox.EnterText(_TestUserName); + + _IDRLoginPage.PasswordTextBox.WaitFor(); + _IDRLoginPage.PasswordTextBox.EnterText(_TestPassword); + + _IDRLoginPage.ContinueButton.WaitFor(); + _IDRLoginPage.ContinueButton.Click(); + + //Add Platform receipient + _DelistingWarningPage.PlatformReceipientDropdown.WaitFor(); + _DelistingWarningPage.PlatformReceipientDropdown.Click(); + _DelistingWarningPage.PlatformReceipientDropdown.ExecuteJavaScript(@"document.querySelector(""#platformId_0"").click()"); + Assert.Contains("AIRBNB", _DelistingWarningPage.PlatformReceipientDropdown.Text.ToUpper()); + + //Click to deselect + _DelistingWarningPage.PlatformReceipientDropdown.Click(); + + //add listing ID + _DelistingWarningPage.ListingIDNumberTextBox.EnterText("0"); + + //add listing URL + + _DelistingWarningPage.ListingUrlTextBox.EnterText("http://listingUrl.com"); + + //Add host email address + _DelistingWarningPage.HostEmailAddressTextBox.EnterText("hostemail@host.com"); + + //check alternative notification sent + _DelistingWarningPage.AlternativeNoticeSentCheckbox.Click(); + + //select a reason for request + _DelistingWarningPage.ReasonDropdown.Click(); + _DelistingWarningPage.ReasonDropdown.ExecuteJavaScript(@"document.querySelector(""#reasonId_0"").click()"); + + //Additional CCs + _DelistingWarningPage.AdditionalCCsTextBox.EnterText("foo@foo.com"); + + + //_DelistingWarningPage.PlatformReceipientDropdown.Click(); + //_DelistingWarningPage.PlatformReceipientDropdown.ExecuteJavaScript(@"document.querySelector(""#platformId_0"").click()"); + //Assert.Contains("AIRBNB", _DelistingWarningPage.PlatformReceipientDropdown.Text.ToUpper()); + + //local gov email + _DelistingWarningPage.LocalGovEmailTextBox.EnterText("Local@gov.com"); + + //local gov phone + _DelistingWarningPage.LocalGovPhoneTextBox.EnterText("999-999-9999"); + + //local gov STR by-law info + _DelistingWarningPage.LocalGovUrlTextBox.EnterText("http://STRBylaw.local.gov"); + + _DelistingWarningPage.ReviewButton.Click(); + + _NoticeOfTakeDownPage.CommentsTextBox.EnterText("get a business license"); + _NoticeOfTakeDownPage.SubmitButton.Click(); + + + } + finally + { + _Driver.Close(); + } + } + } +} \ No newline at end of file diff --git a/Test/UITest/XUnitTests/XUnitTests.csproj b/Test/UITest/XUnitTests/XUnitTests.csproj index 1b822585..6c0af9b0 100644 --- a/Test/UITest/XUnitTests/XUnitTests.csproj +++ b/Test/UITest/XUnitTests/XUnitTests.csproj @@ -25,6 +25,7 @@ + diff --git a/crunchydb/note.txt b/crunchydb/note.txt new file mode 100644 index 00000000..64a7b29c --- /dev/null +++ b/crunchydb/note.txt @@ -0,0 +1,12 @@ +helm install strdssdev . --values values-dev.yaml + + +Warning: this deletes the database file and backups +oc delete PostgresCluster/strdssdev + + +oc port-forward svc/strdssdev-pgbouncer 5434:5432 + +ALTER DATABASE strdssdev OWNER TO strdssdev; + +ALTER DATABASE strdssdev SET search_path TO dss; \ No newline at end of file diff --git a/database/STR_DSS_Data_Seeding_Sprint_2.sql b/database/STR_DSS_Data_Seeding_Sprint_2.sql index 79fb064a..2a4ce75f 100644 --- a/database/STR_DSS_Data_Seeding_Sprint_2.sql +++ b/database/STR_DSS_Data_Seeding_Sprint_2.sql @@ -1,19 +1,93 @@ +MERGE INTO dss_access_request_status AS tgt +USING ( SELECT * FROM (VALUES +('Requested','Requested'), +('Approved','Approved'), +('Denied','Denied')) +AS s (access_request_status_cd, access_request_status_nm) +) AS src +ON (tgt.access_request_status_cd=src.access_request_status_cd) +WHEN MATCHED +THEN UPDATE SET +access_request_status_nm=src.access_request_status_nm +WHEN NOT MATCHED +THEN INSERT (access_request_status_cd, access_request_status_nm) +VALUES (src.access_request_status_cd, src.access_request_status_nm); + +MERGE INTO dss_email_message_type AS tgt +USING ( SELECT * FROM (VALUES +('Notice of Takedown','Notice of Takedown of Short Term Rental Platform Offer'), +('Takedown Request','Takedown Request'), +('Escalation Request','STR Escalation Request'), +('Compliance Order','Provincial Compliance Order'), +('Access Granted','Access Granted Notification'), +('Access Denied','Access Denied Notification')) +AS s (email_message_type, email_message_type_nm) +) AS src +ON (tgt.email_message_type=src.email_message_type) +WHEN MATCHED +THEN UPDATE SET +email_message_type_nm=src.email_message_type_nm +WHEN NOT MATCHED +THEN INSERT (email_message_type, email_message_type_nm) +VALUES (src.email_message_type, src.email_message_type_nm); + +MERGE INTO dss_message_reason AS tgt +USING ( SELECT * FROM (VALUES +('Notice of Takedown','No Business Licence Number on Listing'), +('Notice of Takedown','Invalid Business Licence Number'), +('Notice of Takedown','Expired Business Licence'), +('Notice of Takedown','Suspended Business Licence'), +('Notice of Takedown','Revoked Business Licence'), +('Notice of Takedown','Business Licence Denied')) +AS s (email_message_type, message_reason_dsc) +) AS src +ON (tgt.email_message_type=src.email_message_type AND tgt.message_reason_dsc=src.message_reason_dsc) +WHEN NOT MATCHED +THEN INSERT (email_message_type, message_reason_dsc) +VALUES (src.email_message_type, src.message_reason_dsc); + +MERGE INTO dss_organization_type AS tgt +USING ( SELECT * FROM (VALUES +('BCGov','BC Government Component'), +('LG','Local Government'), +('Platform','Short Term Rental Platform')) +AS s (organization_type, organization_type_nm) +) AS src +ON (tgt.organization_type=src.organization_type) +WHEN MATCHED +THEN UPDATE SET +organization_type_nm=src.organization_type_nm +WHEN NOT MATCHED +THEN INSERT (organization_type, organization_type_nm) +VALUES (src.organization_type, src.organization_type_nm); + MERGE INTO dss_user_privilege AS tgt USING ( SELECT * FROM (VALUES -('takedown_action_write','Create Takedown Action')) -AS s (privilege_cd, privilege_nm) +('user_read','View users'), +('user_write','Manage users'), +('listing_read','View listings'), +('licence_file_upload','Upload business licence files'), +('listing_file_upload','Upload platform listing files'), +('audit_read','View audit logs'), +('takedown_action','Create Takedown Action'), +('ceu_action','Create CEU Action')) +AS s (user_privilege_cd, user_privilege_nm) ) AS src -ON (tgt.privilege_cd=src.privilege_cd) +ON (tgt.user_privilege_cd=src.user_privilege_cd) WHEN MATCHED THEN UPDATE SET -privilege_nm=src.privilege_nm +user_privilege_nm=src.user_privilege_nm WHEN NOT MATCHED -THEN INSERT (privilege_cd, privilege_nm) -VALUES (src.privilege_cd, src.privilege_nm); +THEN INSERT (user_privilege_cd, user_privilege_nm) +VALUES (src.user_privilege_cd, src.user_privilege_nm); MERGE INTO dss_user_role AS tgt USING ( SELECT * FROM (VALUES -('ceu_admin','CEU Admin')) +('ceu_admin','CEU Admin'), +('ceu_staff','CEU Staff'), +('bc_staff','Other Provincial Government'), +('lg_staff','Local Government'), +('platform_staff','Short Term Rental Platform')) AS s (user_role_cd, user_role_nm) ) AS src ON (tgt.user_role_cd=src.user_role_cd) @@ -24,9 +98,37 @@ WHEN NOT MATCHED THEN INSERT (user_role_cd, user_role_nm) VALUES (src.user_role_cd, src.user_role_nm); +MERGE INTO dss_user_role_privilege AS tgt +USING ( SELECT * FROM (VALUES +('ceu_admin','user_read'), +('ceu_admin','user_write'), +('ceu_admin','listing_read'), +('ceu_admin','licence_file_upload'), +('ceu_admin','listing_file_upload'), +('ceu_admin','ceu_action'), +('ceu_staff','listing_read'), +('ceu_staff','audit_read'), +('ceu_staff','ceu_action'), +('bc_staff','listing_read'), +('bc_staff','audit_read'), +('lg_staff','listing_read'), +('lg_staff','licence_file_upload'), +('lg_staff','audit_read'), +('lg_staff','takedown_action'), +('platform_staff','listing_file_upload')) +AS s (user_role_cd, user_privilege_cd) +) AS src +ON (tgt.user_role_cd=src.user_role_cd AND tgt.user_privilege_cd=src.user_privilege_cd) +WHEN NOT MATCHED +THEN INSERT (user_role_cd, user_privilege_cd) +VALUES (src.user_role_cd, src.user_privilege_cd); + MERGE INTO dss_organization AS tgt USING ( SELECT * FROM (VALUES -('BCGov','CEU','Compliance Enforcement Unit')) +('BCGov','CEU','Compliance Enforcement Unit'), +('BCGov','BC','Other BC Government Components'), +('LG','LGTEST','Test Town'), +('Platform','PLATFORMTEST','Test Platform')) AS s (organization_type, organization_cd, organization_nm) ) AS src ON (tgt.organization_cd=src.organization_cd AND tgt.organization_type=src.organization_type) diff --git a/database/STR_DSS_Physical_DB_DDL_Sprint_2.sql b/database/STR_DSS_Physical_DB_DDL_Sprint_2.sql index f597f1fd..2d0906b9 100644 --- a/database/STR_DSS_Physical_DB_DDL_Sprint_2.sql +++ b/database/STR_DSS_Physical_DB_DDL_Sprint_2.sql @@ -1,5 +1,3 @@ -CREATE SCHEMA IF NOT EXISTS dss; - CREATE TABLE dss_organization ( organization_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , organization_type varchar(25) NOT NULL , @@ -12,7 +10,11 @@ CREATE TABLE dss_organization ( CONSTRAINT dss_organization_pk PRIMARY KEY ( organization_id ) ); -ALTER TABLE dss_organization ADD CONSTRAINT dss_organization_ck CHECK ( organization_type in ('BCGov','LG','Platform') ); +CREATE TABLE dss_organization_type ( + organization_type varchar(25) NOT NULL , + organization_type_nm varchar(250) NOT NULL , + CONSTRAINT dss_organization_type_pk PRIMARY KEY ( organization_type ) + ); CREATE TABLE dss_organization_contact_person ( organization_contact_person_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , @@ -20,7 +22,7 @@ CREATE TABLE dss_organization_contact_person ( given_nm varchar(25) NOT NULL , family_nm varchar(25) NOT NULL , phone_no varchar(13) NOT NULL , - email_address_dsc varchar(250) NOT NULL , + email_address_dsc varchar(320) NOT NULL , contacted_through_organization_id bigint NOT NULL , upd_dtm timestamptz NOT NULL , upd_user_guid uuid , @@ -33,7 +35,7 @@ CREATE TABLE dss_user_identity ( display_nm varchar(250) NOT NULL , identity_provider_nm varchar(25) NOT NULL , is_enabled boolean NOT NULL , - access_request_status_dsc varchar(25) NOT NULL , + access_request_status_cd varchar(25) NOT NULL , access_request_dtm timestamptz , access_request_justification_txt varchar(250) , given_nm varchar(25) , @@ -47,34 +49,34 @@ CREATE TABLE dss_user_identity ( CONSTRAINT dss_user_identity_pk PRIMARY KEY ( user_identity_id ) ); -ALTER TABLE dss_user_identity ADD CONSTRAINT dss_user_identity_ck CHECK ( access_request_status_dsc in ('Requested','Approved','Denied') ); +CREATE TABLE dss_access_request_status ( + access_request_status_cd varchar(25) NOT NULL , + access_request_status_nm varchar(250) NOT NULL , + CONSTRAINT dss_access_request_status_pk PRIMARY KEY ( access_request_status_cd ) + ); CREATE TABLE dss_user_privilege ( - user_privilege_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , - privilege_cd varchar(25) NOT NULL , - privilege_nm varchar(250) NOT NULL , - CONSTRAINT dss_user_privilege_pk PRIMARY KEY ( user_privilege_id ) + user_privilege_cd varchar(25) NOT NULL , + user_privilege_nm varchar(250) NOT NULL , + CONSTRAINT dss_user_privilege_pk PRIMARY KEY ( user_privilege_cd ) ); CREATE TABLE dss_user_role ( - user_role_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , user_role_cd varchar(25) NOT NULL , user_role_nm varchar(250) NOT NULL , - CONSTRAINT dss_user_role_pk PRIMARY KEY ( user_role_id ) + CONSTRAINT dss_user_role_pk PRIMARY KEY ( user_role_cd ) ); CREATE TABLE dss_user_role_assignment ( - user_role_assignment_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , user_identity_id bigint NOT NULL , - user_role_id bigint NOT NULL , - CONSTRAINT dss_user_role_assignment_pk PRIMARY KEY ( user_role_assignment_id ) + user_role_cd varchar(25) NOT NULL , + CONSTRAINT dss_user_role_assignment_pk PRIMARY KEY ( user_identity_id, user_role_cd ) ); CREATE TABLE dss_user_role_privilege ( - user_role_privilege_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , - user_privilege_id bigint NOT NULL , - user_role_id bigint NOT NULL , - CONSTRAINT dss_user_role_privilege_pk PRIMARY KEY ( user_role_privilege_id ) + user_privilege_cd varchar(25) NOT NULL , + user_role_cd varchar(25) NOT NULL , + CONSTRAINT dss_user_role_privilege_pk PRIMARY KEY ( user_privilege_cd, user_role_cd ) ); CREATE TABLE dss_email_message ( @@ -82,17 +84,34 @@ CREATE TABLE dss_email_message ( email_message_type varchar(50) NOT NULL , message_delivery_dtm timestamptz NOT NULL , message_template_dsc varchar(4000) NOT NULL , - message_reason_dsc varchar(250) , - unreported_listing_url varchar(250) , - host_email_address_dsc varchar(250) , - cc_email_address_dsc varchar(250) , + is_host_contacted_externally boolean NOT NULL , + is_submitter_cc_required boolean NOT NULL , + message_reason_id bigint , + lg_phone_no varchar(13) , + unreported_listing_no varchar(25) , + host_email_address_dsc varchar(320) , + lg_email_address_dsc varchar(320) , + cc_email_address_dsc varchar(4000) , + unreported_listing_url varchar(4000) , + lg_str_bylaw_url varchar(4000) , initiating_user_identity_id bigint NOT NULL , affected_by_user_identity_id bigint , involved_in_organization_id bigint , CONSTRAINT dss_email_message_pk PRIMARY KEY ( email_message_id ) ); -ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_ck CHECK ( email_message_type in ('Notice of Takedown','Takedown Request','Delisting Warning','Delisting Request','Access Granted Notification','Access Denied Notification') ); +CREATE TABLE dss_email_message_type ( + email_message_type varchar(50) NOT NULL , + email_message_type_nm varchar(250) NOT NULL , + CONSTRAINT dss_email_message_type_pk PRIMARY KEY ( email_message_type ) + ); + +CREATE TABLE dss_message_reason ( + message_reason_id bigint NOT NULL GENERATED ALWAYS AS IDENTITY , + email_message_type varchar(50) NOT NULL , + message_reason_dsc varchar(250) NOT NULL , + CONSTRAINT dss_message_reason_pk PRIMARY KEY ( message_reason_id ) + ); ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_fk_initiated_by FOREIGN KEY ( initiating_user_identity_id ) REFERENCES dss_user_identity( user_identity_id ); @@ -100,19 +119,29 @@ ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_fk_affecting FORE ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_fk_involving FOREIGN KEY ( involved_in_organization_id ) REFERENCES dss_organization( organization_id ); +ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_fk_communicating FOREIGN KEY ( email_message_type ) REFERENCES dss_email_message_type( email_message_type ); + +ALTER TABLE dss_email_message ADD CONSTRAINT dss_email_message_fk_justified_by FOREIGN KEY ( message_reason_id ) REFERENCES dss_message_reason( message_reason_id ); + +ALTER TABLE dss_message_reason ADD CONSTRAINT dss_message_reason_fk_justifying FOREIGN KEY ( email_message_type ) REFERENCES dss_email_message_type( email_message_type ); + ALTER TABLE dss_organization ADD CONSTRAINT dss_organization_fk_managed_by FOREIGN KEY ( managing_organization_id ) REFERENCES dss_organization( organization_id ); +ALTER TABLE dss_organization ADD CONSTRAINT dss_organization_fk_treated_as FOREIGN KEY ( organization_type ) REFERENCES dss_organization_type( organization_type ); + ALTER TABLE dss_organization_contact_person ADD CONSTRAINT dss_organization_contact_person_fk_contacted_for FOREIGN KEY ( contacted_through_organization_id ) REFERENCES dss_organization( organization_id ); ALTER TABLE dss_user_identity ADD CONSTRAINT dss_user_identity_fk_representing FOREIGN KEY ( represented_by_organization_id ) REFERENCES dss_organization( organization_id ); -ALTER TABLE dss_user_role_assignment ADD CONSTRAINT dss_user_role_assignment_fk_granted FOREIGN KEY ( user_role_id ) REFERENCES dss_user_role( user_role_id ); +ALTER TABLE dss_user_identity ADD CONSTRAINT dss_user_identity_fk_given FOREIGN KEY ( access_request_status_cd ) REFERENCES dss_access_request_status( access_request_status_cd ); + +ALTER TABLE dss_user_role_assignment ADD CONSTRAINT dss_user_role_assignment_fk_granted FOREIGN KEY ( user_role_cd ) REFERENCES dss_user_role( user_role_cd ); ALTER TABLE dss_user_role_assignment ADD CONSTRAINT dss_user_role_assignment_fk_granted_to FOREIGN KEY ( user_identity_id ) REFERENCES dss_user_identity( user_identity_id ); -ALTER TABLE dss_user_role_privilege ADD CONSTRAINT dss_user_role_privilege_fk_conferred_by FOREIGN KEY ( user_role_id ) REFERENCES dss_user_role( user_role_id ); +ALTER TABLE dss_user_role_privilege ADD CONSTRAINT dss_user_role_privilege_fk_conferred_by FOREIGN KEY ( user_role_cd ) REFERENCES dss_user_role( user_role_cd ); -ALTER TABLE dss_user_role_privilege ADD CONSTRAINT dss_user_role_privilege_fk_conferring FOREIGN KEY ( user_privilege_id ) REFERENCES dss_user_privilege( user_privilege_id ); +ALTER TABLE dss_user_role_privilege ADD CONSTRAINT dss_user_role_privilege_fk_conferring FOREIGN KEY ( user_privilege_cd ) REFERENCES dss_user_privilege( user_privilege_cd ); COMMENT ON TABLE dss_organization IS 'A private company or governing body that plays a role in short term rental reporting or enforcement'; @@ -164,7 +193,7 @@ COMMENT ON COLUMN dss_user_identity.identity_provider_nm IS 'A directory or doma COMMENT ON COLUMN dss_user_identity.is_enabled IS 'Indicates whether access is currently permitted using this identity'; -COMMENT ON COLUMN dss_user_identity.access_request_status_dsc IS 'The current status of the most recent access request made by the user (restricted to Requested, Approved, or Denied)'; +COMMENT ON COLUMN dss_user_identity.access_request_status_cd IS 'The current status of the most recent access request made by the user (restricted to Requested, Approved, or Denied)'; COMMENT ON COLUMN dss_user_identity.access_request_dtm IS 'A timestamp indicating when the most recent access request was made by the user'; @@ -188,35 +217,27 @@ COMMENT ON COLUMN dss_user_identity.upd_user_guid IS 'The globally unique identi COMMENT ON TABLE dss_user_privilege IS 'A granular access right or privilege within the application that may be granted to a role'; -COMMENT ON COLUMN dss_user_privilege.user_privilege_id IS 'Unique generated key'; +COMMENT ON COLUMN dss_user_privilege.user_privilege_cd IS 'The immutable system code that identifies the privilege'; -COMMENT ON COLUMN dss_user_privilege.privilege_cd IS 'The immutable system code that identifies the privilege'; - -COMMENT ON COLUMN dss_user_privilege.privilege_nm IS 'The human-readable name that is given for the role'; +COMMENT ON COLUMN dss_user_privilege.user_privilege_nm IS 'The human-readable name that is given for the role'; COMMENT ON TABLE dss_user_role IS 'A set of access rights and privileges within the application that may be granted to users'; -COMMENT ON COLUMN dss_user_role.user_role_id IS 'Unique generated key'; - COMMENT ON COLUMN dss_user_role.user_role_cd IS 'The immutable system code that identifies the role'; COMMENT ON COLUMN dss_user_role.user_role_nm IS 'The human-readable name that is given for the role'; COMMENT ON TABLE dss_user_role_assignment IS 'The association of a grantee credential to a role for the purpose of conveying application privileges'; -COMMENT ON COLUMN dss_user_role_assignment.user_role_assignment_id IS 'Unique generated key'; - COMMENT ON COLUMN dss_user_role_assignment.user_identity_id IS 'Foreign key'; -COMMENT ON COLUMN dss_user_role_assignment.user_role_id IS 'Foreign key'; +COMMENT ON COLUMN dss_user_role_assignment.user_role_cd IS 'Foreign key'; COMMENT ON TABLE dss_user_role_privilege IS 'The association of a granular application privilege to a role'; -COMMENT ON COLUMN dss_user_role_privilege.user_role_privilege_id IS 'Unique generated key'; - -COMMENT ON COLUMN dss_user_role_privilege.user_privilege_id IS 'Foreign key'; +COMMENT ON COLUMN dss_user_role_privilege.user_privilege_cd IS 'Foreign key'; -COMMENT ON COLUMN dss_user_role_privilege.user_role_id IS 'Foreign key'; +COMMENT ON COLUMN dss_user_role_privilege.user_role_cd IS 'Foreign key'; COMMENT ON TABLE dss_email_message IS 'A message that is sent to one or more recipients via email'; @@ -228,7 +249,7 @@ COMMENT ON COLUMN dss_email_message.message_delivery_dtm IS 'A timestamp indicat COMMENT ON COLUMN dss_email_message.message_template_dsc IS 'The full text or template for the message that is sent'; -COMMENT ON COLUMN dss_email_message.message_reason_dsc IS 'A description of the justification for initiating the message'; +COMMENT ON COLUMN dss_message_reason.message_reason_dsc IS 'A description of the justification for initiating a message'; COMMENT ON COLUMN dss_email_message.unreported_listing_url IS 'User-provided URL for a short-term rental platform listing that is the subject of the message'; diff --git a/database/STR_DSS_Physical_DB_Design_Sprint_2.dbs b/database/STR_DSS_Physical_DB_Design_Sprint_2.dbs index 1770d369..bd183e9e 100644 --- a/database/STR_DSS_Physical_DB_Design_Sprint_2.dbs +++ b/database/STR_DSS_Physical_DB_Design_Sprint_2.dbs @@ -1,6 +1,13 @@ + + + + + + +
@@ -16,18 +23,22 @@ - - - - + + + + + + - + + - + + @@ -40,9 +51,6 @@ - - - @@ -52,6 +60,35 @@ + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + +
@@ -83,12 +120,12 @@ - - - + + +
@@ -108,7 +145,7 @@ - + @@ -127,6 +164,13 @@
+ + + + + + +
@@ -145,7 +189,7 @@ - + @@ -181,35 +225,27 @@ - - - + + +
- - - - - + - + - +
- - - - @@ -217,26 +253,23 @@ - +
- - - - - + - + - + + - + @@ -244,72 +277,59 @@
- - - - - + - + - + + - + - +
- - { BEFORE | AFTER } { event [ OR ... ] } - ON table [ FOR [ EACH ] { ROW | STATEMENT } ] - EXECUTE PROCEDURE funcname ( arguments );]]> + + - - + + - - { BEFORE | AFTER } { event [ OR ... ] } - ON table [ FOR [ EACH ] { ROW | STATEMENT } ] - EXECUTE PROCEDURE funcname ( arguments );]]> + + - - ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] ) - [ RETURNS rettype - | RETURNS TABLE ( column_name column_type [, ...] ) ] - { LANGUAGE lang_name - | TRANSFORM { FOR TYPE type_name } [, ... ] - | WINDOW - | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF - | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT - | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER - | COST execution_cost - | ROWS result_rows - | SET configuration_parameter { TO value | = value | FROM CURRENT } - | AS 'definition' - | AS 'obj_file', 'link_symbol' - } ... - [ WITH ( attribute [, ...] ) ]]]> + + +
- - - - - - - - - + + + + + + + + + + + + + { const app = fixture.componentInstance; expect(app).toBeTruthy(); }); - - it(`should have the 'str-dss-ui' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('str-dss-ui'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain( - 'Hello, str-dss-ui' - ); - }); }); diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 17f02196..abd8d85b 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; import { ComplianceNoticeComponent } from './features/components/compliance-notice/compliance-notice.component'; import { DashboardComponent } from './features/components/dashboard/dashboard.component'; import { DelistingRequestComponent } from './features/components/delisting-request/delisting-request.component'; +import { AccessRequestComponent } from './features/components/access-request/access-request.component'; export const routes: Routes = [ { @@ -10,11 +11,15 @@ export const routes: Routes = [ }, { path: 'compliance-notice', - component: ComplianceNoticeComponent - } - , + component: ComplianceNoticeComponent, + }, { path: 'delisting-request', - component: DelistingRequestComponent - } + component: DelistingRequestComponent, + }, + { + path: 'access-request', + component: AccessRequestComponent, + }, + ]; diff --git a/frontend/src/app/common/consts/validators.const.ts b/frontend/src/app/common/consts/validators.const.ts index 57bd3c5e..c035b253 100644 --- a/frontend/src/app/common/consts/validators.const.ts +++ b/frontend/src/app/common/consts/validators.const.ts @@ -2,14 +2,14 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function validateUrl(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const url = control.value; + const url: string = control.value; if (!url) { return null; } const urlRegex = new RegExp(/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/); - const validUrl = urlRegex.test(url); + const validUrl = urlRegex.test(url.toLowerCase()); return validUrl ? null : { invalidUrl: true }; }; @@ -17,14 +17,14 @@ export function validateUrl(): ValidatorFn { export function validatePhone(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const url = control.value; + const phone: string = control.value; - if (!url) { + if (!phone) { return null; } const phoneRegex = new RegExp(/^\(\d{3}\) \d{3}-\d{4}$/); - const validUrl = phoneRegex.test(url); + const validUrl = phoneRegex.test(phone); return validUrl ? null : { invalidPhone: true }; }; @@ -44,9 +44,9 @@ export function validateEmailListString(): ValidatorFn { const emailRegex = new RegExp(/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/); const allValid = emailList.every((email: string) => { - return emailRegex.test(email) + return emailRegex.test(email.toLowerCase()) }) - return allValid ? null : { invalidEmailChips: true }; + return allValid ? null : { invalidEmailList: true }; }; } \ No newline at end of file diff --git a/frontend/src/app/common/layout/layout.component.scss b/frontend/src/app/common/layout/layout.component.scss index c23201a5..9466e8e7 100644 --- a/frontend/src/app/common/layout/layout.component.scss +++ b/frontend/src/app/common/layout/layout.component.scss @@ -55,11 +55,9 @@ .content { margin: 0 184px; width: calc(100% - 368px); - background-color: #FFFFFF; overflow-y: auto; display: flex; min-height: calc(100vh - 158px); - padding: 24px; } .footer { diff --git a/frontend/src/app/common/models/access-request.ts b/frontend/src/app/common/models/access-request.ts new file mode 100644 index 00000000..07ef6cfa --- /dev/null +++ b/frontend/src/app/common/models/access-request.ts @@ -0,0 +1,4 @@ +export interface AccessRequest { + organizationType: string, + organizationName: string, +} \ No newline at end of file diff --git a/frontend/src/app/common/models/delisting-request.ts b/frontend/src/app/common/models/delisting-request.ts index afc3d940..41f3034f 100644 --- a/frontend/src/app/common/models/delisting-request.ts +++ b/frontend/src/app/common/models/delisting-request.ts @@ -1,9 +1,8 @@ export interface DelistingRequest { - municipalityId: number; + lgId: number; platformId: number; - listingId: string; + listingId: number; listingUrl: string; sendCopy: boolean; ccList: Array; - comment: string; } diff --git a/frontend/src/app/common/services/delisting.service.spec.ts b/frontend/src/app/common/services/delisting.service.spec.ts index b58ce0e5..ed0f0e0c 100644 --- a/frontend/src/app/common/services/delisting.service.spec.ts +++ b/frontend/src/app/common/services/delisting.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DelistingService } from './delisting.service'; @@ -6,7 +7,11 @@ describe('DelistingService', () => { let service: DelistingService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ] + }); service = TestBed.inject(DelistingService); }); diff --git a/frontend/src/app/common/services/delisting.service.ts b/frontend/src/app/common/services/delisting.service.ts index c40e71cc..f57b800f 100644 --- a/frontend/src/app/common/services/delisting.service.ts +++ b/frontend/src/app/common/services/delisting.service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { environment } from '../../../environments/environment'; import { DropdownOption } from '../models/dropdown-option'; import { ComplianceNotice } from '../models/compliance-notice'; +import { DelistingRequest } from '../models/delisting-request'; @Injectable({ providedIn: 'root' @@ -14,7 +15,11 @@ export class DelistingService { constructor(private httpClient: HttpClient) { } getPlatforms(): Observable> { - return this.httpClient.get>(`${environment.API_HOST}/platforms/dropdown`) + return this.httpClient.get>(`${environment.API_HOST}/organizations/dropdown/?type=Platform`) + } + + getLocalGovernments(): Observable> { + return this.httpClient.get>(`${environment.API_HOST}/organizations?type=LG`) } getReasons(): Observable> { @@ -29,11 +34,11 @@ export class DelistingService { return this.httpClient.post(`${environment.API_HOST}/delisting/warnings`, complianceNotice) } - delistingRequestPreview(delistingRequest: ComplianceNotice): Observable<{ content: string }> { + delistingRequestPreview(delistingRequest: DelistingRequest): Observable<{ content: string }> { return this.httpClient.post<{ content: string }>(`${environment.API_HOST}/delisting/requests/preview`, delistingRequest) } - createDelistingRequest(delistingRequest: ComplianceNotice): Observable { + createDelistingRequest(delistingRequest: DelistingRequest): Observable { return this.httpClient.post(`${environment.API_HOST}/delisting/requests`, delistingRequest) } } diff --git a/frontend/src/app/common/services/request-access.service.spec.ts b/frontend/src/app/common/services/request-access.service.spec.ts new file mode 100644 index 00000000..9f66e644 --- /dev/null +++ b/frontend/src/app/common/services/request-access.service.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing'; + +import { RequestAccessService } from './request-access.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('RequestAccessService', () => { + let service: RequestAccessService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ] + }); + service = TestBed.inject(RequestAccessService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/request-access.service.ts b/frontend/src/app/common/services/request-access.service.ts new file mode 100644 index 00000000..6e3b2c6f --- /dev/null +++ b/frontend/src/app/common/services/request-access.service.ts @@ -0,0 +1,22 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { DropdownOption } from '../models/dropdown-option'; +import { environment } from '../../../environments/environment'; +import { AccessRequest } from '../models/access-request'; + +@Injectable({ + providedIn: 'root' +}) +export class RequestAccessService { + + constructor(private httpClient: HttpClient) { } + + getOrganizationTypes(): Observable> { + return this.httpClient.get>(`${environment.API_HOST}/organizations/types`); + } + + createAccessRequest(body: AccessRequest): Observable { + return this.httpClient.post(`${environment.API_HOST}/users/accessrequests`, body); + } +} diff --git a/frontend/src/app/common/services/user-data.service.spec.ts b/frontend/src/app/common/services/user-data.service.spec.ts new file mode 100644 index 00000000..b5d7ab64 --- /dev/null +++ b/frontend/src/app/common/services/user-data.service.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserDataService } from './user-data.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('UserDataService', () => { + let service: UserDataService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ] + }); + service = TestBed.inject(UserDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/user-data.service.ts b/frontend/src/app/common/services/user-data.service.ts new file mode 100644 index 00000000..5a2940c2 --- /dev/null +++ b/frontend/src/app/common/services/user-data.service.ts @@ -0,0 +1,16 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class UserDataService { + + constructor(private httpClient: HttpClient) { } + + getCurrentUser(): Observable { + return this.httpClient.get(`${environment.API_HOST}/users/currentuser`) + } +} diff --git a/frontend/src/app/features/components/access-request/access-request.component.html b/frontend/src/app/features/components/access-request/access-request.component.html new file mode 100644 index 00000000..e0993c0f --- /dev/null +++ b/frontend/src/app/features/components/access-request/access-request.component.html @@ -0,0 +1,93 @@ + + +
+ Request Access +
+
+ +
+
Hello, {{currentUser.firstName}}
+
Please identify yourself and your reason for requesting access
+
Thank you for submitting your request
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + Local + Role is required + +
+
+ + Local + Organization name is + required +
+
+
+ +
+ [Text coming soon - need to check with legal team, so this is just a place holder!] + +

+ Lorem, ipsum dolor sit amet consectetur adipisicing elit. Explicabo nobis molestiae in ducimus quasi qui et + maxime quaerat rerum mollitia, sunt itaque a, culpa, minima error. Impedit odio blanditiis ab dolorem + mollitia, dicta provident obcaecati corporis? +

+
+ + + +
+ Success icon +
+
+ Request sent successfully! +

Your request has been successfully submitted and is waiting for approval.

+ What happens next +

You will receive an email with further details once your request has been processed.

+
+
+
+ + + +
+ Error icon +
+
+
+ {{error.summary}} +

{{error.detail}}

+
+
+
+
+ + +
\ No newline at end of file diff --git a/frontend/src/app/features/components/access-request/access-request.component.scss b/frontend/src/app/features/components/access-request/access-request.component.scss new file mode 100644 index 00000000..078a7219 --- /dev/null +++ b/frontend/src/app/features/components/access-request/access-request.component.scss @@ -0,0 +1,104 @@ +:host { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + + .message-icon-block { + align-self: start; + margin-right: 16px; + } + + p-card { + display: flex; + justify-content: center; + width: 100%; + + * { + font-family: 'BcSans-regular'; + } + + .bold-text { + font-family: 'BcSans-bold'; + } + + .header-container { + height: 51px; + background-color: #053662; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + color: white; + padding: 10px 25px; + + font-size: 20px; + line-height: 27px; + } + + .content-header { + .text-header { + padding-bottom: 12px; + } + + .sub-header { + padding-bottom: 12px; + } + } + + .form-group-row { + width: 100%; + + .validation-errors { + small { + color: #D32F2F; + } + } + + label { + line-height: 26px; + } + + &.form-spacer { + padding-top: 0; + margin-top: -14px; + } + + &.inline-block { + width: 50%; + display: inline-block; + + &.left-block { + padding-right: 12px; + } + + &.right-block { + padding-left: 12px; + } + } + + span.info-tooltip { + background-image: url(../../../../assets/images/info-icon.svg); + border-radius: 50%; + width: 16px; + height: 16px; + line-height: 26px; + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 4px; + } + + &-col { + &:first-child { + width: 100%; + } + + font-size: 16px; + width: 100%; + + &:last-of-type { + padding: 8px 0 16px 0; + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/features/components/access-request/access-request.component.spec.ts b/frontend/src/app/features/components/access-request/access-request.component.spec.ts new file mode 100644 index 00000000..f984ac39 --- /dev/null +++ b/frontend/src/app/features/components/access-request/access-request.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccessRequestComponent } from './access-request.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('AccessRequestComponent', () => { + let component: AccessRequestComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AccessRequestComponent, HttpClientTestingModule] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AccessRequestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/features/components/access-request/access-request.component.ts b/frontend/src/app/features/components/access-request/access-request.component.ts new file mode 100644 index 00000000..4ba8ab00 --- /dev/null +++ b/frontend/src/app/features/components/access-request/access-request.component.ts @@ -0,0 +1,116 @@ +import { Component, OnInit } from '@angular/core'; +import { Message } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { CardModule } from 'primeng/card'; +import { DropdownModule } from 'primeng/dropdown'; +import { MessagesModule } from 'primeng/messages'; +import { DropdownOption } from '../../../common/models/dropdown-option'; +import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { InputTextModule } from 'primeng/inputtext'; +import { RequestAccessService } from '../../../common/services/request-access.service'; +import { AccessRequest } from '../../../common/models/access-request'; +import { UserDataService } from '../../../common/services/user-data.service'; + +@Component({ + selector: 'app-access-request', + standalone: true, + imports: [ + ReactiveFormsModule, + CommonModule, + CardModule, + ButtonModule, + DropdownModule, + MessagesModule, + InputTextModule, + ], + templateUrl: './access-request.component.html', + styleUrl: './access-request.component.scss' +}) +export class AccessRequestComponent implements OnInit { + myForm!: FormGroup; + + messages = new Array(); + roles = new Array(); + currentUser: any; + + hideForm = false; + showRequestedSuccessfullyMessage = false; + showRequestedFailedMessage = false; + + public get organizationTypeControl(): AbstractControl { + return this.myForm.controls['organizationType']; + } + public get organizationNameControl(): AbstractControl { + return this.myForm.controls['organizationName']; + } + + constructor( + private fb: FormBuilder, + private requestAccessService: RequestAccessService, + private userDataService: UserDataService, + ) { } + + ngOnInit(): void { + this.initData(); + this.initForm(); + } + + onRequest(): void { + this.messages = []; + this.showRequestedFailedMessage = false; + this.showRequestedSuccessfullyMessage = false; + const model: AccessRequest = this.myForm.getRawValue(); + this.requestAccessService.createAccessRequest(model).subscribe({ + next: _ => { + this.hideForm = true; + this.showRequestedSuccessfullyMessage = true; + this.messages = []; + }, + error: (error: { + error: { + errors: { + organizationType: string[], + organizationName: string[], + + entity: string[] + } + } + }) => { + this.showRequestedFailedMessage = true; + if (error.error.errors.entity) { + this.messages = [{ severity: 'error', summary: 'Request cannot be sent!', detail: error.error.errors.entity[0] }]; + } + if (error.error.errors.organizationType) { + this.messages.push({ severity: 'error', summary: 'Request failed!', detail: error.error.errors.organizationType[0] }); + } + if (error.error.errors.organizationName) { + this.messages.push({ severity: 'error', summary: 'Request failed!', detail: error.error.errors.organizationName[0] }); + } + if (!this.messages.length) { + this.messages.push({ severity: 'error', summary: 'Request failed!', detail: 'Unhandled error.' }); + } + }, + }) + } + + private initData(): void { + this.requestAccessService.getOrganizationTypes().subscribe({ + next: (types => { + this.roles = types; + }), + }); + this.userDataService.getCurrentUser().subscribe({ + next: user => { + this.currentUser = user; + } + }) + } + + private initForm(): void { + this.myForm = this.fb.group({ + organizationType: ['', Validators.required], + organizationName: ['', Validators.required], + }); + } +} diff --git a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.html b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.html index 07cd5f5e..b466b427 100644 --- a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.html +++ b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.html @@ -1,14 +1,19 @@
Send Notice of Takedown
-
All fields are required except where stated
+ +
All fields are required except where stated
-
+
+ placeholder="Please Select..." id="platformId" name="platformId" [required]="true"> +
+
+ Platform recipient is + required
@@ -17,8 +22,8 @@
- +
@@ -30,8 +35,13 @@ URL
- + +
+
+ Please make sure the URL format that + you have entered is correct + Listing URL is required
@@ -43,14 +53,22 @@
- + +
+
+ Please make sure the email format + you have entered is correct + Host’s Email Address is + required
+ name="sentAlternatively" inputId="sentAlternativelyInput" + (onChange)="onAlternativeDeliveryChanged($event)">
@@ -62,7 +80,11 @@
+ placeholder="Please Select..." id="reasonId" name="reasonId" [required]="true"> +
+
+ Reason for request is + required
@@ -81,7 +103,13 @@
- + +
+
+ Please make sure the email + format + you have entered is correct
@@ -92,8 +120,8 @@
- +
@@ -108,6 +136,17 @@ +
+
+ Please make sure the email + format + you have entered is correct + Local Government’s + Email Address is + required +
+
+
- + +
+
+ Please make sure the + URL + format that + you have entered is correct
@@ -146,5 +191,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.scss b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.scss index 386030c9..d428b592 100644 --- a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.scss +++ b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.scss @@ -1,7 +1,8 @@ :host { width: 100%; height: 100%; - padding: 4px; + padding: 24px; + background-color: #FFFFFF; .title { font-size: 24px; @@ -18,6 +19,21 @@ .form-group-row { width: 100%; + .validation-errors { + small { + color: #D32F2F; + } + } + + label { + line-height: 26px; + } + + &.form-spacer { + padding-top: 0; + margin-top: -14px; + } + &.inline-block { width: 368px; margin-right: 24px; @@ -33,6 +49,7 @@ border-radius: 50%; width: 16px; height: 16px; + line-height: 26px; display: inline-flex; justify-content: center; align-items: center; @@ -58,6 +75,5 @@ } } } - } } \ No newline at end of file diff --git a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.spec.ts b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.spec.ts index 34d03ad1..10219c18 100644 --- a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.spec.ts +++ b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComplianceNoticeComponent } from './compliance-notice.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DelistingService } from '../../../common/services/delisting.service'; describe('ComplianceNoticeComponent', () => { let component: ComplianceNoticeComponent; @@ -8,10 +10,11 @@ describe('ComplianceNoticeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ComplianceNoticeComponent] + imports: [ComplianceNoticeComponent, HttpClientTestingModule], + providers: [DelistingService] }) - .compileComponents(); - + .compileComponents(); + fixture = TestBed.createComponent(ComplianceNoticeComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.ts b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.ts index d25bcba8..cee0f3dc 100644 --- a/frontend/src/app/features/components/compliance-notice/compliance-notice.component.ts +++ b/frontend/src/app/features/components/compliance-notice/compliance-notice.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { DropdownModule } from 'primeng/dropdown'; import { InputTextModule } from 'primeng/inputtext'; import { InputTextareaModule } from 'primeng/inputtextarea'; -import { CheckboxModule } from 'primeng/checkbox'; +import { CheckboxChangeEvent, CheckboxModule } from 'primeng/checkbox'; import { CommonModule } from '@angular/common'; import { ChipsModule } from 'primeng/chips'; import { DelistingService } from '../../../common/services/delisting.service'; @@ -11,32 +11,72 @@ import { DropdownOption } from '../../../common/models/dropdown-option'; import { DialogModule } from 'primeng/dialog'; import { ButtonModule } from 'primeng/button'; import { validateEmailListString, validateUrl } from '../../../common/consts/validators.const'; -import { ToastModule } from 'primeng/toast'; -import { MessageService } from 'primeng/api'; +import { Message } from 'primeng/api'; import { HttpErrorResponse } from '@angular/common/http'; import { InputMaskModule } from 'primeng/inputmask'; import { TooltipModule } from 'primeng/tooltip'; import { ComplianceNotice } from '../../../common/models/compliance-notice'; +import { MessagesModule } from 'primeng/messages'; +import { Router } from '@angular/router'; @Component({ selector: 'app-compliance-notice', standalone: true, - imports: [ReactiveFormsModule, DropdownModule, InputTextModule, InputTextareaModule, - CheckboxModule, CommonModule, ChipsModule, DialogModule, TooltipModule, InputMaskModule, ButtonModule, ToastModule], + imports: [ + ReactiveFormsModule, + CommonModule, + DropdownModule, + InputTextModule, + InputTextareaModule, + MessagesModule, + CheckboxModule, + ChipsModule, + DialogModule, + TooltipModule, + InputMaskModule, + ButtonModule, + ], templateUrl: './compliance-notice.component.html', styleUrl: './compliance-notice.component.scss' }) export class ComplianceNoticeComponent implements OnInit { - myForm!: FormGroup; platformOptions = new Array(); reasonOptions = new Array(); isPreviewVisible = false; + hideForm = false; previewText = 'No preview' - constructor(private fb: FormBuilder, private delistingService: DelistingService, private messageService: MessageService) { } + messages = new Array(); + + public get platformIdControl(): AbstractControl { + return this.myForm.controls['platformId']; + } + public get listingUrlControl(): AbstractControl { + return this.myForm.controls['listingUrl']; + } + public get hostEmailControl(): AbstractControl { + return this.myForm.controls['hostEmail']; + } + public get reasonIdControl(): AbstractControl { + return this.myForm.controls['reasonId']; + } + public get ccListControl(): AbstractControl { + return this.myForm.controls['ccList']; + } + public get lgContactEmailControl(): AbstractControl { + return this.myForm.controls['LgContactEmail']; + } + public get lgContactPhoneControl(): AbstractControl { + return this.myForm.controls['LgContactPhone']; + } + public get strBylawUrlControl(): AbstractControl { + return this.myForm.controls['StrBylawUrl']; + } + + constructor(private fb: FormBuilder, private delistingService: DelistingService, private router: Router) { } ngOnInit(): void { this.initForm(); @@ -66,12 +106,13 @@ export class ComplianceNoticeComponent implements OnInit { } ) } else { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: "Form is invalid" }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: 'Form is invalid' }]; console.error('Form is invalid!'); } } onSubmit(comment: string, textAreaElement: HTMLTextAreaElement): void { + this.messages = []; if (this.myForm.valid) { const model: ComplianceNotice = this.myForm.value; model.ccList = this.myForm.value['ccList'].prototype === Array @@ -81,7 +122,7 @@ export class ComplianceNoticeComponent implements OnInit { this.delistingService.createComplianceNotice(model) .subscribe({ next: (_) => { - this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Message has been sent successfully' }); + this.showSuccessMessage(); }, error: (error) => { this.showErrors(error); @@ -100,16 +141,35 @@ export class ComplianceNoticeComponent implements OnInit { this.isPreviewVisible = false; } + onAlternativeDeliveryChanged(value: CheckboxChangeEvent): void { + if (value.checked) + this.hostEmailControl.removeValidators([Validators.required]); + else + this.hostEmailControl.addValidators([Validators.required]); + + this.hostEmailControl.updateValueAndValidity(); + this.myForm.updateValueAndValidity(); + } + cleanupPopupComment(commentTextArea: HTMLTextAreaElement): void { commentTextArea.value = ''; } + onReturnHome(): void { + this.router.navigateByUrl('/'); + } + + showSuccessMessage(): void { + this.hideForm = true; + this.messages = [{ severity: 'success', summary: '', detail: 'Your Notice of Takedown was Successfully Submitted!' }]; + } + private initForm(): void { this.myForm = this.fb.group({ platformId: [0, Validators.required], listingId: [null], listingUrl: ['', [Validators.required, validateUrl()]], - hostEmail: ['', Validators.email], + hostEmail: ['', [Validators.required, Validators.email]], sentAlternatively: [false], reasonId: [0, Validators.required,], sendCopy: [true], @@ -124,18 +184,18 @@ export class ComplianceNoticeComponent implements OnInit { private showErrors(error: HttpErrorResponse | any): void { let errorObject = typeof error.error === 'string' ? JSON.parse(error.error) : error.error; if (error.error['detail']) { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: error.error['detail'], life: 10000 }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: error.error['detail'] }]; } else { const errorKeys = Object.keys(errorObject.errors) if (!errorKeys) { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: 'Some properties are not valid' }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: 'Some properties are not valid' }]; } else { errorKeys.forEach(key => { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: errorObject.errors[key] }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: errorObject.errors[key] }]; }); } } } -} +} \ No newline at end of file diff --git a/frontend/src/app/features/components/dashboard/dashboard.component.scss b/frontend/src/app/features/components/dashboard/dashboard.component.scss index 2a6879cf..8f8ba785 100644 --- a/frontend/src/app/features/components/dashboard/dashboard.component.scss +++ b/frontend/src/app/features/components/dashboard/dashboard.component.scss @@ -1,7 +1,7 @@ :host { - // display: flex; width: 100%; height: 100%; + padding: 24px; h2 { padding: 0; diff --git a/frontend/src/app/features/components/delisting-request/delisting-request.component.html b/frontend/src/app/features/components/delisting-request/delisting-request.component.html index bd8a88db..aa9ea4c9 100644 --- a/frontend/src/app/features/components/delisting-request/delisting-request.component.html +++ b/frontend/src/app/features/components/delisting-request/delisting-request.component.html @@ -1,24 +1,60 @@ -
Delisting request
-
All fields are required except where stated
+
Send Takedown Letter Form
+ +
All fields are required except where stated
+ + +
+
+ +
+
+ +
+
+ Request initiator is + required +
+
-
+ placeholder="Please Select..." id="platformId" name="platformId" [required]="true"> +
+
+ Platform recipient is + required
+ +
+
+ +
+
+ +
+
+
- + +
+
+ Please make sure the URL format that + you have entered is correct + Listing URL is required
@@ -29,14 +65,23 @@ +
+
- + +
+
+ Please make sure the email + format you have entered is correct
+ @@ -45,23 +90,9 @@ [style]="{width: '50vw'}">

-
-
-
-
- -
-
- -
-
-
-
- +
- - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/features/components/delisting-request/delisting-request.component.scss b/frontend/src/app/features/components/delisting-request/delisting-request.component.scss index 1e9470b4..d428b592 100644 --- a/frontend/src/app/features/components/delisting-request/delisting-request.component.scss +++ b/frontend/src/app/features/components/delisting-request/delisting-request.component.scss @@ -1,7 +1,8 @@ :host { width: 100%; height: 100%; - padding: 4px; + padding: 24px; + background-color: #FFFFFF; .title { font-size: 24px; @@ -18,7 +19,46 @@ .form-group-row { width: 100%; + .validation-errors { + small { + color: #D32F2F; + } + } + + label { + line-height: 26px; + } + + &.form-spacer { + padding-top: 0; + margin-top: -14px; + } + + &.inline-block { + width: 368px; + margin-right: 24px; + display: inline-block; + + &:last-of-type { + margin-right: 0; + } + } + + span.info-tooltip { + background-image: url(../../../../assets/images/info-icon.svg); + border-radius: 50%; + width: 16px; + height: 16px; + line-height: 26px; + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 4px; + } + &-col { + font-size: 16px; + &.has-checkbox { :first-child { margin-right: 8px; @@ -35,6 +75,5 @@ } } } - } } \ No newline at end of file diff --git a/frontend/src/app/features/components/delisting-request/delisting-request.component.spec.ts b/frontend/src/app/features/components/delisting-request/delisting-request.component.spec.ts index 06f9653b..24dc80cc 100644 --- a/frontend/src/app/features/components/delisting-request/delisting-request.component.spec.ts +++ b/frontend/src/app/features/components/delisting-request/delisting-request.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DelistingRequestComponent } from './delisting-request.component'; +import { DelistingService } from '../../../common/services/delisting.service'; describe('DelistingRequestComponent', () => { let component: DelistingRequestComponent; @@ -8,10 +10,11 @@ describe('DelistingRequestComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DelistingRequestComponent] + imports: [DelistingRequestComponent, HttpClientTestingModule], + providers: [DelistingService] }) - .compileComponents(); - + .compileComponents(); + fixture = TestBed.createComponent(DelistingRequestComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/frontend/src/app/features/components/delisting-request/delisting-request.component.ts b/frontend/src/app/features/components/delisting-request/delisting-request.component.ts index 7d2a1df2..6d288c75 100644 --- a/frontend/src/app/features/components/delisting-request/delisting-request.component.ts +++ b/frontend/src/app/features/components/delisting-request/delisting-request.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { DropdownModule } from 'primeng/dropdown'; import { InputTextModule } from 'primeng/inputtext'; import { InputTextareaModule } from 'primeng/inputtextarea'; @@ -13,13 +13,29 @@ import { ButtonModule } from 'primeng/button'; import { validateEmailListString, validateUrl } from '../../../common/consts/validators.const'; import { ToastModule } from 'primeng/toast'; import { HttpErrorResponse } from '@angular/common/http'; -import { MessageService } from 'primeng/api'; +import { Message } from 'primeng/api'; +import { DelistingRequest } from '../../../common/models/delisting-request'; +import { TooltipModule } from 'primeng/tooltip'; +import { MessagesModule } from 'primeng/messages'; +import { Router } from '@angular/router'; @Component({ selector: 'app-delisting-request', standalone: true, - imports: [ReactiveFormsModule, DropdownModule, InputTextModule, InputTextareaModule, - CheckboxModule, CommonModule, ChipsModule, DialogModule, ButtonModule, ToastModule], + imports: [ + ReactiveFormsModule, + DropdownModule, + InputTextModule, + InputTextareaModule, + MessagesModule, + CheckboxModule, + CommonModule, + ChipsModule, + DialogModule, + TooltipModule, + ButtonModule, + ToastModule, + ], templateUrl: './delisting-request.component.html', styleUrl: './delisting-request.component.scss' }) @@ -27,23 +43,45 @@ export class DelistingRequestComponent implements OnInit { myForm!: FormGroup; platformOptions = new Array(); - reasonOptions = new Array(); + initiatorsOptions = new Array(); isPreviewVisible = false; + hideForm = false; previewText = 'No preview' - constructor(private fb: FormBuilder, private delistingService: DelistingService, private messageService: MessageService) { } + messages = new Array(); + + public get lgIdControl(): AbstractControl { + return this.myForm.controls['lgId']; + } + public get platformIdControl(): AbstractControl { + return this.myForm.controls['platformId']; + } + public get listingUrlControl(): AbstractControl { + return this.myForm.controls['listingUrl']; + } + public get ccListControl(): AbstractControl { + return this.myForm.controls['ccList']; + } + + constructor(private fb: FormBuilder, private delistingService: DelistingService, private router: Router) { } ngOnInit(): void { this.initForm(); this.delistingService.getPlatforms().subscribe((platformOptions) => this.platformOptions = platformOptions); - this.delistingService.getReasons().subscribe((reasonOptions) => this.reasonOptions = reasonOptions); + this.delistingService.getLocalGovernments().subscribe((lgOptions) => this.initiatorsOptions = lgOptions); } onPreview(): void { if (this.myForm.valid) { - this.delistingService.delistingRequestPreview(this.myForm.value).subscribe( + const model: DelistingRequest = Object.assign({}, this.myForm.value); + + model.ccList = this.myForm.value['ccList'].prototype === Array + ? this.myForm.value + : (this.myForm.value['ccList'] as string).split(',').filter(x => !!x).map(x => x.trim()) + + this.delistingService.delistingRequestPreview(model).subscribe( { next: preview => { this.previewText = preview.content; @@ -55,29 +93,32 @@ export class DelistingRequestComponent implements OnInit { } ) } else { + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: 'Form is invalid' }]; console.error('Form is invalid!'); } } - onSubmit(comment: string): void { + onSubmit(): void { if (this.myForm.valid) { - const formValue = this.myForm.value; - formValue.comment = comment; - - this.delistingService.createDelistingRequest(formValue).subscribe({ - next: (_) => { - this.myForm.reset(); - this.initForm(); - this.onPreviewClose(); - this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Message has been sent successfully' }); - }, - error: (error) => { - this.myForm.reset(); - this.initForm(); - this.onPreviewClose(); - this.showErrors(error); - } - }) + const model: DelistingRequest = this.myForm.value; + model.ccList = this.myForm.value['ccList'].prototype === Array + ? this.myForm.value + : (this.myForm.value['ccList'] as string).split(',').filter(x => !!x).map(x => x.trim()) + + this.delistingService.createDelistingRequest(model) + .subscribe({ + next: (_) => { + this.showSuccessMessage(); + }, + error: (error) => { + this.showErrors(error); + }, + complete: () => { + this.myForm.reset(); + this.initForm(); + this.onPreviewClose(); + } + }); } } @@ -85,31 +126,41 @@ export class DelistingRequestComponent implements OnInit { this.isPreviewVisible = false; } + onReturnHome(): void { + this.router.navigateByUrl('/'); + } + + showSuccessMessage(): void { + this.hideForm = true; + this.messages = [{ severity: 'success', summary: '', detail: 'Your Notice of Takedown was Successfully Submitted!' }]; + } + private initForm(): void { this.myForm = this.fb.group({ + lgId: [0, Validators.required], platformId: [0, Validators.required], + listingId: [null], listingUrl: ['', [Validators.required, validateUrl()]], sendCopy: [true], - ccList: [[], validateEmailListString()], - comment: [''], + ccList: ['', validateEmailListString()], }); } - showErrors(error: HttpErrorResponse | any): void { + private showErrors(error: HttpErrorResponse | any): void { let errorObject = typeof error.error === 'string' ? JSON.parse(error.error) : error.error; if (error.error['detail']) { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: error.error['detail'], life: 10000 }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: error.error['detail'] }]; } else { const errorKeys = Object.keys(errorObject.errors) if (!errorKeys) { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: 'Some properties are not valid' }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: 'Some properties are not valid' }]; } else { errorKeys.forEach(key => { - this.messageService.add({ severity: 'error', summary: 'Validation error', detail: errorObject.errors[key] }); + this.messages = [{ severity: 'error', summary: 'Validation error', closable: true, detail: errorObject.errors[key] }]; }); } } } -} +} \ No newline at end of file diff --git a/frontend/src/assets/images/circle-check.svg b/frontend/src/assets/images/circle-check.svg new file mode 100644 index 00000000..eee6fd57 --- /dev/null +++ b/frontend/src/assets/images/circle-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/circle-failed.svg b/frontend/src/assets/images/circle-failed.svg new file mode 100644 index 00000000..8a327214 --- /dev/null +++ b/frontend/src/assets/images/circle-failed.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index af336b92..0497f837 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -45,6 +45,21 @@ body { &.full-width-text-field { width: 100%; + + .p-dropdown { + width: 100%; + } + } + } + + p-dropdown, + input { + &.full-width-text-field { + width: 100%; + + .p-dropdown { + width: 100%; + } } } @@ -72,7 +87,7 @@ body { border-radius: 4px; .p-card-body { - padding: 18px 14px; + padding: 23px 25px; .p-card-title { font-family: 'BcSans-regular'; diff --git a/localdb/backup.sql b/localdb/backup.sql new file mode 100644 index 00000000..a9a17f5d --- /dev/null +++ b/localdb/backup.sql @@ -0,0 +1,2108 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 16.1 (Debian 16.1-1.pgdg110+1) +-- Dumped by pg_dump version 16.1 (Debian 16.1-1.pgdg110+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +ALTER TABLE ONLY public.dss_user_role_privilege DROP CONSTRAINT dss_user_role_privilege_fk_conferring; +ALTER TABLE ONLY public.dss_user_role_privilege DROP CONSTRAINT dss_user_role_privilege_fk_conferred_by; +ALTER TABLE ONLY public.dss_user_role_assignment DROP CONSTRAINT dss_user_role_assignment_fk_granted_to; +ALTER TABLE ONLY public.dss_user_role_assignment DROP CONSTRAINT dss_user_role_assignment_fk_granted; +ALTER TABLE ONLY public.dss_user_identity DROP CONSTRAINT dss_user_identity_fk_representing; +ALTER TABLE ONLY public.dss_user_identity DROP CONSTRAINT dss_user_identity_fk_given; +ALTER TABLE ONLY public.dss_organization DROP CONSTRAINT dss_organization_fk_treated_as; +ALTER TABLE ONLY public.dss_organization DROP CONSTRAINT dss_organization_fk_managed_by; +ALTER TABLE ONLY public.dss_organization_contact_person DROP CONSTRAINT dss_organization_contact_person_fk_contacted_for; +ALTER TABLE ONLY public.dss_message_reason DROP CONSTRAINT dss_message_reason_fk_justifying; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_fk_justified_by; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_fk_involving; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_fk_initiated_by; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_fk_communicating; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_fk_affecting; +ALTER TABLE ONLY hangfire.state DROP CONSTRAINT state_jobid_fkey; +ALTER TABLE ONLY hangfire.jobparameter DROP CONSTRAINT jobparameter_jobid_fkey; +DROP TRIGGER dss_user_identity_br_iu_tr ON public.dss_user_identity; +DROP TRIGGER dss_organization_contact_person_br_iu_tr ON public.dss_organization_contact_person; +DROP TRIGGER dss_organization_br_iu_tr ON public.dss_organization; +DROP INDEX hangfire.jobqueue_queue_fetchat_jobid; +DROP INDEX hangfire.ix_hangfire_state_jobid; +DROP INDEX hangfire.ix_hangfire_set_key_score; +DROP INDEX hangfire.ix_hangfire_set_expireat; +DROP INDEX hangfire.ix_hangfire_list_expireat; +DROP INDEX hangfire.ix_hangfire_jobqueue_queueandfetchedat; +DROP INDEX hangfire.ix_hangfire_jobqueue_jobidandqueue; +DROP INDEX hangfire.ix_hangfire_jobparameter_jobidandname; +DROP INDEX hangfire.ix_hangfire_job_statename; +DROP INDEX hangfire.ix_hangfire_job_expireat; +DROP INDEX hangfire.ix_hangfire_hash_expireat; +DROP INDEX hangfire.ix_hangfire_counter_key; +DROP INDEX hangfire.ix_hangfire_counter_expireat; +ALTER TABLE ONLY public.dss_user_role_privilege DROP CONSTRAINT dss_user_role_privilege_pk; +ALTER TABLE ONLY public.dss_user_role DROP CONSTRAINT dss_user_role_pk; +ALTER TABLE ONLY public.dss_user_role_assignment DROP CONSTRAINT dss_user_role_assignment_pk; +ALTER TABLE ONLY public.dss_user_privilege DROP CONSTRAINT dss_user_privilege_pk; +ALTER TABLE ONLY public.dss_user_identity DROP CONSTRAINT dss_user_identity_pk; +ALTER TABLE ONLY public.dss_organization_type DROP CONSTRAINT dss_organization_type_pk; +ALTER TABLE ONLY public.dss_organization DROP CONSTRAINT dss_organization_pk; +ALTER TABLE ONLY public.dss_organization_contact_person DROP CONSTRAINT dss_organization_contact_person_pk; +ALTER TABLE ONLY public.dss_message_reason DROP CONSTRAINT dss_message_reason_pk; +ALTER TABLE ONLY public.dss_email_message_type DROP CONSTRAINT dss_email_message_type_pk; +ALTER TABLE ONLY public.dss_email_message DROP CONSTRAINT dss_email_message_pk; +ALTER TABLE ONLY public.dss_access_request_status DROP CONSTRAINT dss_access_request_status_pk; +ALTER TABLE ONLY hangfire.state DROP CONSTRAINT state_pkey; +ALTER TABLE ONLY hangfire.set DROP CONSTRAINT set_pkey; +ALTER TABLE ONLY hangfire.set DROP CONSTRAINT set_key_value_key; +ALTER TABLE ONLY hangfire.server DROP CONSTRAINT server_pkey; +ALTER TABLE ONLY hangfire.schema DROP CONSTRAINT schema_pkey; +ALTER TABLE ONLY hangfire.lock DROP CONSTRAINT lock_resource_key; +ALTER TABLE ONLY hangfire.list DROP CONSTRAINT list_pkey; +ALTER TABLE ONLY hangfire.jobqueue DROP CONSTRAINT jobqueue_pkey; +ALTER TABLE ONLY hangfire.jobparameter DROP CONSTRAINT jobparameter_pkey; +ALTER TABLE ONLY hangfire.job DROP CONSTRAINT job_pkey; +ALTER TABLE ONLY hangfire.hash DROP CONSTRAINT hash_pkey; +ALTER TABLE ONLY hangfire.hash DROP CONSTRAINT hash_key_field_key; +ALTER TABLE ONLY hangfire.counter DROP CONSTRAINT counter_pkey; +ALTER TABLE ONLY hangfire.aggregatedcounter DROP CONSTRAINT aggregatedcounter_pkey; +ALTER TABLE ONLY hangfire.aggregatedcounter DROP CONSTRAINT aggregatedcounter_key_key; +ALTER TABLE hangfire.state ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.set ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.list ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.jobqueue ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.jobparameter ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.job ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.hash ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.counter ALTER COLUMN id DROP DEFAULT; +ALTER TABLE hangfire.aggregatedcounter ALTER COLUMN id DROP DEFAULT; +DROP TABLE public.dss_user_role_privilege; +DROP TABLE public.dss_user_role_assignment; +DROP TABLE public.dss_user_role; +DROP TABLE public.dss_user_privilege; +DROP TABLE public.dss_user_identity; +DROP TABLE public.dss_organization_type; +DROP TABLE public.dss_organization_contact_person; +DROP TABLE public.dss_organization; +DROP TABLE public.dss_message_reason; +DROP TABLE public.dss_email_message_type; +DROP TABLE public.dss_email_message; +DROP TABLE public.dss_access_request_status; +DROP SEQUENCE hangfire.state_id_seq; +DROP TABLE hangfire.state; +DROP SEQUENCE hangfire.set_id_seq; +DROP TABLE hangfire.set; +DROP TABLE hangfire.server; +DROP TABLE hangfire.schema; +DROP TABLE hangfire.lock; +DROP SEQUENCE hangfire.list_id_seq; +DROP TABLE hangfire.list; +DROP SEQUENCE hangfire.jobqueue_id_seq; +DROP TABLE hangfire.jobqueue; +DROP SEQUENCE hangfire.jobparameter_id_seq; +DROP TABLE hangfire.jobparameter; +DROP SEQUENCE hangfire.job_id_seq; +DROP TABLE hangfire.job; +DROP SEQUENCE hangfire.hash_id_seq; +DROP TABLE hangfire.hash; +DROP SEQUENCE hangfire.counter_id_seq; +DROP TABLE hangfire.counter; +DROP SEQUENCE hangfire.aggregatedcounter_id_seq; +DROP TABLE hangfire.aggregatedcounter; +DROP FUNCTION public.dss_update_audit_columns(); +DROP EXTENSION postgis; +DROP SCHEMA hangfire; +-- +-- Name: hangfire; Type: SCHEMA; Schema: -; Owner: strdssdev +-- + +CREATE SCHEMA hangfire; + + +ALTER SCHEMA hangfire OWNER TO strdssdev; + +-- +-- Name: postgis; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; + + +-- +-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions'; + + +-- +-- Name: dss_update_audit_columns(); Type: FUNCTION; Schema: public; Owner: strdssdev +-- + +CREATE FUNCTION public.dss_update_audit_columns() RETURNS trigger + LANGUAGE plpgsql + AS $$BEGIN + NEW.upd_dtm := current_timestamp; + RETURN NEW; +END;$$; + + +ALTER FUNCTION public.dss_update_audit_columns() OWNER TO strdssdev; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: aggregatedcounter; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.aggregatedcounter ( + id bigint NOT NULL, + key text NOT NULL, + value bigint NOT NULL, + expireat timestamp with time zone +); + + +ALTER TABLE hangfire.aggregatedcounter OWNER TO strdssdev; + +-- +-- Name: aggregatedcounter_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.aggregatedcounter_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.aggregatedcounter_id_seq OWNER TO strdssdev; + +-- +-- Name: aggregatedcounter_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.aggregatedcounter_id_seq OWNED BY hangfire.aggregatedcounter.id; + + +-- +-- Name: counter; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.counter ( + id bigint NOT NULL, + key text NOT NULL, + value bigint NOT NULL, + expireat timestamp with time zone +); + + +ALTER TABLE hangfire.counter OWNER TO strdssdev; + +-- +-- Name: counter_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.counter_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.counter_id_seq OWNER TO strdssdev; + +-- +-- Name: counter_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.counter_id_seq OWNED BY hangfire.counter.id; + + +-- +-- Name: hash; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.hash ( + id bigint NOT NULL, + key text NOT NULL, + field text NOT NULL, + value text, + expireat timestamp with time zone, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.hash OWNER TO strdssdev; + +-- +-- Name: hash_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.hash_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.hash_id_seq OWNER TO strdssdev; + +-- +-- Name: hash_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.hash_id_seq OWNED BY hangfire.hash.id; + + +-- +-- Name: job; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.job ( + id bigint NOT NULL, + stateid bigint, + statename text, + invocationdata jsonb NOT NULL, + arguments jsonb NOT NULL, + createdat timestamp with time zone NOT NULL, + expireat timestamp with time zone, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.job OWNER TO strdssdev; + +-- +-- Name: job_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.job_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.job_id_seq OWNER TO strdssdev; + +-- +-- Name: job_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.job_id_seq OWNED BY hangfire.job.id; + + +-- +-- Name: jobparameter; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.jobparameter ( + id bigint NOT NULL, + jobid bigint NOT NULL, + name text NOT NULL, + value text, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.jobparameter OWNER TO strdssdev; + +-- +-- Name: jobparameter_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.jobparameter_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.jobparameter_id_seq OWNER TO strdssdev; + +-- +-- Name: jobparameter_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.jobparameter_id_seq OWNED BY hangfire.jobparameter.id; + + +-- +-- Name: jobqueue; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.jobqueue ( + id bigint NOT NULL, + jobid bigint NOT NULL, + queue text NOT NULL, + fetchedat timestamp with time zone, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.jobqueue OWNER TO strdssdev; + +-- +-- Name: jobqueue_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.jobqueue_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.jobqueue_id_seq OWNER TO strdssdev; + +-- +-- Name: jobqueue_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.jobqueue_id_seq OWNED BY hangfire.jobqueue.id; + + +-- +-- Name: list; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.list ( + id bigint NOT NULL, + key text NOT NULL, + value text, + expireat timestamp with time zone, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.list OWNER TO strdssdev; + +-- +-- Name: list_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.list_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.list_id_seq OWNER TO strdssdev; + +-- +-- Name: list_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.list_id_seq OWNED BY hangfire.list.id; + + +-- +-- Name: lock; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.lock ( + resource text NOT NULL, + updatecount integer DEFAULT 0 NOT NULL, + acquired timestamp with time zone +); + + +ALTER TABLE hangfire.lock OWNER TO strdssdev; + +-- +-- Name: schema; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.schema ( + version integer NOT NULL +); + + +ALTER TABLE hangfire.schema OWNER TO strdssdev; + +-- +-- Name: server; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.server ( + id text NOT NULL, + data jsonb, + lastheartbeat timestamp with time zone NOT NULL, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.server OWNER TO strdssdev; + +-- +-- Name: set; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.set ( + id bigint NOT NULL, + key text NOT NULL, + score double precision NOT NULL, + value text NOT NULL, + expireat timestamp with time zone, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.set OWNER TO strdssdev; + +-- +-- Name: set_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.set_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.set_id_seq OWNER TO strdssdev; + +-- +-- Name: set_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.set_id_seq OWNED BY hangfire.set.id; + + +-- +-- Name: state; Type: TABLE; Schema: hangfire; Owner: strdssdev +-- + +CREATE TABLE hangfire.state ( + id bigint NOT NULL, + jobid bigint NOT NULL, + name text NOT NULL, + reason text, + createdat timestamp with time zone NOT NULL, + data jsonb, + updatecount integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE hangfire.state OWNER TO strdssdev; + +-- +-- Name: state_id_seq; Type: SEQUENCE; Schema: hangfire; Owner: strdssdev +-- + +CREATE SEQUENCE hangfire.state_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE hangfire.state_id_seq OWNER TO strdssdev; + +-- +-- Name: state_id_seq; Type: SEQUENCE OWNED BY; Schema: hangfire; Owner: strdssdev +-- + +ALTER SEQUENCE hangfire.state_id_seq OWNED BY hangfire.state.id; + + +-- +-- Name: dss_access_request_status; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_access_request_status ( + access_request_status_cd character varying(25) NOT NULL, + access_request_status_nm character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_access_request_status OWNER TO strdssdev; + +-- +-- Name: dss_email_message; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_email_message ( + email_message_id bigint NOT NULL, + email_message_type character varying(50) NOT NULL, + message_delivery_dtm timestamp with time zone NOT NULL, + message_template_dsc character varying(4000) NOT NULL, + is_host_contacted_externally boolean NOT NULL, + is_submitter_cc_required boolean NOT NULL, + message_reason_id bigint, + lg_phone_no character varying(13), + unreported_listing_no character varying(25), + host_email_address_dsc character varying(320), + lg_email_address_dsc character varying(320), + cc_email_address_dsc character varying(4000), + unreported_listing_url character varying(4000), + lg_str_bylaw_url character varying(4000), + initiating_user_identity_id bigint NOT NULL, + affected_by_user_identity_id bigint, + involved_in_organization_id bigint +); + + +ALTER TABLE public.dss_email_message OWNER TO strdssdev; + +-- +-- Name: TABLE dss_email_message; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_email_message IS 'A message that is sent to one or more recipients via email'; + + +-- +-- Name: COLUMN dss_email_message.email_message_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.email_message_id IS 'Unique generated key'; + + +-- +-- Name: COLUMN dss_email_message.email_message_type; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.email_message_type IS 'Business term for the type or purpose of the message (e.g. Notice of Takedown, Takedown Request, Delisting Warning, Delisting Request, Access Granted Notification, Access Denied Notification)'; + + +-- +-- Name: COLUMN dss_email_message.message_delivery_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.message_delivery_dtm IS 'A timestamp indicating when the message delivery was initiated'; + + +-- +-- Name: COLUMN dss_email_message.message_template_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.message_template_dsc IS 'The full text or template for the message that is sent'; + + +-- +-- Name: COLUMN dss_email_message.host_email_address_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.host_email_address_dsc IS 'E-mail address of a short term rental host (directly entered by the user as a message recipient)'; + + +-- +-- Name: COLUMN dss_email_message.cc_email_address_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.cc_email_address_dsc IS 'E-mail address of a secondary message recipient (directly entered by the user)'; + + +-- +-- Name: COLUMN dss_email_message.unreported_listing_url; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.unreported_listing_url IS 'User-provided URL for a short-term rental platform listing that is the subject of the message'; + + +-- +-- Name: COLUMN dss_email_message.initiating_user_identity_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.initiating_user_identity_id IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_email_message.affected_by_user_identity_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.affected_by_user_identity_id IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_email_message.involved_in_organization_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_email_message.involved_in_organization_id IS 'Foreign key'; + + +-- +-- Name: dss_email_message_email_message_id_seq; Type: SEQUENCE; Schema: public; Owner: strdssdev +-- + +ALTER TABLE public.dss_email_message ALTER COLUMN email_message_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.dss_email_message_email_message_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: dss_email_message_type; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_email_message_type ( + email_message_type character varying(50) NOT NULL, + email_message_type_nm character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_email_message_type OWNER TO strdssdev; + +-- +-- Name: dss_message_reason; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_message_reason ( + message_reason_id bigint NOT NULL, + email_message_type character varying(50) NOT NULL, + message_reason_dsc character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_message_reason OWNER TO strdssdev; + +-- +-- Name: COLUMN dss_message_reason.message_reason_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_message_reason.message_reason_dsc IS 'A description of the justification for initiating a message'; + + +-- +-- Name: dss_message_reason_message_reason_id_seq; Type: SEQUENCE; Schema: public; Owner: strdssdev +-- + +ALTER TABLE public.dss_message_reason ALTER COLUMN message_reason_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.dss_message_reason_message_reason_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: dss_organization; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_organization ( + organization_id bigint NOT NULL, + organization_type character varying(25) NOT NULL, + organization_cd character varying(25) NOT NULL, + organization_nm character varying(250) NOT NULL, + local_government_geometry public.geometry, + managing_organization_id bigint, + upd_dtm timestamp with time zone NOT NULL, + upd_user_guid uuid +); + + +ALTER TABLE public.dss_organization OWNER TO strdssdev; + +-- +-- Name: TABLE dss_organization; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_organization IS 'A private company or governing body that plays a role in short term rental reporting or enforcement'; + + +-- +-- Name: COLUMN dss_organization.organization_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.organization_id IS 'Unique generated key'; + + +-- +-- Name: COLUMN dss_organization.organization_type; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.organization_type IS 'a level of government or business category'; + + +-- +-- Name: COLUMN dss_organization.organization_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.organization_cd IS 'An immutable system code that identifies the organization (e.g. CEU, AIRBNB)'; + + +-- +-- Name: COLUMN dss_organization.organization_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.organization_nm IS 'A human-readable name that identifies the organization (e.g. Corporate Enforecement Unit, City of Victoria)'; + + +-- +-- Name: COLUMN dss_organization.local_government_geometry; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.local_government_geometry IS 'the shape identifying the boundaries of a local government'; + + +-- +-- Name: COLUMN dss_organization.managing_organization_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.managing_organization_id IS 'Self-referential hierarchical foreign key'; + + +-- +-- Name: COLUMN dss_organization.upd_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.upd_dtm IS 'Trigger-updated timestamp of last change'; + + +-- +-- Name: COLUMN dss_organization.upd_user_guid; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization.upd_user_guid IS 'The globally unique identifier (assigned by the identity provider) for the most recent user to record a change'; + + +-- +-- Name: dss_organization_contact_person; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_organization_contact_person ( + organization_contact_person_id bigint NOT NULL, + is_primary boolean NOT NULL, + given_nm character varying(25) NOT NULL, + family_nm character varying(25) NOT NULL, + phone_no character varying(13) NOT NULL, + email_address_dsc character varying(320) NOT NULL, + contacted_through_organization_id bigint NOT NULL, + upd_dtm timestamp with time zone NOT NULL, + upd_user_guid uuid +); + + +ALTER TABLE public.dss_organization_contact_person OWNER TO strdssdev; + +-- +-- Name: TABLE dss_organization_contact_person; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_organization_contact_person IS 'A person who has been identified as a notable contact for a particular organization'; + + +-- +-- Name: COLUMN dss_organization_contact_person.organization_contact_person_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.organization_contact_person_id IS 'Unique generated key'; + + +-- +-- Name: COLUMN dss_organization_contact_person.is_primary; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.is_primary IS 'Indicates whether the contact should receive all communications directed at the organization'; + + +-- +-- Name: COLUMN dss_organization_contact_person.given_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.given_nm IS 'A name given to a person so that they can easily be identified among their family members (in some cultures, this is often the first name)'; + + +-- +-- Name: COLUMN dss_organization_contact_person.family_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.family_nm IS 'A name that is often shared amongst members of the same family (commonly known as a surname within some cultures)'; + + +-- +-- Name: COLUMN dss_organization_contact_person.phone_no; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.phone_no IS 'Phone number given for the contact by the organization (contains only digits)'; + + +-- +-- Name: COLUMN dss_organization_contact_person.email_address_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.email_address_dsc IS 'E-mail address given for the contact by the organization'; + + +-- +-- Name: COLUMN dss_organization_contact_person.contacted_through_organization_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.contacted_through_organization_id IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_organization_contact_person.upd_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.upd_dtm IS 'Trigger-updated timestamp of last change'; + + +-- +-- Name: COLUMN dss_organization_contact_person.upd_user_guid; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_organization_contact_person.upd_user_guid IS 'The globally unique identifier (assigned by the identity provider) for the most recent user to record a change'; + + +-- +-- Name: dss_organization_contact_pers_organization_contact_person_i_seq; Type: SEQUENCE; Schema: public; Owner: strdssdev +-- + +ALTER TABLE public.dss_organization_contact_person ALTER COLUMN organization_contact_person_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.dss_organization_contact_pers_organization_contact_person_i_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: dss_organization_organization_id_seq; Type: SEQUENCE; Schema: public; Owner: strdssdev +-- + +ALTER TABLE public.dss_organization ALTER COLUMN organization_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.dss_organization_organization_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: dss_organization_type; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_organization_type ( + organization_type character varying(25) NOT NULL, + organization_type_nm character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_organization_type OWNER TO strdssdev; + +-- +-- Name: dss_user_identity; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_user_identity ( + user_identity_id bigint NOT NULL, + user_guid uuid NOT NULL, + display_nm character varying(250) NOT NULL, + identity_provider_nm character varying(25) NOT NULL, + is_enabled boolean NOT NULL, + access_request_status_cd character varying(25) NOT NULL, + access_request_dtm timestamp with time zone, + access_request_justification_txt character varying(250), + given_nm character varying(25), + family_nm character varying(25), + email_address_dsc character varying(320), + business_nm character varying(250), + terms_acceptance_dtm timestamp with time zone, + represented_by_organization_id bigint, + upd_dtm timestamp with time zone NOT NULL, + upd_user_guid uuid +); + + +ALTER TABLE public.dss_user_identity OWNER TO strdssdev; + +-- +-- Name: TABLE dss_user_identity; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_user_identity IS 'An externally defined domain directory object representing a potential application user or group'; + + +-- +-- Name: COLUMN dss_user_identity.user_identity_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.user_identity_id IS 'Unique generated key'; + + +-- +-- Name: COLUMN dss_user_identity.user_guid; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.user_guid IS 'An immutable unique identifier assigned by the identity provider'; + + +-- +-- Name: COLUMN dss_user_identity.display_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.display_nm IS 'A human-readable full name that is assigned by the identity provider (this may include a preferred name and/or business unit name)'; + + +-- +-- Name: COLUMN dss_user_identity.identity_provider_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.identity_provider_nm IS 'A directory or domain that authenticates system users to allow access to the application or its API (e.g. idir, bceidbusiness)'; + + +-- +-- Name: COLUMN dss_user_identity.is_enabled; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.is_enabled IS 'Indicates whether access is currently permitted using this identity'; + + +-- +-- Name: COLUMN dss_user_identity.access_request_status_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.access_request_status_cd IS 'The current status of the most recent access request made by the user (restricted to Requested, Approved, or Denied)'; + + +-- +-- Name: COLUMN dss_user_identity.access_request_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.access_request_dtm IS 'A timestamp indicating when the most recent access request was made by the user'; + + +-- +-- Name: COLUMN dss_user_identity.access_request_justification_txt; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.access_request_justification_txt IS 'The most recent user-provided reason for requesting application access'; + + +-- +-- Name: COLUMN dss_user_identity.given_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.given_nm IS 'A name given to a person so that they can easily be identified among their family members (in some cultures, this is often the first name)'; + + +-- +-- Name: COLUMN dss_user_identity.family_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.family_nm IS 'A name that is often shared amongst members of the same family (commonly known as a surname within some cultures)'; + + +-- +-- Name: COLUMN dss_user_identity.email_address_dsc; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.email_address_dsc IS 'E-mail address associated with the user by the identity provider'; + + +-- +-- Name: COLUMN dss_user_identity.business_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.business_nm IS 'A human-readable organization name that is associated with the user by the identity provider'; + + +-- +-- Name: COLUMN dss_user_identity.terms_acceptance_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.terms_acceptance_dtm IS 'A timestamp indicating when the user most recently accepted the published Terms and Conditions of application access'; + + +-- +-- Name: COLUMN dss_user_identity.represented_by_organization_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.represented_by_organization_id IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_user_identity.upd_dtm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.upd_dtm IS 'Trigger-updated timestamp of last change'; + + +-- +-- Name: COLUMN dss_user_identity.upd_user_guid; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_identity.upd_user_guid IS 'The globally unique identifier (assigned by the identity provider) for the most recent user to record a change'; + + +-- +-- Name: dss_user_identity_user_identity_id_seq; Type: SEQUENCE; Schema: public; Owner: strdssdev +-- + +ALTER TABLE public.dss_user_identity ALTER COLUMN user_identity_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.dss_user_identity_user_identity_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: dss_user_privilege; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_user_privilege ( + user_privilege_cd character varying(25) NOT NULL, + user_privilege_nm character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_user_privilege OWNER TO strdssdev; + +-- +-- Name: TABLE dss_user_privilege; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_user_privilege IS 'A granular access right or privilege within the application that may be granted to a role'; + + +-- +-- Name: COLUMN dss_user_privilege.user_privilege_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_privilege.user_privilege_cd IS 'The immutable system code that identifies the privilege'; + + +-- +-- Name: COLUMN dss_user_privilege.user_privilege_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_privilege.user_privilege_nm IS 'The human-readable name that is given for the role'; + + +-- +-- Name: dss_user_role; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_user_role ( + user_role_cd character varying(25) NOT NULL, + user_role_nm character varying(250) NOT NULL +); + + +ALTER TABLE public.dss_user_role OWNER TO strdssdev; + +-- +-- Name: TABLE dss_user_role; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_user_role IS 'A set of access rights and privileges within the application that may be granted to users'; + + +-- +-- Name: COLUMN dss_user_role.user_role_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role.user_role_cd IS 'The immutable system code that identifies the role'; + + +-- +-- Name: COLUMN dss_user_role.user_role_nm; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role.user_role_nm IS 'The human-readable name that is given for the role'; + + +-- +-- Name: dss_user_role_assignment; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_user_role_assignment ( + user_identity_id bigint NOT NULL, + user_role_cd character varying(25) NOT NULL +); + + +ALTER TABLE public.dss_user_role_assignment OWNER TO strdssdev; + +-- +-- Name: TABLE dss_user_role_assignment; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_user_role_assignment IS 'The association of a grantee credential to a role for the purpose of conveying application privileges'; + + +-- +-- Name: COLUMN dss_user_role_assignment.user_identity_id; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role_assignment.user_identity_id IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_user_role_assignment.user_role_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role_assignment.user_role_cd IS 'Foreign key'; + + +-- +-- Name: dss_user_role_privilege; Type: TABLE; Schema: public; Owner: strdssdev +-- + +CREATE TABLE public.dss_user_role_privilege ( + user_privilege_cd character varying(25) NOT NULL, + user_role_cd character varying(25) NOT NULL +); + + +ALTER TABLE public.dss_user_role_privilege OWNER TO strdssdev; + +-- +-- Name: TABLE dss_user_role_privilege; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON TABLE public.dss_user_role_privilege IS 'The association of a granular application privilege to a role'; + + +-- +-- Name: COLUMN dss_user_role_privilege.user_privilege_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role_privilege.user_privilege_cd IS 'Foreign key'; + + +-- +-- Name: COLUMN dss_user_role_privilege.user_role_cd; Type: COMMENT; Schema: public; Owner: strdssdev +-- + +COMMENT ON COLUMN public.dss_user_role_privilege.user_role_cd IS 'Foreign key'; + + +-- +-- Name: aggregatedcounter id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.aggregatedcounter ALTER COLUMN id SET DEFAULT nextval('hangfire.aggregatedcounter_id_seq'::regclass); + + +-- +-- Name: counter id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.counter ALTER COLUMN id SET DEFAULT nextval('hangfire.counter_id_seq'::regclass); + + +-- +-- Name: hash id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.hash ALTER COLUMN id SET DEFAULT nextval('hangfire.hash_id_seq'::regclass); + + +-- +-- Name: job id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.job ALTER COLUMN id SET DEFAULT nextval('hangfire.job_id_seq'::regclass); + + +-- +-- Name: jobparameter id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.jobparameter ALTER COLUMN id SET DEFAULT nextval('hangfire.jobparameter_id_seq'::regclass); + + +-- +-- Name: jobqueue id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.jobqueue ALTER COLUMN id SET DEFAULT nextval('hangfire.jobqueue_id_seq'::regclass); + + +-- +-- Name: list id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.list ALTER COLUMN id SET DEFAULT nextval('hangfire.list_id_seq'::regclass); + + +-- +-- Name: set id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.set ALTER COLUMN id SET DEFAULT nextval('hangfire.set_id_seq'::regclass); + + +-- +-- Name: state id; Type: DEFAULT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.state ALTER COLUMN id SET DEFAULT nextval('hangfire.state_id_seq'::regclass); + + +-- +-- Data for Name: aggregatedcounter; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.aggregatedcounter (id, key, value, expireat) FROM stdin; +\. + + +-- +-- Data for Name: counter; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.counter (id, key, value, expireat) FROM stdin; +\. + + +-- +-- Data for Name: hash; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.hash (id, key, field, value, expireat, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: job; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.job (id, stateid, statename, invocationdata, arguments, createdat, expireat, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: jobparameter; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.jobparameter (id, jobid, name, value, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: jobqueue; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.jobqueue (id, jobid, queue, fetchedat, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: list; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.list (id, key, value, expireat, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: lock; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.lock (resource, updatecount, acquired) FROM stdin; +\. + + +-- +-- Data for Name: schema; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.schema (version) FROM stdin; +21 +\. + + +-- +-- Data for Name: server; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.server (id, data, lastheartbeat, updatecount) FROM stdin; +cnd2214638-n:17896:acbd8bd9-502f-480f-b565-de9d136c842d {"Queues": ["default"], "StartedAt": "2024-03-25T15:53:58.0476485Z", "WorkerCount": 1} 2024-03-25 16:20:36.276805+00 0 +\. + + +-- +-- Data for Name: set; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.set (id, key, score, value, expireat, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: state; Type: TABLE DATA; Schema: hangfire; Owner: strdssdev +-- + +COPY hangfire.state (id, jobid, name, reason, createdat, data, updatecount) FROM stdin; +\. + + +-- +-- Data for Name: dss_access_request_status; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_access_request_status (access_request_status_cd, access_request_status_nm) FROM stdin; +Denied Denied +Requested Requested +Approved Approved +\. + + +-- +-- Data for Name: dss_email_message; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_email_message (email_message_id, email_message_type, message_delivery_dtm, message_template_dsc, is_host_contacted_externally, is_submitter_cc_required, message_reason_id, lg_phone_no, unreported_listing_no, host_email_address_dsc, lg_email_address_dsc, cc_email_address_dsc, unreported_listing_url, lg_str_bylaw_url, initiating_user_identity_id, affected_by_user_identity_id, involved_in_organization_id) FROM stdin; +\. + + +-- +-- Data for Name: dss_email_message_type; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_email_message_type (email_message_type, email_message_type_nm) FROM stdin; +Compliance Order Provincial Compliance Order +Access Granted Access Granted Notification +Escalation Request STR Escalation Request +Takedown Request Takedown Request +Access Denied Access Denied Notification +Notice of Takedown Notice of Takedown of Short Term Rental Platform Offer +\. + + +-- +-- Data for Name: dss_message_reason; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_message_reason (message_reason_id, email_message_type, message_reason_dsc) FROM stdin; +1 Notice of Takedown Expired Business Licence +2 Notice of Takedown Suspended Business Licence +3 Notice of Takedown Business Licence Denied +4 Notice of Takedown Invalid Business Licence Number +5 Notice of Takedown No Business Licence Number on Listing +6 Notice of Takedown Revoked Business Licence +\. + + +-- +-- Data for Name: dss_organization; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_organization (organization_id, organization_type, organization_cd, organization_nm, local_government_geometry, managing_organization_id, upd_dtm, upd_user_guid) FROM stdin; +1 LG LGTEST Test Town \N \N 2024-03-25 16:21:48.115257+00 \N +2 Platform PLATFORMTEST Test Platform \N \N 2024-03-25 16:21:48.115257+00 \N +3 BCGov BC Other BC Government Components \N \N 2024-03-25 16:21:48.115257+00 \N +4 BCGov CEU Compliance Enforcement Unit \N \N 2024-03-25 16:21:48.115257+00 \N +\. + + +-- +-- Data for Name: dss_organization_contact_person; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_organization_contact_person (organization_contact_person_id, is_primary, given_nm, family_nm, phone_no, email_address_dsc, contacted_through_organization_id, upd_dtm, upd_user_guid) FROM stdin; +1 t John Doe young-jin.chung@dxcas.com 2 2024-03-25 16:25:03.665664+00 550e8400-e29b-41d4-a716-446655440014 +\. + + +-- +-- Data for Name: dss_organization_type; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_organization_type (organization_type, organization_type_nm) FROM stdin; +BCGov BC Government Component +Platform Short Term Rental Platform +LG Local Government +\. + + +-- +-- Data for Name: dss_user_identity; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_user_identity (user_identity_id, user_guid, display_nm, identity_provider_nm, is_enabled, access_request_status_cd, access_request_dtm, access_request_justification_txt, given_nm, family_nm, email_address_dsc, business_nm, terms_acceptance_dtm, represented_by_organization_id, upd_dtm, upd_user_guid) FROM stdin; +1 550e8400-e29b-41d4-a716-446655440000 User1 Display Name idir f Requested \N BCGov, Ministry of Housing User1 Given Name User1 Family Name user1@example.com User1 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +2 550e8400-e29b-41d4-a716-446655440001 User2 Display Name idir f Requested \N BCGov, Ministry of Housing User2 Given Name User2 Family Name user2@example.com User2 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +3 550e8400-e29b-41d4-a716-446655440002 User3 Display Name idir f Requested \N BCGov, Ministry of Housing User3 Given Name User3 Family Name user3@example.com User3 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +4 550e8400-e29b-41d4-a716-446655440003 User4 Display Name idir f Requested \N BCGov, Ministry of Housing User4 Given Name User4 Family Name user4@example.com User4 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +5 550e8400-e29b-41d4-a716-446655440004 User5 Display Name idir f Requested \N BCGov, Ministry of Housing User5 Given Name User5 Family Name user5@example.com User5 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +6 550e8400-e29b-41d4-a716-446655440005 User6 Display Name idir f Requested \N BCGov, Ministry of Housing User6 Given Name User6 Family Name user6@example.com User6 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +7 550e8400-e29b-41d4-a716-446655440006 User7 Display Name idir f Requested \N BCGov, Ministry of Housing User7 Given Name User7 Family Name user7@example.com User7 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +8 550e8400-e29b-41d4-a716-446655440007 User8 Display Name idir f Requested \N BCGov, Ministry of Housing User8 Given Name User8 Family Name user8@example.com User8 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +9 550e8400-e29b-41d4-a716-446655440008 User9 Display Name idir f Requested \N BCGov, Ministry of Housing User9 Given Name User9 Family Name user9@example.com User9 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +10 550e8400-e29b-41d4-a716-446655440009 User10 Display Name idir f Requested \N BCGov, Ministry of Housing User10 Given Name User10 Family Name user10@example.com User10 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +11 550e8400-e29b-41d4-a716-446655440010 User11 Display Name idir f Requested \N BCGov, Ministry of Housing User11 Given Name User11 Family Name user11@example.com User11 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +12 550e8400-e29b-41d4-a716-446655440011 User12 Display Name idir f Requested \N BCGov, Ministry of Housing User12 Given Name User12 Family Name user12@example.com User12 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +13 550e8400-e29b-41d4-a716-446655440012 User13 Display Name idir f Requested \N BCGov, Ministry of Housing User13 Given Name User13 Family Name user13@example.com User13 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +14 550e8400-e29b-41d4-a716-446655440013 User14 Display Name idir f Requested \N BCGov, Ministry of Housing User14 Given Name User14 Family Name user14@example.com User14 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +15 550e8400-e29b-41d4-a716-446655440014 User15 Display Name idir f Requested \N BCGov, Ministry of Housing User15 Given Name User15 Family Name user15@example.com User15 Business Name \N \N 2024-03-25 16:23:00.821534+00 \N +\. + + +-- +-- Data for Name: dss_user_privilege; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_user_privilege (user_privilege_cd, user_privilege_nm) FROM stdin; +user_read View users +listing_read View listings +ceu_action Create CEU Action +takedown_action Create Takedown Action +user_write Manage users +listing_file_upload Upload platform listing files +licence_file_upload Upload business licence files +audit_read View audit logs +\. + + +-- +-- Data for Name: dss_user_role; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_user_role (user_role_cd, user_role_nm) FROM stdin; +ceu_staff CEU Staff +ceu_admin CEU Admin +bc_staff Other Provincial Government +lg_staff Local Government +platform_staff Short Term Rental Platform +\. + + +-- +-- Data for Name: dss_user_role_assignment; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_user_role_assignment (user_identity_id, user_role_cd) FROM stdin; +\. + + +-- +-- Data for Name: dss_user_role_privilege; Type: TABLE DATA; Schema: public; Owner: strdssdev +-- + +COPY public.dss_user_role_privilege (user_privilege_cd, user_role_cd) FROM stdin; +ceu_action ceu_staff +audit_read bc_staff +ceu_action ceu_admin +listing_read lg_staff +takedown_action lg_staff +listing_read ceu_admin +user_read ceu_admin +listing_read ceu_staff +licence_file_upload ceu_admin +audit_read ceu_staff +listing_file_upload ceu_admin +listing_read bc_staff +user_write ceu_admin +licence_file_upload lg_staff +listing_file_upload platform_staff +audit_read lg_staff +\. + + +-- +-- Data for Name: spatial_ref_sys; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text) FROM stdin; +\. + + +-- +-- Name: aggregatedcounter_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.aggregatedcounter_id_seq', 1, false); + + +-- +-- Name: counter_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.counter_id_seq', 1, false); + + +-- +-- Name: hash_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.hash_id_seq', 1, false); + + +-- +-- Name: job_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.job_id_seq', 1, false); + + +-- +-- Name: jobparameter_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.jobparameter_id_seq', 1, false); + + +-- +-- Name: jobqueue_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.jobqueue_id_seq', 1, false); + + +-- +-- Name: list_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.list_id_seq', 1, false); + + +-- +-- Name: set_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.set_id_seq', 1, false); + + +-- +-- Name: state_id_seq; Type: SEQUENCE SET; Schema: hangfire; Owner: strdssdev +-- + +SELECT pg_catalog.setval('hangfire.state_id_seq', 1, false); + + +-- +-- Name: dss_email_message_email_message_id_seq; Type: SEQUENCE SET; Schema: public; Owner: strdssdev +-- + +SELECT pg_catalog.setval('public.dss_email_message_email_message_id_seq', 1, false); + + +-- +-- Name: dss_message_reason_message_reason_id_seq; Type: SEQUENCE SET; Schema: public; Owner: strdssdev +-- + +SELECT pg_catalog.setval('public.dss_message_reason_message_reason_id_seq', 6, true); + + +-- +-- Name: dss_organization_contact_pers_organization_contact_person_i_seq; Type: SEQUENCE SET; Schema: public; Owner: strdssdev +-- + +SELECT pg_catalog.setval('public.dss_organization_contact_pers_organization_contact_person_i_seq', 1, true); + + +-- +-- Name: dss_organization_organization_id_seq; Type: SEQUENCE SET; Schema: public; Owner: strdssdev +-- + +SELECT pg_catalog.setval('public.dss_organization_organization_id_seq', 4, true); + + +-- +-- Name: dss_user_identity_user_identity_id_seq; Type: SEQUENCE SET; Schema: public; Owner: strdssdev +-- + +SELECT pg_catalog.setval('public.dss_user_identity_user_identity_id_seq', 15, true); + + +-- +-- Name: aggregatedcounter aggregatedcounter_key_key; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.aggregatedcounter + ADD CONSTRAINT aggregatedcounter_key_key UNIQUE (key); + + +-- +-- Name: aggregatedcounter aggregatedcounter_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.aggregatedcounter + ADD CONSTRAINT aggregatedcounter_pkey PRIMARY KEY (id); + + +-- +-- Name: counter counter_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.counter + ADD CONSTRAINT counter_pkey PRIMARY KEY (id); + + +-- +-- Name: hash hash_key_field_key; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.hash + ADD CONSTRAINT hash_key_field_key UNIQUE (key, field); + + +-- +-- Name: hash hash_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.hash + ADD CONSTRAINT hash_pkey PRIMARY KEY (id); + + +-- +-- Name: job job_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.job + ADD CONSTRAINT job_pkey PRIMARY KEY (id); + + +-- +-- Name: jobparameter jobparameter_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.jobparameter + ADD CONSTRAINT jobparameter_pkey PRIMARY KEY (id); + + +-- +-- Name: jobqueue jobqueue_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.jobqueue + ADD CONSTRAINT jobqueue_pkey PRIMARY KEY (id); + + +-- +-- Name: list list_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.list + ADD CONSTRAINT list_pkey PRIMARY KEY (id); + + +-- +-- Name: lock lock_resource_key; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.lock + ADD CONSTRAINT lock_resource_key UNIQUE (resource); + +ALTER TABLE ONLY hangfire.lock REPLICA IDENTITY USING INDEX lock_resource_key; + + +-- +-- Name: schema schema_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.schema + ADD CONSTRAINT schema_pkey PRIMARY KEY (version); + + +-- +-- Name: server server_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.server + ADD CONSTRAINT server_pkey PRIMARY KEY (id); + + +-- +-- Name: set set_key_value_key; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.set + ADD CONSTRAINT set_key_value_key UNIQUE (key, value); + + +-- +-- Name: set set_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.set + ADD CONSTRAINT set_pkey PRIMARY KEY (id); + + +-- +-- Name: state state_pkey; Type: CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.state + ADD CONSTRAINT state_pkey PRIMARY KEY (id); + + +-- +-- Name: dss_access_request_status dss_access_request_status_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_access_request_status + ADD CONSTRAINT dss_access_request_status_pk PRIMARY KEY (access_request_status_cd); + + +-- +-- Name: dss_email_message dss_email_message_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_pk PRIMARY KEY (email_message_id); + + +-- +-- Name: dss_email_message_type dss_email_message_type_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message_type + ADD CONSTRAINT dss_email_message_type_pk PRIMARY KEY (email_message_type); + + +-- +-- Name: dss_message_reason dss_message_reason_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_message_reason + ADD CONSTRAINT dss_message_reason_pk PRIMARY KEY (message_reason_id); + + +-- +-- Name: dss_organization_contact_person dss_organization_contact_person_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization_contact_person + ADD CONSTRAINT dss_organization_contact_person_pk PRIMARY KEY (organization_contact_person_id); + + +-- +-- Name: dss_organization dss_organization_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization + ADD CONSTRAINT dss_organization_pk PRIMARY KEY (organization_id); + + +-- +-- Name: dss_organization_type dss_organization_type_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization_type + ADD CONSTRAINT dss_organization_type_pk PRIMARY KEY (organization_type); + + +-- +-- Name: dss_user_identity dss_user_identity_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_identity + ADD CONSTRAINT dss_user_identity_pk PRIMARY KEY (user_identity_id); + + +-- +-- Name: dss_user_privilege dss_user_privilege_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_privilege + ADD CONSTRAINT dss_user_privilege_pk PRIMARY KEY (user_privilege_cd); + + +-- +-- Name: dss_user_role_assignment dss_user_role_assignment_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_assignment + ADD CONSTRAINT dss_user_role_assignment_pk PRIMARY KEY (user_identity_id, user_role_cd); + + +-- +-- Name: dss_user_role dss_user_role_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role + ADD CONSTRAINT dss_user_role_pk PRIMARY KEY (user_role_cd); + + +-- +-- Name: dss_user_role_privilege dss_user_role_privilege_pk; Type: CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_privilege + ADD CONSTRAINT dss_user_role_privilege_pk PRIMARY KEY (user_privilege_cd, user_role_cd); + + +-- +-- Name: ix_hangfire_counter_expireat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_counter_expireat ON hangfire.counter USING btree (expireat); + + +-- +-- Name: ix_hangfire_counter_key; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_counter_key ON hangfire.counter USING btree (key); + + +-- +-- Name: ix_hangfire_hash_expireat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_hash_expireat ON hangfire.hash USING btree (expireat); + + +-- +-- Name: ix_hangfire_job_expireat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_job_expireat ON hangfire.job USING btree (expireat); + + +-- +-- Name: ix_hangfire_job_statename; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_job_statename ON hangfire.job USING btree (statename); + + +-- +-- Name: ix_hangfire_jobparameter_jobidandname; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_jobparameter_jobidandname ON hangfire.jobparameter USING btree (jobid, name); + + +-- +-- Name: ix_hangfire_jobqueue_jobidandqueue; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_jobqueue_jobidandqueue ON hangfire.jobqueue USING btree (jobid, queue); + + +-- +-- Name: ix_hangfire_jobqueue_queueandfetchedat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_jobqueue_queueandfetchedat ON hangfire.jobqueue USING btree (queue, fetchedat); + + +-- +-- Name: ix_hangfire_list_expireat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_list_expireat ON hangfire.list USING btree (expireat); + + +-- +-- Name: ix_hangfire_set_expireat; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_set_expireat ON hangfire.set USING btree (expireat); + + +-- +-- Name: ix_hangfire_set_key_score; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_set_key_score ON hangfire.set USING btree (key, score); + + +-- +-- Name: ix_hangfire_state_jobid; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX ix_hangfire_state_jobid ON hangfire.state USING btree (jobid); + + +-- +-- Name: jobqueue_queue_fetchat_jobid; Type: INDEX; Schema: hangfire; Owner: strdssdev +-- + +CREATE INDEX jobqueue_queue_fetchat_jobid ON hangfire.jobqueue USING btree (queue, fetchedat, jobid); + + +-- +-- Name: dss_organization dss_organization_br_iu_tr; Type: TRIGGER; Schema: public; Owner: strdssdev +-- + +CREATE TRIGGER dss_organization_br_iu_tr BEFORE INSERT OR UPDATE ON public.dss_organization FOR EACH ROW EXECUTE FUNCTION public.dss_update_audit_columns(); + + +-- +-- Name: dss_organization_contact_person dss_organization_contact_person_br_iu_tr; Type: TRIGGER; Schema: public; Owner: strdssdev +-- + +CREATE TRIGGER dss_organization_contact_person_br_iu_tr BEFORE INSERT OR UPDATE ON public.dss_organization_contact_person FOR EACH ROW EXECUTE FUNCTION public.dss_update_audit_columns(); + + +-- +-- Name: dss_user_identity dss_user_identity_br_iu_tr; Type: TRIGGER; Schema: public; Owner: strdssdev +-- + +CREATE TRIGGER dss_user_identity_br_iu_tr BEFORE INSERT OR UPDATE ON public.dss_user_identity FOR EACH ROW EXECUTE FUNCTION public.dss_update_audit_columns(); + + +-- +-- Name: jobparameter jobparameter_jobid_fkey; Type: FK CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.jobparameter + ADD CONSTRAINT jobparameter_jobid_fkey FOREIGN KEY (jobid) REFERENCES hangfire.job(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: state state_jobid_fkey; Type: FK CONSTRAINT; Schema: hangfire; Owner: strdssdev +-- + +ALTER TABLE ONLY hangfire.state + ADD CONSTRAINT state_jobid_fkey FOREIGN KEY (jobid) REFERENCES hangfire.job(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: dss_email_message dss_email_message_fk_affecting; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_fk_affecting FOREIGN KEY (affected_by_user_identity_id) REFERENCES public.dss_user_identity(user_identity_id); + + +-- +-- Name: dss_email_message dss_email_message_fk_communicating; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_fk_communicating FOREIGN KEY (email_message_type) REFERENCES public.dss_email_message_type(email_message_type); + + +-- +-- Name: dss_email_message dss_email_message_fk_initiated_by; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_fk_initiated_by FOREIGN KEY (initiating_user_identity_id) REFERENCES public.dss_user_identity(user_identity_id); + + +-- +-- Name: dss_email_message dss_email_message_fk_involving; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_fk_involving FOREIGN KEY (involved_in_organization_id) REFERENCES public.dss_organization(organization_id); + + +-- +-- Name: dss_email_message dss_email_message_fk_justified_by; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_email_message + ADD CONSTRAINT dss_email_message_fk_justified_by FOREIGN KEY (message_reason_id) REFERENCES public.dss_message_reason(message_reason_id); + + +-- +-- Name: dss_message_reason dss_message_reason_fk_justifying; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_message_reason + ADD CONSTRAINT dss_message_reason_fk_justifying FOREIGN KEY (email_message_type) REFERENCES public.dss_email_message_type(email_message_type); + + +-- +-- Name: dss_organization_contact_person dss_organization_contact_person_fk_contacted_for; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization_contact_person + ADD CONSTRAINT dss_organization_contact_person_fk_contacted_for FOREIGN KEY (contacted_through_organization_id) REFERENCES public.dss_organization(organization_id); + + +-- +-- Name: dss_organization dss_organization_fk_managed_by; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization + ADD CONSTRAINT dss_organization_fk_managed_by FOREIGN KEY (managing_organization_id) REFERENCES public.dss_organization(organization_id); + + +-- +-- Name: dss_organization dss_organization_fk_treated_as; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_organization + ADD CONSTRAINT dss_organization_fk_treated_as FOREIGN KEY (organization_type) REFERENCES public.dss_organization_type(organization_type); + + +-- +-- Name: dss_user_identity dss_user_identity_fk_given; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_identity + ADD CONSTRAINT dss_user_identity_fk_given FOREIGN KEY (access_request_status_cd) REFERENCES public.dss_access_request_status(access_request_status_cd); + + +-- +-- Name: dss_user_identity dss_user_identity_fk_representing; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_identity + ADD CONSTRAINT dss_user_identity_fk_representing FOREIGN KEY (represented_by_organization_id) REFERENCES public.dss_organization(organization_id); + + +-- +-- Name: dss_user_role_assignment dss_user_role_assignment_fk_granted; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_assignment + ADD CONSTRAINT dss_user_role_assignment_fk_granted FOREIGN KEY (user_role_cd) REFERENCES public.dss_user_role(user_role_cd); + + +-- +-- Name: dss_user_role_assignment dss_user_role_assignment_fk_granted_to; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_assignment + ADD CONSTRAINT dss_user_role_assignment_fk_granted_to FOREIGN KEY (user_identity_id) REFERENCES public.dss_user_identity(user_identity_id); + + +-- +-- Name: dss_user_role_privilege dss_user_role_privilege_fk_conferred_by; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_privilege + ADD CONSTRAINT dss_user_role_privilege_fk_conferred_by FOREIGN KEY (user_role_cd) REFERENCES public.dss_user_role(user_role_cd); + + +-- +-- Name: dss_user_role_privilege dss_user_role_privilege_fk_conferring; Type: FK CONSTRAINT; Schema: public; Owner: strdssdev +-- + +ALTER TABLE ONLY public.dss_user_role_privilege + ADD CONSTRAINT dss_user_role_privilege_fk_conferring FOREIGN KEY (user_privilege_cd) REFERENCES public.dss_user_privilege(user_privilege_cd); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/localdb/note.txt b/localdb/note.txt new file mode 100644 index 00000000..01224f81 --- /dev/null +++ b/localdb/note.txt @@ -0,0 +1,21 @@ +psql -U postgres +CREATE ROLE strdssdev WITH LOGIN PASSWORD 'postgres'; +CREATE DATABASE strdssdev; +ALTER DATABASE strdssdev OWNER TO strdssdev; +\c strdssdev +CREATE EXTENSION postgis; + +# backup - Do in on WSL (linux) +docker exec -i strdss-strdss-db-1 pg_dump -U postgres -d strdssdev --clean > backup.sql + +# restore when the project folder is strdss +docker cp backup.sql strdss-strdss-db-1:/backup.sql +docker exec -i strdss-strdss-db-1 psql -U postgres -d strdssdev -f /backup.sql + +# restore when the project folder is house-policy-strdss +docker cp backup.sql house-policy-strdss-strdss-db-1:/backup.sql +docker exec -i house-policy-strdss-strdss-db-1 psql -U postgres -d strdssdev -f /backup.sql + + + + diff --git a/postgres/note.txt b/postgres/note.txt new file mode 100644 index 00000000..a15de913 --- /dev/null +++ b/postgres/note.txt @@ -0,0 +1,27 @@ +oc login +oc project f4a30d-dev / test + +helm install strdssdev bitnami/postgresql -f values-dev.yaml + +helm install strdsstest bitnami/postgresql -f values-test.yaml + +Enable PostGIS extension +https://github.com/bitnami/charts/issues/2830 + +psql -U postgres +CREATE ROLE strdssdev WITH LOGIN PASSWORD 'postgres'; +CREATE DATABASE strdssdev; +ALTER DATABASE strdssdev OWNER TO strdssdev; +\c strdssdev +CREATE EXTENSION postgis; + + + +# Drop +REASSIGN OWNED BY strdssdev TO postgres; +DROP OWNED BY strdssdev; +DROP ROLE strdssdev; + +Create Secret + +oc port-forward svc/strdssdev 5433:5432 \ No newline at end of file diff --git a/postgres/values-dev.yaml b/postgres/values-dev.yaml index 16552406..8aef3b9f 100644 --- a/postgres/values-dev.yaml +++ b/postgres/values-dev.yaml @@ -4,7 +4,7 @@ primary: containerSecurityContext: enabled: false persistence: - size: 200Mi + size: 400Mi resources: limits: cpu: 150m @@ -14,6 +14,6 @@ primary: memory: 150Mi readReplicas: persistence: - size: 200Mi + size: 400Mi nameOverride: strdssdev fullnameOverride: strdssdev \ No newline at end of file diff --git a/postgres/values-test.yaml b/postgres/values-test.yaml index 873b5355..8c77e2ba 100644 --- a/postgres/values-test.yaml +++ b/postgres/values-test.yaml @@ -4,7 +4,7 @@ primary: containerSecurityContext: enabled: false persistence: - size: 200Mi + size: 400Mi resources: limits: cpu: 150m @@ -14,6 +14,6 @@ primary: memory: 150Mi readReplicas: persistence: - size: 200Mi + size: 400Mi nameOverride: strdsstest fullnameOverride: strdsstest \ No newline at end of file diff --git a/postman/str-dss.postman_collection.json b/postman/str-dss.postman_collection.json index 98ba8276..ee8bae42 100644 --- a/postman/str-dss.postman_collection.json +++ b/postman/str-dss.postman_collection.json @@ -31,12 +31,12 @@ "response": [] }, { - "name": "GetPlatformDrowdown", + "name": "GetReqeustReasonDrowdown", "request": { "method": "GET", "header": [], "url": { - "raw": "http://127.0.0.1:5174/api/platforms/dropdown", + "raw": "http://127.0.0.1:5174/api/delisting/reasons/dropdown", "protocol": "http", "host": [ "127", @@ -47,7 +47,8 @@ "port": "5174", "path": [ "api", - "platforms", + "delisting", + "reasons", "dropdown" ] } @@ -55,12 +56,21 @@ "response": [] }, { - "name": "GetLocalGovernmentsDrowdown", + "name": "CreateDelistingWarning", "request": { - "method": "GET", + "method": "POST", "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"platformId\": 2,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"hostEmail\": \"young-jin.chung@gov.bc.ca\",\r\n \"hostEmailSent\": false, \r\n \"reasonId\": 1,\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.chung@dxcas.com\"\r\n ],\r\n \"lgContactEmail\": \"lgcontact@lg.ca\",\r\n \"lgContactPhone\": \"(123) 456-7890\",\r\n \"strBylawUrl\": \"https://bylaw.ca\",\r\n \"comment\": \"Test comment\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "http://127.0.0.1:5174/api/localgovernments/dropdown", + "raw": "http://127.0.0.1:5174/api/delisting/warnings", "protocol": "http", "host": [ "127", @@ -71,20 +81,29 @@ "port": "5174", "path": [ "api", - "localgovernments", - "dropdown" + "delisting", + "warnings" ] } }, "response": [] }, { - "name": "GetReqeustReasonDrowdown", + "name": "GetDelistingWarningPreview", "request": { - "method": "GET", + "method": "POST", "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"platformId\": 2,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"hostEmail\": \"young-jin.chung@dxc.com\",\r\n \"hostEmailSent\": false, \r\n \"reasonId\": 1,\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.chung@dxcas.com\"\r\n ],\r\n \"lgContactEmail\": \"lgcontact@lg.ca\",\r\n \"lgContactPhone\": \"(123) 456-7890\",\r\n \"strBylawUrl\": \"https://bylaw.ca\",\r\n \"comment\": \"Test comment\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "http://127.0.0.1:5174/api/delisting/reasons/dropdown", + "raw": "http://127.0.0.1:5174/api/delisting/warnings/preview", "protocol": "http", "host": [ "127", @@ -96,21 +115,21 @@ "path": [ "api", "delisting", - "reasons", - "dropdown" + "warnings", + "preview" ] } }, "response": [] }, { - "name": "CreateDelistingWarning", + "name": "CreateDelistingRequest", "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"platformId\": 1,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"hostEmail\": \"young-jin.chung@gov.bc.ca\",\r\n \"hostEmailSent\": false, \r\n \"reasonId\": 1,\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.chung@dxcas.com\"\r\n ],\r\n \"lgContactEmail\": \"lgcontact@lg.ca\",\r\n \"lgContactPhone\": \"(123) 456-7890\",\r\n \"strBylawUrl\": \"https://bylaw.ca\",\r\n \"comment\": \"Test comment\"\r\n}", + "raw": "{\r\n \"lgId\": 1,\r\n \"platformId\": 2,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.1.chung@gov.bc.ca\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -118,7 +137,7 @@ } }, "url": { - "raw": "http://127.0.0.1:5174/api/delisting/warnings", + "raw": "http://127.0.0.1:5174/api/delisting/requests", "protocol": "http", "host": [ "127", @@ -130,20 +149,20 @@ "path": [ "api", "delisting", - "warnings" + "requests" ] } }, "response": [] }, { - "name": "GetDelistingWarningPreview", + "name": "GetDelistingRequestPreview", "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"platformId\": 1,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"hostEmail\": \"young-jin.chung@gov.bc.ca\",\r\n \"hostEmailSent\": false, \r\n \"reasonId\": 1,\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.chung@dxcas.com\"\r\n ],\r\n \"lgContactEmail\": \"lgcontact@lg.ca\",\r\n \"lgContactPhone\": \"(123) 456-7890\",\r\n \"strBylawUrl\": \"https://bylaw.ca\",\r\n \"comment\": \"Test comment\"\r\n}", + "raw": "{\r\n \"lgId\": 1,\r\n \"platformId\": 2,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.1.chung@gov.bc.ca\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -151,7 +170,7 @@ } }, "url": { - "raw": "http://127.0.0.1:5174/api/delisting/warnings/preview", + "raw": "http://127.0.0.1:5174/api/delisting/requests/preview", "protocol": "http", "host": [ "127", @@ -163,7 +182,7 @@ "path": [ "api", "delisting", - "warnings", + "requests", "preview" ] } @@ -171,13 +190,16 @@ "response": [] }, { - "name": "CreateDelistingRequest", + "name": "Get Organization Types", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "POST", + "method": "GET", "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"lgId\": 1,\r\n \"platformId\": 1,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.1.chung@gov.bc.ca\"\r\n ]\r\n}", + "raw": "{\r\n \"organiztionType\": \"BCGov\",\r\n \"organiztionName\": \"Ministry of Housing\"\r\n}", "options": { "raw": { "language": "json" @@ -185,7 +207,7 @@ } }, "url": { - "raw": "http://127.0.0.1:5174/api/delisting/requests", + "raw": "http://127.0.0.1:5174/api/organizations/types", "protocol": "http", "host": [ "127", @@ -196,21 +218,225 @@ "port": "5174", "path": [ "api", - "delisting", - "requests" + "organizations", + "types" ] } }, "response": [] }, { - "name": "GetDelistingRequestPreview", + "name": "Get Platforms", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"organiztionType\": \"BCGov\",\r\n \"organiztionName\": \"Ministry of Housing\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/organizations?type=Platform", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "organizations" + ], + "query": [ + { + "key": "type", + "value": "Platform" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Platforms Dropdown", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"organiztionType\": \"BCGov\",\r\n \"organiztionName\": \"Ministry of Housing\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/organizations/dropdown/?type=Platform", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "organizations", + "dropdown", + "" + ], + "query": [ + { + "key": "type", + "value": "Platform" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Local Governments", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"organiztionType\": \"BCGov\",\r\n \"organiztionName\": \"Ministry of Housing\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/organizations?type=LG", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "organizations" + ], + "query": [ + { + "key": "type", + "value": "LG" + } + ] + } + }, + "response": [] + }, + { + "name": "Get BC Governments", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"organiztionType\": \"BCGov\",\r\n \"organiztionName\": \"Ministry of Housing\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/organizations?type=BCGov", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "organizations" + ], + "query": [ + { + "key": "type", + "value": "BCGov" + } + ] + } + }, + "response": [] + }, + { + "name": "GetRequestAccessList", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://127.0.0.1:5174/api/users/accessrequests?status=&pageSize=10&pageNumber=1", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "users", + "accessrequests" + ], + "query": [ + { + "key": "status", + "value": "" + }, + { + "key": "pageSize", + "value": "10" + }, + { + "key": "pageNumber", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Create Access Request", "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"lgId\": 1,\r\n \"platformId\": 1,\r\n \"listingId\": 1,\r\n \"listingUrl\": \"https://www.airbnb.com/rooms/16120388?check_in=2024-04-25&check_out=2024-04-27&guests=1&adults=1&s=67&unique_share_id=fa107d24-b0df-49bd-91e4-576dd7d368ab\",\r\n \"sendCopy\": true,\r\n \"ccList\": [\r\n \"young-jin.1.chung@gov.bc.ca\"\r\n ]\r\n}", + "raw": "{\r\n \"organizationType\": \"BCGov\",\r\n \"organizationName\": \"Ministry of Housing\"\r\n}", "options": { "raw": { "language": "json" @@ -218,7 +444,7 @@ } }, "url": { - "raw": "http://127.0.0.1:5174/api/delisting/requests/preview", + "raw": "http://127.0.0.1:5174/api/users/accessrequests", "protocol": "http", "host": [ "127", @@ -229,9 +455,76 @@ "port": "5174", "path": [ "api", - "delisting", - "requests", - "preview" + "users", + "accessrequests" + ] + } + }, + "response": [] + }, + { + "name": "Deny Access Request", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"userIdentityId\": 4,\r\n \"updDtm\": \"2024-03-21T15:02:28.966095Z\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/users/accessrequests/deny", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "users", + "accessrequests", + "deny" + ] + } + }, + "response": [] + }, + { + "name": "Approve Access Request", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"userIdentityId\": 6,\r\n \"representedByOrganizationId\": 4,\r\n \"updDtm\": \"2024-03-21T16:04:17.210947Z\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5174/api/users/accessrequests/approve", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5174", + "path": [ + "api", + "users", + "accessrequests", + "approve" ] } }, diff --git a/server/StrDss.Api/Authentication/KcJwtBearerEvents.cs b/server/StrDss.Api/Authentication/KcJwtBearerEvents.cs index cdbe037a..400e9e1c 100644 --- a/server/StrDss.Api/Authentication/KcJwtBearerEvents.cs +++ b/server/StrDss.Api/Authentication/KcJwtBearerEvents.cs @@ -1,22 +1,50 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; +using StrDss.Common; using StrDss.Model; +using StrDss.Service; namespace StrDss.Api.Authentication { public class KcJwtBearerEvents : JwtBearerEvents { private ICurrentUser _currentUser; + private IUserService _userService; - public KcJwtBearerEvents(ICurrentUser currentUser) : base() + public KcJwtBearerEvents(ICurrentUser currentUser, IUserService userService) : base() { _currentUser = currentUser; + _userService = userService; } public override async Task TokenValidated(TokenValidatedContext context) { _currentUser.LoadUserSession(context.Principal); - await Task.CompletedTask; + var (user, permissions) = await _userService.GetUserByGuidAsync(_currentUser.UserGuid); + + if (user == null) + { + _currentUser.AccessRequestStatus = AccessRequestStatuses.None; + _currentUser.IsActive = false; + _currentUser.AccessRequestRequired = true; + } + else + { + _currentUser.Id = user.UserIdentityId; + _currentUser.IsActive = user.IsEnabled; + _currentUser.AccessRequestStatus = user.AccessRequestStatusCd; + _currentUser.Permissions = permissions; + _currentUser.AccessRequestRequired = _currentUser.AccessRequestStatus == AccessRequestStatuses.Denied; + _currentUser.OrganizationType = user.RepresentedByOrganization?.OrganizationType ?? ""; + + if (user.IsEnabled) + { + foreach (var permission in permissions) + { + _currentUser.AddClaim(context.Principal, StrDssClaimTypes.Permission, permission); + } + } + } } } } diff --git a/server/StrDss.Api/Controllers/DelistingController.cs b/server/StrDss.Api/Controllers/DelistingController.cs index 7b7aaaaa..b00e3b5d 100644 --- a/server/StrDss.Api/Controllers/DelistingController.cs +++ b/server/StrDss.Api/Controllers/DelistingController.cs @@ -5,13 +5,9 @@ using StrDss.Common; using StrDss.Model; using StrDss.Model.DelistingDtos; -using StrDss.Model.LocalGovernmentDtos; -using StrDss.Model.PlatformDtos; using StrDss.Model.WarningReasonDtos; using StrDss.Service; using StrDss.Service.HttpClients; -using System.Text; -using System.Text.RegularExpressions; namespace StrDss.Api.Controllers { @@ -25,107 +21,75 @@ public class DelistingController : BaseApiController private ILogger _logger { get; } private IDelistingService _delistingService { get; } + private IEmailMessageService _emailService; + public DelistingController(ICurrentUser currentUser, IMapper mapper, IConfiguration config, IChesTokenApi chesTokenApi, ILogger logger, - IDelistingService delistingService) + IDelistingService delistingService, IEmailMessageService emailService) : base(currentUser, mapper, config) { _chesTokenApi = chesTokenApi; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _delistingService = delistingService; + _emailService = emailService; } - [HttpGet("reasons/dropdown", Name = "GetWarningReasonDrowdown")] [ApiAuthorize] - public async Task> GetWarningReasonDrowdown() + [HttpGet("reasons/dropdown", Name = "GetWarningReasonDrowdown")] + public async Task>> GetWarningReasonDrowdown() { - await Task.CompletedTask; - return Ok(WarningReasonDto.WarningReasons.Select(x => new DropdownDto { Id = x.WarningReasonId, Description = x.Reason })); + return await _emailService.GetMessageReasons(EmailMessageTypes.NoticeOfTakedown); } - [HttpPost("warnings", Name = "CreateDelistingWarning")] [ApiAuthorize] + [HttpPost("warnings", Name = "CreateDelistingWarning")] public async Task CreateDelistingWarning(DelistingWarningCreateDto dto) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); - var reason = WarningReasonDto.WarningReasons.FirstOrDefault(x => x.WarningReasonId == dto.ReasonId)?.Reason; - - var errors = await _delistingService.ValidateDelistingWarning(dto, platform, reason); + var errors = await _delistingService.CreateDelistingWarningAsync(dto); if (errors.Count > 0) { return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); } - var error = await _delistingService.SendDelistingWarningAsync(dto, platform); - - if (!error.IsEmpty()) - { - return Problem($"There were some errors in sending email."); - } - return NoContent(); } - [HttpPost("warnings/preview", Name = "GetDelistingWarningPreview")] [ApiAuthorize] + [HttpPost("warnings/preview", Name = "GetDelistingWarningPreview")] public async Task> GetDelistingWarningPreview(DelistingWarningCreateDto dto) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); - var reason = WarningReasonDto.WarningReasons.FirstOrDefault(x => x.WarningReasonId == dto.ReasonId)?.Reason; - - var errors = await _delistingService.ValidateDelistingWarning(dto, platform, reason); + var (errors, preview) = await _delistingService.GetDelistingWarningPreviewAsync(dto); if (errors.Count > 0) { return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); } - var toList = new List { platform?.Email ?? "" }; - if (dto.HostEmail.IsNotEmpty()) - { - toList.Add(dto.HostEmail); - } - - return new EmailPreview { Content = _delistingService.FormatDelistingWarningEmailContent(dto, false).HtmlToPlainText() }; + return preview; } - [HttpPost("requests", Name = "CreateDelistingRequest")] [ApiAuthorize] + [HttpPost("requests", Name = "CreateDelistingRequest")] public async Task CreateDelistingRequest(DelistingRequestCreateDto dto) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); - var lg = LocalGovernmentDto.localGovernments.FirstOrDefault(x => x.LocalGovernmentId == dto.LgId); - - var errors = await _delistingService.ValidateDelistingRequest(dto, platform, lg); + var errors = await _delistingService.CreateDelistingRequestAsync(dto); if (errors.Count > 0) { return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); } - var error = await _delistingService.SendDelistingRequestAsync(dto, platform); - - if (!error.IsEmpty()) - { - return Problem($"There were some errors in sending email."); - } - return NoContent(); } - [HttpPost("requests/preview", Name = "GetDelistingRequestPreview")] [ApiAuthorize] + [HttpPost("requests/preview", Name = "GetDelistingRequestPreview")] public async Task> GetDelistingRequestPreview(DelistingRequestCreateDto dto) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); - var lg = LocalGovernmentDto.localGovernments.FirstOrDefault(x => x.LocalGovernmentId == dto.LgId); - - var errors = await _delistingService.ValidateDelistingRequest(dto, platform, lg); + var (errors, preview) = await _delistingService.GetDelistingRequestPreviewAsync(dto); if (errors.Count > 0) { return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); } - var toList = new List { platform?.Email ?? "" }; - - return new EmailPreview { Content = _delistingService.FormatDelistingRequestEmailContent(dto, false).HtmlToPlainText() }; + return preview; } } } diff --git a/server/StrDss.Api/Controllers/LocalGovernmentsController.cs b/server/StrDss.Api/Controllers/LocalGovernmentsController.cs deleted file mode 100644 index 55b42e8d..00000000 --- a/server/StrDss.Api/Controllers/LocalGovernmentsController.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Asp.Versioning; -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using StrDss.Api.Authorization; -using StrDss.Model; -using StrDss.Model.LocalGovernmentDtos; -using StrDss.Model.PlatformDtos; - -namespace StrDss.Api.Controllers -{ - [ApiVersion("1.0")] - [Route("api/[controller]")] - [ApiController] - public class LocalGovernmentsController : BaseApiController - { - public LocalGovernmentsController(ICurrentUser currentUser, IMapper mapper, IConfiguration config) - : base(currentUser, mapper, config) - { - } - - [HttpGet("dropdown", Name = "GetLocalGovernmentsDrowdown")] - [ApiAuthorize] - public ActionResult GetLocalGovernmentsDrowdown() - { - return Ok(LocalGovernmentDto.localGovernments.Select(x => new DropdownDto { Id = x.LocalGovernmentId, Description = x.Name })); - } - } -} diff --git a/server/StrDss.Api/Controllers/OrganizationsController.cs b/server/StrDss.Api/Controllers/OrganizationsController.cs new file mode 100644 index 00000000..8b674a12 --- /dev/null +++ b/server/StrDss.Api/Controllers/OrganizationsController.cs @@ -0,0 +1,46 @@ +using Asp.Versioning; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using StrDss.Api.Authorization; +using StrDss.Model; +using StrDss.Model.OrganizationDtos; +using StrDss.Service; + +namespace StrDss.Api.Controllers +{ + [ApiVersion("1.0")] + [Route("api/[controller]")] + [ApiController] + public class OrganizationsController : BaseApiController + { + private IOrganizationService _orgService; + + public OrganizationsController(ICurrentUser currentUser, IMapper mapper, IConfiguration config, + IOrganizationService orgService) + : base(currentUser, mapper, config) + { + _orgService = orgService; + } + + [ApiAuthorize] + [HttpGet("types", Name = "GetOrganizationTypes")] + public async Task>> GetOrganizationTypes() + { + return Ok(await _orgService.GetOrganizationTypesAsnc()); + } + + [ApiAuthorize] + [HttpGet("", Name = "GetOrganizations")] + public async Task>> GetOrganizations(string? type) + { + return Ok(await _orgService.GetOrganizationsAsync(type)); + } + + [ApiAuthorize] + [HttpGet("dropdown", Name = "GetOrganizationsDropdown")] + public async Task>> GetOrganizationsDropdown(string? type) + { + return Ok(await _orgService.GetOrganizationsDropdownAsync(type)); + } + } +} diff --git a/server/StrDss.Api/Controllers/PlatformsController.cs b/server/StrDss.Api/Controllers/PlatformsController.cs deleted file mode 100644 index 75fb9990..00000000 --- a/server/StrDss.Api/Controllers/PlatformsController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Asp.Versioning; -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using StrDss.Api.Authorization; -using StrDss.Model; -using StrDss.Model.PlatformDtos; - -namespace StrDss.Api.Controllers -{ - [ApiVersion("1.0")] - [Route("api/[controller]")] - [ApiController] - public class PlatformsController : BaseApiController - { - public PlatformsController(ICurrentUser currentUser, IMapper mapper, IConfiguration config) - : base(currentUser, mapper, config) - { - } - - [HttpGet("dropdown", Name = "GetPlatformsDrowdown")] - [ApiAuthorize] - public ActionResult GetPlatformsDrowdown() - { - return Ok(PlatformDto.Platforms.Select(x => new DropdownDto { Id = x.PlatformId, Description = x.Name })); - } - } -} diff --git a/server/StrDss.Api/Controllers/UsersController.cs b/server/StrDss.Api/Controllers/UsersController.cs index bf147892..0743a50a 100644 --- a/server/StrDss.Api/Controllers/UsersController.cs +++ b/server/StrDss.Api/Controllers/UsersController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc; using StrDss.Api.Authorization; using StrDss.Model; +using StrDss.Model.UserDtos; +using StrDss.Service; namespace StrDss.Api.Controllers { @@ -11,16 +13,70 @@ namespace StrDss.Api.Controllers [ApiController] public class UsersController : BaseApiController { - public UsersController(ICurrentUser currentUser, IMapper mapper, IConfiguration config) + private IUserService _userService; + + public UsersController(ICurrentUser currentUser, IMapper mapper, IConfiguration config, + IUserService userService) : base(currentUser, mapper, config) { + _userService = userService; } - [HttpGet("currentuser", Name = "GetCurrentUser")] [ApiAuthorize] + [HttpGet("currentuser", Name = "GetCurrentUser")] public ActionResult GetCurrentUser() { return Ok(_currentUser); } + + [ApiAuthorize] + [HttpGet("accessrequests", Name = "GetAccessRequestList")] + public async Task>> GetAccessRequestList(string? status, int pageSize, int pageNumber, string orderBy = "AccessRequestDtm", string direction = "") + { + var list = await _userService.GetAccessRequestListAsync(status ?? "", pageSize, pageNumber, orderBy, direction); + return Ok(list); + } + + [ApiAuthorize] + [HttpPost("accessrequests", Name = "CreateAccessRequest")] + public async Task CreateAccessRequest(AccessRequestCreateDto dto) + { + var errors = await _userService.CreateAccessRequestAsync(dto); + + if (errors.Count > 0) + { + return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); + } + + return Ok(); + } + + [ApiAuthorize] + [HttpPut("accessrequests/deny", Name = "DenyRequest")] + public async Task DenyRequest(AccessRequestDenyDto dto) + { + var errors = await _userService.DenyAccessRequest(dto); + + if (errors.Count > 0) + { + return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); + } + + return Ok(); + } + + [ApiAuthorize] + [HttpPut("accessrequests/approve", Name = "ApproveRequest")] + public async Task ApproveRequest(AccessRequestApproveDto dto) + { + var errors = await _userService.ApproveAccessRequest(dto); + + if (errors.Count > 0) + { + return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); + } + + return Ok(); + } } } diff --git a/server/StrDss.Api/HttpResponseExtensions.cs b/server/StrDss.Api/HttpResponseExtensions.cs new file mode 100644 index 00000000..0c35ded6 --- /dev/null +++ b/server/StrDss.Api/HttpResponseExtensions.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace StrDss.Api +{ + public static class HttpResponseExtensions + { + private static JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + public static async Task WriteJsonAsync(this HttpResponse response, T obj, string contentType = null) + { + response.ContentType = contentType ?? "application/json"; + await response.WriteAsync(JsonSerializer.Serialize(obj, _jsonOptions)); + } + } +} diff --git a/server/StrDss.Api/Middlewares/ExceptionMiddleware.cs b/server/StrDss.Api/Middlewares/ExceptionMiddleware.cs new file mode 100644 index 00000000..26d59291 --- /dev/null +++ b/server/StrDss.Api/Middlewares/ExceptionMiddleware.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Net; + +namespace StrDss.Api.Middlewares +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionMiddleware(RequestDelegate next, ILogger logger) + { + _logger = logger; + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + await _next(httpContext); + } + catch (DbUpdateConcurrencyException ex) + { + if (httpContext.Response.HasStarted || httpContext.RequestAborted.IsCancellationRequested) + return; + + var guid = Guid.NewGuid(); + await HandleConcurrencyExceptionAsync(httpContext, guid, ex.Message); + } + catch (Exception ex) + { + if (httpContext.Response.HasStarted || httpContext.RequestAborted.IsCancellationRequested) + return; + + var guid = Guid.NewGuid(); + _logger.LogError($"STRDSS Exception {guid}: {ex}"); + await HandleExceptionAsync(httpContext, guid); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Guid guid) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + + var problem = new ValidationProblemDetails() + { + Type = "https://strdss.bc.gov.ca/exception", + Title = "An unexpected error occurred!", + Status = StatusCodes.Status500InternalServerError, + Detail = "The instance value should be used to identify the problem when calling customer support", + Instance = $"urn:crt:error:{guid}" + }; + + problem.Extensions.Add("traceId", context.TraceIdentifier); + + await context.Response.WriteJsonAsync(problem, "application/problem+json"); + } + + private async Task HandleConcurrencyExceptionAsync(HttpContext context, Guid guid, string message) + { + context.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity; + + var errors = new Dictionary + { + { "entity", new string[] { message } } + }; + + var problem = new ValidationProblemDetails(errors) + { + Type = "https://strdss.bc.gov.ca/model-validation-error", + Title = "Update conflict detected", + Status = StatusCodes.Status422UnprocessableEntity, + Detail = "Please refer to the errors property for additional details", + Instance = $"urn:crt:error:{guid}" + }; + + problem.Extensions.Add("traceId", context.TraceIdentifier); + + await context.Response.WriteJsonAsync(problem, "application/problem+json"); + } + + + } +} diff --git a/server/StrDss.Api/Program.cs b/server/StrDss.Api/Program.cs index a8841cf8..3bd1f55d 100644 --- a/server/StrDss.Api/Program.cs +++ b/server/StrDss.Api/Program.cs @@ -15,8 +15,8 @@ using System.Reflection; using StrDss.Common; using StrDss.Data.Entities; -using Microsoft.Extensions.Options; using Microsoft.EntityFrameworkCore; +using StrDss.Api.Middlewares; var builder = WebApplication.CreateBuilder(args); @@ -182,4 +182,6 @@ app.UseHangfireDashboard(); +app.UseMiddleware(); + app.Run(); diff --git a/server/StrDss.Api/StrDss.Api.csproj b/server/StrDss.Api/StrDss.Api.csproj index b05cb7a0..538fdfcd 100644 --- a/server/StrDss.Api/StrDss.Api.csproj +++ b/server/StrDss.Api/StrDss.Api.csproj @@ -20,6 +20,7 @@ + diff --git a/server/StrDss.Common/CommonExtensions.cs b/server/StrDss.Common/CommonExtensions.cs index ef38ee74..97a4b4e9 100644 --- a/server/StrDss.Common/CommonExtensions.cs +++ b/server/StrDss.Common/CommonExtensions.cs @@ -204,5 +204,16 @@ public static string GetStringBeforeFirstDot(this string input) return dotIndex > 0 ? input.Substring(0, dotIndex) : input; } + + public static bool IsNumericType(this Type type) + { + if (type == typeof(int) || type == typeof(long) || type == typeof(float) || + type == typeof(double) || type == typeof(decimal)) + { + return true; + } + + return false; + } } } diff --git a/server/StrDss.Common/CommonUtils.cs b/server/StrDss.Common/CommonUtils.cs index 60a3fecf..01044d48 100644 --- a/server/StrDss.Common/CommonUtils.cs +++ b/server/StrDss.Common/CommonUtils.cs @@ -1,4 +1,5 @@ using NetTopologySuite.Geometries; +using System.Reflection; namespace StrDss.Common { @@ -30,5 +31,21 @@ public static string GetFullName(string firstName, string lastName) return firstName; } } + + public static T CloneObject(T obj) + { + T newObj = Activator.CreateInstance(); + PropertyInfo[] properties = typeof(T).GetProperties(); + + foreach (PropertyInfo property in properties) + { + if (property.CanWrite) + { + property.SetValue(newObj, property.GetValue(obj)); + } + } + + return newObj; + } } } diff --git a/server/StrDss.Common/Constants.cs b/server/StrDss.Common/Constants.cs index 70b43d10..3b20102a 100644 --- a/server/StrDss.Common/Constants.cs +++ b/server/StrDss.Common/Constants.cs @@ -32,6 +32,15 @@ public static class Fields public const string RoleId = "RoleId"; } + + public static class AccessRequestStatuses + { + public const string Requested = "Requested"; + public const string Approved = "Approved"; + public const string Denied = "Denied"; + public const string None = "None"; + } + public static class FieldTypes { public const string String = "S"; @@ -47,9 +56,9 @@ public static class CodeSet public const string ComplianceStatus = "COMPLIANCE_STATUS"; } - public static class StrDssUserTypes + public static class StrDssIdProviders { - public const string IDIR = "idir"; + public const string Idir = "idir"; public const string BceidBusiness = "bceidbusiness"; public const string External = "external"; public const string Aps = "aps"; @@ -59,9 +68,9 @@ public static string GetBceidUserType(string userType) { switch (userType.ToLowerInvariant()) { - case StrDssUserTypes.IDIR: + case StrDssIdProviders.Idir: return BceidUserTypes.Internal; - case StrDssUserTypes.BceidBusiness: + case StrDssIdProviders.BceidBusiness: return BceidUserTypes.Business; default: return "Unknown"; @@ -101,4 +110,33 @@ public static string GetSimpleName(string fullName) return fullName.Contains("/") ? fullName.Substring(fullName.LastIndexOf("/") + 1) : fullName; } } + public static class OrganizationTypes + { + public const string BCGov = "BCGov"; + public const string Platform = "Platform"; + public const string LG = "LG"; + } + + public static class Roles + { + public const string CeuAdmin = "ceu_admin"; + public const string CeuStaff = "ceu_staff"; + public const string LgStaff = "lg_staff"; + public const string PlatformStaff = "platform_staff"; + } + + public static class NoReply + { + public const string Default = "no_reply@gov.bc.ca"; + } + + public static class EmailMessageTypes + { + public const string AccessGrantedNotification = "Access Granted Notification"; + public const string DelistingWarning = "Delisting Warning"; + public const string TakedownRequest = "Takedown Request"; + public const string DelistingRequest = "Delisting Request"; + public const string AccessDeniedNotification = "Access Denied Notification"; + public const string NoticeOfTakedown = "Notice of Takedown"; + } } diff --git a/server/StrDss.Data/Entities/DssAccessRequestStatus.cs b/server/StrDss.Data/Entities/DssAccessRequestStatus.cs new file mode 100644 index 00000000..60871f5d --- /dev/null +++ b/server/StrDss.Data/Entities/DssAccessRequestStatus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace StrDss.Data.Entities; + +public partial class DssAccessRequestStatus +{ + public string AccessRequestStatusCd { get; set; } = null!; + + public string AccessRequestStatusNm { get; set; } = null!; + + public virtual ICollection DssUserIdentities { get; set; } = new List(); +} diff --git a/server/StrDss.Data/Entities/DssDbContext.cs b/server/StrDss.Data/Entities/DssDbContext.cs index fd54382d..b43068af 100644 --- a/server/StrDss.Data/Entities/DssDbContext.cs +++ b/server/StrDss.Data/Entities/DssDbContext.cs @@ -11,31 +11,49 @@ public DssDbContext(DbContextOptions options) { } + public virtual DbSet DssAccessRequestStatuses { get; set; } + public virtual DbSet DssEmailMessages { get; set; } + public virtual DbSet DssEmailMessageTypes { get; set; } + + public virtual DbSet DssMessageReasons { get; set; } + public virtual DbSet DssOrganizations { get; set; } public virtual DbSet DssOrganizationContactPeople { get; set; } + public virtual DbSet DssOrganizationTypes { get; set; } + public virtual DbSet DssUserIdentities { get; set; } public virtual DbSet DssUserPrivileges { get; set; } public virtual DbSet DssUserRoles { get; set; } - public virtual DbSet DssUserRoleAssignments { get; set; } - - public virtual DbSet DssUserRolePrivileges { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasPostgresExtension("postgis"); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.AccessRequestStatusCd).HasName("dss_access_request_status_pk"); + + entity.ToTable("dss_access_request_status"); + + entity.Property(e => e.AccessRequestStatusCd) + .HasMaxLength(25) + .HasColumnName("access_request_status_cd"); + entity.Property(e => e.AccessRequestStatusNm) + .HasMaxLength(250) + .HasColumnName("access_request_status_nm"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.EmailMessageId).HasName("dss_email_message_pk"); - entity.ToTable("dss_email_message", "dss", tb => tb.HasComment("A message that is sent to one or more recipients via email")); + entity.ToTable("dss_email_message", tb => tb.HasComment("A message that is sent to one or more recipients via email")); entity.Property(e => e.EmailMessageId) .HasComment("Unique generated key") @@ -45,7 +63,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasComment("Foreign key") .HasColumnName("affected_by_user_identity_id"); entity.Property(e => e.CcEmailAddressDsc) - .HasMaxLength(250) + .HasMaxLength(4000) .HasComment("E-mail address of a secondary message recipient (directly entered by the user)") .HasColumnName("cc_email_address_dsc"); entity.Property(e => e.EmailMessageType) @@ -53,7 +71,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasComment("Business term for the type or purpose of the message (e.g. Notice of Takedown, Takedown Request, Delisting Warning, Delisting Request, Access Granted Notification, Access Denied Notification)") .HasColumnName("email_message_type"); entity.Property(e => e.HostEmailAddressDsc) - .HasMaxLength(250) + .HasMaxLength(320) .HasComment("E-mail address of a short term rental host (directly entered by the user as a message recipient)") .HasColumnName("host_email_address_dsc"); entity.Property(e => e.InitiatingUserIdentityId) @@ -62,19 +80,30 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.InvolvedInOrganizationId) .HasComment("Foreign key") .HasColumnName("involved_in_organization_id"); + entity.Property(e => e.IsHostContactedExternally).HasColumnName("is_host_contacted_externally"); + entity.Property(e => e.IsSubmitterCcRequired).HasColumnName("is_submitter_cc_required"); + entity.Property(e => e.LgEmailAddressDsc) + .HasMaxLength(320) + .HasColumnName("lg_email_address_dsc"); + entity.Property(e => e.LgPhoneNo) + .HasMaxLength(13) + .HasColumnName("lg_phone_no"); + entity.Property(e => e.LgStrBylawUrl) + .HasMaxLength(4000) + .HasColumnName("lg_str_bylaw_url"); entity.Property(e => e.MessageDeliveryDtm) .HasComment("A timestamp indicating when the message delivery was initiated") .HasColumnName("message_delivery_dtm"); - entity.Property(e => e.MessageReasonDsc) - .HasMaxLength(250) - .HasComment("A description of the justification for initiating the message") - .HasColumnName("message_reason_dsc"); + entity.Property(e => e.MessageReasonId).HasColumnName("message_reason_id"); entity.Property(e => e.MessageTemplateDsc) .HasMaxLength(4000) .HasComment("The full text or template for the message that is sent") .HasColumnName("message_template_dsc"); + entity.Property(e => e.UnreportedListingNo) + .HasMaxLength(25) + .HasColumnName("unreported_listing_no"); entity.Property(e => e.UnreportedListingUrl) - .HasMaxLength(250) + .HasMaxLength(4000) .HasComment("User-provided URL for a short-term rental platform listing that is the subject of the message") .HasColumnName("unreported_listing_url"); @@ -82,6 +111,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(d => d.AffectedByUserIdentityId) .HasConstraintName("dss_email_message_fk_affecting"); + entity.HasOne(d => d.EmailMessageTypeNavigation).WithMany(p => p.DssEmailMessages) + .HasForeignKey(d => d.EmailMessageType) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_email_message_fk_communicating"); + entity.HasOne(d => d.InitiatingUserIdentity).WithMany(p => p.DssEmailMessageInitiatingUserIdentities) .HasForeignKey(d => d.InitiatingUserIdentityId) .OnDelete(DeleteBehavior.ClientSetNull) @@ -90,13 +124,54 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasOne(d => d.InvolvedInOrganization).WithMany(p => p.DssEmailMessages) .HasForeignKey(d => d.InvolvedInOrganizationId) .HasConstraintName("dss_email_message_fk_involving"); + + entity.HasOne(d => d.MessageReason).WithMany(p => p.DssEmailMessages) + .HasForeignKey(d => d.MessageReasonId) + .HasConstraintName("dss_email_message_fk_justified_by"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.EmailMessageType).HasName("dss_email_message_type_pk"); + + entity.ToTable("dss_email_message_type"); + + entity.Property(e => e.EmailMessageType) + .HasMaxLength(50) + .HasColumnName("email_message_type"); + entity.Property(e => e.EmailMessageTypeNm) + .HasMaxLength(250) + .HasColumnName("email_message_type_nm"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.MessageReasonId).HasName("dss_message_reason_pk"); + + entity.ToTable("dss_message_reason"); + + entity.Property(e => e.MessageReasonId) + .UseIdentityAlwaysColumn() + .HasColumnName("message_reason_id"); + entity.Property(e => e.EmailMessageType) + .HasMaxLength(50) + .HasColumnName("email_message_type"); + entity.Property(e => e.MessageReasonDsc) + .HasMaxLength(250) + .HasComment("A description of the justification for initiating a message") + .HasColumnName("message_reason_dsc"); + + entity.HasOne(d => d.EmailMessageTypeNavigation).WithMany(p => p.DssMessageReasons) + .HasForeignKey(d => d.EmailMessageType) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_message_reason_fk_justifying"); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.OrganizationId).HasName("dss_organization_pk"); - entity.ToTable("dss_organization", "dss", tb => tb.HasComment("A private company or governing body that plays a role in short term rental reporting or enforcement")); + entity.ToTable("dss_organization", tb => tb.HasComment("A private company or governing body that plays a role in short term rental reporting or enforcement")); entity.Property(e => e.OrganizationId) .HasComment("Unique generated key") @@ -130,13 +205,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasOne(d => d.ManagingOrganization).WithMany(p => p.InverseManagingOrganization) .HasForeignKey(d => d.ManagingOrganizationId) .HasConstraintName("dss_organization_fk_managed_by"); + + entity.HasOne(d => d.OrganizationTypeNavigation).WithMany(p => p.DssOrganizations) + .HasForeignKey(d => d.OrganizationType) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_organization_fk_treated_as"); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.OrganizationContactPersonId).HasName("dss_organization_contact_person_pk"); - entity.ToTable("dss_organization_contact_person", "dss", tb => tb.HasComment("A person who has been identified as a notable contact for a particular organization")); + entity.ToTable("dss_organization_contact_person", tb => tb.HasComment("A person who has been identified as a notable contact for a particular organization")); entity.Property(e => e.OrganizationContactPersonId) .HasComment("Unique generated key") @@ -146,7 +226,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasComment("Foreign key") .HasColumnName("contacted_through_organization_id"); entity.Property(e => e.EmailAddressDsc) - .HasMaxLength(250) + .HasMaxLength(320) .HasComment("E-mail address given for the contact by the organization") .HasColumnName("email_address_dsc"); entity.Property(e => e.FamilyNm) @@ -177,11 +257,25 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasConstraintName("dss_organization_contact_person_fk_contacted_for"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.OrganizationType).HasName("dss_organization_type_pk"); + + entity.ToTable("dss_organization_type"); + + entity.Property(e => e.OrganizationType) + .HasMaxLength(25) + .HasColumnName("organization_type"); + entity.Property(e => e.OrganizationTypeNm) + .HasMaxLength(250) + .HasColumnName("organization_type_nm"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.UserIdentityId).HasName("dss_user_identity_pk"); - entity.ToTable("dss_user_identity", "dss", tb => tb.HasComment("An externally defined domain directory object representing a potential application user or group")); + entity.ToTable("dss_user_identity", tb => tb.HasComment("An externally defined domain directory object representing a potential application user or group")); entity.Property(e => e.UserIdentityId) .HasComment("Unique generated key") @@ -194,10 +288,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasMaxLength(250) .HasComment("The most recent user-provided reason for requesting application access") .HasColumnName("access_request_justification_txt"); - entity.Property(e => e.AccessRequestStatusDsc) + entity.Property(e => e.AccessRequestStatusCd) .HasMaxLength(25) .HasComment("The current status of the most recent access request made by the user (restricted to Requested, Approved, or Denied)") - .HasColumnName("access_request_status_dsc"); + .HasColumnName("access_request_status_cd"); entity.Property(e => e.BusinessNm) .HasMaxLength(250) .HasComment("A human-readable organization name that is associated with the user by the identity provider") @@ -241,41 +335,87 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasComment("An immutable unique identifier assigned by the identity provider") .HasColumnName("user_guid"); + entity.HasOne(d => d.AccessRequestStatusCdNavigation).WithMany(p => p.DssUserIdentities) + .HasForeignKey(d => d.AccessRequestStatusCd) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_user_identity_fk_given"); + entity.HasOne(d => d.RepresentedByOrganization).WithMany(p => p.DssUserIdentities) .HasForeignKey(d => d.RepresentedByOrganizationId) .HasConstraintName("dss_user_identity_fk_representing"); + + entity.HasMany(d => d.UserRoleCds).WithMany(p => p.UserIdentities) + .UsingEntity>( + "DssUserRoleAssignment", + r => r.HasOne().WithMany() + .HasForeignKey("UserRoleCd") + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_user_role_assignment_fk_granted"), + l => l.HasOne().WithMany() + .HasForeignKey("UserIdentityId") + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_user_role_assignment_fk_granted_to"), + j => + { + j.HasKey("UserIdentityId", "UserRoleCd").HasName("dss_user_role_assignment_pk"); + j.ToTable("dss_user_role_assignment", tb => tb.HasComment("The association of a grantee credential to a role for the purpose of conveying application privileges")); + j.IndexerProperty("UserIdentityId") + .HasComment("Foreign key") + .HasColumnName("user_identity_id"); + j.IndexerProperty("UserRoleCd") + .HasMaxLength(25) + .HasComment("Foreign key") + .HasColumnName("user_role_cd"); + }); }); modelBuilder.Entity(entity => { - entity.HasKey(e => e.UserPrivilegeId).HasName("dss_user_privilege_pk"); + entity.HasKey(e => e.UserPrivilegeCd).HasName("dss_user_privilege_pk"); - entity.ToTable("dss_user_privilege", "dss", tb => tb.HasComment("A granular access right or privilege within the application that may be granted to a role")); + entity.ToTable("dss_user_privilege", tb => tb.HasComment("A granular access right or privilege within the application that may be granted to a role")); - entity.Property(e => e.UserPrivilegeId) - .HasComment("Unique generated key") - .UseIdentityAlwaysColumn() - .HasColumnName("user_privilege_id"); - entity.Property(e => e.PrivilegeCd) + entity.Property(e => e.UserPrivilegeCd) .HasMaxLength(25) .HasComment("The immutable system code that identifies the privilege") - .HasColumnName("privilege_cd"); - entity.Property(e => e.PrivilegeNm) + .HasColumnName("user_privilege_cd"); + entity.Property(e => e.UserPrivilegeNm) .HasMaxLength(250) .HasComment("The human-readable name that is given for the role") - .HasColumnName("privilege_nm"); + .HasColumnName("user_privilege_nm"); + + entity.HasMany(d => d.UserRoleCds).WithMany(p => p.UserPrivilegeCds) + .UsingEntity>( + "DssUserRolePrivilege", + r => r.HasOne().WithMany() + .HasForeignKey("UserRoleCd") + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_user_role_privilege_fk_conferred_by"), + l => l.HasOne().WithMany() + .HasForeignKey("UserPrivilegeCd") + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("dss_user_role_privilege_fk_conferring"), + j => + { + j.HasKey("UserPrivilegeCd", "UserRoleCd").HasName("dss_user_role_privilege_pk"); + j.ToTable("dss_user_role_privilege", tb => tb.HasComment("The association of a granular application privilege to a role")); + j.IndexerProperty("UserPrivilegeCd") + .HasMaxLength(25) + .HasComment("Foreign key") + .HasColumnName("user_privilege_cd"); + j.IndexerProperty("UserRoleCd") + .HasMaxLength(25) + .HasComment("Foreign key") + .HasColumnName("user_role_cd"); + }); }); modelBuilder.Entity(entity => { - entity.HasKey(e => e.UserRoleId).HasName("dss_user_role_pk"); + entity.HasKey(e => e.UserRoleCd).HasName("dss_user_role_pk"); - entity.ToTable("dss_user_role", "dss", tb => tb.HasComment("A set of access rights and privileges within the application that may be granted to users")); + entity.ToTable("dss_user_role", tb => tb.HasComment("A set of access rights and privileges within the application that may be granted to users")); - entity.Property(e => e.UserRoleId) - .HasComment("Unique generated key") - .UseIdentityAlwaysColumn() - .HasColumnName("user_role_id"); entity.Property(e => e.UserRoleCd) .HasMaxLength(25) .HasComment("The immutable system code that identifies the role") @@ -286,62 +426,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("user_role_nm"); }); - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.UserRoleAssignmentId).HasName("dss_user_role_assignment_pk"); - - entity.ToTable("dss_user_role_assignment", "dss", tb => tb.HasComment("The association of a grantee credential to a role for the purpose of conveying application privileges")); - - entity.Property(e => e.UserRoleAssignmentId) - .HasComment("Unique generated key") - .UseIdentityAlwaysColumn() - .HasColumnName("user_role_assignment_id"); - entity.Property(e => e.UserIdentityId) - .HasComment("Foreign key") - .HasColumnName("user_identity_id"); - entity.Property(e => e.UserRoleId) - .HasComment("Foreign key") - .HasColumnName("user_role_id"); - - entity.HasOne(d => d.UserIdentity).WithMany(p => p.DssUserRoleAssignments) - .HasForeignKey(d => d.UserIdentityId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("dss_user_role_assignment_fk_granted_to"); - - entity.HasOne(d => d.UserRole).WithMany(p => p.DssUserRoleAssignments) - .HasForeignKey(d => d.UserRoleId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("dss_user_role_assignment_fk_granted"); - }); - - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.UserRolePrivilegeId).HasName("dss_user_role_privilege_pk"); - - entity.ToTable("dss_user_role_privilege", "dss", tb => tb.HasComment("The association of a granular application privilege to a role")); - - entity.Property(e => e.UserRolePrivilegeId) - .HasComment("Unique generated key") - .UseIdentityAlwaysColumn() - .HasColumnName("user_role_privilege_id"); - entity.Property(e => e.UserPrivilegeId) - .HasComment("Foreign key") - .HasColumnName("user_privilege_id"); - entity.Property(e => e.UserRoleId) - .HasComment("Foreign key") - .HasColumnName("user_role_id"); - - entity.HasOne(d => d.UserPrivilege).WithMany(p => p.DssUserRolePrivileges) - .HasForeignKey(d => d.UserPrivilegeId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("dss_user_role_privilege_fk_conferring"); - - entity.HasOne(d => d.UserRole).WithMany(p => p.DssUserRolePrivileges) - .HasForeignKey(d => d.UserRoleId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("dss_user_role_privilege_fk_conferred_by"); - }); - OnModelCreatingPartial(modelBuilder); } diff --git a/server/StrDss.Data/Entities/DssDbContextPartial.cs b/server/StrDss.Data/Entities/DssDbContextPartial.cs new file mode 100644 index 00000000..dc9c05ea --- /dev/null +++ b/server/StrDss.Data/Entities/DssDbContextPartial.cs @@ -0,0 +1,105 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using StrDss.Model; +using Microsoft.Extensions.Logging; + +namespace StrDss.Data.Entities +{ + public partial class DssDbContext + { + private ICurrentUser _currentUser; + private ILogger _logger; + + public DssDbContext(DbContextOptions options, ICurrentUser currentUser, ILogger logger) + : base(options) + { + _currentUser = currentUser; + _logger = logger; + } + + public override int SaveChanges() + { + var modifiedEntries = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Modified); + + DateTime utcNow = DateTime.UtcNow; + + foreach (var entry in modifiedEntries) + { + if (entry.Members.Any(m => m.Metadata.Name == "UpdDtm")) + { + CheckConcurrency(entry); + + entry.Member("UpdDtm").CurrentValue = utcNow; + entry.Member("UpdUserGuid").CurrentValue = _currentUser.UserGuid; + } + } + + var addedEntries = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added); + + foreach (var entry in addedEntries) + { + if (entry.Members.Any(m => m.Metadata.Name == "UpdDtm")) + { + entry.Member("UpdDtm").CurrentValue = utcNow; + entry.Member("UpdUserGuid").CurrentValue = _currentUser.UserGuid; + } + } + + int result = 0; + + try + { + result = base.SaveChanges(); + } + catch (Exception e) + { + throw; + } + + return result; + } + + /// + /// Checks for concurrency conflicts in the entity being updated. + /// This method is used as a workaround for the limitations of the database-first approach, + /// where the ConcurrencyCheck annotation is wiped out during scaffolding and must be manually added. + /// While this implementation has its limitations, it can handle most cases. + /// + /// The EntityEntry representing the entity being updated. + /// + /// Thrown when a concurrency conflict is detected during the update operation. + /// + + private void CheckConcurrency(EntityEntry entry) + { + // values from the database before the values of the DTO is applied + var originalValues = entry.OriginalValues; + + // values from the DTO. UpdDtm is not supposed be updated and remain as it was retrieved. + var currentValues = entry.CurrentValues; + + var originalValue = (DateTime) (originalValues["UpdDtm"] ?? DateTime.MinValue); + var currentValue = (DateTime) (currentValues["UpdDtm"] ?? DateTime.MinValue); + + if (originalValue != currentValue) + { + var entityName = Model.FindEntityType(entry.Entity.GetType())?.ShortName(); + if (entityName != null && entityName.StartsWith("Dss") ) + { + entityName = entityName.Substring(3); + } + else + { + entityName = "?"; + } + + var message = $"Update conflict detected when updating {entityName}!"; + _logger.LogInformation(message); + + throw new DbUpdateConcurrencyException(message); + } + } + } +} diff --git a/server/StrDss.Data/Entities/DssEmailMessage.cs b/server/StrDss.Data/Entities/DssEmailMessage.cs index 09e75bb1..ace4afa7 100644 --- a/server/StrDss.Data/Entities/DssEmailMessage.cs +++ b/server/StrDss.Data/Entities/DssEmailMessage.cs @@ -28,26 +28,35 @@ public partial class DssEmailMessage /// public string MessageTemplateDsc { get; set; } = null!; - /// - /// A description of the justification for initiating the message - /// - public string? MessageReasonDsc { get; set; } + public bool IsHostContactedExternally { get; set; } - /// - /// User-provided URL for a short-term rental platform listing that is the subject of the message - /// - public string? UnreportedListingUrl { get; set; } + public bool IsSubmitterCcRequired { get; set; } + + public long? MessageReasonId { get; set; } + + public string? LgPhoneNo { get; set; } + + public string? UnreportedListingNo { get; set; } /// /// E-mail address of a short term rental host (directly entered by the user as a message recipient) /// public string? HostEmailAddressDsc { get; set; } + public string? LgEmailAddressDsc { get; set; } + /// /// E-mail address of a secondary message recipient (directly entered by the user) /// public string? CcEmailAddressDsc { get; set; } + /// + /// User-provided URL for a short-term rental platform listing that is the subject of the message + /// + public string? UnreportedListingUrl { get; set; } + + public string? LgStrBylawUrl { get; set; } + /// /// Foreign key /// @@ -65,7 +74,11 @@ public partial class DssEmailMessage public virtual DssUserIdentity? AffectedByUserIdentity { get; set; } + public virtual DssEmailMessageType EmailMessageTypeNavigation { get; set; } = null!; + public virtual DssUserIdentity InitiatingUserIdentity { get; set; } = null!; public virtual DssOrganization? InvolvedInOrganization { get; set; } + + public virtual DssMessageReason? MessageReason { get; set; } } diff --git a/server/StrDss.Data/Entities/DssEmailMessageType.cs b/server/StrDss.Data/Entities/DssEmailMessageType.cs new file mode 100644 index 00000000..1c3d52b7 --- /dev/null +++ b/server/StrDss.Data/Entities/DssEmailMessageType.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace StrDss.Data.Entities; + +public partial class DssEmailMessageType +{ + public string EmailMessageType { get; set; } = null!; + + public string EmailMessageTypeNm { get; set; } = null!; + + public virtual ICollection DssEmailMessages { get; set; } = new List(); + + public virtual ICollection DssMessageReasons { get; set; } = new List(); +} diff --git a/server/StrDss.Data/Entities/DssMessageReason.cs b/server/StrDss.Data/Entities/DssMessageReason.cs new file mode 100644 index 00000000..7ed9a1f9 --- /dev/null +++ b/server/StrDss.Data/Entities/DssMessageReason.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace StrDss.Data.Entities; + +public partial class DssMessageReason +{ + public long MessageReasonId { get; set; } + + public string EmailMessageType { get; set; } = null!; + + /// + /// A description of the justification for initiating a message + /// + public string MessageReasonDsc { get; set; } = null!; + + public virtual ICollection DssEmailMessages { get; set; } = new List(); + + public virtual DssEmailMessageType EmailMessageTypeNavigation { get; set; } = null!; +} diff --git a/server/StrDss.Data/Entities/DssOrganization.cs b/server/StrDss.Data/Entities/DssOrganization.cs index 645062c8..ba5c18cd 100644 --- a/server/StrDss.Data/Entities/DssOrganization.cs +++ b/server/StrDss.Data/Entities/DssOrganization.cs @@ -58,4 +58,6 @@ public partial class DssOrganization public virtual ICollection InverseManagingOrganization { get; set; } = new List(); public virtual DssOrganization? ManagingOrganization { get; set; } + + public virtual DssOrganizationType OrganizationTypeNavigation { get; set; } = null!; } diff --git a/server/StrDss.Data/Entities/DssOrganizationType.cs b/server/StrDss.Data/Entities/DssOrganizationType.cs new file mode 100644 index 00000000..4736bf06 --- /dev/null +++ b/server/StrDss.Data/Entities/DssOrganizationType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace StrDss.Data.Entities; + +public partial class DssOrganizationType +{ + public string OrganizationType { get; set; } = null!; + + public string OrganizationTypeNm { get; set; } = null!; + + public virtual ICollection DssOrganizations { get; set; } = new List(); +} diff --git a/server/StrDss.Data/Entities/DssUserIdentity.cs b/server/StrDss.Data/Entities/DssUserIdentity.cs index 2d4e7ea9..0f94e0a8 100644 --- a/server/StrDss.Data/Entities/DssUserIdentity.cs +++ b/server/StrDss.Data/Entities/DssUserIdentity.cs @@ -36,7 +36,7 @@ public partial class DssUserIdentity /// /// The current status of the most recent access request made by the user (restricted to Requested, Approved, or Denied) /// - public string AccessRequestStatusDsc { get; set; } = null!; + public string AccessRequestStatusCd { get; set; } = null!; /// /// A timestamp indicating when the most recent access request was made by the user @@ -88,11 +88,13 @@ public partial class DssUserIdentity /// public Guid? UpdUserGuid { get; set; } + public virtual DssAccessRequestStatus AccessRequestStatusCdNavigation { get; set; } = null!; + public virtual ICollection DssEmailMessageAffectedByUserIdentities { get; set; } = new List(); public virtual ICollection DssEmailMessageInitiatingUserIdentities { get; set; } = new List(); - public virtual ICollection DssUserRoleAssignments { get; set; } = new List(); - public virtual DssOrganization? RepresentedByOrganization { get; set; } + + public virtual ICollection UserRoleCds { get; set; } = new List(); } diff --git a/server/StrDss.Data/Entities/DssUserPrivilege.cs b/server/StrDss.Data/Entities/DssUserPrivilege.cs index 9e2e7602..369eb365 100644 --- a/server/StrDss.Data/Entities/DssUserPrivilege.cs +++ b/server/StrDss.Data/Entities/DssUserPrivilege.cs @@ -8,20 +8,15 @@ namespace StrDss.Data.Entities; /// public partial class DssUserPrivilege { - /// - /// Unique generated key - /// - public long UserPrivilegeId { get; set; } - /// /// The immutable system code that identifies the privilege /// - public string PrivilegeCd { get; set; } = null!; + public string UserPrivilegeCd { get; set; } = null!; /// /// The human-readable name that is given for the role /// - public string PrivilegeNm { get; set; } = null!; + public string UserPrivilegeNm { get; set; } = null!; - public virtual ICollection DssUserRolePrivileges { get; set; } = new List(); + public virtual ICollection UserRoleCds { get; set; } = new List(); } diff --git a/server/StrDss.Data/Entities/DssUserRole.cs b/server/StrDss.Data/Entities/DssUserRole.cs index d755b0f3..21af78e2 100644 --- a/server/StrDss.Data/Entities/DssUserRole.cs +++ b/server/StrDss.Data/Entities/DssUserRole.cs @@ -8,11 +8,6 @@ namespace StrDss.Data.Entities; /// public partial class DssUserRole { - /// - /// Unique generated key - /// - public long UserRoleId { get; set; } - /// /// The immutable system code that identifies the role /// @@ -23,7 +18,7 @@ public partial class DssUserRole /// public string UserRoleNm { get; set; } = null!; - public virtual ICollection DssUserRoleAssignments { get; set; } = new List(); + public virtual ICollection UserIdentities { get; set; } = new List(); - public virtual ICollection DssUserRolePrivileges { get; set; } = new List(); + public virtual ICollection UserPrivilegeCds { get; set; } = new List(); } diff --git a/server/StrDss.Data/Entities/DssUserRoleAssignment.cs b/server/StrDss.Data/Entities/DssUserRoleAssignment.cs deleted file mode 100644 index 59f4c4ed..00000000 --- a/server/StrDss.Data/Entities/DssUserRoleAssignment.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StrDss.Data.Entities; - -/// -/// The association of a grantee credential to a role for the purpose of conveying application privileges -/// -public partial class DssUserRoleAssignment -{ - /// - /// Unique generated key - /// - public long UserRoleAssignmentId { get; set; } - - /// - /// Foreign key - /// - public long UserIdentityId { get; set; } - - /// - /// Foreign key - /// - public long UserRoleId { get; set; } - - public virtual DssUserIdentity UserIdentity { get; set; } = null!; - - public virtual DssUserRole UserRole { get; set; } = null!; -} diff --git a/server/StrDss.Data/Entities/DssUserRolePrivilege.cs b/server/StrDss.Data/Entities/DssUserRolePrivilege.cs deleted file mode 100644 index 1518768c..00000000 --- a/server/StrDss.Data/Entities/DssUserRolePrivilege.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StrDss.Data.Entities; - -/// -/// The association of a granular application privilege to a role -/// -public partial class DssUserRolePrivilege -{ - /// - /// Unique generated key - /// - public long UserRolePrivilegeId { get; set; } - - /// - /// Foreign key - /// - public long UserPrivilegeId { get; set; } - - /// - /// Foreign key - /// - public long UserRoleId { get; set; } - - public virtual DssUserPrivilege UserPrivilege { get; set; } = null!; - - public virtual DssUserRole UserRole { get; set; } = null!; -} diff --git a/server/StrDss.Data/Mappings/EntityToModelProfile.cs b/server/StrDss.Data/Mappings/EntityToModelProfile.cs index 8fc18647..540f0b25 100644 --- a/server/StrDss.Data/Mappings/EntityToModelProfile.cs +++ b/server/StrDss.Data/Mappings/EntityToModelProfile.cs @@ -1,4 +1,7 @@ using AutoMapper; +using StrDss.Data.Entities; +using StrDss.Model.OrganizationDtos; +using StrDss.Model.UserDtos; namespace StrDss.Data.Mappings { @@ -6,6 +9,21 @@ public class EntityToModelProfile : Profile { public EntityToModelProfile() { + CreateMap() + .ForMember(o => o.OrganizationType, opt => opt.MapFrom(i => i.RepresentedByOrganization == null ? "" : i.RepresentedByOrganization.OrganizationType)) + .ForMember(o => o.OrganizationCd, opt => opt.MapFrom(i => i.RepresentedByOrganization == null ? "" : i.RepresentedByOrganization.OrganizationCd)) + .ForMember(o => o.OrganizationNm, opt => opt.MapFrom(i => i.RepresentedByOrganization == null ? "" : i.RepresentedByOrganization.OrganizationNm)) + ; + + CreateMap(); + CreateMap() + .ForMember(o => o.ContactPeople, opt => opt.MapFrom(i => i.DssOrganizationContactPeople)) + ; + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } } diff --git a/server/StrDss.Data/Mappings/ModelToEntityProfile.cs b/server/StrDss.Data/Mappings/ModelToEntityProfile.cs index faed597c..a9998d7b 100644 --- a/server/StrDss.Data/Mappings/ModelToEntityProfile.cs +++ b/server/StrDss.Data/Mappings/ModelToEntityProfile.cs @@ -1,4 +1,6 @@ using AutoMapper; +using StrDss.Data.Entities; +using StrDss.Model.UserDtos; namespace StrDss.Data.Mappings { @@ -6,6 +8,10 @@ public class ModelToEntityProfile : Profile { public ModelToEntityProfile() { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } } diff --git a/server/StrDss.Data/Repositories/EmailMessageRepository.cs b/server/StrDss.Data/Repositories/EmailMessageRepository.cs new file mode 100644 index 00000000..cdebaa2b --- /dev/null +++ b/server/StrDss.Data/Repositories/EmailMessageRepository.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using StrDss.Data.Entities; +using StrDss.Model; + +namespace StrDss.Data.Repositories +{ + public interface IEmailMessageRepository + { + Task> GetMessageReasons(string messageType); + } + public class EmailMessageRepository : RepositoryBase, IEmailMessageRepository + { + public EmailMessageRepository(DssDbContext dbContext, IMapper mapper, ICurrentUser currentUser) + : base(dbContext, mapper, currentUser) + { + } + + public async Task> GetMessageReasons(string messageType) + { + var reasons = await _dbContext.DssMessageReasons.AsNoTracking() + .Where(x => x.EmailMessageType == messageType) + .Select(x => new DropdownNumDto { Id = x.MessageReasonId, Description = x.MessageReasonDsc }) + .ToListAsync(); + + return reasons; + } + } +} diff --git a/server/StrDss.Data/Repositories/OrganizationRepository.cs b/server/StrDss.Data/Repositories/OrganizationRepository.cs new file mode 100644 index 00000000..1ae54f0a --- /dev/null +++ b/server/StrDss.Data/Repositories/OrganizationRepository.cs @@ -0,0 +1,52 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using StrDss.Data.Entities; +using StrDss.Model; +using StrDss.Model.OrganizationDtos; + +namespace StrDss.Data.Repositories +{ + public interface IOrganizationRepository + { + Task> GetOrganizationTypesAsnc(); + Task> GetOrganizationsAsync(string? type); + Task GetOrganizationByIdAsync(long id); + } + public class OrganizationRepository : RepositoryBase, IOrganizationRepository + { + public OrganizationRepository(DssDbContext dbContext, IMapper mapper, ICurrentUser currentUser) + : base(dbContext, mapper, currentUser) + { + } + + public async Task> GetOrganizationTypesAsnc() + { + var types = _mapper.Map>(await _dbContext.DssOrganizationTypes.AsNoTracking().ToListAsync()); + + return types; + } + + public async Task> GetOrganizationsAsync(string? type) + { + var query = _dbSet.AsNoTracking(); + + if (type != null && type != "All") + { + query = query.Where(x => x.OrganizationType == type); + } + + query = query.Include(x => x.DssOrganizationContactPeople); + + return _mapper.Map>(await query.ToListAsync()); + } + + public async Task GetOrganizationByIdAsync(long id) + { + var org = await _dbSet.AsNoTracking() + .Include(x => x.DssOrganizationContactPeople) + .FirstOrDefaultAsync(x => x.OrganizationId == id); + + return _mapper.Map(org); + } + } +} diff --git a/server/StrDss.Data/Repositories/UserRepository.cs b/server/StrDss.Data/Repositories/UserRepository.cs new file mode 100644 index 00000000..431c9995 --- /dev/null +++ b/server/StrDss.Data/Repositories/UserRepository.cs @@ -0,0 +1,108 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using StrDss.Common; +using StrDss.Data.Entities; +using StrDss.Model; +using StrDss.Model.UserDtos; + +namespace StrDss.Data.Repositories +{ + public interface IUserRepository + { + Task> GetAccessRequestListAsync(string status, int pageSize, int pageNumber, string orderBy, string direction); + Task CreateUserAsync(UserCreateDto dto); + Task<(UserDto? user, List permissions)> GetUserAndPermissionsByGuidAsync(Guid guid); + Task GetUserById(long id); + Task GetUserByGuid(Guid guid); + Task UpdateUserAsync(UserDto dto); + Task DenyAccessRequest(AccessRequestDenyDto dto); + Task ApproveAccessRequest(AccessRequestApproveDto dto, string role); + } + public class UserRepository : RepositoryBase, IUserRepository + { + public UserRepository(DssDbContext dbContext, IMapper mapper, ICurrentUser currentUser) : base(dbContext, mapper, currentUser) + { + } + + public async Task> GetAccessRequestListAsync(string status, int pageSize, int pageNumber, string orderBy, string direction) + { + var query = _dbSet.AsNoTracking(); + + if (status.IsNotEmpty() && status != "All") + { + query = query.Where(x => x.AccessRequestStatusCd == status); + } + + query = query.Include(x => x.RepresentedByOrganization); + + var results = await Page(query, pageSize, pageNumber, orderBy, direction); + + return results; + } + + public async Task CreateUserAsync(UserCreateDto dto) + { + await _dbContext.AddAsync(_mapper.Map(dto)); + } + + public async Task<(UserDto? user, List permissions)> GetUserAndPermissionsByGuidAsync(Guid guid) + { + var query = await _dbSet.AsNoTracking() + .Include(x => x.UserRoleCds) + .Include(x => x.RepresentedByOrganization) + .FirstOrDefaultAsync(x => x.UserGuid == guid); + + if (query == null) + return (null, new List()); + + var user = _mapper.Map(query); + + var roles = user.UserRoleCds.Select(x => x.UserRoleCd).ToList(); + + var permssions = _dbContext.DssUserRoles + .Where(x => roles.Contains(x.UserRoleCd)) + .SelectMany(x => x.UserPrivilegeCds) + .ToLookup(x => x.UserPrivilegeCd) + .Select(x => x.First()) + .Select(x => x.UserPrivilegeCd) + .ToList(); + + return (user, permssions); + } + + public async Task GetUserById(long id) + { + var entity = await _dbSet.AsNoTracking() + .Include(x => x.RepresentedByOrganization) + .FirstOrDefaultAsync(x => x.UserIdentityId == id); + return _mapper.Map(entity); + } + + public async Task GetUserByGuid(Guid guid) + { + var entity = await _dbSet.AsNoTracking().FirstOrDefaultAsync(x => x.UserGuid == guid); + return _mapper.Map(entity); + } + + public async Task UpdateUserAsync(UserDto dto) + { + var entity = await _dbSet.FirstAsync(x => x.UserIdentityId == dto.UserIdentityId); + _mapper.Map(dto, entity); + } + + public async Task DenyAccessRequest(AccessRequestDenyDto dto) + { + var entity = await _dbSet.FirstAsync(x => x.UserIdentityId == dto.UserIdentityId); + _mapper.Map(dto, entity); + } + + public async Task ApproveAccessRequest(AccessRequestApproveDto dto, string role) + { + var entity = await _dbSet.FirstAsync(x => x.UserIdentityId == dto.UserIdentityId); + _mapper.Map(dto, entity); + + var roleEntity = await _dbContext.DssUserRoles.FirstAsync(x => x.UserRoleCd == role); + entity.UserRoleCds.Add(roleEntity); + } + } +} diff --git a/server/StrDss.Data/Scaffolding.txt b/server/StrDss.Data/Scaffolding.txt index ab7c0af9..a59cd92e 100644 --- a/server/StrDss.Data/Scaffolding.txt +++ b/server/StrDss.Data/Scaffolding.txt @@ -2,5 +2,5 @@ Generate / Update Model: ******************************************** CD StrDss.Data -dotnet ef dbcontext scaffold "Host=localhost;Database=strdssdev;Username=strdssdev;Password=postgres" Npgsql.EntityFrameworkCore.PostgreSQL -o Entities --context "DssDbContext" --no-onconfiguring --force +dotnet ef dbcontext scaffold "Host=localhost;Database=strdssdev;Username=strdssdev;Password=postgres" Npgsql.EntityFrameworkCore.PostgreSQL -o Entities --context "DssDbContext" --no-onconfiguring --force --schema public diff --git a/server/StrDss.Data/StrDss.Data.csproj b/server/StrDss.Data/StrDss.Data.csproj index 3c452c42..1fb8f9c6 100644 --- a/server/StrDss.Data/StrDss.Data.csproj +++ b/server/StrDss.Data/StrDss.Data.csproj @@ -21,10 +21,6 @@ - - - - diff --git a/server/StrDss.Model/CurrentUser.cs b/server/StrDss.Model/CurrentUser.cs index a616081c..d9db55f2 100644 --- a/server/StrDss.Model/CurrentUser.cs +++ b/server/StrDss.Model/CurrentUser.cs @@ -10,15 +10,18 @@ public interface ICurrentUser public long Id { get; set; } public string UserName { get; set; } public Guid UserGuid { get; set; } - public string UserType { get; set; } + public string IdentityProviderNm { get; set; } public string EmailAddress { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get; set; } public string DisplayName { get; set; } public bool IsActive { get; set; } + public string BusinessNm { get; set; } public string AccessRequestStatus { get; set; } - + public bool AccessRequestRequired { get; set; } + public List Permissions { get; set; } + public string OrganizationType { get; set; } void LoadUserSession(ClaimsPrincipal user); void AddClaim(ClaimsPrincipal user, string claimType, string value); } @@ -29,14 +32,18 @@ public class CurrentUser : ICurrentUser public string UserName { get; set; } = ""; [JsonIgnore] public Guid UserGuid { get; set; } - public string UserType { get; set; } = ""; + public string IdentityProviderNm { get; set; } = ""; public string EmailAddress { get; set; } = ""; public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public string FullName { get; set; } = ""; public string DisplayName { get; set; } = ""; - public bool IsActive { get; set; } = true; + public bool IsActive { get; set; } = false; + public string BusinessNm { get; set; } = ""; public string AccessRequestStatus { get; set; } = ""; + public bool AccessRequestRequired { get; set; } + public List Permissions { get; set; } = new List(); + public string OrganizationType { get; set; } public void LoadUserSession(ClaimsPrincipal user) { @@ -45,23 +52,24 @@ public void LoadUserSession(ClaimsPrincipal user) var textInfo = new CultureInfo("en-US", false).TextInfo; - UserType = user.GetCustomClaim(StrDssClaimTypes.IdentityProvider); + IdentityProviderNm = user.GetCustomClaim(StrDssClaimTypes.IdentityProvider); EmailAddress = user.GetCustomClaim(ClaimTypes.Email); FirstName = textInfo.ToTitleCase(user.GetCustomClaim(ClaimTypes.GivenName)); LastName = textInfo.ToTitleCase(user.GetCustomClaim(ClaimTypes.Surname)); DisplayName = user.GetCustomClaim(StrDssClaimTypes.DisplayName); - switch (UserType) + switch (IdentityProviderNm) { - case StrDssUserTypes.IDIR: + case StrDssIdProviders.Idir: UserGuid = new Guid(user.GetCustomClaim(StrDssClaimTypes.IdirUserGuid)); UserName = user.GetCustomClaim(StrDssClaimTypes.IdirUsername); break; - case StrDssUserTypes.BceidBusiness: + case StrDssIdProviders.BceidBusiness: UserGuid = new Guid(user.GetCustomClaim(StrDssClaimTypes.BceidUserGuid)); UserName = user.GetCustomClaim(StrDssClaimTypes.BceidUsername); + BusinessNm = user.GetCustomClaim(StrDssClaimTypes.BceidBusinessName); break; - case StrDssUserTypes.StrDss: + case StrDssIdProviders.StrDss: UserGuid = new Guid(user.GetCustomClaim(StrDssClaimTypes.StrDssUserGuid)); UserName = user.GetCustomClaim(StrDssClaimTypes.StrDssUsername); break; diff --git a/server/StrDss.Model/DropdownDto.cs b/server/StrDss.Model/DropdownNumDto.cs similarity index 88% rename from server/StrDss.Model/DropdownDto.cs rename to server/StrDss.Model/DropdownNumDto.cs index a79a2c1a..ae47b9fa 100644 --- a/server/StrDss.Model/DropdownDto.cs +++ b/server/StrDss.Model/DropdownNumDto.cs @@ -2,7 +2,7 @@ namespace StrDss.Model { - public class DropdownDto + public class DropdownNumDto { [JsonPropertyName("value")] public long Id { get; set; } diff --git a/server/StrDss.Model/DropdownStrDto.cs b/server/StrDss.Model/DropdownStrDto.cs new file mode 100644 index 00000000..288e1562 --- /dev/null +++ b/server/StrDss.Model/DropdownStrDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace StrDss.Model +{ + public class DropdownStrDto + { + [JsonPropertyName("value")] + public string Id { get; set; } + [JsonPropertyName("label")] + public string Description { get; set; } = ""; + } +} diff --git a/server/StrDss.Model/EmailContent.cs b/server/StrDss.Model/EmailContent.cs index 33e733a1..a364de99 100644 --- a/server/StrDss.Model/EmailContent.cs +++ b/server/StrDss.Model/EmailContent.cs @@ -9,7 +9,7 @@ public class EmailContent public IEnumerable Bcc { get; set; } - public string BodyType { get; set; } = ""; + public string BodyType { get; set; } = "html"; public string Body { get; set; } = ""; diff --git a/server/StrDss.Model/LocalGovernmentDtos/LocalGovernmentDto.cs b/server/StrDss.Model/LocalGovernmentDtos/LocalGovernmentDto.cs deleted file mode 100644 index 3cd3851f..00000000 --- a/server/StrDss.Model/LocalGovernmentDtos/LocalGovernmentDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace StrDss.Model.LocalGovernmentDtos -{ - public class LocalGovernmentDto - { - public long LocalGovernmentId { get; set; } - public string Name { get; set; } = ""; - public string Email { get; set; } = ""; - public static readonly LocalGovernmentDto[] localGovernments = new LocalGovernmentDto[] - { - new LocalGovernmentDto {LocalGovernmentId = 1, Name = "Victoria", Email = "young-jin.chung@gov.bc.ca"}, - new LocalGovernmentDto {LocalGovernmentId = 2, Name = "Vancouver", Email = "young-jin.chung@gov.bc.ca" } - }; - } -} diff --git a/server/StrDss.Model/OrganizationDtos/ContactPersonDto.cs b/server/StrDss.Model/OrganizationDtos/ContactPersonDto.cs new file mode 100644 index 00000000..b50ec9c3 --- /dev/null +++ b/server/StrDss.Model/OrganizationDtos/ContactPersonDto.cs @@ -0,0 +1,23 @@ +namespace StrDss.Model.OrganizationDtos +{ + public class ContactPersonDto + { + public long OrganizationContactPersonId { get; set; } + + public bool IsPrimary { get; set; } + + public string GivenNm { get; set; } = null!; + + public string FamilyNm { get; set; } = null!; + + public string PhoneNo { get; set; } = null!; + + public string EmailAddressDsc { get; set; } = null!; + + public long ContactedThroughOrganizationId { get; set; } + + public DateTime UpdDtm { get; set; } + + public Guid? UpdUserGuid { get; set; } + } +} diff --git a/server/StrDss.Model/OrganizationDtos/OrganizationDto.cs b/server/StrDss.Model/OrganizationDtos/OrganizationDto.cs new file mode 100644 index 00000000..9450cacc --- /dev/null +++ b/server/StrDss.Model/OrganizationDtos/OrganizationDto.cs @@ -0,0 +1,29 @@ +using NetTopologySuite.Geometries; +using System.Text.Json.Serialization; + +namespace StrDss.Model.OrganizationDtos +{ + public class OrganizationDto + { + [JsonPropertyName("value")] + public long OrganizationId { get; set; } + + public string OrganizationType { get; set; } = null!; + + public string OrganizationCd { get; set; } = null!; + + [JsonPropertyName("label")] + public string OrganizationNm { get; set; } = null!; + + public Geometry? LocalGovernmentGeometry { get; set; } + + public long? ManagingOrganizationId { get; set; } + + public DateTime UpdDtm { get; set; } + + public Guid? UpdUserGuid { get; set; } + + public virtual ICollection ContactPeople { get; set; } = new List(); + + } +} diff --git a/server/StrDss.Model/OrganizationDtos/OrganizationTypeDto.cs b/server/StrDss.Model/OrganizationDtos/OrganizationTypeDto.cs new file mode 100644 index 00000000..99aa3b77 --- /dev/null +++ b/server/StrDss.Model/OrganizationDtos/OrganizationTypeDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace StrDss.Model.OrganizationDtos +{ + public class OrganizationTypeDto + { + [JsonPropertyName("value")] + public string OrganizationType { get; set; } = ""; + [JsonPropertyName("label")] + public string OrganizationTypeNm { get; set; } = ""; + } +} diff --git a/server/StrDss.Model/PlatformDtos/PlatformDto.cs b/server/StrDss.Model/PlatformDtos/PlatformDto.cs deleted file mode 100644 index 8ce767c1..00000000 --- a/server/StrDss.Model/PlatformDtos/PlatformDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace StrDss.Model.PlatformDtos -{ - public class PlatformDto - { - public long PlatformId { get; set; } - public string Name { get; set; } = ""; - public string Email { get; set; } = ""; - public static readonly PlatformDto[] Platforms = new PlatformDto[] - { - new PlatformDto {PlatformId = 1, Name = "Airbnb", Email = "young-jin.chung@gov.bc.ca"}, - new PlatformDto {PlatformId = 2, Name = "Vrbo", Email = "young-jin.chung@gov.bc.ca" } - }; - } -} diff --git a/server/StrDss.Model/UserDtos/AccessRequestApproveDto.cs b/server/StrDss.Model/UserDtos/AccessRequestApproveDto.cs new file mode 100644 index 00000000..7f5ca47c --- /dev/null +++ b/server/StrDss.Model/UserDtos/AccessRequestApproveDto.cs @@ -0,0 +1,15 @@ +using StrDss.Common; +using System.Text.Json.Serialization; + +namespace StrDss.Model.UserDtos +{ + public class AccessRequestApproveDto + { + public long UserIdentityId { get; set; } + public long RepresentedByOrganizationId { get; set; } + [JsonIgnore] + public string AccessRequestStatusCd { get; set; } = AccessRequestStatuses.Approved; + public bool IsEnabled { get; set; } = true; + public DateTime UpdDtm { get; set; } + } +} diff --git a/server/StrDss.Model/UserDtos/AccessRequestCreateDto.cs b/server/StrDss.Model/UserDtos/AccessRequestCreateDto.cs new file mode 100644 index 00000000..e4b9ae88 --- /dev/null +++ b/server/StrDss.Model/UserDtos/AccessRequestCreateDto.cs @@ -0,0 +1,8 @@ +namespace StrDss.Model.UserDtos +{ + public class AccessRequestCreateDto + { + public string OrganizationType { get; set; } = ""; + public string OrganizationName { get; set; } = ""; + } +} diff --git a/server/StrDss.Model/UserDtos/AccessRequestDenyDto.cs b/server/StrDss.Model/UserDtos/AccessRequestDenyDto.cs new file mode 100644 index 00000000..6fabd02d --- /dev/null +++ b/server/StrDss.Model/UserDtos/AccessRequestDenyDto.cs @@ -0,0 +1,13 @@ +using StrDss.Common; +using System.Text.Json.Serialization; + +namespace StrDss.Model.UserDtos +{ + public class AccessRequestDenyDto + { + public long UserIdentityId { get; set; } + [JsonIgnore] + public string AccessRequestStatusCd { get; set; } = AccessRequestStatuses.Denied; + public DateTime UpdDtm { get; set; } + } +} diff --git a/server/StrDss.Model/UserDtos/AccessRequestDto.cs b/server/StrDss.Model/UserDtos/AccessRequestDto.cs new file mode 100644 index 00000000..ade1ee82 --- /dev/null +++ b/server/StrDss.Model/UserDtos/AccessRequestDto.cs @@ -0,0 +1,34 @@ +namespace StrDss.Model.UserDtos +{ + public class AccessRequestDto + { + public long UserIdentityId { get; set; } + + public bool IsEnabled { get; set; } + + public string AccessRequestStatusCd { get; set; } = null!; + + public DateTime? AccessRequestDtm { get; set; } + + public string? AccessRequestJustificationTxt { get; set; } + + public string? GivenNm { get; set; } + + public string? FamilyNm { get; set; } + + public string? EmailAddressDsc { get; set; } + + public string? BusinessNm { get; set; } + + public DateTime? TermsAcceptanceDtm { get; set; } + + public long? RepresentedByOrganizationId { get; set; } + + public string OrganizationType { get; set; } = null!; + + public string OrganizationCd { get; set; } = null!; + + public string OrganizationNm { get; set; } = null!; + public DateTime UpdDtm { get; set; } + } +} diff --git a/server/StrDss.Model/UserDtos/AccessRequestStatusDto.cs b/server/StrDss.Model/UserDtos/AccessRequestStatusDto.cs new file mode 100644 index 00000000..d998224d --- /dev/null +++ b/server/StrDss.Model/UserDtos/AccessRequestStatusDto.cs @@ -0,0 +1,9 @@ +namespace StrDss.Model.UserDtos +{ + public class AccessRequestStatusDto + { + public string AccessRequestStatusCd { get; set; } = null!; + + public string AccessRequestStatusNm { get; set; } = null!; + } +} diff --git a/server/StrDss.Model/UserDtos/PermissionDto.cs b/server/StrDss.Model/UserDtos/PermissionDto.cs new file mode 100644 index 00000000..470fc153 --- /dev/null +++ b/server/StrDss.Model/UserDtos/PermissionDto.cs @@ -0,0 +1,9 @@ +namespace StrDss.Model.UserDtos +{ + public class PermissionDto + { + public string UserPrivilegeCd { get; set; } = null!; + public string UserPrivilegeNm { get; set; } = null!; + + } +} diff --git a/server/StrDss.Model/UserDtos/RoleDto.cs b/server/StrDss.Model/UserDtos/RoleDto.cs new file mode 100644 index 00000000..f73f2c01 --- /dev/null +++ b/server/StrDss.Model/UserDtos/RoleDto.cs @@ -0,0 +1,9 @@ +namespace StrDss.Model.UserDtos +{ + public class RoleDto + { + public string UserRoleCd { get; set; } = null!; + public string UserRoleNm { get; set; } = null!; + public virtual ICollection UserPrivilegeCds { get; set; } = new List(); + } +} diff --git a/server/StrDss.Model/UserDtos/UserCreateDto.cs b/server/StrDss.Model/UserDtos/UserCreateDto.cs new file mode 100644 index 00000000..dd958ae1 --- /dev/null +++ b/server/StrDss.Model/UserDtos/UserCreateDto.cs @@ -0,0 +1,31 @@ +namespace StrDss.Model.UserDtos +{ + public class UserCreateDto + { + public Guid UserGuid { get; set; } + + public string DisplayNm { get; set; } = null!; + + public string IdentityProviderNm { get; set; } = null!; + + public bool IsEnabled { get; set; } + + public string AccessRequestStatusCd { get; set; } = null!; + + public DateTime? AccessRequestDtm { get; set; } + + public string? AccessRequestJustificationTxt { get; set; } + + public string? GivenNm { get; set; } + + public string? FamilyNm { get; set; } + + public string? EmailAddressDsc { get; set; } + + public string? BusinessNm { get; set; } + + public DateTime? TermsAcceptanceDtm { get; set; } + + public long? RepresentedByOrganizationId { get; set; } + } +} diff --git a/server/StrDss.Model/UserDtos/UserDto.cs b/server/StrDss.Model/UserDtos/UserDto.cs new file mode 100644 index 00000000..b71ec6da --- /dev/null +++ b/server/StrDss.Model/UserDtos/UserDto.cs @@ -0,0 +1,41 @@ +using StrDss.Model.OrganizationDtos; +using System.Text.Json.Serialization; + +namespace StrDss.Model.UserDtos +{ + public class UserDto + { + public long UserIdentityId { get; set; } + + [JsonIgnore] + public Guid UserGuid { get; set; } + + public string DisplayNm { get; set; } = null!; + + public string IdentityProviderNm { get; set; } = null!; + + public bool IsEnabled { get; set; } + + public string AccessRequestStatusCd { get; set; } = null!; + + public DateTime? AccessRequestDtm { get; set; } + + public string? AccessRequestJustificationTxt { get; set; } + + public string? GivenNm { get; set; } + + public string? FamilyNm { get; set; } + + public string? EmailAddressDsc { get; set; } + + public string? BusinessNm { get; set; } + + public DateTime? TermsAcceptanceDtm { get; set; } + + public long? RepresentedByOrganizationId { get; set; } + + public DateTime UpdDtm { get; set; } + public virtual OrganizationDto? RepresentedByOrganization { get; set; } + public virtual ICollection UserRoleCds { get; set; } = new List(); + } +} diff --git a/server/StrDss.Service/DelistingService.cs b/server/StrDss.Service/DelistingService.cs index 0a889973..44a30033 100644 --- a/server/StrDss.Service/DelistingService.cs +++ b/server/StrDss.Service/DelistingService.cs @@ -1,56 +1,82 @@ using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using NetTopologySuite.Geometries; using StrDss.Common; using StrDss.Data; using StrDss.Model; using StrDss.Model.DelistingDtos; -using StrDss.Model.LocalGovernmentDtos; -using StrDss.Model.PlatformDtos; +using StrDss.Model.OrganizationDtos; using StrDss.Model.WarningReasonDtos; -using StrDss.Service.HttpClients; -using System.Text; using System.Text.RegularExpressions; namespace StrDss.Service { public interface IDelistingService { - Task>> ValidateDelistingWarning(DelistingWarningCreateDto dto, PlatformDto? platform, string? reason); - Task SendDelistingWarningAsync(DelistingWarningCreateDto dto, PlatformDto? platform); - string FormatDelistingWarningEmailContent(DelistingWarningCreateDto dto, bool contentOnly); - Task>> ValidateDelistingRequest(DelistingRequestCreateDto dto, PlatformDto? platform, LocalGovernmentDto? lg); - Task SendDelistingRequestAsync(DelistingRequestCreateDto dto, PlatformDto? platform); - string FormatDelistingRequestEmailContent(DelistingRequestCreateDto dto, bool contentOnly); + Task>> CreateDelistingWarningAsync(DelistingWarningCreateDto dto); + Task<(Dictionary> errors, EmailPreview preview)> GetDelistingWarningPreviewAsync(DelistingWarningCreateDto dto); + Task>> CreateDelistingRequestAsync(DelistingRequestCreateDto dto); + Task<(Dictionary> errors, EmailPreview preview)> GetDelistingRequestPreviewAsync(DelistingRequestCreateDto dto); } public class DelistingService : ServiceBase, IDelistingService { private IConfiguration _config; - private IEmailService _emailService; + private IEmailMessageService _emailService; + private IOrganizationService _orgService; private ILogger _logger; - public DelistingService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, - IConfiguration config, IEmailService emailService, ILogger logger) - : base(currentUser, validator, unitOfWork, mapper) + public DelistingService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, IHttpContextAccessor httpContextAccessor, + IConfiguration config, IEmailMessageService emailService, IOrganizationService orgService, ILogger logger) + : base(currentUser, validator, unitOfWork, mapper, httpContextAccessor) { _config = config; _emailService = emailService; + _orgService = orgService; _logger = logger; } - public async Task>> ValidateDelistingWarning(DelistingWarningCreateDto dto, PlatformDto? platform, string? reason) + + public async Task>> CreateDelistingWarningAsync(DelistingWarningCreateDto dto) + { + var platform = await _orgService.GetOrganizationByIdAsync(dto.PlatformId); + + var reason = WarningReasonDto.WarningReasons.FirstOrDefault(x => x.WarningReasonId == dto.ReasonId)?.Reason; + + var errors = await ValidateDelistingWarningAsync(dto, platform, reason); + if (errors.Count > 0) + { + return errors; + } + + await SendDelistingWarningAsync(dto, platform); + + return errors; + } + + private async Task>> ValidateDelistingWarningAsync(DelistingWarningCreateDto dto, OrganizationDto? platform, string? reason) { await Task.CompletedTask; var errors = new Dictionary>(); RegexInfo regex; - //Todo: Validate Platform ID if (platform == null) { errors.AddItem("platformId", $"Platform ID ({dto.PlatformId}) does not exist."); } + else + { + if (platform.OrganizationType != OrganizationTypes.Platform) + { + errors.AddItem("platformId", $"Organization ({dto.PlatformId}) is not a platform"); + } + if (platform.ContactPeople == null || !platform.ContactPeople.Any(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty())) + { + errors.AddItem("platformId", $"Platform ({dto.PlatformId}) does not have the primary contact info"); + } + } if (dto.ListingUrl.IsEmpty()) { @@ -123,9 +149,11 @@ public async Task>> ValidateDelistingWarning(Del return errors; } - public async Task SendDelistingWarningAsync(DelistingWarningCreateDto dto, PlatformDto? platform) + private async Task SendDelistingWarningAsync(DelistingWarningCreateDto dto, OrganizationDto? platform) { - dto.ToList.Add(platform?.Email ?? ""); + var contact = platform.ContactPeople.First(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty()); + + dto.ToList.Add(contact.EmailAddressDsc); if (dto.HostEmail.IsNotEmpty()) { dto.ToList.Add(dto.HostEmail); @@ -144,23 +172,44 @@ public async Task SendDelistingWarningAsync(DelistingWarningCreateDto dt Cc = dto.CcList.ToArray(), DelayTS = 0, Encoding = "utf-8", - From = "no_reply@gov.bc.ca", + From = NoReply.Default, Priority = "normal", Subject = "Notice of Takedown", To = dto.ToList.ToArray(), Info = dto.ListingUrl }; - return await _emailService.SendEmailAsync(emailContent); + await _emailService.SendEmailAsync(emailContent); } - public string FormatDelistingWarningEmailContent(DelistingWarningCreateDto dto, bool contentOnly) + public async Task<(Dictionary> errors, EmailPreview preview)> GetDelistingWarningPreviewAsync(DelistingWarningCreateDto dto) + { + var platform = await _orgService.GetOrganizationByIdAsync(dto.PlatformId); + var reason = WarningReasonDto.WarningReasons.FirstOrDefault(x => x.WarningReasonId == dto.ReasonId)?.Reason; + + var errors = await ValidateDelistingWarningAsync(dto, platform, reason); + if (errors.Count > 0) + { + return (errors, new EmailPreview()); + } + + var contact = platform.ContactPeople.First(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty()); + dto.ToList.Add(contact.EmailAddressDsc); + + if (dto.HostEmail.IsNotEmpty()) + { + dto.ToList.Add(dto.HostEmail); + } + + return (errors, new EmailPreview { Content = FormatDelistingWarningEmailContent(dto, false).HtmlToPlainText() }); + } + + private string FormatDelistingWarningEmailContent(DelistingWarningCreateDto dto, bool contentOnly) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); var reason = WarningReasonDto.WarningReasons.FirstOrDefault(x => x.WarningReasonId == dto.ReasonId)?.Reason; var nl = Environment.NewLine; - return (contentOnly ? "" : $@"To: {string.Join(";", dto.ToList)} {dto.HostEmail}
cc: {string.Join(";", dto.CcList)}

") + return (contentOnly ? "" : $@"To: {string.Join(";", dto.ToList)}
cc: {string.Join(";", dto.CcList)}

") + $@"Dear Short-term Rental Host,
" + $@"
Short-term rental accommodations in your community must obtain a short-term rental (STR) business licence from the local government in order to operate.

Short-term rental accommodations are also regulated by the Province of B.C. Under the Short-term Rental Accommodations Act, short-term rental hosts in communities with a short-term rental business licence requirement must include a valid business licence number on any short-term rental listings advertised on an online platform. Short-term rental platforms are required to remove listings that do not meet this requirement if requested by the local government.

The short-term rental listing below is not in compliance with an applicable local government business licence requirement for the following reason:" + $@" {reason ?? ""}" @@ -173,52 +222,57 @@ public string FormatDelistingWarningEmailContent(DelistingWarningCreateDto dto, + (dto.Comment.IsEmpty() ? "" : $@"

{dto.Comment}"); } - public async Task SendDelistingRequestAsync(DelistingRequestCreateDto dto, PlatformDto? platform) + public async Task>> CreateDelistingRequestAsync(DelistingRequestCreateDto dto) { - dto.ToList.Add(platform?.Email ?? ""); + var platform = await _orgService.GetOrganizationByIdAsync(dto.PlatformId); + var lg = await _orgService.GetOrganizationByIdAsync(dto.LgId); - if (dto.SendCopy) + var errors = await ValidateDelistingRequestAsync(dto, platform, lg); + if (errors.Count > 0) { - dto.CcList.Add(_currentUser.EmailAddress); + return errors; } - var emailContent = new EmailContent - { - Bcc = Array.Empty(), - BodyType = "html", - Body = FormatDelistingRequestEmailContent(dto, true), - Cc = dto.CcList.ToArray(), - DelayTS = 0, - Encoding = "utf-8", - From = "no_reply@gov.bc.ca", - Priority = "normal", - Subject = "Takedown Request", - To = dto.ToList.ToArray(), - Info = dto.ListingUrl - }; + await SendDelistingRequestAsync(dto, platform); - return await _emailService.SendEmailAsync(emailContent); + return errors; } - public async Task>> ValidateDelistingRequest(DelistingRequestCreateDto dto, PlatformDto? platform, LocalGovernmentDto? lg) + private async Task>> ValidateDelistingRequestAsync(DelistingRequestCreateDto dto, OrganizationDto? platform, OrganizationDto? lg) { await Task.CompletedTask; var errors = new Dictionary>(); RegexInfo regex; - //Todo: Validate Platform ID if (platform == null) { errors.AddItem("platformId", $"Platform ID ({dto.PlatformId}) does not exist."); } + else + { + if (platform.OrganizationType != OrganizationTypes.Platform) + { + errors.AddItem("platformId", $"Organization ({dto.PlatformId}) is not a platform"); + } + + if (platform.ContactPeople == null || !platform.ContactPeople.Any(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty())) + { + errors.AddItem("platformId", $"Platform ({dto.PlatformId}) does not have the primary contact info"); + } + } if (lg == null) { errors.AddItem("lgId", $"Local Government ID ({dto.LgId}) does not exist."); } - - //Todo: Validate LG ID + else + { + if (lg.OrganizationType != OrganizationTypes.LG) + { + errors.AddItem("platformId", $"Organization ({dto.PlatformId}) is not a local government"); + } + } if (dto.ListingUrl.IsEmpty()) { @@ -246,9 +300,54 @@ public async Task>> ValidateDelistingRequest(Del return errors; } - public string FormatDelistingRequestEmailContent(DelistingRequestCreateDto dto, bool contentOnly) + private async Task SendDelistingRequestAsync(DelistingRequestCreateDto dto, OrganizationDto? platform) + { + var contact = platform.ContactPeople.First(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty()); + dto.ToList.Add(contact.EmailAddressDsc); + + if (dto.SendCopy) + { + dto.CcList.Add(_currentUser.EmailAddress); + } + + var emailContent = new EmailContent + { + Bcc = Array.Empty(), + BodyType = "html", + Body = FormatDelistingRequestEmailContent(dto, true), + Cc = dto.CcList.ToArray(), + DelayTS = 0, + Encoding = "utf-8", + From = NoReply.Default, + Priority = "normal", + Subject = "Takedown Request", + To = dto.ToList.ToArray(), + Info = dto.ListingUrl + }; + + await _emailService.SendEmailAsync(emailContent); + } + + public async Task<(Dictionary> errors, EmailPreview preview)> GetDelistingRequestPreviewAsync(DelistingRequestCreateDto dto) + { + var platform = await _orgService.GetOrganizationByIdAsync(dto.PlatformId); + var lg = await _orgService.GetOrganizationByIdAsync(dto.LgId); + + var errors = await ValidateDelistingRequestAsync(dto, platform, lg); + if (errors.Count > 0) + { + return (errors, new EmailPreview()); + } + + var contact = platform.ContactPeople.First(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty()); + dto.ToList.Add(contact.EmailAddressDsc); + + return (errors, new EmailPreview { Content = FormatDelistingRequestEmailContent(dto, false).HtmlToPlainText() }); + + } + + private string FormatDelistingRequestEmailContent(DelistingRequestCreateDto dto, bool contentOnly) { - var platform = PlatformDto.Platforms.FirstOrDefault(x => x.PlatformId == dto.PlatformId); var nl = Environment.NewLine; return (contentOnly ? "" : $@"To: {string.Join(";", dto.ToList)}
cc: {string.Join(";", dto.CcList)}") @@ -259,5 +358,6 @@ public string FormatDelistingRequestEmailContent(DelistingRequestCreateDto dto, + $@"

In accordance, with 17(2) of the Short-term Rental Accommodations Act, please cease providing platform services in respect of the above platform offer within 3 days." + $@"

[Name]
[Title]
[Local government]
[Contact Information]"; } + } } diff --git a/server/StrDss.Service/EmailService.cs b/server/StrDss.Service/EmailMessageService.cs similarity index 59% rename from server/StrDss.Service/EmailService.cs rename to server/StrDss.Service/EmailMessageService.cs index d725f50a..06124e33 100644 --- a/server/StrDss.Service/EmailService.cs +++ b/server/StrDss.Service/EmailMessageService.cs @@ -1,36 +1,41 @@ using AutoMapper; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using StrDss.Data; +using StrDss.Data.Repositories; using StrDss.Model; using StrDss.Service.HttpClients; using System.Text; namespace StrDss.Service { - public interface IEmailService + public interface IEmailMessageService { - Task SendEmailAsync(EmailContent emailContent); + Task SendEmailAsync(EmailContent emailContent); + Task> GetMessageReasons(string messageType); } - public class EmailService : ServiceBase, IEmailService + public class EmailMessageService : ServiceBase, IEmailMessageService { private readonly HttpClient _httpClient; + private readonly IEmailMessageRepository _emailRepo; private readonly IConfiguration _config; private readonly IChesTokenApi _chesTokenApi; - private readonly ILogger _logger; + private readonly ILogger _logger; - public EmailService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, - IConfiguration config, IChesTokenApi chesTokenApi, HttpClient httpClient, ILogger logger) - : base(currentUser, validator, unitOfWork, mapper) + public EmailMessageService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, IHttpContextAccessor httpContextAccessor, + IEmailMessageRepository emailRepo, IConfiguration config, IChesTokenApi chesTokenApi, HttpClient httpClient, ILogger logger) + : base(currentUser, validator, unitOfWork, mapper, httpContextAccessor) { + _emailRepo = emailRepo; _config = config; _httpClient = httpClient; _chesTokenApi = chesTokenApi; _logger = logger; } - public async Task SendEmailAsync(EmailContent emailContent) + public async Task SendEmailAsync(EmailContent emailContent) { var env = _config.GetValue("ENV_NAME") ?? "dev"; @@ -60,16 +65,20 @@ public async Task SendEmailAsync(EmailContent emailContent) { var error = $"Failed to send '{emailContent.Subject}' for {emailContent.Info}. Status code: {response.StatusCode}"; _logger.LogError(error); - return error; + throw new Exception(error); } } catch (Exception ex) { - _logger.LogError($"Exception raised when sending '{emailContent.Subject}' for {emailContent.Info} - {ex}"); - return ex.Message; + var error = $"Exception raised when sending '{emailContent.Subject}' for {emailContent.Info}."; + _logger.LogError($"{error} - {ex}"); + throw new Exception(error); } + } - return ""; + public async Task> GetMessageReasons(string messageType) + { + return await _emailRepo.GetMessageReasons(messageType); } } diff --git a/server/StrDss.Service/EmailTemplates/AccessRequestApproval.cs b/server/StrDss.Service/EmailTemplates/AccessRequestApproval.cs new file mode 100644 index 00000000..31857879 --- /dev/null +++ b/server/StrDss.Service/EmailTemplates/AccessRequestApproval.cs @@ -0,0 +1,20 @@ +namespace StrDss.Service.EmailTemplates +{ + public class AccessRequestApproval : EmailTemplateBase + { + public AccessRequestApproval(IEmailMessageService emailService) : base(emailService) + { + } + + public string Link { get; set; } = ""; + public string AdminEmail { get; set; } = ""; + public override string GetContent() + { + Subject = "STR Data Portal - Access Granted"; + + return +$@"You have been granted access to the Short Term Rental Data Portal. Please access the portal here: {Link}. If you have any issues accessing this link, please contact {AdminEmail}."; + } + + } +} diff --git a/server/StrDss.Service/EmailTemplates/AccessRequestDenial.cs b/server/StrDss.Service/EmailTemplates/AccessRequestDenial.cs new file mode 100644 index 00000000..23de1637 --- /dev/null +++ b/server/StrDss.Service/EmailTemplates/AccessRequestDenial.cs @@ -0,0 +1,19 @@ +namespace StrDss.Service.EmailTemplates +{ + public class AccessRequestDenial : EmailTemplateBase + { + public AccessRequestDenial(IEmailMessageService emailService) : base(emailService) + { + } + + public string AdminEmail { get; set; } = ""; + + public override string GetContent() + { + Subject = "STR Data Portal - Access Denied"; + + return +$@"Access to the STR Data Portal is restricted to authorized provincial and local government staff and short-term rental platforms. Please contact {AdminEmail} for more information."; + } + } +} diff --git a/server/StrDss.Service/EmailTemplates/EmailTemplateBase.cs b/server/StrDss.Service/EmailTemplates/EmailTemplateBase.cs new file mode 100644 index 00000000..219a622d --- /dev/null +++ b/server/StrDss.Service/EmailTemplates/EmailTemplateBase.cs @@ -0,0 +1,42 @@ +using StrDss.Common; +using StrDss.Model; + +namespace StrDss.Service.EmailTemplates +{ + public class EmailTemplateBase + { + public IEmailMessageService _emailService { get; } + public EmailTemplateBase(IEmailMessageService emailService) + { + _emailService = emailService; + } + + public string Subject { get; set; } = ""; + public string From { get; set; } = NoReply.Default; + public IEnumerable To { get; set; } = new List(); + public IEnumerable Cc { get; set; } = new List(); + public IEnumerable Bcc { get; set; } = new List(); + public string Info { get; set; } + + public virtual string GetContent() + { + return ""; + } + + public async Task SendEmail() + { + var emailContent = new EmailContent + { + Body = GetContent(), + From = NoReply.Default, + Subject = Subject, + To = To, + Cc = Cc, + Bcc = Bcc, + Info = Info + }; + + await _emailService.SendEmailAsync(emailContent); + } + } +} diff --git a/server/StrDss.Service/OrganizationService.cs b/server/StrDss.Service/OrganizationService.cs new file mode 100644 index 00000000..252ae71e --- /dev/null +++ b/server/StrDss.Service/OrganizationService.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using StrDss.Data; +using StrDss.Data.Repositories; +using StrDss.Model; +using StrDss.Model.OrganizationDtos; + +namespace StrDss.Service +{ + public interface IOrganizationService + { + Task> GetOrganizationTypesAsnc(); + Task> GetOrganizationsAsync(string? type); + Task> GetOrganizationsDropdownAsync(string? type); + Task GetOrganizationByIdAsync(long id); + } + public class OrganizationService : ServiceBase, IOrganizationService + { + private IOrganizationRepository _orgRepo; + + public OrganizationService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, IHttpContextAccessor httpContextAccessor, + IOrganizationRepository orgRepo) + : base(currentUser, validator, unitOfWork, mapper, httpContextAccessor) + { + _orgRepo = orgRepo; + } + + public async Task> GetOrganizationTypesAsnc() + { + return await _orgRepo.GetOrganizationTypesAsnc(); + } + + public async Task> GetOrganizationsAsync(string? type) + { + return await _orgRepo.GetOrganizationsAsync(type); + } + + public async Task> GetOrganizationsDropdownAsync(string? type) + { + var orgs = await _orgRepo.GetOrganizationsAsync(type); + return orgs.Select(x => new DropdownNumDto { Id = x.OrganizationId, Description = x.OrganizationNm }).ToList(); + } + + public async Task GetOrganizationByIdAsync(long id) + { + return await _orgRepo.GetOrganizationByIdAsync(id); + } + + } +} diff --git a/server/StrDss.Service/ServiceBase.cs b/server/StrDss.Service/ServiceBase.cs index d5adbcc1..8be69bc0 100644 --- a/server/StrDss.Service/ServiceBase.cs +++ b/server/StrDss.Service/ServiceBase.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Microsoft.AspNetCore.Http; using StrDss.Data; using StrDss.Model; @@ -10,13 +11,21 @@ public class ServiceBase protected IFieldValidatorService _validator; protected IUnitOfWork _unitOfWork; protected IMapper _mapper; + protected readonly IHttpContextAccessor _httpContextAccessor; - public ServiceBase(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper) + public ServiceBase(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, IHttpContextAccessor httpContextAccessor) { _currentUser = currentUser; _validator = validator; _unitOfWork = unitOfWork; _mapper = mapper; + _httpContextAccessor = httpContextAccessor; + } + + protected string GetHostUrl() + { + var request = _httpContextAccessor?.HttpContext?.Request; + return $"{request?.Scheme}://{request?.Host}"; } } } diff --git a/server/StrDss.Service/UserService.cs b/server/StrDss.Service/UserService.cs new file mode 100644 index 00000000..6e90bbac --- /dev/null +++ b/server/StrDss.Service/UserService.cs @@ -0,0 +1,258 @@ +using AutoMapper; +using Microsoft.AspNetCore.Http; +using StrDss.Common; +using StrDss.Data; +using StrDss.Data.Repositories; +using StrDss.Model; +using StrDss.Model.UserDtos; +using StrDss.Service.EmailTemplates; + +namespace StrDss.Service +{ + public interface IUserService + { + Task> GetAccessRequestListAsync(string status, int pageSize, int pageNumber, string orderBy, string direction); + Task<(UserDto? user, List permissions)> GetUserByGuidAsync(Guid guid); + Task>> CreateAccessRequestAsync(AccessRequestCreateDto dto); + Task>> DenyAccessRequest(AccessRequestDenyDto dto); + Task>> ApproveAccessRequest(AccessRequestApproveDto dto); + } + public class UserService : ServiceBase, IUserService + { + private IUserRepository _userRepo; + private IOrganizationRepository _orgRepo; + private IEmailMessageService _emailService; + + public UserService(ICurrentUser currentUser, IFieldValidatorService validator, IUnitOfWork unitOfWork, IMapper mapper, IHttpContextAccessor httpContextAccessor, + IUserRepository userRepo, IOrganizationRepository orgRepo, IEmailMessageService emailService) + : base(currentUser, validator, unitOfWork, mapper, httpContextAccessor) + { + _userRepo = userRepo; + _orgRepo = orgRepo; + _emailService = emailService; + } + + public async Task> GetAccessRequestListAsync(string status, int pageSize, int pageNumber, string orderBy, string direction) + { + return await _userRepo.GetAccessRequestListAsync(status, pageSize, pageNumber, orderBy, direction); + } + + public async Task<(UserDto? user, List permissions)> GetUserByGuidAsync(Guid guid) + { + return await _userRepo.GetUserAndPermissionsByGuidAsync(guid); + } + + public async Task>> CreateAccessRequestAsync(AccessRequestCreateDto dto) + { + var (errors, userDto) = await ValidateAccessRequestCreateDtoAsync(dto); + + if (errors.Count > 0) + { + return errors; + } + + if (userDto == null) + { + var userCreateDto = new UserCreateDto + { + UserGuid = _currentUser.UserGuid, + DisplayNm = _currentUser.DisplayName, + IdentityProviderNm = _currentUser.IdentityProviderNm, + IsEnabled = false, + AccessRequestStatusCd = AccessRequestStatuses.Requested, + AccessRequestDtm = DateTime.UtcNow, + AccessRequestJustificationTxt = $"{dto.OrganizationType}, {dto.OrganizationName}", + GivenNm = _currentUser.FirstName, + FamilyNm = _currentUser.LastName, + EmailAddressDsc = _currentUser.EmailAddress, + BusinessNm = _currentUser.BusinessNm, + }; + + await _userRepo.CreateUserAsync(userCreateDto); + } + else + { + userDto.DisplayNm = _currentUser.DisplayName; + userDto.IdentityProviderNm = _currentUser.IdentityProviderNm; + userDto.IsEnabled = false; + userDto.AccessRequestStatusCd = AccessRequestStatuses.Requested; + userDto.AccessRequestDtm = DateTime.UtcNow; + userDto.AccessRequestJustificationTxt = $"{dto.OrganizationType}, {dto.OrganizationName}"; + userDto.GivenNm = _currentUser.FirstName; + userDto.FamilyNm = _currentUser.LastName; + userDto.EmailAddressDsc = _currentUser.EmailAddress; + userDto.BusinessNm = _currentUser.BusinessNm; + userDto.RepresentedByOrganizationId = null; + + await _userRepo.UpdateUserAsync(userDto); + } + + _unitOfWork.Commit(); + + return errors; + } + + private async Task<(Dictionary> errors, UserDto user)> ValidateAccessRequestCreateDtoAsync(AccessRequestCreateDto dto) + { + var errors = new Dictionary>(); + + var userDto = await _userRepo.GetUserByGuid(_currentUser.UserGuid); + if (userDto != null) + { + if (userDto.AccessRequestStatusCd == AccessRequestStatuses.Requested) + { + errors.AddItem("entity", "Your access request is pending"); + return (errors, userDto); + } + + if (userDto.AccessRequestStatusCd == AccessRequestStatuses.Approved && !userDto.IsEnabled) + { + errors.AddItem("entity", "Your access has been disabled"); + return (errors, userDto); + } + + if (userDto.AccessRequestStatusCd == AccessRequestStatuses.Approved) + { + errors.AddItem("entity", "Your access has been already approved"); + return (errors, userDto); + } + } + + var orgTypes = await _orgRepo.GetOrganizationTypesAsnc(); + + if (!orgTypes.Any(x => x.OrganizationType == dto.OrganizationType)) + { + errors.AddItem("organizationType", "Organization type is not valid"); + } + + if (dto.OrganizationName.IsEmpty()) + { + errors.AddItem("organizationName", "Organization name is mandatory"); + } + + return (errors, userDto); + } + + public async Task>> DenyAccessRequest(AccessRequestDenyDto dto) + { + var errors = new Dictionary>(); + + var user = await _userRepo.GetUserById(dto.UserIdentityId); + + if (user == null) + { + errors.AddItem("entity", $"Access request ({dto.UserIdentityId}) doesn't exist"); + return errors; + } + else + { + if (user.AccessRequestStatusCd != AccessRequestStatuses.Requested) + { + errors.AddItem("entity", $"Unable to deny access request. The request is currently in status '{user.AccessRequestStatusCd}', which does not allow denial."); + return errors; + } + } + + if (errors.Count > 0) + { + return errors; + } + + await _userRepo.DenyAccessRequest(dto); + + _unitOfWork.Commit(); + + if (user.EmailAddressDsc.IsEmpty()) + { + errors.AddItem("entity", $"The user doesn't have email address."); + return errors; + } + + var template = new AccessRequestDenial(_emailService) + { + AdminEmail = _currentUser.EmailAddress, + To = new string[] { user.EmailAddressDsc }, + Info = $"Denial email for {user.DisplayNm}" + }; + + await template.SendEmail(); + + return errors; + } + + public async Task>> ApproveAccessRequest(AccessRequestApproveDto dto) + { + var errors = new Dictionary>(); + + var user = await _userRepo.GetUserById(dto.UserIdentityId); + + if (user == null) + { + errors.AddItem("entity", $"Access request ({dto.UserIdentityId}) doesn't exist"); + return errors; + } + else + { + if (user.AccessRequestStatusCd != AccessRequestStatuses.Requested) + { + errors.AddItem("entity", $"Unable to approve access request. The request is currently in status '{user.AccessRequestStatusCd}', which does not allow approval."); + return errors; + } + } + + var org = await _orgRepo.GetOrganizationByIdAsync(dto.RepresentedByOrganizationId); + + if (org == null) + { + errors.AddItem("representedByOrganizationId", $"Organization ({dto.RepresentedByOrganizationId}) doesn't exist"); + return errors; + } + + //only IDIR account can have BCGov org type + if (user.IdentityProviderNm != StrDssIdProviders.Idir && org.OrganizationType == OrganizationTypes.BCGov) + { + errors.AddItem("representedByOrganizationId", $"Not IDIR account cannot be associated with {OrganizationTypes.BCGov} type organization"); + } + + if (errors.Count > 0) + { + return errors; + } + + var role = ""; + switch (org.OrganizationType) + { + case OrganizationTypes.BCGov: + role = Roles.CeuStaff; break; + case OrganizationTypes.LG: + role = Roles.LgStaff; break; + case OrganizationTypes.Platform: + role = Roles.PlatformStaff; break; + default: + throw new Exception($"Unknow organization type {org.OrganizationType}"); + } + + await _userRepo.ApproveAccessRequest(dto, role); + + _unitOfWork.Commit(); + + if (user.EmailAddressDsc.IsEmpty()) + { + errors.AddItem("entity", $"The user doesn't have email address."); + return errors; + } + + var template = new AccessRequestApproval(_emailService) + { + Link = GetHostUrl(), + AdminEmail = _currentUser.EmailAddress, + To = new string[] { user.EmailAddressDsc }, + Info = $"Approval email for {user.DisplayNm}" + }; + + await template.SendEmail(); + + return errors; + } + } +} diff --git a/server/StrDss.Test/AutoDomainDataAttribute.cs b/server/StrDss.Test/AutoDomainDataAttribute.cs index 5ec9e6a1..cb0c4443 100644 --- a/server/StrDss.Test/AutoDomainDataAttribute.cs +++ b/server/StrDss.Test/AutoDomainDataAttribute.cs @@ -14,6 +14,8 @@ public AutoDomainDataAttribute() .Customize(new DelistingWarningCreateDtoCustomization()) .Customize(new DelistingRequestCreateDtoCustomization()) .Customize(new EmailContentCustomization()) + .Customize(new OrganizationDtoCustomization()) + .Customize(new UserDtoCustomization()) ) { } diff --git a/server/StrDss.Test/AutoDomainDataBuilder/DelistingRequestCreateDtoBuilder.cs b/server/StrDss.Test/AutoDomainDataBuilder/DelistingRequestCreateDtoBuilder.cs index a62b8446..b177e7b1 100644 --- a/server/StrDss.Test/AutoDomainDataBuilder/DelistingRequestCreateDtoBuilder.cs +++ b/server/StrDss.Test/AutoDomainDataBuilder/DelistingRequestCreateDtoBuilder.cs @@ -29,7 +29,7 @@ public object Create(object request, ISpecimenContext context) case nameof(DelistingRequestCreateDto.ListingId): return 1; case nameof(DelistingRequestCreateDto.LgId): - return 1; + return 2; case nameof(DelistingRequestCreateDto.ListingUrl): return "https://example.com/listing"; diff --git a/server/StrDss.Test/AutoDomainDataBuilder/OrganizationDtoBuilder.cs b/server/StrDss.Test/AutoDomainDataBuilder/OrganizationDtoBuilder.cs new file mode 100644 index 00000000..6934aa91 --- /dev/null +++ b/server/StrDss.Test/AutoDomainDataBuilder/OrganizationDtoBuilder.cs @@ -0,0 +1,59 @@ +using AutoFixture; +using AutoFixture.Kernel; +using StrDss.Common; +using StrDss.Model.OrganizationDtos; +using System.Reflection; + +namespace StrDss.Test.AutoDomainDataBuilder +{ + public class OrganizationDtoCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new OrganizationDtoBuilder()); + } + } + public class OrganizationDtoBuilder : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) + { + var pi = request as PropertyInfo; + + if (pi == null) + return new NoSpecimen(); + + switch (pi.Name) + { + case nameof(OrganizationDto.OrganizationId): + return 1; + case nameof(OrganizationDto.OrganizationType): + return OrganizationTypes.Platform; + case nameof(OrganizationDto.OrganizationCd): + return "PLATFORMTEST"; + case nameof(OrganizationDto.OrganizationNm): + return "Test Platform"; + case nameof(OrganizationDto.LocalGovernmentGeometry): + return null; + case nameof(OrganizationDto.ManagingOrganizationId): + return null; + case nameof(OrganizationDto.ContactPeople): + return new List + { + new ContactPersonDto + { + OrganizationContactPersonId = 1, + IsPrimary = true, + GivenNm = "John", + FamilyNm = "Doe", + PhoneNo = "(999) 999-9999", + EmailAddressDsc = "foo@foo.com", + ContactedThroughOrganizationId = 0 + } + }; + default: + return new NoSpecimen(); + } + } + } + +} diff --git a/server/StrDss.Test/AutoDomainDataBuilder/UserDtoBuilder.cs b/server/StrDss.Test/AutoDomainDataBuilder/UserDtoBuilder.cs new file mode 100644 index 00000000..8e0244fb --- /dev/null +++ b/server/StrDss.Test/AutoDomainDataBuilder/UserDtoBuilder.cs @@ -0,0 +1,65 @@ +using AutoFixture.Kernel; +using AutoFixture; +using StrDss.Model.UserDtos; +using System.Reflection; + +namespace StrDss.Test.AutoDomainDataBuilder +{ + public class UserDtoCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new UserDtoBuilder()); + } + } + + public class UserDtoBuilder : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) + { + var pi = request as PropertyInfo; + + if (pi == null) + return new NoSpecimen(); + + switch (pi.Name) + { + case nameof(UserDto.UserIdentityId): + return 1; + case nameof(UserDto.UserGuid): + return Guid.NewGuid(); + case nameof(UserDto.DisplayNm): + return "Test User"; + case nameof(UserDto.IdentityProviderNm): + return "idir"; + case nameof(UserDto.IsEnabled): + return true; + case nameof(UserDto.AccessRequestStatusCd): + return "Requested"; + case nameof(UserDto.AccessRequestDtm): + return DateTime.UtcNow; + case nameof(UserDto.AccessRequestJustificationTxt): + return "Test Justification"; + case nameof(UserDto.GivenNm): + return "Test"; + case nameof(UserDto.FamilyNm): + return "User"; + case nameof(UserDto.EmailAddressDsc): + return "test@example.com"; + case nameof(UserDto.BusinessNm): + return "Test Business"; + case nameof(UserDto.TermsAcceptanceDtm): + return DateTime.UtcNow; + case nameof(UserDto.RepresentedByOrganizationId): + return null; + case nameof(UserDto.UpdDtm): + return DateTime.UtcNow; + case nameof(UserDto.UserRoleCds): + return new List { new RoleDto { UserRoleCd = "Role1" }, new RoleDto { UserRoleCd = "Role2" } }; + default: + return new NoSpecimen(); + } + } + } + +} diff --git a/server/StrDss.Test/DelistingServiceShould.cs b/server/StrDss.Test/DelistingServiceShould.cs index bdadf53e..092c56bd 100644 --- a/server/StrDss.Test/DelistingServiceShould.cs +++ b/server/StrDss.Test/DelistingServiceShould.cs @@ -2,10 +2,10 @@ using Castle.Core.Configuration; using Microsoft.Extensions.Logging; using Moq; +using StrDss.Common; using StrDss.Model; using StrDss.Model.DelistingDtos; -using StrDss.Model.LocalGovernmentDtos; -using StrDss.Model.PlatformDtos; +using StrDss.Model.OrganizationDtos; using StrDss.Service; using StrDss.Service.HttpClients; using Xunit; @@ -18,17 +18,19 @@ public class DelistingServiceShould [AutoDomainData] public async Task ValidateDelistingWarning_ValidDto_ReturnsNoErrors( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange configMock.Setup(x => x.GetValue(typeof(string), "")).Returns("https://ches.example.com"); + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); // Act - var result = await sut.ValidateDelistingWarning(dto, platform, "reason"); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Empty(result); @@ -45,10 +47,10 @@ public async Task ValidateDelistingWarning_NullPlatform_ReturnsPlatformIdError( DelistingService sut) { // Arrange - PlatformDto platform = null; + dto.PlatformId = 0; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("platformId", result.Keys); @@ -60,7 +62,7 @@ public async Task ValidateDelistingWarning_NullPlatform_ReturnsPlatformIdError( [AutoDomainData] public async Task ValidateDelistingWarning_EmptyListingUrl_ReturnsListingUrlError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -71,7 +73,7 @@ public async Task ValidateDelistingWarning_EmptyListingUrl_ReturnsListingUrlErro dto.ListingUrl = string.Empty; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("listingUrl", result.Keys); @@ -83,7 +85,7 @@ public async Task ValidateDelistingWarning_EmptyListingUrl_ReturnsListingUrlErro [AutoDomainData] public async Task ValidateDelistingWarning_InvalidListingUrl_ReturnsInvalidUrlError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -94,7 +96,7 @@ public async Task ValidateDelistingWarning_InvalidListingUrl_ReturnsInvalidUrlEr dto.ListingUrl = "invalidurl"; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("listingUrl", result.Keys); @@ -106,7 +108,7 @@ public async Task ValidateDelistingWarning_InvalidListingUrl_ReturnsInvalidUrlEr [AutoDomainData] public async Task ValidateDelistingWarning_HostEmailSentFalseAndEmptyHostEmail_ReturnsHostEmailRequiredError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -118,7 +120,7 @@ public async Task ValidateDelistingWarning_HostEmailSentFalseAndEmptyHostEmail_R dto.HostEmail = string.Empty; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("hostEmail", result.Keys); @@ -130,7 +132,7 @@ public async Task ValidateDelistingWarning_HostEmailSentFalseAndEmptyHostEmail_R [AutoDomainData] public async Task ValidateDelistingWarning_InvalidHostEmail_ReturnsInvalidHostEmailError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -141,7 +143,7 @@ public async Task ValidateDelistingWarning_InvalidHostEmail_ReturnsInvalidHostEm dto.HostEmail = "invalidemail"; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("hostEmail", result.Keys); @@ -153,7 +155,7 @@ public async Task ValidateDelistingWarning_InvalidHostEmail_ReturnsInvalidHostEm [AutoDomainData] public async Task ValidateDelistingWarning_NullReason_ReturnsReasonIdError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -161,10 +163,10 @@ public async Task ValidateDelistingWarning_NullReason_ReturnsReasonIdError( DelistingService sut) { // Arrange - reason = null; + dto.ReasonId = 0; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("reasonId", result.Keys); @@ -176,7 +178,7 @@ public async Task ValidateDelistingWarning_NullReason_ReturnsReasonIdError( [AutoDomainData] public async Task ValidateDelistingWarning_InvalidCcListEmail_ReturnsInvalidCcListEmailError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -187,7 +189,7 @@ public async Task ValidateDelistingWarning_InvalidCcListEmail_ReturnsInvalidCcLi dto.CcList = new List { "invalidemail" }; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("ccList", result.Keys); @@ -199,7 +201,7 @@ public async Task ValidateDelistingWarning_InvalidCcListEmail_ReturnsInvalidCcLi [AutoDomainData] public async Task ValidateDelistingWarning_EmptyLgContactEmail_ReturnsLgContactEmailRequiredError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -210,7 +212,7 @@ public async Task ValidateDelistingWarning_EmptyLgContactEmail_ReturnsLgContactE dto.LgContactEmail = string.Empty; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("lgContactEmail", result.Keys); @@ -222,7 +224,7 @@ public async Task ValidateDelistingWarning_EmptyLgContactEmail_ReturnsLgContactE [AutoDomainData] public async Task ValidateDelistingWarning_InvalidLgContactEmail_ReturnsInvalidLgContactEmailError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -233,7 +235,7 @@ public async Task ValidateDelistingWarning_InvalidLgContactEmail_ReturnsInvalidL dto.LgContactEmail = "invalidemail"; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("lgContactEmail", result.Keys); @@ -245,7 +247,7 @@ public async Task ValidateDelistingWarning_InvalidLgContactEmail_ReturnsInvalidL [AutoDomainData] public async Task ValidateDelistingWarning_InvalidLgContactPhone_ReturnsInvalidLgContactPhoneError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -256,7 +258,7 @@ public async Task ValidateDelistingWarning_InvalidLgContactPhone_ReturnsInvalidL dto.LgContactPhone = "invalidphone"; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("lgContactPhone", result.Keys); @@ -268,7 +270,7 @@ public async Task ValidateDelistingWarning_InvalidLgContactPhone_ReturnsInvalidL [AutoDomainData] public async Task ValidateDelistingWarning_InvalidStrBylawUrl_ReturnsStrBylawUrlRequiredError( DelistingWarningCreateDto dto, - PlatformDto platform, + OrganizationDto platform, string reason, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, @@ -279,7 +281,7 @@ public async Task ValidateDelistingWarning_InvalidStrBylawUrl_ReturnsStrBylawUrl dto.StrBylawUrl = "invalidurl"; // Act - var result = await sut.ValidateDelistingWarning(dto, platform, reason); + var result = await sut.CreateDelistingWarningAsync(dto);; // Assert Assert.Contains("strByLawUrl", result.Keys); @@ -291,17 +293,17 @@ public async Task ValidateDelistingWarning_InvalidStrBylawUrl_ReturnsStrBylawUrl [AutoDomainData] public async Task ValidateDelistingRequest_NullPlatform_ReturnsPlatformIdError( DelistingRequestCreateDto dto, - LocalGovernmentDto lg, + OrganizationDto lg, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, DelistingService sut) { // Arrange - PlatformDto platform = null; + dto.PlatformId = 0; // Act - var result = await sut.ValidateDelistingRequest(dto, platform, lg); + var result = await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains("platformId", result.Keys); @@ -313,17 +315,17 @@ public async Task ValidateDelistingRequest_NullPlatform_ReturnsPlatformIdError( [AutoDomainData] public async Task ValidateDelistingRequest_NullLocalGovernment_ReturnsLocalGovernmentIdError( DelistingRequestCreateDto dto, - PlatformDto platform, + OrganizationDto platform, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, DelistingService sut) { // Arrange - LocalGovernmentDto lg = null; + dto.LgId = 0; // Act - var result = await sut.ValidateDelistingRequest(dto, platform, lg); + var result = await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains("lgId", result.Keys); @@ -335,8 +337,8 @@ public async Task ValidateDelistingRequest_NullLocalGovernment_ReturnsLocalGover [AutoDomainData] public async Task ValidateDelistingRequest_EmptyListingUrl_ReturnsListingUrlError( DelistingRequestCreateDto dto, - PlatformDto platform, - LocalGovernmentDto lg, + OrganizationDto platform, + OrganizationDto lg, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, @@ -346,7 +348,7 @@ public async Task ValidateDelistingRequest_EmptyListingUrl_ReturnsListingUrlErro dto.ListingUrl = string.Empty; // Act - var result = await sut.ValidateDelistingRequest(dto, platform, lg); + var result = await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains("listingUrl", result.Keys); @@ -358,8 +360,8 @@ public async Task ValidateDelistingRequest_EmptyListingUrl_ReturnsListingUrlErro [AutoDomainData] public async Task ValidateDelistingRequest_InvalidListingUrl_ReturnsInvalidUrlError( DelistingRequestCreateDto dto, - PlatformDto platform, - LocalGovernmentDto lg, + OrganizationDto platform, + OrganizationDto lg, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, @@ -369,7 +371,7 @@ public async Task ValidateDelistingRequest_InvalidListingUrl_ReturnsInvalidUrlEr dto.ListingUrl = "invalidurl"; // Act - var result = await sut.ValidateDelistingRequest(dto, platform, lg); + var result = await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains("listingUrl", result.Keys); @@ -381,8 +383,8 @@ public async Task ValidateDelistingRequest_InvalidListingUrl_ReturnsInvalidUrlEr [AutoDomainData] public async Task ValidateDelistingRequest_InvalidCcListEmail_ReturnsInvalidCcListEmailError( DelistingRequestCreateDto dto, - PlatformDto platform, - LocalGovernmentDto lg, + OrganizationDto platform, + OrganizationDto lg, [Frozen] Mock configMock, [Frozen] Mock chesTokenApiMock, [Frozen] Mock> loggerMock, @@ -392,7 +394,7 @@ public async Task ValidateDelistingRequest_InvalidCcListEmail_ReturnsInvalidCcLi dto.CcList = new List { "invalidemail" }; // Act - var result = await sut.ValidateDelistingRequest(dto, platform, lg); + var result = await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains("ccList", result.Keys); @@ -404,40 +406,39 @@ public async Task ValidateDelistingRequest_InvalidCcListEmail_ReturnsInvalidCcLi [AutoDomainData] public async Task SendDelistingWarningAsync_WhenCalled_ShouldSendEmail( DelistingWarningCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange currentUserMock.Setup(m => m.EmailAddress).Returns("currentUser@example.com"); - - emailServiceMock - .Setup(m => m.SendEmailAsync(It.IsAny())) - .ReturnsAsync(""); + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); // Act - var result = await sut.SendDelistingWarningAsync(dto, platform); + await sut.CreateDelistingWarningAsync(dto); // Assert emailServiceMock.Verify(m => m.SendEmailAsync(It.IsAny()), Times.Once); - Assert.Equal("", result); } [Theory] [AutoDomainData] public async Task SendDelistingWarningAsync_WhenHostEmailIsNotEmpty_AddsHostEmailToToList( DelistingWarningCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange dto.HostEmail = "host@example.com"; + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); // Act - var result = await sut.SendDelistingWarningAsync(dto, platform); + await sut.CreateDelistingWarningAsync(dto); // Assert Assert.Contains(dto.HostEmail, dto.ToList); @@ -447,8 +448,8 @@ public async Task SendDelistingWarningAsync_WhenHostEmailIsNotEmpty_AddsHostEmai [AutoDomainData] public async Task SendDelistingWarningAsync_WhenHostEmailIsEmpty_DoesNotAddHostEmailToToList( DelistingWarningCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, DelistingService sut) { @@ -456,7 +457,7 @@ public async Task SendDelistingWarningAsync_WhenHostEmailIsEmpty_DoesNotAddHostE dto.HostEmail = string.Empty; // Act - var result = await sut.SendDelistingWarningAsync(dto, platform); + await sut.CreateDelistingWarningAsync(dto); // Assert Assert.DoesNotContain(dto.HostEmail, dto.ToList); @@ -466,18 +467,20 @@ public async Task SendDelistingWarningAsync_WhenHostEmailIsEmpty_DoesNotAddHostE [AutoDomainData] public async Task SendDelistingWarningAsync_WhenSendCopyIsTrue_AddsCurrentUserEmailToCcList( DelistingWarningCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange dto.SendCopy = true; var currentUserEmail = "user@example.com"; currentUserMock.Setup(m => m.EmailAddress).Returns(currentUserEmail); + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); // Act - var result = await sut.SendDelistingWarningAsync(dto, platform); + await sut.CreateDelistingWarningAsync(dto); // Assert Assert.Contains(currentUserEmail, dto.CcList); @@ -487,8 +490,8 @@ public async Task SendDelistingWarningAsync_WhenSendCopyIsTrue_AddsCurrentUserEm [AutoDomainData] public async Task SendDelistingWarningAsync_WhenSendCopyIsFalse_DoesNotAddCurrentUserEmailToCcList( DelistingWarningCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, DelistingService sut) { @@ -498,7 +501,7 @@ public async Task SendDelistingWarningAsync_WhenSendCopyIsFalse_DoesNotAddCurren currentUserMock.Setup(m => m.EmailAddress).Returns("user@example.com"); // Act - var result = await sut.SendDelistingWarningAsync(dto, platform); + await sut.CreateDelistingWarningAsync(dto); // Assert Assert.DoesNotContain(currentUserEmail, dto.CcList); @@ -508,42 +511,49 @@ public async Task SendDelistingWarningAsync_WhenSendCopyIsFalse_DoesNotAddCurren [AutoDomainData] public async Task SendDelistingRequestAsync_WhenCalled_ShouldSendEmail( DelistingRequestCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange currentUserMock.Setup(m => m.EmailAddress).Returns("currentUser@example.com"); + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); - emailServiceMock - .Setup(m => m.SendEmailAsync(It.IsAny())) - .ReturnsAsync(""); + var lg = CommonUtils.CloneObject(platform); + lg.OrganizationType = OrganizationTypes.LG; + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.LgId)).ReturnsAsync(lg); // Act - var result = await sut.SendDelistingRequestAsync(dto, platform); + await sut.CreateDelistingRequestAsync(dto); // Assert emailServiceMock.Verify(m => m.SendEmailAsync(It.IsAny()), Times.Once); - Assert.Equal("", result); } [Theory] [AutoDomainData] public async Task SendDelistingRequestAsync_WhenSendCopyIsTrue_AddsCurrentUserEmailToCcList( DelistingRequestCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, + [Frozen] Mock orgServiceMock, DelistingService sut) { // Arrange dto.SendCopy = true; var currentUserEmail = "user@example.com"; currentUserMock.Setup(m => m.EmailAddress).Returns(currentUserEmail); + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.PlatformId)).ReturnsAsync(platform); + + var lg = CommonUtils.CloneObject(platform); + lg.OrganizationType = OrganizationTypes.LG; + orgServiceMock.Setup(x => x.GetOrganizationByIdAsync(dto.LgId)).ReturnsAsync(lg); // Act - var result = await sut.SendDelistingRequestAsync(dto, platform); + await sut.CreateDelistingRequestAsync(dto); // Assert Assert.Contains(currentUserEmail, dto.CcList); @@ -553,8 +563,8 @@ public async Task SendDelistingRequestAsync_WhenSendCopyIsTrue_AddsCurrentUserEm [AutoDomainData] public async Task SendDelistingRequestAsync_WhenSendCopyIsFalse_DoesNotAddCurrentUserEmailToCcList( DelistingRequestCreateDto dto, - PlatformDto platform, - [Frozen] Mock emailServiceMock, + OrganizationDto platform, + [Frozen] Mock emailServiceMock, [Frozen] Mock currentUserMock, DelistingService sut) { @@ -564,7 +574,7 @@ public async Task SendDelistingRequestAsync_WhenSendCopyIsFalse_DoesNotAddCurren currentUserMock.Setup(m => m.EmailAddress).Returns("user@example.com"); // Act - var result = await sut.SendDelistingRequestAsync(dto, platform); + await sut.CreateDelistingRequestAsync(dto); // Assert Assert.DoesNotContain(currentUserEmail, dto.CcList); diff --git a/server/StrDss.Test/UserServiceShould.cs b/server/StrDss.Test/UserServiceShould.cs new file mode 100644 index 00000000..0106d79b --- /dev/null +++ b/server/StrDss.Test/UserServiceShould.cs @@ -0,0 +1,345 @@ +using Moq; +using StrDss.Data.Repositories; +using StrDss.Data; +using StrDss.Model.UserDtos; +using StrDss.Model; +using AutoFixture.Xunit2; +using StrDss.Service; +using Xunit; +using StrDss.Model.OrganizationDtos; +using StrDss.Common; + +namespace StrDss.Test +{ + public class UserServiceShould + { + [Theory] + [AutoDomainData] + public async Task CreateAccessRequestAsync_ValidDto_NoErrors( + AccessRequestCreateDto dto, + UserDto userDto, + OrganizationDto organizationDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock currentUserMock, + [Frozen] Mock unitOfWorkMock, + [Frozen] Mock organizationRepoMock, + [Frozen] Mock emailServiceMock, + UserService sut) + { + // Arrange + SetupCurrentUser(currentUserMock); + SetupUserRepository(userRepoMock, (UserDto)null); + SetupUnitOfWork(unitOfWorkMock); + SetupOrganizationRepository(organizationRepoMock, organizationDto); + + // Act + var result = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Empty(result); + unitOfWorkMock.Verify(x => x.Commit(), Times.Once); + } + + [Theory] + [AutoDomainData] + public async Task ValidateAccessRequestCreateDtoAsync_AccessRequestPending_ReturnsError( + AccessRequestCreateDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + userDto.AccessRequestStatusCd = AccessRequestStatuses.Requested; + userRepoMock.Setup(x => x.GetUserByGuid(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal("Your access request is pending", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ValidateAccessRequestCreateDtoAsync_AccessDisabled_ReturnsError( + AccessRequestCreateDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + userDto.AccessRequestStatusCd = AccessRequestStatuses.Approved; + userDto.IsEnabled = false; + userRepoMock.Setup(x => x.GetUserByGuid(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal("Your access has been disabled", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ValidateAccessRequestCreateDtoAsync_AccessAlreadyApproved_ReturnsError( + AccessRequestCreateDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + userDto.AccessRequestStatusCd = AccessRequestStatuses.Approved; + userRepoMock.Setup(x => x.GetUserByGuid(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal("Your access has been already approved", errors["entity"].First()); + } + + + [Theory] + [AutoDomainData] + public async Task ValidateAccessRequestCreateDtoAsync_InvalidOrganizationType_ReturnsError( + AccessRequestCreateDto dto, + UserDto userDto, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + orgRepoMock.Setup(x => x.GetOrganizationTypesAsnc()).ReturnsAsync(new List()); + + // Act + var errors = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("organizationType")); + Assert.Equal("Organization type is not valid", errors["organizationType"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ValidateAccessRequestCreateDtoAsync_EmptyOrganizationName_ReturnsError( + AccessRequestCreateDto dto, + UserDto userDto, + OrganizationDto organizationDto, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + SetupOrganizationRepository(orgRepoMock, organizationDto); + dto.OrganizationName = ""; + + // Act + var errors = await sut.CreateAccessRequestAsync(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("organizationName")); + Assert.Equal("Organization name is mandatory", errors["organizationName"].First()); + } + + [Theory] + [AutoDomainData] + public async Task DenyAccessRequest_UserNotFound_ReturnsError( + AccessRequestDenyDto dto, + [Frozen] Mock userRepoMock, + UserService sut) + { + // Arrange + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync((UserDto)null); + + // Act + var errors = await sut.DenyAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal($"Access request ({dto.UserIdentityId}) doesn't exist", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task DenyAccessRequest_RequestNotPending_ReturnsError( + AccessRequestDenyDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + UserService sut) + { + // Arrange + userDto.AccessRequestStatusCd = AccessRequestStatuses.Approved; + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.DenyAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal($"Unable to deny access request. The request is currently in status '{userDto.AccessRequestStatusCd}', which does not allow denial.", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task DenyAccessRequest_UserEmailEmpty_ReturnsError( + AccessRequestDenyDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + UserService sut) + { + // Arrange + userDto.EmailAddressDsc = ""; + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.DenyAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal("The user doesn't have email address.", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ApproveAccessRequest_UserNotFound_ReturnsError( + AccessRequestApproveDto dto, + [Frozen] Mock userRepoMock, + UserService sut) + { + // Arrange + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync((UserDto)null); + + // Act + var errors = await sut.ApproveAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal($"Access request ({dto.UserIdentityId}) doesn't exist", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ApproveAccessRequest_RequestNotPending_ReturnsError( + AccessRequestApproveDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + UserService sut) + { + // Arrange + userDto.AccessRequestStatusCd = AccessRequestStatuses.Approved; + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync(userDto); + + // Act + var errors = await sut.ApproveAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("entity")); + Assert.Equal($"Unable to approve access request. The request is currently in status '{userDto.AccessRequestStatusCd}', which does not allow approval.", errors["entity"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ApproveAccessRequest_OrganizationNotFound_ReturnsError( + AccessRequestApproveDto dto, + UserDto userDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync(userDto); + orgRepoMock.Setup(x => x.GetOrganizationByIdAsync(It.IsAny())).ReturnsAsync((OrganizationDto)null); + + // Act + var errors = await sut.ApproveAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("representedByOrganizationId")); + Assert.Equal($"Organization ({dto.RepresentedByOrganizationId}) doesn't exist", errors["representedByOrganizationId"].First()); + } + + [Theory] + [AutoDomainData] + public async Task ApproveAccessRequest_NonIdirWithBcGovOrgType_ReturnsError( + AccessRequestApproveDto dto, + UserDto userDto, + OrganizationDto orgDto, + [Frozen] Mock userRepoMock, + [Frozen] Mock orgRepoMock, + UserService sut) + { + // Arrange + userDto.IdentityProviderNm = "NonIdirProvider"; + userRepoMock.Setup(x => x.GetUserById(It.IsAny())).ReturnsAsync(userDto); + orgDto.OrganizationType = OrganizationTypes.BCGov; + orgRepoMock.Setup(x => x.GetOrganizationByIdAsync(It.IsAny())).ReturnsAsync(orgDto); + + // Act + var errors = await sut.ApproveAccessRequest(dto); + + // Assert + Assert.Single(errors); + Assert.True(errors.ContainsKey("representedByOrganizationId")); + Assert.Equal($"Not IDIR account cannot be associated with {OrganizationTypes.BCGov} type organization", errors["representedByOrganizationId"].First()); + } + + + public void SetupCurrentUser(Mock currentUserMock) + { + currentUserMock.Setup(x => x.UserGuid).Returns(Guid.NewGuid()); + currentUserMock.Setup(x => x.DisplayName).Returns("Test User"); + currentUserMock.Setup(x => x.IdentityProviderNm).Returns("Test Provider"); + currentUserMock.Setup(x => x.FirstName).Returns("Test"); + currentUserMock.Setup(x => x.LastName).Returns("User"); + currentUserMock.Setup(x => x.EmailAddress).Returns("test@example.com"); + currentUserMock.Setup(x => x.BusinessNm).Returns("Test Business"); + } + + public void SetupUserRepository(Mock userRepoMock, UserDto currentUserDto) + { + userRepoMock.Setup(x => x.GetUserByGuid(It.IsAny())).ReturnsAsync(currentUserDto); + userRepoMock.Setup(x => x.CreateUserAsync(It.IsAny())).Returns(Task.CompletedTask); + } + + public void SetupUnitOfWork(Mock unitOfWorkMock) + { + unitOfWorkMock.Setup(x => x.Commit()).Verifiable(); + } + + public void SetupOrganizationRepository(Mock organizationRepoMock, OrganizationDto organizationDto) + { + organizationRepoMock.Setup(x => x.GetOrganizationTypesAsnc()).ReturnsAsync(new List + { + new OrganizationTypeDto { OrganizationType = "BCGov", OrganizationTypeNm = "BC Government Component" }, + new OrganizationTypeDto { OrganizationType = "Platform", OrganizationTypeNm = "Short Term Rental Platform" }, + new OrganizationTypeDto { OrganizationType = "LG", OrganizationTypeNm = "Local Government" }, + }); + + organizationRepoMock.Setup(x => x.GetOrganizationsAsync(It.IsAny())).ReturnsAsync(new List + { + organizationDto, + }); + + organizationRepoMock.Setup(x => x.GetOrganizationByIdAsync(It.IsAny())).ReturnsAsync((long id) => + { + return organizationDto; + }); + } + + } +}