diff --git a/src/i18n/Keys/Promenade.cs b/src/i18n/Keys/Promenade.cs index 4b363108f..cddf5ba32 100644 --- a/src/i18n/Keys/Promenade.cs +++ b/src/i18n/Keys/Promenade.cs @@ -18,6 +18,13 @@ public static class Promenade public const string ContactInformation = "Contact Information"; public const string ContactUs = "Contact Us"; public const string CoordinatesErrorItem = "There was a problem locating these coordinates: {0}, {1}"; + public const string EmployeeCardAddressInfo = "please do not enter your work address here"; + public const string EmployeeCardAddressLabel = "Home Address"; + public const string EmployeeCardEmployeeLabel = "Employee Information"; + public const string EmployeeCardPersonalLabel = "Personal Information"; + public const string EmployeeCardRenewInfo = "Please select yes if you already have a library card!"; + public const string EmployeeCardRenewLabel = "Renew an existing Account?"; + public const string EmployeeCardThankYou = "Thank you for submitting your request!"; public const string Error = "Error"; public const string ErrorAvailableDaysItem = "This service is only available on the following days: {0}"; public const string ErrorCouldNotGeocode = "An error occurred and we could not find libraries near that location."; @@ -40,6 +47,8 @@ public static class Promenade public const string HelpItem = "{0} help"; public const string HowCanWeHelp = "How can we help?"; public const string KeywordsItem = "Keywords: {0}"; + public const string LabelNo = "No"; + public const string LabelYes = "Yes"; public const string LinkAdultVolunteerForm = "Use our adult volunteer form."; public const string LinkTeenVolunteerForm = "Use our teen volunteer form."; public const string LocationBackToLocationName = "Back to {0}"; @@ -83,21 +92,30 @@ public static class Promenade public const string ProductInventoryStatus = "Status"; public const string PromptAreYouAdultVolunteer = "Are you an adult looking to volunteer?"; public const string PromptAreYouTeenVolunteer = "Are you a teen looking to volunteer?"; + public const string PromptBirthDate = "Birth Date"; + public const string PromptCity = "City"; + public const string PromptCountyDepartment = "County Department"; public const string PromptEmail = "Email"; + public const string PromptEmployeeNumber = "Employee Number"; public const string PromptExperience = "Why do you want to volunteer? Describe any previous volunteer experience."; + public const string PromptFirstName = "First Name"; public const string PromptGuardianEmail = "Parent/Guardian Email"; public const string PromptGuardianName = "Parent/Guardian Name"; public const string PromptGuardianPhone = "Parent/Guardian Phone"; public const string PromptLanguage = "Language"; + public const string PromptLastName = "Last Name"; + public const string PromptLibraryCardNumber = "Library Card#"; public const string PromptName = "Name"; public const string PromptNotes = "Notes"; public const string PromptPhone = "Phone"; public const string PromptRequestedDate = "Requested date"; public const string PromptRequestedDateAndTime = "Requested date and time"; public const string PromptRequestedTime = "Requested time"; + public const string PromptStreetAddress = "Street Address"; public const string PromptSubject = "Subject"; public const string PromptVolunteerRegularity = "Are you interested in regular volunteer work or certain number of hours?"; public const string PromptWeeklyAvailability = "Weekly Availability"; + public const string PromptZipCode = "Zip Code"; public const string RequiredField = "You must supply a value for: {0}"; public const string RequiredFieldItem = "The {0} field is required."; public const string ScheduleAppointmentDetails = "Here are the details of your appointment:"; From 3aa2a558130412b187e7579c950d132d2576bbcd Mon Sep 17 00:00:00 2001 From: Dan Wilcox Date: Mon, 29 Dec 2025 12:57:13 -0700 Subject: [PATCH 02/13] Add initial card renewal code --- PolarisHelper/PolarisClient.cs | 9 - PolarisHelper/PolarisHelper.csproj | 13 - ocuda.sln | 18 +- .../CardRenewalController.cs | 252 +++++++++++ .../Areas/ContentManagement/HomeController.cs | 2 + .../CardRenewal/ResponseViewModel.cs | 15 + .../CardRenewal/ResponsesViewModel.cs | 11 + .../ViewModels/Home/IndexViewModel.cs | 1 + .../Areas/Services/CardRenewalController.cs | 283 ++++++++++++ .../Areas/Services/EmployeeCardController.cs | 4 +- .../CardRenewal/DetailsViewModel.cs | 44 ++ .../ViewModels/CardRenewal/IndexViewModel.cs | 14 + src/Ops.Controllers/Ops.Controllers.csproj | 1 + .../Ops/CardRenewalResponseRepository.cs | 71 +++ .../Ops/CardRenewalResultRepository.cs | 27 ++ src/Ops.Data/Ops/EmailSetupRepository.cs | 27 ++ src/Ops.Data/OpsContext.cs | 2 + .../Promenade/CardRenewalRequestRepository.cs | 75 ++++ .../EmployeeCardRequestRepository.cs | 2 +- src/Ops.Data/Promenade/LanguageRepository.cs | 9 + src/Ops.Data/PromenadeContext.cs | 1 + src/Ops.Models/Defaults/SiteSettings.cs | 50 +++ .../ApplicationPermissionDefinitions.cs | 6 + .../Entities/CardRenewalResponse.cs | 31 ++ src/Ops.Models/Entities/CardRenewalResult.cs | 29 ++ src/Ops.Models/Entities/EmailSetupText.cs | 4 +- src/Ops.Models/Keys/ApplicationPermission.cs | 1 + src/Ops.Models/Keys/SiteSetting.cs | 9 + src/Ops.Service/CardRenewalRequestService.cs | 46 ++ src/Ops.Service/CardRenewalService.cs | 312 +++++++++++++ src/Ops.Service/EmailService.cs | 29 +- src/Ops.Service/EmployeeCardService.cs | 2 +- ...EmployeeCardFilter.cs => RequestFilter.cs} | 4 +- .../ICardRenewalResponseRepository.cs | 15 + .../ICardRenewalResultRepository.cs | 10 + .../Ops/Repositories/IEmailSetupRepository.cs | 11 + .../Ops/Services/ICardRenewalService.cs | 25 ++ .../Interfaces/Ops/Services/IEmailService.cs | 5 +- .../ICardRenewalRequestRepository.cs | 14 + .../IEmployeeCardRequestRepository.cs | 2 +- .../Repositories/ILanguageRepository.cs | 2 + .../Services/ICardRenewalRequestService.cs | 14 + .../Services/IEmployeeCardService.cs | 2 +- .../Promenade/Services/ILanguageService.cs | 2 + src/Ops.Service/Keys/CardRenewal.cs | 14 + src/Ops.Service/LanguageService.cs | 5 + .../Models/CardRenewal/ProcessResult.cs | 14 + src/Ops.Service/Ops.Service.csproj | 1 + .../Views/CardRenewal/Response.cshtml | 128 ++++++ .../Views/CardRenewal/Responses.cshtml | 170 +++++++ .../ContentManagement/Views/Home/Index.cshtml | 16 + .../Services/Views/CardRenewal/Details.cshtml | 415 ++++++++++++++++++ .../Services/Views/CardRenewal/Index.cshtml | 96 ++++ src/Ops.Web/Startup.cs | 15 + src/Ops.Web/Views/Shared/_Layout.cshtml | 7 + src/PolarisHelper/IPolarisHelper.cs | 15 + .../Models/RenewRegistrationResult.cs | 8 + src/PolarisHelper/PolarisHelper.cs | 136 ++++++ src/PolarisHelper/PolarisHelper.csproj | 25 ++ .../CardRenewalController.cs | 354 +++++++++++++++ .../Promenade.Controllers.csproj | 1 + .../ViewModels/CardRenewal/IndexViewModel.cs | 24 + .../CardRenewal/JuvenileViewModel.cs | 18 + .../CardRenewal/SubmittedViewModel.cs | 10 + .../CardRenewal/VerifyAddressViewModel.cs | 22 + .../Promenade/CardRenewalRequestRepository.cs | 34 ++ src/Promenade.Data/PromenadeContext.cs | 1 + src/Promenade.Models/Defaults/SiteSetting.cs | 69 +++ .../Entities/CardRenewalRequest.cs | 31 ++ src/Promenade.Models/Keys/SiteSetting.cs | 8 + src/Promenade.Service/CardRenewalService.cs | 38 ++ .../ICardRenewalRequestRepository.cs | 11 + src/Promenade.Web/Startup.cs | 8 + .../Views/CardRenewal/Index.cshtml | 65 +++ .../Views/CardRenewal/Juvenile.cshtml | 21 + .../Views/CardRenewal/Submitted.cshtml | 14 + .../Views/CardRenewal/VerifyAddress.cshtml | 86 ++++ src/Utility/Keys/Cache.cs | 5 + src/i18n/Keys/Promenade.cs | 16 +- 79 files changed, 3366 insertions(+), 41 deletions(-) delete mode 100644 PolarisHelper/PolarisClient.cs delete mode 100644 PolarisHelper/PolarisHelper.csproj create mode 100644 src/Ops.Controllers/Areas/ContentManagement/CardRenewalController.cs create mode 100644 src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponseViewModel.cs create mode 100644 src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponsesViewModel.cs create mode 100644 src/Ops.Controllers/Areas/Services/CardRenewalController.cs create mode 100644 src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs create mode 100644 src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs create mode 100644 src/Ops.Data/Ops/CardRenewalResponseRepository.cs create mode 100644 src/Ops.Data/Ops/CardRenewalResultRepository.cs create mode 100644 src/Ops.Data/Ops/EmailSetupRepository.cs create mode 100644 src/Ops.Data/Promenade/CardRenewalRequestRepository.cs create mode 100644 src/Ops.Models/Entities/CardRenewalResponse.cs create mode 100644 src/Ops.Models/Entities/CardRenewalResult.cs create mode 100644 src/Ops.Service/CardRenewalRequestService.cs create mode 100644 src/Ops.Service/CardRenewalService.cs rename src/Ops.Service/Filters/{EmployeeCardFilter.cs => RequestFilter.cs} (53%) create mode 100644 src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResponseRepository.cs create mode 100644 src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs create mode 100644 src/Ops.Service/Interfaces/Ops/Repositories/IEmailSetupRepository.cs create mode 100644 src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs create mode 100644 src/Ops.Service/Interfaces/Promenade/Repositories/ICardRenewalRequestRepository.cs create mode 100644 src/Ops.Service/Interfaces/Promenade/Services/ICardRenewalRequestService.cs create mode 100644 src/Ops.Service/Keys/CardRenewal.cs create mode 100644 src/Ops.Service/Models/CardRenewal/ProcessResult.cs create mode 100644 src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Response.cshtml create mode 100644 src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Responses.cshtml create mode 100644 src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml create mode 100644 src/Ops.Web/Areas/Services/Views/CardRenewal/Index.cshtml create mode 100644 src/PolarisHelper/IPolarisHelper.cs create mode 100644 src/PolarisHelper/Models/RenewRegistrationResult.cs create mode 100644 src/PolarisHelper/PolarisHelper.cs create mode 100644 src/PolarisHelper/PolarisHelper.csproj create mode 100644 src/Promenade.Controllers/CardRenewalController.cs create mode 100644 src/Promenade.Controllers/ViewModels/CardRenewal/IndexViewModel.cs create mode 100644 src/Promenade.Controllers/ViewModels/CardRenewal/JuvenileViewModel.cs create mode 100644 src/Promenade.Controllers/ViewModels/CardRenewal/SubmittedViewModel.cs create mode 100644 src/Promenade.Controllers/ViewModels/CardRenewal/VerifyAddressViewModel.cs create mode 100644 src/Promenade.Data/Promenade/CardRenewalRequestRepository.cs create mode 100644 src/Promenade.Models/Entities/CardRenewalRequest.cs create mode 100644 src/Promenade.Service/CardRenewalService.cs create mode 100644 src/Promenade.Service/Interfaces/Repositories/ICardRenewalRequestRepository.cs create mode 100644 src/Promenade.Web/Views/CardRenewal/Index.cshtml create mode 100644 src/Promenade.Web/Views/CardRenewal/Juvenile.cshtml create mode 100644 src/Promenade.Web/Views/CardRenewal/Submitted.cshtml create mode 100644 src/Promenade.Web/Views/CardRenewal/VerifyAddress.cshtml diff --git a/PolarisHelper/PolarisClient.cs b/PolarisHelper/PolarisClient.cs deleted file mode 100644 index f7ecf4f33..000000000 --- a/PolarisHelper/PolarisClient.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Clc.Polaris.Api; -using Clc.Polaris.Api.Models; - -namespace PolarisHelper -{ - public static class PolarisClient - { - } -} diff --git a/PolarisHelper/PolarisHelper.csproj b/PolarisHelper/PolarisHelper.csproj deleted file mode 100644 index 704cb2f6e..000000000 --- a/PolarisHelper/PolarisHelper.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/ocuda.sln b/ocuda.sln index 950033228..39d1d0abe 100644 --- a/ocuda.sln +++ b/ocuda.sln @@ -89,7 +89,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "i18n.Test", "test\i18n\i18n EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlideUploader", "src\SlideUploader\SlideUploader.csproj", "{0DF031C8-95B2-48D1-ACD9-01E56B10F3D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolarisHelper", "PolarisHelper\PolarisHelper.csproj", "{2A8949EC-DC44-40C5-B07A-9FF935238A30}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolarisHelper", "src\PolarisHelper\PolarisHelper.csproj", "{E11E3598-BE6D-C9D8-D480-4EE48E7F4120}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clc.Polaris.Api", "..\polaris-api-csharp\src\polaris-api-csharp\Clc.Polaris.Api.csproj", "{85306281-EC61-C4E5-8FC4-3CF8E64C83D5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -173,10 +175,14 @@ Global {0DF031C8-95B2-48D1-ACD9-01E56B10F3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {0DF031C8-95B2-48D1-ACD9-01E56B10F3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {0DF031C8-95B2-48D1-ACD9-01E56B10F3D6}.Release|Any CPU.Build.0 = Release|Any CPU - {2A8949EC-DC44-40C5-B07A-9FF935238A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A8949EC-DC44-40C5-B07A-9FF935238A30}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A8949EC-DC44-40C5-B07A-9FF935238A30}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A8949EC-DC44-40C5-B07A-9FF935238A30}.Release|Any CPU.Build.0 = Release|Any CPU + {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Release|Any CPU.Build.0 = Release|Any CPU + {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -204,7 +210,7 @@ Global {1C0384E0-C8AD-46D9-9319-C75D67E6474E} = {EF2E402F-3D9C-4EAD-93A2-68D759A003A7} {28C9B95E-DD9B-458B-86EC-37B265D66181} = {1C0384E0-C8AD-46D9-9319-C75D67E6474E} {0DF031C8-95B2-48D1-ACD9-01E56B10F3D6} = {FC70E8FE-76E9-4D23-8D85-F219DD8BAC37} - {2A8949EC-DC44-40C5-B07A-9FF935238A30} = {FC70E8FE-76E9-4D23-8D85-F219DD8BAC37} + {E11E3598-BE6D-C9D8-D480-4EE48E7F4120} = {FC70E8FE-76E9-4D23-8D85-F219DD8BAC37} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E2592F73-781B-49DC-8020-540473F814F7} diff --git a/src/Ops.Controllers/Areas/ContentManagement/CardRenewalController.cs b/src/Ops.Controllers/Areas/ContentManagement/CardRenewalController.cs new file mode 100644 index 000000000..5445be938 --- /dev/null +++ b/src/Ops.Controllers/Areas/ContentManagement/CardRenewalController.cs @@ -0,0 +1,252 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Controllers.Abstract; +using Ocuda.Ops.Controllers.Areas.ContentManagement.ViewModels.CardRenewal; +using Ocuda.Ops.Controllers.Filters; +using Ocuda.Ops.Models; +using Ocuda.Ops.Models.Keys; +using Ocuda.Ops.Service.Interfaces.Ops.Services; +using Ocuda.Ops.Service.Interfaces.Promenade.Services; +using Ocuda.Utility.Exceptions; +using Ocuda.Utility.Keys; + +namespace Ocuda.Ops.Controllers.Areas.ContentManagement +{ + [Area("ContentManagement")] + [Route("[area]/[controller]")] + public class CardRenewalController : BaseController + { + private readonly ICardRenewalRequestService _cardRenewalRequestService; + private readonly ICardRenewalService _cardRenewalService; + private readonly IEmailService _emailService; + private readonly ILanguageService _languageService; + private readonly IPermissionGroupService _permissionGroupService; + + public CardRenewalController(ServiceFacades.Controller context, + ICardRenewalRequestService cardRenewalRequestService, + ICardRenewalService cardRenewalService, + IEmailService emailService, + ILanguageService languageService, + IPermissionGroupService permissionGroupService) + : base(context) + { + ArgumentNullException.ThrowIfNull(cardRenewalRequestService); + ArgumentNullException.ThrowIfNull(cardRenewalService); + ArgumentNullException.ThrowIfNull(emailService); + ArgumentNullException.ThrowIfNull(languageService); + ArgumentNullException.ThrowIfNull(permissionGroupService); + + _cardRenewalRequestService = cardRenewalRequestService; + _cardRenewalService = cardRenewalService; + _emailService = emailService; + _languageService = languageService; + _permissionGroupService = permissionGroupService; + } + + public static string Area + { get { return "ContentManagement"; } } + + public static string Name + { get { return "CardRenewal"; } } + + [HttpPost("[action]")] + public async Task ChangeResponseSort(int id, bool increase) + { + JsonResponse response; + + if (await HasCardRenewalManagementRightsAsync()) + { + try + { + await _cardRenewalService.UpdateResponseSortOrderAsync(id, increase); + response = new JsonResponse + { + Success = true + }; + } + catch (OcudaException ex) + { + response = new JsonResponse + { + Message = ex.Message, + Success = false + }; + } + } + else + { + response = new JsonResponse + { + Message = "Unauthorized", + Success = false + }; + } + + return Json(response); + } + + [HttpPost("[action]")] + public async Task CreateResponse(ResponsesViewModel viewModel) + { + if (!await HasCardRenewalManagementRightsAsync()) + { + return RedirectToUnauthorized(); + } + + ArgumentNullException.ThrowIfNull(viewModel); + + var response = await _cardRenewalService.CreateResponseAsync(viewModel.Response); + + return RedirectToAction(nameof(Response), new { id = response.Id }); + } + + [HttpPost("[action]")] + public async Task DeleteResponse(ResponsesViewModel viewModel) + { + if (!await HasCardRenewalManagementRightsAsync()) + { + return RedirectToUnauthorized(); + } + + ArgumentNullException.ThrowIfNull(viewModel); + + await _cardRenewalService.DeleteResponseAsync(viewModel.Response.Id); + + ShowAlertSuccess($"Deleted response: {viewModel.Response.Name}"); + + return RedirectToAction(nameof(Responses)); + } + + [HttpGet("[action]")] + public async Task GetEmailSetupText(int emailSetupId, string languageName) + { + JsonResponse response; + + if (await HasCardRenewalManagementRightsAsync()) + { + try + { + var emailSetupText = await _emailService.GetSetupTextByLanguageAsync( + emailSetupId, languageName); + + return Json(new + { + success = true, + emailSetupText?.Subject, + emailSetupText?.Preview, + emailSetupText?.BodyText + }); + } + catch (OcudaException ex) + { + response = new JsonResponse + { + Message = ex.Message, + Success = false + }; + } + } + else + { + response = new JsonResponse + { + Message = "Unauthorized", + Success = false + }; + } + + return Json(response); + } + + [HttpGet("[action]/{id}")] + [RestoreModelState] + public async Task Response(int id) + { + if (!await HasCardRenewalManagementRightsAsync()) + { + return RedirectToUnauthorized(); + } + + var response = await _cardRenewalService.GetResponseAsync(id); + if (response == null) + { + ShowAlertDanger($"Could not find Response with ID: {id}"); + return RedirectToAction(nameof(Responses)); + } + + var emailSetups = await _emailService.GetEmailSetupsAsync(); + + var viewModel = new ResponseViewModel + { + Response = response, + EmailSetups = new SelectList(emailSetups, "Key", "Value"), + Languages = await _languageService.GetActiveAsync() + }; + + if (response.EmailSetupId.HasValue && viewModel.Languages.Count > 0) + { + viewModel.EmailSetupText = await _emailService.GetSetupTextByLanguageAsync( + response.EmailSetupId.Value, + viewModel.Languages.FirstOrDefault()?.Name); + } + + return View(viewModel); + } + + [HttpPost("[action]/{id}")] + [SaveModelState] + public async Task Response(ResponseViewModel viewModel) + { + if (!await HasCardRenewalManagementRightsAsync()) + { + return RedirectToUnauthorized(); + } + + ArgumentNullException.ThrowIfNull(viewModel); + + if (ModelState.IsValid) + { + try + { + await _cardRenewalService.UpdateResponseAsync(viewModel.Response); + ShowAlertSuccess("Updated response"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating card renewal response: {Message}", + ex.Message); + ShowAlertDanger("Error updating response"); + } + } + + return RedirectToAction(nameof(Response), new { id = viewModel.Response.Id }); + } + + [HttpGet("[action]")] + public async Task Responses() + { + if (!await HasCardRenewalManagementRightsAsync()) + { + return RedirectToUnauthorized(); + } + + var viewModel = new ResponsesViewModel + { + Responses = await _cardRenewalService.GetResponsesAsync() + }; + + return View(viewModel); + } + + private async Task HasCardRenewalManagementRightsAsync() + { + return !string.IsNullOrEmpty(UserClaim(ClaimType.SiteManager)) + || await HasAppPermissionAsync(_permissionGroupService, + ApplicationPermission.CardRenewalManagement); + } + } +} diff --git a/src/Ops.Controllers/Areas/ContentManagement/HomeController.cs b/src/Ops.Controllers/Areas/ContentManagement/HomeController.cs index 6eb16d0d1..9e9a8e229 100644 --- a/src/Ops.Controllers/Areas/ContentManagement/HomeController.cs +++ b/src/Ops.Controllers/Areas/ContentManagement/HomeController.cs @@ -35,6 +35,8 @@ public async Task Index() var viewModel = new IndexViewModel { IsSiteManager = !string.IsNullOrEmpty(UserClaim(ClaimType.SiteManager)), + HasCardRenewalPermissions = await HasAppPermissionAsync(_permissionGroupService, + ApplicationPermission.CardRenewalManagement), HasRosterPermissions = await HasAppPermissionAsync(_permissionGroupService, ApplicationPermission.RosterManagement), HasUserSyncPermissions = await HasAppPermissionAsync(_permissionGroupService, diff --git a/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponseViewModel.cs b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponseViewModel.cs new file mode 100644 index 000000000..48e589595 --- /dev/null +++ b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponseViewModel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; +using Ocuda.Ops.Models.Entities; +using Ocuda.Promenade.Models.Entities; + +namespace Ocuda.Ops.Controllers.Areas.ContentManagement.ViewModels.CardRenewal +{ + public class ResponseViewModel + { + public SelectList EmailSetups { get; set; } + public EmailSetupText EmailSetupText { get; set; } + public ICollection Languages { get; set; } + public CardRenewalResponse Response { get; set; } + } +} diff --git a/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponsesViewModel.cs b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponsesViewModel.cs new file mode 100644 index 000000000..f7569c34b --- /dev/null +++ b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/CardRenewal/ResponsesViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Ocuda.Ops.Models.Entities; + +namespace Ocuda.Ops.Controllers.Areas.ContentManagement.ViewModels.CardRenewal +{ + public class ResponsesViewModel + { + public IEnumerable Responses { get; set; } + public CardRenewalResponse Response { get; set; } + } +} diff --git a/src/Ops.Controllers/Areas/ContentManagement/ViewModels/Home/IndexViewModel.cs b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/Home/IndexViewModel.cs index 46a9fdd7b..5801db119 100644 --- a/src/Ops.Controllers/Areas/ContentManagement/ViewModels/Home/IndexViewModel.cs +++ b/src/Ops.Controllers/Areas/ContentManagement/ViewModels/Home/IndexViewModel.cs @@ -13,6 +13,7 @@ public bool HasPermissions } } + public bool HasCardRenewalPermissions { get; set; } public bool HasRosterPermissions { get; set; } public bool HasSectionManagerPermissions { get; set; } public bool HasUserSyncPermissions { get; set; } diff --git a/src/Ops.Controllers/Areas/Services/CardRenewalController.cs b/src/Ops.Controllers/Areas/Services/CardRenewalController.cs new file mode 100644 index 000000000..bf70502af --- /dev/null +++ b/src/Ops.Controllers/Areas/Services/CardRenewalController.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Controllers.Abstract; +using Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal; +using Ocuda.Ops.Controllers.Filters; +using Ocuda.Ops.Models; +using Ocuda.Ops.Service.Filters; +using Ocuda.Ops.Service.Interfaces.Ops.Services; +using Ocuda.Ops.Service.Interfaces.Promenade.Services; +using Ocuda.PolarisHelper; +using Ocuda.Utility.Exceptions; + +namespace Ocuda.Ops.Controllers.Areas.Services +{ + [Area("Services")] + [Route("[area]/[controller]")] + public class CardRenewalController : BaseController + { + private readonly ICardRenewalRequestService _cardRenewalRequestService; + private readonly ICardRenewalService _cardRenewalService; + private readonly IPolarisHelper _polarisHelper; + + public CardRenewalController(ServiceFacades.Controller context, + ICardRenewalRequestService cardRenewalRequestService, + ICardRenewalService cardRenewalService, + IPolarisHelper polarisHelper) + : base(context) + { + ArgumentNullException.ThrowIfNull(cardRenewalRequestService); + ArgumentNullException.ThrowIfNull(cardRenewalService); + ArgumentNullException.ThrowIfNull(polarisHelper); + + _cardRenewalRequestService = cardRenewalRequestService; + _cardRenewalService = cardRenewalService; + _polarisHelper = polarisHelper; + } + + public static string Name + { get { return "CardRenewal"; } } + + [HttpGet("[action]/{id}")] + [RestoreModelState] + public async Task Details(int id) + { + var request = await _cardRenewalRequestService.GetRequestAsync(id); + + if (request == null) + { + return RedirectToAction(nameof(Index)); + } + + var patronData = _polarisHelper.GetPatronDataOverride(request.Barcode); + + var viewModel = new DetailsViewModel + { + AddressLookupPath = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .AddressLookupUrl), + AssessorLookupPath = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .AssessorLookupUrl), + PatronCode = await _polarisHelper.GetPatronCodeNameAsync(patronData.PatronCodeID), + PatronData = patronData, + PatronName = $"{patronData.NameFirst} {patronData.NameLast}", + Request = request + }; + + if (!viewModel.Request.ProcessedAt.HasValue) + { + var responses = await _cardRenewalService.GetAvailableResponsesAsync(); + viewModel.ResponseList = responses.Select(_ => new SelectListItem + { + Text = _.Name, + Value = _.Id.ToString(CultureInfo.InvariantCulture) + }); + } + else + { + viewModel.Result = await _cardRenewalService + .GetResultForRequestAsync(request.Id); + } + + var acceptedCounty = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .AcceptedCounty); + + if (!string.IsNullOrWhiteSpace(acceptedCounty)) + { + viewModel.AcceptedCounty = acceptedCounty; + viewModel.InCounty = patronData.PatronAddresses.Any(_ => + string.Equals(_.County, acceptedCounty, StringComparison.OrdinalIgnoreCase)); + } + + var juvenilePatronCodes = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .JuvenilePatronCodes); + + if (!string.IsNullOrWhiteSpace(juvenilePatronCodes)) + { + var patronCodeList = juvenilePatronCodes + .Split(",", StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries) + .ToList(); + + foreach (var patronCode in patronCodeList) + { + int patronCodeId; + + if (int.TryParse(patronCode, out patronCodeId)) + { + if (patronData.PatronCodeID == patronCodeId) + { + viewModel.IsJuvenile = true; + + if (patronData.BirthDate.HasValue + && patronData.BirthDate.Value != DateTime.MinValue) + { + DateTime today = DateTime.Today; + var age = today.Year - patronData.BirthDate.Value.Year; + if (patronData.BirthDate.Value > today.AddYears(-age)) + { + age--; + } + viewModel.PatronAge = age; + } + } + } + else + { + _logger.LogError($"Invalid juvenile patron code id '{patronCode}'"); + } + } + } + + var leapPatronUrl = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .LeapPatronUrl); + + if (!string.IsNullOrWhiteSpace(leapPatronUrl)) + { + viewModel.LeapPath = leapPatronUrl + request.PatronId; + } + + return View(viewModel); + } + + [HttpPost("[action]/{id}")] + public async Task Details(DetailsViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(viewModel); + + if (ModelState.IsValid) + { + try + { + var request = await _cardRenewalRequestService + .GetRequestAsync(viewModel.RequestId); + + if (request.ProcessedAt.HasValue) + { + _logger.LogError($"Attempted to process request {request.Id} which has already been processed."); + } + + + var result = _polarisHelper.RenewPatronRegistration( + request.Barcode, + request.Email); + + await _cardRenewalService.ProcessRequestAsync( + request.Id, + viewModel.ResponseId.Value, + viewModel.ResponseText, + viewModel.PatronName); + } + catch (OcudaException ex) + { + + } + } + + return RedirectToAction(nameof(Details), new + { + id = viewModel.RequestId + }); + } + + [HttpPost("[action]")] + public async Task Discard(int id) + { + try + { + await _cardRenewalService.DiscardRequestAsync(id); + ShowAlertSuccess($"Request {id} has been discarded"); + } + catch (OcudaException ex) + { + ShowAlertDanger($"Unable to discard request: {ex.Message}"); + return RedirectToAction(nameof(Details), new { id }); + } + + return RedirectToAction(nameof(Index)); + } + + [HttpGet("[action]")] + public async Task GetResponseText(int responseId, int languageId) + { + try + { + var response = await _cardRenewalService + .GetResponseTextAsync(responseId, languageId); + + var options = new JsonSerializerOptions + { + Converters = { new JsonStringEnumConverter() } + }; + + return Json(new { success = true, response.Text, response.Type }, options); + } + catch (OcudaException ex) + { + var response = new JsonResponse + { + Message = ex.Message, + Success = false + }; + return Json(response); + } + } + + [HttpGet("[action]")] + public async Task Index(int? page, bool processed) + { + page ??= 1; + + var filter = new RequestFilter(page.Value) + { + IsProcessed = processed + }; + + var requests = await _cardRenewalRequestService.GetRequestsAsync(filter); + + var viewModel = new IndexViewModel + { + CardRequests = requests.Data, + CurrentPage = page.Value, + IsProcessed = processed, + ItemCount = requests.Count, + ItemsPerPage = filter.Take.Value + }; + + if (processed) + { + viewModel.PendingCount = await _cardRenewalRequestService.GetRequestCountAsync(false); + viewModel.ProcessedCount = viewModel.ItemCount; + } + else + { + viewModel.PendingCount = viewModel.ItemCount; + viewModel.ProcessedCount = await _cardRenewalRequestService.GetRequestCountAsync(true); + } + + return View(viewModel); + } + } +} diff --git a/src/Ops.Controllers/Areas/Services/EmployeeCardController.cs b/src/Ops.Controllers/Areas/Services/EmployeeCardController.cs index 26e2c12ad..07051f963 100644 --- a/src/Ops.Controllers/Areas/Services/EmployeeCardController.cs +++ b/src/Ops.Controllers/Areas/Services/EmployeeCardController.cs @@ -24,7 +24,7 @@ public EmployeeCardController(ServiceFacades.Controller _employeeCardService = employeeCardService; } - public static String Name + public static string Name { get { return "EmployeeCard"; } } [HttpGet("[action]/{id}")] @@ -67,7 +67,7 @@ public async Task Index(int? page, bool processed) { page ??= 1; - var filter = new EmployeeCardFilter(page.Value) + var filter = new RequestFilter(page.Value) { IsProcessed = processed }; diff --git a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs new file mode 100644 index 000000000..48b20d2f9 --- /dev/null +++ b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Clc.Polaris.Api.Models; +using Microsoft.AspNetCore.Mvc.Rendering; +using Ocuda.Ops.Models.Entities; +using Ocuda.Promenade.Models.Entities; + +namespace Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal +{ + public class DetailsViewModel + { + private const int _maxNotesDisplayLength = 400; + + public CardRenewalRequest Request { get; set; } + public CardRenewalResult Result { get; set; } + public PatronData PatronData { get; set; } + public string AcceptedCounty { get; set; } + public string AddressLookupPath { get; set; } + public string AssessorLookupPath { get; set; } + public bool InCounty { get; set; } + public bool IsJuvenile { get; set; } + public string LeapPath { get; set; } + public int? PatronAge { get; set; } + public string PatronCode { get; set; } + public string PatronName { get; set; } + public IEnumerable ResponseList { get; set; } + + public int RequestId { get; set; } + + [DisplayName("Response")] + [Required] + public int? ResponseId { get; set; } + + [DisplayName("Email Text")] + [Required] + public string ResponseText { get; set; } + + public int MaxNotesDisplayLength + { + get { return _maxNotesDisplayLength; } + } + } +} diff --git a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs new file mode 100644 index 000000000..ca729bc0b --- /dev/null +++ b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility.Models; + +namespace Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal +{ + public class IndexViewModel : PaginateModel + { + public ICollection CardRequests { get; set; } + public bool IsProcessed { get; set; } + public int PendingCount { get; set; } + public int ProcessedCount { get; set; } + } +} diff --git a/src/Ops.Controllers/Ops.Controllers.csproj b/src/Ops.Controllers/Ops.Controllers.csproj index 104847931..79ebc663a 100644 --- a/src/Ops.Controllers/Ops.Controllers.csproj +++ b/src/Ops.Controllers/Ops.Controllers.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Ops.Data/Ops/CardRenewalResponseRepository.cs b/src/Ops.Data/Ops/CardRenewalResponseRepository.cs new file mode 100644 index 000000000..db237deb3 --- /dev/null +++ b/src/Ops.Data/Ops/CardRenewalResponseRepository.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Models.Entities; +using Ocuda.Ops.Service.Interfaces.Ops.Repositories; + +namespace Ocuda.Ops.Data.Ops +{ + public class CardRenewalResponseRepository + : OpsRepository, + ICardRenewalResponseRepository + { + public CardRenewalResponseRepository(ServiceFacade.Repository repositoryFacade, + ILogger logger) : base(repositoryFacade, logger) + { + } + + public override async Task FindAsync(int id) + { + return await DbSet + .AsNoTracking() + .Where(_ => !_.IsDeleted && _.Id == id) + .SingleOrDefaultAsync(); + } + + public async Task GetBySortOrderAsync(int sortOrder) + { + return await DbSet + .AsNoTracking() + .Where(_ => !_.IsDeleted && _.SortOrder == sortOrder) + .FirstOrDefaultAsync(); + } + + public async Task GetMaxSortOrderAsync() + { + return await DbSet + .AsNoTracking() + .Where(_ => !_.IsDeleted) + .MaxAsync(_ => (int?)_.SortOrder); + } + + public async Task> GetAllAsync() + { + return await DbSet + .AsNoTracking() + .Include(_ => _.EmailSetup) + .Where(_ => !_.IsDeleted) + .OrderBy(_ => _.SortOrder) + .ToListAsync(); + } + + public async Task> GetAvailableAsync() + { + return await DbSet + .AsNoTracking() + .Where(_ => !_.IsDeleted && _.EmailSetupId.HasValue) + .OrderBy(_ => _.SortOrder) + .ToListAsync(); + } + + public async Task> GetSubsequentAsync(int sortOrder) + { + return await DbSet + .AsNoTracking() + .Where(_ => !_.IsDeleted && _.SortOrder > sortOrder) + .ToListAsync(); + } + } +} diff --git a/src/Ops.Data/Ops/CardRenewalResultRepository.cs b/src/Ops.Data/Ops/CardRenewalResultRepository.cs new file mode 100644 index 000000000..581edc08b --- /dev/null +++ b/src/Ops.Data/Ops/CardRenewalResultRepository.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Models.Entities; +using Ocuda.Ops.Service.Interfaces.Ops.Repositories; + +namespace Ocuda.Ops.Data.Ops +{ + public class CardRenewalResultRepository : OpsRepository, + ICardRenewalResultRepository + { + public CardRenewalResultRepository(ServiceFacade.Repository repositoryFacade, + ILogger logger) : base(repositoryFacade, logger) + { + } + + public async Task GetForRequestAsync(int requestId) + { + return await DbSet + .AsNoTracking() + .Include(_ => _.CreatedByUser) + .Where(_ => _.CardRenewalRequestId == requestId) + .SingleOrDefaultAsync(); + } + } +} diff --git a/src/Ops.Data/Ops/EmailSetupRepository.cs b/src/Ops.Data/Ops/EmailSetupRepository.cs new file mode 100644 index 000000000..f9d9a3219 --- /dev/null +++ b/src/Ops.Data/Ops/EmailSetupRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Models.Entities; +using Ocuda.Ops.Service.Interfaces.Ops.Repositories; + +namespace Ocuda.Ops.Data.Ops +{ + public class EmailSetupRepository + : GenericRepository, IEmailSetupRepository + { + public EmailSetupRepository(ServiceFacade.Repository repositoryFacade, + ILogger logger) : base(repositoryFacade, logger) + { + } + + public async Task> GetAllAsync() + { + return await DbSet + .AsNoTracking() + .OrderBy(_ => _.Description) + .ToListAsync(); + } + } +} diff --git a/src/Ops.Data/OpsContext.cs b/src/Ops.Data/OpsContext.cs index 5a8ad3965..1bec9e8c1 100644 --- a/src/Ops.Data/OpsContext.cs +++ b/src/Ops.Data/OpsContext.cs @@ -16,6 +16,8 @@ protected OpsContext(DbContextOptions options) : base(options) } public DbSet ApiKeys { get; set; } + public DbSet CardRenewalResponses { get; set; } + public DbSet CardRenewalResults { get; set; } public DbSet Categories { get; set; } public DbSet ClaimGroups { get; set; } public DbSet CoverIssueDetails { get; set; } diff --git a/src/Ops.Data/Promenade/CardRenewalRequestRepository.cs b/src/Ops.Data/Promenade/CardRenewalRequestRepository.cs new file mode 100644 index 000000000..061d57ee6 --- /dev/null +++ b/src/Ops.Data/Promenade/CardRenewalRequestRepository.cs @@ -0,0 +1,75 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Data.Extensions; +using Ocuda.Ops.Data.ServiceFacade; +using Ocuda.Ops.Service.Filters; +using Ocuda.Ops.Service.Interfaces.Promenade.Repositories; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility.Models; + +namespace Ocuda.Ops.Data.Promenade +{ + public class CardRenewalRequestRepository : + GenericRepository, ICardRenewalRequestRepository + { + public CardRenewalRequestRepository(Repository repositoryFacade, + ILogger logger) + : base(repositoryFacade, logger) + { + } + + public async Task GetByIdAsync(int id) + { + return await DbSet + .AsNoTracking() + .Where(_ => _.Id == id && !_.IsDiscarded) + .SingleOrDefaultAsync(); + } + + public async Task GetCountAsync(bool? isProcessed) + { + var query = DbSet + .AsNoTracking() + .Where(_ => !_.IsDiscarded); + + if (isProcessed.HasValue) + { + query = query.Where(_ => _.ProcessedAt.HasValue == isProcessed); + } + + return await query.CountAsync(); + } + + public async Task> GetPaginatedAsync( + RequestFilter filter) + { + var query = DbSet + .AsNoTracking() + .Where(_ => !_.IsDiscarded); + + if (filter?.IsProcessed.HasValue == true) + { + query = query.Where(_ => _.ProcessedAt.HasValue == filter.IsProcessed.Value); + } + + if (filter?.IsProcessed == true) + { + query = query.OrderByDescending(_ => _.ProcessedAt); + } + else + { + query = query.OrderBy(_ => _.SubmittedAt); + } + + return new CollectionWithCount + { + Count = await query.CountAsync(), + Data = await query + .ApplyPagination(filter) + .ToListAsync() + }; + } + } +} diff --git a/src/Ops.Data/Promenade/EmployeeCardRequestRepository.cs b/src/Ops.Data/Promenade/EmployeeCardRequestRepository.cs index f060b754f..51310b986 100644 --- a/src/Ops.Data/Promenade/EmployeeCardRequestRepository.cs +++ b/src/Ops.Data/Promenade/EmployeeCardRequestRepository.cs @@ -41,7 +41,7 @@ public async Task GetCountAsync(bool? isProcessed) } public async Task> GetPaginatedAsync( - EmployeeCardFilter filter) + RequestFilter filter) { var query = DbSet.AsNoTracking(); diff --git a/src/Ops.Data/Promenade/LanguageRepository.cs b/src/Ops.Data/Promenade/LanguageRepository.cs index 0b1ab8d13..5a2200220 100644 --- a/src/Ops.Data/Promenade/LanguageRepository.cs +++ b/src/Ops.Data/Promenade/LanguageRepository.cs @@ -60,6 +60,15 @@ public async Task GetDefaultLanguageId() .SingleOrDefaultAsync(); } + public async Task GetDefaultLanguageNameAsync() + { + return await DbSet + .AsNoTracking() + .Where(_ => _.IsActive && _.IsDefault) + .Select(_ => _.Name) + .SingleOrDefaultAsync(); + } + public async Task GetLanguageId(string culture) { return await DbSet diff --git a/src/Ops.Data/PromenadeContext.cs b/src/Ops.Data/PromenadeContext.cs index 8f50c439b..cf3f81240 100644 --- a/src/Ops.Data/PromenadeContext.cs +++ b/src/Ops.Data/PromenadeContext.cs @@ -14,6 +14,7 @@ protected PromenadeContext(DbContextOptions options) : base(options) } public DbSet CardDetails { get; set; } + public DbSet CardRenewalRequests { get; set; } public DbSet Cards { get; set; } public DbSet CarouselButtonLabels { get; set; } diff --git a/src/Ops.Models/Defaults/SiteSettings.cs b/src/Ops.Models/Defaults/SiteSettings.cs index c2f415e4b..b6a2a1cdd 100644 --- a/src/Ops.Models/Defaults/SiteSettings.cs +++ b/src/Ops.Models/Defaults/SiteSettings.cs @@ -8,6 +8,56 @@ public static class SiteSettings { public static IEnumerable Get { get; } = new[] { + #region CardRenewal + + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.AcceptedCounty, + Name = "Accepted county", + Description = "Accepted county for card renewal addresses", + Category = "CardRenewal", + Value = "", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.AddressLookupUrl, + Name = "Address lookup url", + Description = "Address lookup url with scheme, host and path", + Category = "CardRenewal", + Value = "", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.AssessorLookupUrl, + Name = "Assessor lookup url", + Description = "Assessor lookup with scheme, host and path", + Category = "CardRenewal", + Value = "", + Type= SiteSettingType.StringNullable + }, + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.JuvenilePatronCodes, + Name = "Juvenile patron codes", + Description = "Juvenile patron code ids, comma delimited", + Category = "CardRenewal", + Value = "", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.LeapPatronUrl, + Name = "Leap patron record url", + Description = "Leap patron record url with scheme, host and path", + Category = "CardRenewal", + Value = "", + Type = SiteSettingType.StringNullable + }, + + #endregion CardRenwal + #region Carousel new SiteSetting diff --git a/src/Ops.Models/Definitions/ApplicationPermissionDefinitions.cs b/src/Ops.Models/Definitions/ApplicationPermissionDefinitions.cs index c3d7c316b..37a0001ae 100644 --- a/src/Ops.Models/Definitions/ApplicationPermissionDefinitions.cs +++ b/src/Ops.Models/Definitions/ApplicationPermissionDefinitions.cs @@ -7,6 +7,12 @@ public static class ApplicationPermissionDefinitions { public static readonly ApplicationPermissionDefinition[] ApplicationPermissions = { + new() + { + Id = ApplicationPermission.CardRenewalManagement, + Info = "Manage settings related to card renewal.", + Name = "Card Renewal Management" + }, new() { Id = ApplicationPermission.CoverIssueManagement, Info = "Able to mark cover issues as resolved.", diff --git a/src/Ops.Models/Entities/CardRenewalResponse.cs b/src/Ops.Models/Entities/CardRenewalResponse.cs new file mode 100644 index 000000000..a8842b14d --- /dev/null +++ b/src/Ops.Models/Entities/CardRenewalResponse.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ocuda.Ops.Models.Entities +{ + public class CardRenewalResponse : Abstract.BaseEntity + { + [DisplayName("Email")] + public int? EmailSetupId { get; set; } + public EmailSetup EmailSetup { get; set; } + + public bool IsDeleted { get; set; } + + [Required] + [MaxLength(255)] + public string Name { get; set; } + + [NotMapped] + public string Text { get; set; } + + public ResponseType Type { get; set; } + public int SortOrder { get; set; } + + public enum ResponseType + { + Accept, + Deny + } + } +} diff --git a/src/Ops.Models/Entities/CardRenewalResult.cs b/src/Ops.Models/Entities/CardRenewalResult.cs new file mode 100644 index 000000000..1143b5de1 --- /dev/null +++ b/src/Ops.Models/Entities/CardRenewalResult.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ocuda.Ops.Models.Entities +{ + public class CardRenewalResult + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + [Required] + public int CardRenewalRequestId { get; set; } + + public int? CardRenewalResponseId { get; set; } + public CardRenewalResponse CardRenewalResponse { get; set; } + + public DateTime CreatedAt { get; set; } + + [ForeignKey(nameof(CreatedByUser))] + public int CreatedBy { get; set; } + public User CreatedByUser { get; set; } + + public bool IsDiscarded { get; set; } + + [DisplayName("Email Text")] + public string ResponseText { get; set; } + } +} diff --git a/src/Ops.Models/Entities/EmailSetupText.cs b/src/Ops.Models/Entities/EmailSetupText.cs index 46bc173a1..23e27dd9b 100644 --- a/src/Ops.Models/Entities/EmailSetupText.cs +++ b/src/Ops.Models/Entities/EmailSetupText.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Ocuda.Ops.Models.Entities { @@ -20,6 +21,7 @@ public class EmailSetupText [MaxLength(255)] public string Preview { get; set; } + [DisplayName("Text")] public string BodyText { get; set; } public string BodyHtml { get; set; } diff --git a/src/Ops.Models/Keys/ApplicationPermission.cs b/src/Ops.Models/Keys/ApplicationPermission.cs index cdd7005de..a1bb6da92 100644 --- a/src/Ops.Models/Keys/ApplicationPermission.cs +++ b/src/Ops.Models/Keys/ApplicationPermission.cs @@ -5,6 +5,7 @@ Justification = "Descriptive key name")] public static class ApplicationPermission { + public static readonly string CardRenewalManagement = nameof(CardRenewalManagement); public static readonly string CoverIssueManagement = nameof(CoverIssueManagement); public static readonly string DigitalDisplayContentManagement = nameof(DigitalDisplayContentManagement); public static readonly string EmediaManagement = nameof(EmediaManagement); diff --git a/src/Ops.Models/Keys/SiteSetting.cs b/src/Ops.Models/Keys/SiteSetting.cs index 344910eeb..086fd541d 100644 --- a/src/Ops.Models/Keys/SiteSetting.cs +++ b/src/Ops.Models/Keys/SiteSetting.cs @@ -2,6 +2,15 @@ { namespace SiteSetting { + public static class CardRenewal + { + public static readonly string AcceptedCounty = "CardRenewal.AcceptedCounty"; + public static readonly string AddressLookupUrl = "CardRenewal.AddressLookupUrl"; + public static readonly string AssessorLookupUrl = "CardRenewal.AssessorLookupUrl"; + public static readonly string JuvenilePatronCodes = "CardRenewal.JuvenilePatronCodes"; + public static readonly string LeapPatronUrl = "CardRenewal.LeapPatronUrl"; + } + public static class Carousel { public static readonly string ImageRestrictToDomains = "Carousel.ImageRestricToDomains"; diff --git a/src/Ops.Service/CardRenewalRequestService.cs b/src/Ops.Service/CardRenewalRequestService.cs new file mode 100644 index 000000000..3b5661bd0 --- /dev/null +++ b/src/Ops.Service/CardRenewalRequestService.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Service.Abstract; +using Ocuda.Ops.Service.Filters; +using Ocuda.Ops.Service.Interfaces.Promenade.Repositories; +using Ocuda.Ops.Service.Interfaces.Promenade.Services; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility.Models; + +namespace Ocuda.Ops.Service +{ + public class CardRenewalRequestService : BaseService, + ICardRenewalRequestService + { + private readonly ICardRenewalRequestRepository _cardRenewalRequestRepository; + + public CardRenewalRequestService(ILogger logger, + IHttpContextAccessor httpContext, + ICardRenewalRequestRepository cardRenewalRequestRepository) + : base(logger, httpContext) + { + ArgumentNullException.ThrowIfNull(cardRenewalRequestRepository); + + _cardRenewalRequestRepository = cardRenewalRequestRepository; + + } + + public async Task GetRequestAsync(int id) + { + return await _cardRenewalRequestRepository.GetByIdAsync(id); + } + + public async Task GetRequestCountAsync(bool? isProcessed) + { + return await _cardRenewalRequestRepository.GetCountAsync(isProcessed); + } + + public async Task> GetRequestsAsync( + RequestFilter filter) + { + return await _cardRenewalRequestRepository.GetPaginatedAsync(filter); + } + } +} diff --git a/src/Ops.Service/CardRenewalService.cs b/src/Ops.Service/CardRenewalService.cs new file mode 100644 index 000000000..40432a532 --- /dev/null +++ b/src/Ops.Service/CardRenewalService.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocuda.Ops.Models.Entities; +using Ocuda.Ops.Service.Abstract; +using Ocuda.Ops.Service.Interfaces.Ops.Repositories; +using Ocuda.Ops.Service.Interfaces.Ops.Services; +using Ocuda.Ops.Service.Interfaces.Promenade.Repositories; +using Ocuda.Ops.Service.Interfaces.Promenade.Services; +using Ocuda.Ops.Service.Models.CardRenewal; +using Ocuda.PolarisHelper; +using Ocuda.Utility.Abstract; +using Ocuda.Utility.Exceptions; + +namespace Ocuda.Ops.Service +{ + public class CardRenewalService : BaseService, + ICardRenewalService + { + private readonly ICardRenewalRequestRepository _cardRenewalRequestRepository; + private readonly ICardRenewalResponseRepository _cardRenewalResponseRepository; + private readonly ICardRenewalResultRepository _cardRenewalResultRepository; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IEmailService _emailService; + private readonly ILanguageService _languageService; + private readonly IPolarisHelper _polarisHelper; + + public CardRenewalService(ILogger logger, + IHttpContextAccessor httpContext, + ICardRenewalRequestRepository cardRenewalRequestRepository, + ICardRenewalResponseRepository cardRenewalResponseRepository, + ICardRenewalResultRepository cardRenewalResultRepository, + IDateTimeProvider dateTimeProvider, + IEmailService emailService, + ILanguageService languageService, + IPolarisHelper polarisHelper) + : base(logger, httpContext) + { + ArgumentNullException.ThrowIfNull(cardRenewalRequestRepository); + ArgumentNullException.ThrowIfNull(cardRenewalResponseRepository); + ArgumentNullException.ThrowIfNull(cardRenewalResultRepository); + ArgumentNullException.ThrowIfNull(dateTimeProvider); + ArgumentNullException.ThrowIfNull(emailService); + ArgumentNullException.ThrowIfNull(languageService); + ArgumentNullException.ThrowIfNull(polarisHelper); + + _cardRenewalRequestRepository = cardRenewalRequestRepository; + _cardRenewalResponseRepository = cardRenewalResponseRepository; + _cardRenewalResultRepository = cardRenewalResultRepository; + _dateTimeProvider = dateTimeProvider; + _emailService = emailService; + _languageService = languageService; + _polarisHelper = polarisHelper; + } + + public async Task CreateResponseAsync(CardRenewalResponse response) + { + ArgumentNullException.ThrowIfNull(response); + + response.CreatedAt = _dateTimeProvider.Now; + response.CreatedBy = GetCurrentUserId(); + response.Name = response.Name.Trim(); + + var maxSortOrder = await _cardRenewalResponseRepository.GetMaxSortOrderAsync(); + if (maxSortOrder.HasValue) + { + response.SortOrder = maxSortOrder.Value + 1; + } + + await _cardRenewalResponseRepository.AddAsync(response); + await _cardRenewalResponseRepository.SaveAsync(); + + return response; + } + + public async Task DeleteResponseAsync(int id) + { + var response = await _cardRenewalResponseRepository.FindAsync(id); + + var subsequentResponses = await _cardRenewalResponseRepository + .GetSubsequentAsync(response.SortOrder); + if (subsequentResponses.Count > 0) + { + subsequentResponses.ForEach(_ => _.SortOrder--); + _cardRenewalResponseRepository.UpdateRange(subsequentResponses); + } + + response.IsDeleted = true; + _cardRenewalResponseRepository.Update(response); + await _cardRenewalResponseRepository.SaveAsync(); + } + + public async Task DiscardRequestAsync(int id) + { + var request = await _cardRenewalRequestRepository.GetByIdAsync(id); + + if (request == null) + { + throw new OcudaException("Request does not exist"); + } + + if (request.ProcessedAt.HasValue) + { + throw new OcudaException("Request has already been processed"); + } + + var now = _dateTimeProvider.Now; + + var result = new CardRenewalResult + { + CardRenewalRequestId = request.Id, + CreatedAt = now, + CreatedBy = GetCurrentUserId(), + IsDiscarded = true + }; + await _cardRenewalResultRepository.AddAsync(result); + + + request.IsDiscarded = true; + request.ProcessedAt = now; + _cardRenewalRequestRepository.Update(request); + + await _cardRenewalResultRepository.SaveAsync(); + await _cardRenewalRequestRepository.SaveAsync(); + } + + public async Task> GetAvailableResponsesAsync() + { + return await _cardRenewalResponseRepository.GetAvailableAsync(); + } + + public async Task GetResponseAsync(int id) + { + return await _cardRenewalResponseRepository.FindAsync(id); + } + + public async Task GetResponseTextAsync(int responseId, int languageId) + { + var response = await _cardRenewalResponseRepository.FindAsync(responseId); + if (!response.EmailSetupId.HasValue) + { + _logger.LogError($"Invalid card renewal response '{responseId}': no email setup set."); + throw new OcudaException("Invalid response"); + } + + var language = await _languageService.GetActiveByIdAsync(languageId); + + var emailSetupText = await _emailService.GetSetupTextByLanguageAsync( + response.EmailSetupId.Value, + language.Name); + + if (emailSetupText == null) + { + var defaultLanguage = await _languageService.GetDefaultLanguageNameAsync(); + + emailSetupText = await _emailService.GetSetupTextByLanguageAsync( + response.EmailSetupId.Value, + defaultLanguage); + if (emailSetupText == null) + { + _logger.LogError($"Invalid card renewal response '{responseId}': email setup {response.EmailSetupId} is missing text."); + throw new OcudaException("Invalid response"); + } + } + + response.Text = emailSetupText.BodyText; + + return response; + } + + public async Task> GetResponsesAsync() + { + return await _cardRenewalResponseRepository.GetAllAsync(); + } + + + public async Task GetResultForRequestAsync(int requestId) + { + return await _cardRenewalResultRepository.GetForRequestAsync(requestId); + } + + public async Task ProcessRequestAsync(int requestId, + int responseId, + string responseText, + string patronName) + { + var request = await _cardRenewalRequestRepository + .GetByIdAsync(requestId); + if (request.ProcessedAt.HasValue) + { + throw new OcudaException("Request has already been processed."); + } + + var processResult = new ProcessResult(); + + var response = await _cardRenewalResponseRepository.FindAsync(responseId); + + if (response.Type == CardRenewalResponse.ResponseType.Accept) + { + var renewResult = _polarisHelper.RenewPatronRegistration( + request.Barcode, + request.Email); + + if (!renewResult.Success) + { + throw new OcudaException("Unable to update the record in Polaris."); + } + + processResult.EmailNotUpdated = renewResult.EmailNotUpdated; + } + + var language = await _languageService.GetActiveByIdAsync(request.LanguageId); + var tags = new Dictionary + { + { Keys.CardRenewal.CustomerBarcode, request.Barcode }, + { Keys.CardRenewal.CustomerName, patronName } + }; + + var emailDetails = await _emailService.GetDetailsAsync(response.EmailSetupId.Value, + language.Name, + tags, + responseText); + + emailDetails.ToEmailAddress = request.Email; + emailDetails.ToName = patronName; + + EmailRecord sentEmail = null; + + try + { + sentEmail = await _emailService.SendAsync(emailDetails); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error sending email setup {EmailSetupId} to {EmailTo}: {ErrorMessage}", + response.EmailSetupId.Value, + emailDetails.ToEmailAddress, + ex.Message); + + throw new OcudaException("Unable to send email."); + } + + if (sentEmail != null) + { + var now = _dateTimeProvider.Now; + + request.ProcessedAt = now; + _cardRenewalRequestRepository.Update(request); + + var result = new CardRenewalResult + { + CardRenewalRequestId = request.Id, + CreatedAt = now, + CreatedBy = GetCurrentUserId(), + ResponseText = sentEmail.BodyText + }; + + await _cardRenewalResultRepository.AddAsync(result); + //await _cardRenewalResponseRepository.SaveAsync(); + } + + + return processResult; + } + + public async Task UpdateResponseAsync(CardRenewalResponse response) + { + ArgumentNullException.ThrowIfNull(response); + + var currentResponse = await _cardRenewalResponseRepository.FindAsync(response.Id); + + currentResponse.EmailSetupId = response.EmailSetupId; + currentResponse.Name = response.Name.Trim(); + + _cardRenewalResponseRepository.Update(currentResponse); + await _cardRenewalResponseRepository.SaveAsync(); + } + + public async Task UpdateResponseSortOrderAsync(int id, bool increase) + { + var response = await _cardRenewalResponseRepository.FindAsync(id); + + int newSortOrder; + if (increase) + { + newSortOrder = response.SortOrder + 1; + } + else + { + if (response.SortOrder == 0) + { + throw new OcudaException("Response is already in the first position."); + } + newSortOrder = response.SortOrder - 1; + } + + var responseInPosition = await _cardRenewalResponseRepository.GetBySortOrderAsync( + newSortOrder) + ?? throw new OcudaException("Response is already in the last position."); + + responseInPosition.SortOrder = response.SortOrder; + response.SortOrder = newSortOrder; + + _cardRenewalResponseRepository.Update(response); + _cardRenewalResponseRepository.Update(responseInPosition); + await _cardRenewalResponseRepository.SaveAsync(); + } + } +} diff --git a/src/Ops.Service/EmailService.cs b/src/Ops.Service/EmailService.cs index dbcfede5f..aefc94205 100644 --- a/src/Ops.Service/EmailService.cs +++ b/src/Ops.Service/EmailService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Threading.Tasks; using CommonMark; using Microsoft.AspNetCore.Http; @@ -21,6 +22,7 @@ public class EmailService : BaseService, IEmailService private readonly IOcudaCache _cache; private readonly IEmailRecordRepository _emailRecordRepository; + private readonly IEmailSetupRepository _emailSetupRepository; private readonly IEmailSetupTextRepository _emailSetupTextRepository; private readonly IEmailTemplateTextRepository _emailTemplateTextRepository; private readonly Utility.Email.Sender _sender; @@ -28,6 +30,7 @@ public class EmailService : BaseService, IEmailService public EmailService(ILogger logger, IEmailRecordRepository emailRecordRepository, + IEmailSetupRepository emailSetupRepository, IEmailSetupTextRepository emailSetupTextRepository, IEmailTemplateTextRepository emailTemplateTextRepository, IHttpContextAccessor httpContextAccessor, @@ -38,6 +41,7 @@ public EmailService(ILogger logger, { ArgumentNullException.ThrowIfNull(cache); ArgumentNullException.ThrowIfNull(emailRecordRepository); + ArgumentNullException.ThrowIfNull(emailSetupRepository); ArgumentNullException.ThrowIfNull(emailSetupTextRepository); ArgumentNullException.ThrowIfNull(emailTemplateTextRepository); ArgumentNullException.ThrowIfNull(sender); @@ -45,6 +49,7 @@ public EmailService(ILogger logger, _cache = cache; _emailRecordRepository = emailRecordRepository; + _emailSetupRepository = emailSetupRepository; _emailSetupTextRepository = emailSetupTextRepository; _emailTemplateTextRepository = emailTemplateTextRepository; _sender = sender; @@ -53,11 +58,18 @@ public EmailService(ILogger logger, public async Task GetDetailsAsync(int emailSetupId, string languageName, - IDictionary tags) + IDictionary tags, + string overrideText = null) { var emailSetupText = await GetEmailSetupAsync(emailSetupId, languageName) ?? throw new OcudaEmailException($"Unable to find email setup {emailSetupId} in the requested or default language."); + if (!string.IsNullOrWhiteSpace(overrideText)) + { + emailSetupText.BodyHtml = null; + emailSetupText.BodyText = overrideText; + } + var emailTemplateText = await GetEmailTemplateAsync(emailSetupText.EmailSetup.EmailTemplateId, languageName) @@ -69,7 +81,8 @@ var emailTemplateText { try { - emailSetupText.BodyHtml = CommonMarkConverter.Convert(emailSetupText.BodyText); + emailSetupText.BodyHtml = CommonMarkConverter.Convert(overrideText + ?? emailSetupText.BodyText); } catch (CommonMarkException cmex) { @@ -100,6 +113,18 @@ var emailTemplateText }; } + public async Task> GetEmailSetupsAsync() + { + var emailSetups = await _emailSetupRepository.GetAllAsync(); + return emailSetups.ToDictionary(_ => _.Id, _ => _.Description); + } + + public async Task GetSetupTextByLanguageAsync(int emailSetupId, + string languageName) + { + return await _emailSetupTextRepository.GetByIdLanguageAsync(emailSetupId, languageName); + } + public async Task SendAsync(Utility.Email.Details emailDetails) { var record = await _sender.SendEmailAsync(emailDetails); diff --git a/src/Ops.Service/EmployeeCardService.cs b/src/Ops.Service/EmployeeCardService.cs index a21caa765..a82d9557a 100644 --- a/src/Ops.Service/EmployeeCardService.cs +++ b/src/Ops.Service/EmployeeCardService.cs @@ -42,7 +42,7 @@ public async Task GetRequestCountAsync(bool? isProcessed) } public async Task> GetRequestsAsync( - EmployeeCardFilter filter) + RequestFilter filter) { return await _employeeCardRequestRepository.GetPaginatedAsync(filter); } diff --git a/src/Ops.Service/Filters/EmployeeCardFilter.cs b/src/Ops.Service/Filters/RequestFilter.cs similarity index 53% rename from src/Ops.Service/Filters/EmployeeCardFilter.cs rename to src/Ops.Service/Filters/RequestFilter.cs index bd7f64e6c..0f4b28563 100644 --- a/src/Ops.Service/Filters/EmployeeCardFilter.cs +++ b/src/Ops.Service/Filters/RequestFilter.cs @@ -1,8 +1,8 @@ namespace Ocuda.Ops.Service.Filters { - public class EmployeeCardFilter : BaseFilter + public class RequestFilter : BaseFilter { - public EmployeeCardFilter(int page) : base(page) + public RequestFilter(int page) : base(page) { } diff --git a/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResponseRepository.cs b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResponseRepository.cs new file mode 100644 index 000000000..0be78bb00 --- /dev/null +++ b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResponseRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocuda.Ops.Models.Entities; + +namespace Ocuda.Ops.Service.Interfaces.Ops.Repositories +{ + public interface ICardRenewalResponseRepository : IOpsRepository + { + Task GetBySortOrderAsync(int sortOrder); + Task GetMaxSortOrderAsync(); + Task> GetAllAsync(); + Task> GetAvailableAsync(); + Task> GetSubsequentAsync(int sortOrder); + } +} diff --git a/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs new file mode 100644 index 000000000..014cc74d1 --- /dev/null +++ b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ocuda.Ops.Models.Entities; + +namespace Ocuda.Ops.Service.Interfaces.Ops.Repositories +{ + public interface ICardRenewalResultRepository : IOpsRepository + { + Task GetForRequestAsync(int requestId); + } +} diff --git a/src/Ops.Service/Interfaces/Ops/Repositories/IEmailSetupRepository.cs b/src/Ops.Service/Interfaces/Ops/Repositories/IEmailSetupRepository.cs new file mode 100644 index 000000000..4e8f776df --- /dev/null +++ b/src/Ops.Service/Interfaces/Ops/Repositories/IEmailSetupRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocuda.Ops.Models.Entities; + +namespace Ocuda.Ops.Service.Interfaces.Ops.Repositories +{ + public interface IEmailSetupRepository : IGenericRepository + { + Task> GetAllAsync(); + } +} diff --git a/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs b/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs new file mode 100644 index 000000000..b4ef0323a --- /dev/null +++ b/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocuda.Ops.Models.Entities; +using Ocuda.Ops.Service.Models.CardRenewal; + +namespace Ocuda.Ops.Service.Interfaces.Ops.Services +{ + public interface ICardRenewalService + { + Task CreateResponseAsync(CardRenewalResponse response); + Task DeleteResponseAsync(int id); + Task DiscardRequestAsync(int id); + Task> GetAvailableResponsesAsync(); + Task GetResponseAsync(int id); + Task> GetResponsesAsync(); + Task GetResponseTextAsync(int responseId, int languageId); + Task GetResultForRequestAsync(int requestId); + Task ProcessRequestAsync(int requestId, + int responseId, + string responseText, + string patronName); + Task UpdateResponseAsync(CardRenewalResponse response); + Task UpdateResponseSortOrderAsync(int id, bool increase); + } +} diff --git a/src/Ops.Service/Interfaces/Ops/Services/IEmailService.cs b/src/Ops.Service/Interfaces/Ops/Services/IEmailService.cs index 8b6a82dcc..4cbef7752 100644 --- a/src/Ops.Service/Interfaces/Ops/Services/IEmailService.cs +++ b/src/Ops.Service/Interfaces/Ops/Services/IEmailService.cs @@ -8,7 +8,10 @@ namespace Ocuda.Ops.Service.Interfaces.Ops.Services public interface IEmailService { Task
GetDetailsAsync(int emailSetupId, string languageName, - IDictionary tags); + IDictionary tags, string overrideText = null); + + Task> GetEmailSetupsAsync(); + Task GetSetupTextByLanguageAsync(int emailSetupId, string languageName); Task SendAsync(Details emailDetails); } diff --git a/src/Ops.Service/Interfaces/Promenade/Repositories/ICardRenewalRequestRepository.cs b/src/Ops.Service/Interfaces/Promenade/Repositories/ICardRenewalRequestRepository.cs new file mode 100644 index 000000000..bf245d978 --- /dev/null +++ b/src/Ops.Service/Interfaces/Promenade/Repositories/ICardRenewalRequestRepository.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Ocuda.Ops.Service.Filters; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility.Models; + +namespace Ocuda.Ops.Service.Interfaces.Promenade.Repositories +{ + public interface ICardRenewalRequestRepository : IGenericRepository + { + Task GetByIdAsync(int id); + Task GetCountAsync(bool? isProcessed); + Task> GetPaginatedAsync(RequestFilter filter); + } +} diff --git a/src/Ops.Service/Interfaces/Promenade/Repositories/IEmployeeCardRequestRepository.cs b/src/Ops.Service/Interfaces/Promenade/Repositories/IEmployeeCardRequestRepository.cs index ad15b3299..6b3493880 100644 --- a/src/Ops.Service/Interfaces/Promenade/Repositories/IEmployeeCardRequestRepository.cs +++ b/src/Ops.Service/Interfaces/Promenade/Repositories/IEmployeeCardRequestRepository.cs @@ -9,6 +9,6 @@ public interface IEmployeeCardRequestRepository : IGenericRepository GetByIdAsync(int Id); Task GetCountAsync(bool? isProcessed); - Task> GetPaginatedAsync(EmployeeCardFilter filter); + Task> GetPaginatedAsync(RequestFilter filter); } } diff --git a/src/Ops.Service/Interfaces/Promenade/Repositories/ILanguageRepository.cs b/src/Ops.Service/Interfaces/Promenade/Repositories/ILanguageRepository.cs index ced6a2e9f..cd765c304 100644 --- a/src/Ops.Service/Interfaces/Promenade/Repositories/ILanguageRepository.cs +++ b/src/Ops.Service/Interfaces/Promenade/Repositories/ILanguageRepository.cs @@ -16,6 +16,8 @@ public interface ILanguageRepository : IGenericRepository Task GetDefaultLanguageId(); + Task GetDefaultLanguageNameAsync(); + Task GetLanguageId(string culture); } } \ No newline at end of file diff --git a/src/Ops.Service/Interfaces/Promenade/Services/ICardRenewalRequestService.cs b/src/Ops.Service/Interfaces/Promenade/Services/ICardRenewalRequestService.cs new file mode 100644 index 000000000..cc88586a7 --- /dev/null +++ b/src/Ops.Service/Interfaces/Promenade/Services/ICardRenewalRequestService.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Ocuda.Ops.Service.Filters; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility.Models; + +namespace Ocuda.Ops.Service.Interfaces.Promenade.Services +{ + public interface ICardRenewalRequestService + { + Task GetRequestAsync(int id); + Task GetRequestCountAsync(bool? isProcessed); + Task> GetRequestsAsync(RequestFilter filter); + } +} diff --git a/src/Ops.Service/Interfaces/Promenade/Services/IEmployeeCardService.cs b/src/Ops.Service/Interfaces/Promenade/Services/IEmployeeCardService.cs index 715367847..3a4906bb6 100644 --- a/src/Ops.Service/Interfaces/Promenade/Services/IEmployeeCardService.cs +++ b/src/Ops.Service/Interfaces/Promenade/Services/IEmployeeCardService.cs @@ -9,7 +9,7 @@ public interface IEmployeeCardService { Task GetRequestAsync(int requestId); Task GetRequestCountAsync(bool? isProcessed); - Task> GetRequestsAsync(EmployeeCardFilter filter); + Task> GetRequestsAsync(RequestFilter filter); Task UpdateNotesAsync(EmployeeCardRequest cardRequest); } } diff --git a/src/Ops.Service/Interfaces/Promenade/Services/ILanguageService.cs b/src/Ops.Service/Interfaces/Promenade/Services/ILanguageService.cs index 775a51f3e..66284f444 100644 --- a/src/Ops.Service/Interfaces/Promenade/Services/ILanguageService.cs +++ b/src/Ops.Service/Interfaces/Promenade/Services/ILanguageService.cs @@ -13,5 +13,7 @@ public interface ILanguageService Task> GetActiveNamesAsync(); Task GetDefaultLanguageId(); + + Task GetDefaultLanguageNameAsync(); } } \ No newline at end of file diff --git a/src/Ops.Service/Keys/CardRenewal.cs b/src/Ops.Service/Keys/CardRenewal.cs new file mode 100644 index 000000000..01bf676e4 --- /dev/null +++ b/src/Ops.Service/Keys/CardRenewal.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ocuda.Ops.Service.Keys +{ + public class CardRenewal + { + public static readonly string CustomerBarcode = "CustomerBarcode"; + public static readonly string CustomerName = "CustomerName"; + } +} diff --git a/src/Ops.Service/LanguageService.cs b/src/Ops.Service/LanguageService.cs index effbc0b1f..1fc9a3276 100644 --- a/src/Ops.Service/LanguageService.cs +++ b/src/Ops.Service/LanguageService.cs @@ -36,5 +36,10 @@ public async Task GetDefaultLanguageId() { return await _languageRepository.GetDefaultLanguageId(); } + + public async Task GetDefaultLanguageNameAsync() + { + return await _languageRepository.GetDefaultLanguageNameAsync(); + } } } \ No newline at end of file diff --git a/src/Ops.Service/Models/CardRenewal/ProcessResult.cs b/src/Ops.Service/Models/CardRenewal/ProcessResult.cs new file mode 100644 index 000000000..1c64fd34c --- /dev/null +++ b/src/Ops.Service/Models/CardRenewal/ProcessResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ocuda.Ops.Service.Models.CardRenewal +{ + public class ProcessResult + { + public bool APIRenew { get; set; } + public bool EmailNotUpdated { get; set; } + } +} diff --git a/src/Ops.Service/Ops.Service.csproj b/src/Ops.Service/Ops.Service.csproj index 9919f422d..74d9b1013 100644 --- a/src/Ops.Service/Ops.Service.csproj +++ b/src/Ops.Service/Ops.Service.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Response.cshtml b/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Response.cshtml new file mode 100644 index 000000000..93b0ebfde --- /dev/null +++ b/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Response.cshtml @@ -0,0 +1,128 @@ +@model Ocuda.Ops.Controllers.Areas.ContentManagement.ViewModels.CardRenewal.ResponseViewModel + +
+

+ Card Renewal Response + @Model.Response.Name +

+
+ Back +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+ + + + + +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+

+ Email Text Preview + +

+
+ @foreach (var language in Model.Languages) + { + + } +
+
+
+ + + + + +
+
+ Replacement Keys – {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerBarcode}}, {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerName}} +
+
+ + +@section Scripts { + +} \ No newline at end of file diff --git a/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Responses.cshtml b/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Responses.cshtml new file mode 100644 index 000000000..898518a44 --- /dev/null +++ b/src/Ops.Web/Areas/ContentManagement/Views/CardRenewal/Responses.cshtml @@ -0,0 +1,170 @@ +@model Ocuda.Ops.Controllers.Areas.ContentManagement.ViewModels.CardRenewal.ResponsesViewModel + +
+

+ Card Renewal Responses +

+
+ +
+
+ + + + + + + + + + + + @if (Model.Responses.Count() == 0) + { + + + + } + else + { + @foreach (var response in Model.Responses) + { + + + + + + + } + } + +
TypeNameEmail 
+ No responses found. +
@response.Type + + @response.Name + + @response.EmailSetup?.Description + + + +
+ +
+
+ + +
+
+ +
+ + + +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/src/Ops.Web/Areas/ContentManagement/Views/Home/Index.cshtml b/src/Ops.Web/Areas/ContentManagement/Views/Home/Index.cshtml index cdd976e6c..d824a6b5e 100644 --- a/src/Ops.Web/Areas/ContentManagement/Views/Home/Index.cshtml +++ b/src/Ops.Web/Areas/ContentManagement/Views/Home/Index.cshtml @@ -4,6 +4,22 @@

Intranet management

+ @if (Model.IsSiteManager || Model.HasCardRenewalPermissions) + { +
+ +
+ }
diff --git a/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml b/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml new file mode 100644 index 000000000..4e94d1bd8 --- /dev/null +++ b/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml @@ -0,0 +1,415 @@ +@model Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal.DetailsViewModel + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (!string.IsNullOrWhiteSpace(Model.AcceptedCounty)) + { + + + + + } + + + + +
Record id + @Model.Request.PatronId + @if (!string.IsNullOrEmpty(Model.LeapPath)) + { + + + + } +
Request barcode@Model.Request.Barcode
First name@Model.PatronData.NameFirst
Last name@Model.PatronData.NameLast
Email address@Model.Request.Email
Expiration date@Model.PatronData.ExpirationDate.ToShortDateString()
Address check date@Model.PatronData.AddrCheckDate?.ToShortDateString()
At the same address@(Model.Request.SameAddress ? "Yes" : "No")
In @Model.AcceptedCounty County@(Model.InCounty ? "Yes" : "No")
Patron code@Model.PatronCode
+ + @if (Model.IsJuvenile) + { + + @if (Model.PatronAge.HasValue) + { + + + + + } + + + + + + + + + + + + + + + + +
Age@Model.PatronAge
Request guardian's name@Model.Request.GuardianName
Request guardian's barcode@Model.Request.GuardianBarcode
Polaris guardian's name@Model.PatronData.User3
Polaris guardian's barcode@Model.PatronData.User2
+ } +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Charges@Model.PatronData.ChargeBalance.ToString("C")
Has system blocks@(Model.PatronData.PatronSystemBlocks.Any() ? "Yes" : "No")
BlocksTODO
+ Blocking notes + @if (Model.PatronData.PatronNotes?.BlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) + { + + } + @(!string.IsNullOrWhiteSpace(Model.PatronData.PatronNotes?.BlockingStatusNotes) ? new string(Model.PatronData.PatronNotes?.BlockingStatusNotes.Take(Model.MaxNotesDisplayLength).ToArray()) : "None")
+ Non-blocking notes + @if (Model.PatronData.PatronNotes?.NonBlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) + { + + } + @(!string.IsNullOrWhiteSpace(Model.PatronData.PatronNotes?.NonBlockingStatusNotes) ? new string(Model.PatronData.PatronNotes?.NonBlockingStatusNotes.Take(Model.MaxNotesDisplayLength).ToArray()) : "None")
Proof of address@Model.PatronData.User4
+ @if (Model.Request.ProcessedAt.HasValue) + { + + + + + + + + + +
Processed by + @if (Model.Result.CreatedByUser.IsDeleted == false) + { + @Model.Result.CreatedByUser.Name + } + else + { + @Model.Result.CreatedByUser.Name + } +
Processed at@Model.Request.ProcessedAt
+ } +
+
+ +
+
+
+
+

+ +

+
+
+
+
+
Address(es) in Polaris:
+ @foreach (var address in Model.PatronData.PatronAddresses) + { +
+ @address.FreeTextLabel + @if (!string.IsNullOrWhiteSpace(Model.AddressLookupPath) || !string.IsNullOrWhiteSpace(Model.AssessorLookupPath)) + { + + (Look up) + + + } +
+
+ @address.StreetOne
+ @if (!string.IsNullOrWhiteSpace(address.StreetTwo)) + { + @address.StreetTwo +
+ } + @address.City @address.State, @address.PostalCode
+ County: @address.County +
+ } +
+ @if (!string.IsNullOrWhiteSpace(Model.AddressLookupPath) || !string.IsNullOrWhiteSpace(Model.AssessorLookupPath)) + { +
+ @if (!string.IsNullOrWhiteSpace(Model.AddressLookupPath)) + { +
+ } + @if (!string.IsNullOrWhiteSpace(Model.AssessorLookupPath)) + { +
+ } +
+ } +
+
+
+
+
+
+
+ +
+ + + +
+
+
+ + +
+ +
+ + +
+ +
+
+ Replacement Keys – {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerBarcode}}, {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerName}} +
+
+ + +
+ + + Back +
+
+
+
+ +
+ + + +
+ +@if (Model.PatronData.PatronNotes?.BlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) +{ +
+ @Model.PatronData.PatronNotes?.BlockingStatusNotes +
+} + +@if (Model.PatronData.PatronNotes?.NonBlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) +{ +
+ @Model.PatronData.PatronNotes?.NonBlockingStatusNotes +
+} + +@section scripts { + + + @if (!Model.Request.ProcessedAt.HasValue) + { + + } +} \ No newline at end of file diff --git a/src/Ops.Web/Areas/Services/Views/CardRenewal/Index.cshtml b/src/Ops.Web/Areas/Services/Views/CardRenewal/Index.cshtml new file mode 100644 index 000000000..aeacccbfd --- /dev/null +++ b/src/Ops.Web/Areas/Services/Views/CardRenewal/Index.cshtml @@ -0,0 +1,96 @@ +@model Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal.IndexViewModel + +
+
+

+ Card Renewal Requests + @(Model.IsProcessed ? "Processed" : "Pending") +

+ +
+
+ + + + + + + + + + @if (Model.IsProcessed) + { + + } + + + + @if (Model.ItemCount == 0) + { + + + + } + else + { + @foreach (var request in Model.CardRequests) + { + + + + + @if (Model.IsProcessed) + { + /* + + */ + } + + } + } + +
@(Model.IsProcessed ? "Processed" : "Submitted")BarcodeSame Address?Accepted?
+ No requests found. +
+ + @(Model.IsProcessed? request.ProcessedAt: request.SubmittedAt) + + @request.Barcode@(request.SameAddress ? "Yes" : "No") + @if (request.Accepted == true) + { + + } + else + { + + } +
+ + \ No newline at end of file diff --git a/src/Ops.Web/Startup.cs b/src/Ops.Web/Startup.cs index a57104568..246b1318f 100644 --- a/src/Ops.Web/Startup.cs +++ b/src/Ops.Web/Startup.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Net.Http.Headers; +using Clc.Polaris.Api.Configuration; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -22,6 +23,7 @@ using Ocuda.Ops.Service.Interfaces.Promenade.Services; using Ocuda.Ops.Web.JobScheduling; using Ocuda.Ops.Web.StartupHelper; +using Ocuda.PolarisHelper; using Ocuda.Utility.Abstract; using Ocuda.Utility.Exceptions; using Ocuda.Utility.Keys; @@ -123,6 +125,8 @@ public void ConfigureServices(IServiceCollection services) = new Microsoft.AspNetCore.Localization.RequestCulture(culture); }); + services.Configure(_config.GetSection(PapiSettings.SECTION_NAME)); + services.AddHealthChecks(); switch (_config[Configuration.OpsDistributedCache]) @@ -327,10 +331,15 @@ string cacheDiscriminator // helpers services.AddScoped(); services.AddScoped(); + services.AddScoped(); // repositories services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Ops.Web/Views/Shared/_Layout.cshtml b/src/Ops.Web/Views/Shared/_Layout.cshtml index 823d1a2ef..22606d704 100644 --- a/src/Ops.Web/Views/Shared/_Layout.cshtml +++ b/src/Ops.Web/Views/Shared/_Layout.cshtml @@ -130,6 +130,13 @@ Book a Librarian + + + Card Renewal + GetPatronCodeNameAsync(int patronCodeId); + PatronData GetPatronData(string barcode, string password); + PatronData GetPatronDataOverride(string barcode); + RenewRegistrationResult RenewPatronRegistration(string barcode, string email); + } +} diff --git a/src/PolarisHelper/Models/RenewRegistrationResult.cs b/src/PolarisHelper/Models/RenewRegistrationResult.cs new file mode 100644 index 000000000..7e8fd849d --- /dev/null +++ b/src/PolarisHelper/Models/RenewRegistrationResult.cs @@ -0,0 +1,8 @@ +namespace Ocuda.PolarisHelper.Models +{ + public class RenewRegistrationResult + { + public bool EmailNotUpdated { get; set; } + public bool Success { get; set; } + } +} diff --git a/src/PolarisHelper/PolarisHelper.cs b/src/PolarisHelper/PolarisHelper.cs new file mode 100644 index 000000000..64fa679db --- /dev/null +++ b/src/PolarisHelper/PolarisHelper.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Clc.Polaris.Api; +using Clc.Polaris.Api.Configuration; +using Clc.Polaris.Api.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ocuda.PolarisHelper.Models; +using Ocuda.Utility.Keys; +using Ocuda.Utility.Services.Interfaces; + +namespace Ocuda.PolarisHelper +{ + public class PolarisHelper : IPolarisHelper + { + private const int CacheCodesHours = 1; + private const int PAPIInvalidEmailErrorCode = -3518; + + private readonly IOcudaCache _cache; + private readonly ILogger _logger; + private readonly IPapiClient _papiClient; + + public PolarisHelper(IOcudaCache cache, + ILogger logger, + IOptions options) + { + + ArgumentNullException.ThrowIfNull(cache); + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(options); + + _cache = cache; + _logger = logger; + _papiClient = new PapiClient(options.Value); + } + + public PatronValidateResult AuthenticatePatron(string barcode, string password) + { + return _papiClient.PatronValidate(barcode, password)?.Data; + } + + public async Task GetPatronCodeNameAsync(int patronCodeId) + { + var patronCodes = await _cache + .GetObjectFromCacheAsync>(Cache.PolarisPatronCodes); + + if (patronCodes == null) + { + patronCodes = _papiClient.PatronCodesGet().Data.PatronCodesRows; + await _cache.SaveToCacheAsync(Cache.PolarisPatronCodes, + patronCodes, + CacheCodesHours); + } + + return patronCodes + .Where(_ => _.PatronCodeID == patronCodeId) + .Select(_ => _.Description) + .SingleOrDefault(); + } + + //public async Task<>- + + public PatronData GetPatronData(string barcode, string password) + { + return _papiClient.PatronBasicDataGet(barcode, password, true).Data.PatronBasicData; + } + + public PatronData GetPatronDataOverride(string barcode) + { + return _papiClient.PatronBasicDataGet(barcode, addresses: true, notes: true) + .Data + .PatronBasicData; + } + + public RenewRegistrationResult RenewPatronRegistration(string barcode, string email) + { + var date = DateTime.Now.AddYears(1); + var updateParams = new PatronUpdateParams + { + BranchId = _papiClient.OrganizationId, + UserId = _papiClient.UserId, + LogonWorkstationId = _papiClient.WorkstationId, + ExpirationDate = date, + AddrCheckDate = date, + EmailAddress = email + }; + + var renewResult = new RenewRegistrationResult(); + + var updateResults = _papiClient.PatronUpdate(barcode, updateParams); + + if (updateResults.Exception != null) + { + _logger.LogCritical("PAPI call was not successful: {ErrorMessage}", + updateResults.Exception.Message); + } + else if (!updateResults.Response.IsSuccessStatusCode) + { + _logger.LogCritical("PAPI call was not successful after {Elapsed} ms: {StatusCode}", + updateResults.ResponseTime, + updateResults.Response.StatusCode); + } + else + { + if (updateResults.Data.PAPIErrorCode == PAPIInvalidEmailErrorCode) + { + renewResult.EmailNotUpdated = true; + updateParams.EmailAddress = null; + + updateResults = _papiClient.PatronUpdate(barcode, updateParams); + } + + if (updateResults.Data.PAPIErrorCode != 0) + { + _logger.LogCritical("PAPI error after {Elapsed} ms: {PAPIErrorCode}", + updateResults.ResponseTime, + updateResults.Data.PAPIErrorCode); + } + else if (updateResults.Data.PAPIErrorCode == 0) + { + renewResult.Success = true; + if (renewResult.EmailNotUpdated) + { + _logger.LogWarning("Unable to update email to {EmailAddress} for barcode {Barcode}", + email, + barcode); + } + } + } + + return renewResult; + } + } +} diff --git a/src/PolarisHelper/PolarisHelper.csproj b/src/PolarisHelper/PolarisHelper.csproj new file mode 100644 index 000000000..403b46685 --- /dev/null +++ b/src/PolarisHelper/PolarisHelper.csproj @@ -0,0 +1,25 @@ + + + + Ocuda.PolarisHelper + Maricopa County Library District Web developers + ../../OcudaRuleSet.ruleset + Maricopa County Library District + Copyright 2018 Maricopa County Library District + https://github.com/MCLD/ocuda/blob/develop/LICENSE + Ocuda + Git + https://github.com/MCLD/ocuda/ + Ocuda.PolarisHelper + net8.0 + + + + + + + + + + + diff --git a/src/Promenade.Controllers/CardRenewalController.cs b/src/Promenade.Controllers/CardRenewalController.cs new file mode 100644 index 000000000..f864a99bd --- /dev/null +++ b/src/Promenade.Controllers/CardRenewalController.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Clc.Polaris.Api.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Ocuda.PolarisHelper; +using Ocuda.Promenade.Controllers.Abstract; +using Ocuda.Promenade.Controllers.Filters; +using Ocuda.Promenade.Controllers.ViewModels.CardRenewal; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Promenade.Service; +using Ocuda.Utility.Abstract; + +namespace Ocuda.Promenade.Controllers +{ + [Route("[Controller]")] + [Route("{culture:cultureConstraint?}/[Controller]")] + public class CardRenewalController : BaseController + { + private const string TempDataAddresses = "TempData.Addresses"; + private const string TempDataEmail = "TempData.Email"; + private const string TempDataJuvenile = "TempData.Juvenile"; + private const string TempDataRequest = "TempData.Request"; + private const string TempDataTimeout = "TempData.Timeout"; + + private readonly CardRenewalService _cardRenewalService; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IPolarisHelper _polarisHelper; + private readonly SegmentService _segmentService; + + public CardRenewalController(ServiceFacades.Controller context, + CardRenewalService cardRenewalService, + IDateTimeProvider dateTimeProvider, + IPolarisHelper polarisHelper, + SegmentService segmentService) + : base(context) + { + ArgumentNullException.ThrowIfNull(cardRenewalService); + ArgumentNullException.ThrowIfNull(dateTimeProvider); + ArgumentNullException.ThrowIfNull(polarisHelper); + ArgumentNullException.ThrowIfNull(segmentService); + + _cardRenewalService = cardRenewalService; + _dateTimeProvider = dateTimeProvider; + _polarisHelper = polarisHelper; + _segmentService = segmentService; + } + + public static string Name + { get { return "CardRenewal"; } } + + [HttpGet] + [RestoreModelState] + public async Task Index(string cardNumber) + { + if (TempData.ContainsKey(TempDataTimeout)) + { + ModelState.AddModelError(nameof(IndexViewModel.Invalid), + _localizer[i18n.Keys.Promenade.CardRenewalSessionTimeout]); + TempData.Remove(TempDataTimeout); + } + + var viewModel = new IndexViewModel() + { + Barcode = cardNumber, + ForgotPasswordLink = await _siteSettingService + .GetSettingStringAsync(Models.Keys.SiteSetting.Card.ForgotPasswordLink) + }; + + var cardRenewalSegmentId = await _siteSettingService + .GetSettingIntAsync(Models.Keys.SiteSetting.Card.CardRenewalSegment); + if (cardRenewalSegmentId > 0) + { + viewModel.SegmentText = await _segmentService + .GetSegmentTextBySegmentIdAsync(cardRenewalSegmentId, false); + } + + return View(viewModel); + } + + [HttpPost] + [SaveModelState] + public async Task Index(IndexViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(viewModel); + + if (ModelState.IsValid) + { + var barcode = viewModel.Barcode.Trim() + .Replace(" ", "", StringComparison.OrdinalIgnoreCase); + var password = viewModel.Password.Trim(); + + var patronValidateResult = _polarisHelper.AuthenticatePatron(barcode, password); + + if (patronValidateResult == null) + { + _logger.LogInformation($"Invalid card number or password for Barcode '{barcode}'"); + ModelState.AddModelError(nameof(viewModel.Invalid), + _localizer[i18n.Keys.Promenade.CardRenewalInvalidLogin]); + } + else + { + var pendingRequest = await _cardRenewalService + .GetPendingRequestAsync(patronValidateResult.PatronID); + if (pendingRequest != null) + { + // TODO + // Send to Pending page + } + + var cutoffDays = await _siteSettingService + .GetSettingIntAsync(Models.Keys.SiteSetting.Card.ExpirationCutoffDays); + if (cutoffDays > -1) + { + var renewalAllowedDate = patronValidateResult + .ExpirationDate.Value.AddDays(-cutoffDays).Date; + if (renewalAllowedDate > _dateTimeProvider.Now.Date) + { + ModelState.AddModelError(nameof(viewModel.Invalid), + _localizer[i18n.Keys.Promenade.CardRenewalInvalidCutoff, + cutoffDays, + renewalAllowedDate.ToShortDateString()]); + return RedirectToAction(nameof(Index)); + } + } + + // TODO + // Check patron code for staff ids + + // TODO + // Check patron code for non resident + + // TODO? + // Check patron code for juvenile restricted + + var patronData = _polarisHelper.GetPatronData(barcode, password); + + IEnumerable addresses = patronData.PatronAddresses; + + var acceptedCounties = await _siteSettingService + .GetSettingStringAsync(Models.Keys.SiteSetting.Card.AcceptedCounties); + if (!string.IsNullOrWhiteSpace(acceptedCounties)) + { + var counties = acceptedCounties.Split(",", + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + addresses = addresses.Where(_ => counties.Contains(_.County, + StringComparer.OrdinalIgnoreCase)); + } + addresses = addresses + .DistinctBy(_ => new { _.StreetOne, _.StreetTwo, _.City, _.PostalCode }); + + var request = new CardRenewalRequest + { + Barcode = barcode, + PatronId = patronData.PatronID + }; + + TempData[TempDataAddresses] = JsonSerializer.Serialize(addresses); + TempData[TempDataEmail] = patronData.EmailAddress; + TempData[TempDataRequest] = JsonSerializer.Serialize(request); + + var juvenilePatronCodes = await _siteSettingService + .GetSettingStringAsync(Models.Keys.SiteSetting.Card.JuvenilePatronCodes,true); + if (!string.IsNullOrWhiteSpace(juvenilePatronCodes)) + { + var patronCodeList = juvenilePatronCodes + .Split(",", StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries) + .ToList(); + + foreach (var patronCode in patronCodeList) + { + int patronCodeId; + + if (int.TryParse(patronCode, out patronCodeId)) + { + if (patronValidateResult.PatronCodeID == patronCodeId) + { + TempData[TempDataJuvenile] = true; + return RedirectToAction(nameof(Juvenile)); + } + } + else + { + _logger.LogError($"Invalid juvenile patron code id '{patronCode}'"); + } + } + } + + // TODO + // Check for unpaid fines + + // TODO + // Check for juvenile turned adult + + // TODO? + // Check for student turned adult + + + + return RedirectToAction(nameof(VerifyAddress)); + } + } + return RedirectToAction(nameof(Index)); + } + + [HttpGet("[action]")] + [RestoreModelState] + public IActionResult Juvenile() + { + if (!TempData.ContainsKey(TempDataJuvenile)) + { + return RedirectToAction(nameof(Index)); + } + + return View(); + } + + [HttpPost("[action]")] + [SaveModelState] + public IActionResult Juvenile(JuvenileViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(viewModel); + + if (!TempData.ContainsKey(TempDataJuvenile)) + { + TempData[TempDataTimeout] = true; + return RedirectToAction(nameof(Index)); + } + + if (ModelState.IsValid) + { + var request = JsonSerializer + .Deserialize((string)TempData.Peek(TempDataRequest)); + + request.GuardianBarcode = viewModel.GuardianBarcode; + request.GuardianName = viewModel.GuardianName; + + TempData[TempDataRequest] = JsonSerializer.Serialize(request); + + return RedirectToAction(nameof(VerifyAddress)); + } + + return RedirectToAction(nameof(Juvenile)); + } + + [HttpGet("[action]")] + public async Task Submitted() + { + if (!TempData.ContainsKey(TempDataRequest)) + { + return RedirectToAction(nameof(Index)); + } + + var viewModel = new SubmittedViewModel() + { + Request = JsonSerializer + .Deserialize((string)TempData.Peek(TempDataRequest)) + }; + + var submittedSegmentId = await _siteSettingService + .GetSettingIntAsync(Models.Keys.SiteSetting.Card.SubmittedSegment); + if (submittedSegmentId > 0) + { + viewModel.SegmentText = await _segmentService + .GetSegmentTextBySegmentIdAsync(submittedSegmentId, false); + } + + TempData.Remove(TempDataRequest); + + return View(viewModel); + } + + [HttpGet("[action]")] + [RestoreModelState] + public async Task VerifyAddress() + { + if (!TempData.ContainsKey(TempDataRequest)) + { + return RedirectToAction(nameof(Index)); + } + + var viewModel = new VerifyAddressViewModel() + { + Addresses = JsonSerializer + .Deserialize>((string)TempData.Peek(TempDataAddresses)), + Email = (string)TempData.Peek(TempDataEmail) + }; + + var verifyAddressSegmentId = await _siteSettingService + .GetSettingIntAsync(Models.Keys.SiteSetting.Card.VerifyAddressSegment); + if (verifyAddressSegmentId > 0) + { + viewModel.HeaderSegmentText = await _segmentService + .GetSegmentTextBySegmentIdAsync(verifyAddressSegmentId, false); + } + + if (viewModel.Addresses.Count == 0) + { + var noAddressSegmentId = await _siteSettingService + .GetSettingIntAsync(Models.Keys.SiteSetting.Card.NoAddressSegment); + if (noAddressSegmentId > 0) + { + viewModel.NoAddressSegmentText = await _segmentService + .GetSegmentTextBySegmentIdAsync(noAddressSegmentId, false); + } + } + + return View(viewModel); + } + + [HttpPost("[action]")] + [SaveModelState] + public async Task VerifyAddress(VerifyAddressViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(viewModel); + + if (!TempData.ContainsKey(TempDataRequest)) + { + TempData[TempDataTimeout] = true; + return RedirectToAction(nameof(Index)); + } + + if (ModelState.IsValid) + { + var request = JsonSerializer + .Deserialize((string)TempData.Peek(TempDataRequest)); + + request.Email = viewModel.Email.Trim(); + + if (viewModel.SameAddress) + { + var addresses = JsonSerializer + .Deserialize>((string)TempData.Peek(TempDataAddresses)); + if (addresses.Count > 0) + { + request.SameAddress = true; + } + } + await _cardRenewalService.CreateRequestAsync(request); + + TempData.Remove(TempDataAddresses); + TempData.Remove(TempDataEmail); + + return RedirectToAction(nameof(Submitted)); + } + + return RedirectToAction(nameof(VerifyAddress)); + } + } +} diff --git a/src/Promenade.Controllers/Promenade.Controllers.csproj b/src/Promenade.Controllers/Promenade.Controllers.csproj index f883e7a9d..a76926e79 100644 --- a/src/Promenade.Controllers/Promenade.Controllers.csproj +++ b/src/Promenade.Controllers/Promenade.Controllers.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Promenade.Controllers/ViewModels/CardRenewal/IndexViewModel.cs b/src/Promenade.Controllers/ViewModels/CardRenewal/IndexViewModel.cs new file mode 100644 index 000000000..22cbfabb0 --- /dev/null +++ b/src/Promenade.Controllers/ViewModels/CardRenewal/IndexViewModel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility; + +namespace Ocuda.Promenade.Controllers.ViewModels.CardRenewal +{ + public class IndexViewModel + { + [Required(ErrorMessage = ErrorMessage.FieldRequired)] + [DisplayName(i18n.Keys.Promenade.PromptLibraryCardNumber)] + public string Barcode { get; set; } + + public bool Invalid { get; set; } + + public string ForgotPasswordLink { get; set; } + + [Required(ErrorMessage = ErrorMessage.FieldRequired)] + [DisplayName(i18n.Keys.Promenade.PromptPassword)] + public string Password { get; set; } + + public SegmentText SegmentText { get; set; } + } +} diff --git a/src/Promenade.Controllers/ViewModels/CardRenewal/JuvenileViewModel.cs b/src/Promenade.Controllers/ViewModels/CardRenewal/JuvenileViewModel.cs new file mode 100644 index 000000000..bcda0c03e --- /dev/null +++ b/src/Promenade.Controllers/ViewModels/CardRenewal/JuvenileViewModel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility; + +namespace Ocuda.Promenade.Controllers.ViewModels.CardRenewal +{ + public class JuvenileViewModel + { + [Required(ErrorMessage = ErrorMessage.FieldRequired)] + [DisplayName(i18n.Keys.Promenade.PromptGuardianBarcode)] + public string GuardianBarcode { get; set; } + + [Required(ErrorMessage = ErrorMessage.FieldRequired)] + [DisplayName(i18n.Keys.Promenade.PromptGuardianName)] + public string GuardianName { get; set; } + } +} diff --git a/src/Promenade.Controllers/ViewModels/CardRenewal/SubmittedViewModel.cs b/src/Promenade.Controllers/ViewModels/CardRenewal/SubmittedViewModel.cs new file mode 100644 index 000000000..306fad662 --- /dev/null +++ b/src/Promenade.Controllers/ViewModels/CardRenewal/SubmittedViewModel.cs @@ -0,0 +1,10 @@ +using Ocuda.Promenade.Models.Entities; + +namespace Ocuda.Promenade.Controllers.ViewModels.CardRenewal +{ + public class SubmittedViewModel + { + public CardRenewalRequest Request { get; set; } + public SegmentText SegmentText { get; set; } + } +} diff --git a/src/Promenade.Controllers/ViewModels/CardRenewal/VerifyAddressViewModel.cs b/src/Promenade.Controllers/ViewModels/CardRenewal/VerifyAddressViewModel.cs new file mode 100644 index 000000000..97c49d3c8 --- /dev/null +++ b/src/Promenade.Controllers/ViewModels/CardRenewal/VerifyAddressViewModel.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Clc.Polaris.Api.Models; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Utility; + +namespace Ocuda.Promenade.Controllers.ViewModels.CardRenewal +{ + public class VerifyAddressViewModel + { + public List Addresses { get; set; } + + [Required(ErrorMessage = ErrorMessage.FieldRequired)] + [DisplayName(i18n.Keys.Promenade.PromptEmail)] + public string Email { get; set; } + + public SegmentText HeaderSegmentText { get; set; } + public SegmentText NoAddressSegmentText {get; set; } + public bool SameAddress { get; set; } + } +} diff --git a/src/Promenade.Data/Promenade/CardRenewalRequestRepository.cs b/src/Promenade.Data/Promenade/CardRenewalRequestRepository.cs new file mode 100644 index 000000000..23b4b569d --- /dev/null +++ b/src/Promenade.Data/Promenade/CardRenewalRequestRepository.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ocuda.Promenade.Data.ServiceFacade; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Promenade.Service.Interfaces.Repositories; + +namespace Ocuda.Promenade.Data.Promenade +{ + public class CardRenewalRequestRepository + : GenericRepository, ICardRenewalRequestRepository + { + public CardRenewalRequestRepository(Repository repositoryFacade, + ILogger logger) : base(repositoryFacade, logger) + { + } + + public async Task AddSaveAsync(CardRenewalRequest request) + { + await DbSet.AddAsync(request); + await _context.SaveChangesAsync(); + } + + public async Task GetPendingRequestAsync(int patronId) + { + return await DbSet + .AsNoTracking() + .Where(_ => _.PatronId == patronId && !_.IsDiscarded && !_.ProcessedAt.HasValue) + .OrderByDescending(_ => _.SubmittedAt) + .FirstOrDefaultAsync(); + } + } +} diff --git a/src/Promenade.Data/PromenadeContext.cs b/src/Promenade.Data/PromenadeContext.cs index 20c6b10f7..7298eff8d 100644 --- a/src/Promenade.Data/PromenadeContext.cs +++ b/src/Promenade.Data/PromenadeContext.cs @@ -11,6 +11,7 @@ protected PromenadeContext(DbContextOptions options) : base(options) } public DbSet CardDetails { get; } + public DbSet CardRenewalRequests { get; set; } public DbSet Cards { get; } public DbSet CarouselButtonLabels { get; } public DbSet CarouselButtonLabelTexts { get; } diff --git a/src/Promenade.Models/Defaults/SiteSetting.cs b/src/Promenade.Models/Defaults/SiteSetting.cs index 97bb50002..8ad080f57 100644 --- a/src/Promenade.Models/Defaults/SiteSetting.cs +++ b/src/Promenade.Models/Defaults/SiteSetting.cs @@ -9,6 +9,23 @@ public static class SiteSettings public static IEnumerable Get { get; } = new[] { #region Card + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Accepted counties for card renewal addresses, comma delimited", + Id = Keys.SiteSetting.Card.AcceptedCounties, + Name = "Accepted counties", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Segment to show on the card renewal page", + Id = Keys.SiteSetting.Card.CardRenewalSegment, + Name = "Card renewal segment", + Type = SiteSettingType.Int, + Value = "-1" + }, new SiteSetting { Category = nameof(Keys.SiteSetting.Card), @@ -27,6 +44,58 @@ public static class SiteSettings Type = SiteSettingType.Int, Value = "-1" }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Number of days before a cards expiration that it's eligible for online renewal", + Id = Keys.SiteSetting.Card.ExpirationCutoffDays, + Name = "Card renewal expiration cutoff days", + Type= SiteSettingType.Int, + Value = "-1" + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Link for 'Forgot Password' on card renewal page", + Id = Keys.SiteSetting.Card.ForgotPasswordLink, + Name = "Forgot Password link", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Juvenile patron code ids, comma delimited", + Id = Keys.SiteSetting.Card.JuvenilePatronCodes, + Name = "Juvenile patron codes", + Type = SiteSettingType.StringNullable + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Segment to show on the verify address page when there's no valid addresses", + Id = Keys.SiteSetting.Card.NoAddressSegment, + Name = "No address segment", + Type = SiteSettingType.Int, + Value = "-1" + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Segment to show on the verify address page header", + Id = Keys.SiteSetting.Card.SubmittedSegment, + Name = "Submitted segment", + Type = SiteSettingType.Int, + Value = "-1" + }, + new SiteSetting + { + Category = nameof(Keys.SiteSetting.Card), + Description = "Segment to show on the submitted page", + Id = Keys.SiteSetting.Card.VerifyAddressSegment, + Name = "Verify address segment", + Type = SiteSettingType.Int, + Value = "-1" + }, #endregion #region Contact diff --git a/src/Promenade.Models/Entities/CardRenewalRequest.cs b/src/Promenade.Models/Entities/CardRenewalRequest.cs new file mode 100644 index 000000000..cc755dcd9 --- /dev/null +++ b/src/Promenade.Models/Entities/CardRenewalRequest.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Ocuda.Promenade.Models.Entities +{ + public class CardRenewalRequest + { + [Required] + public string Barcode { get; set; } + + [Required] + public string Email { get; set; } + + public string GuardianBarcode { get; set; } + public string GuardianName { get; set; } + + [Key] + [Required] + public int Id { get; set; } + + public bool IsDiscarded { get; set; } + + public int LanguageId { get; set; } + public Language Language { get; set; } + + public int PatronId { get; set; } + public DateTime? ProcessedAt {get;set;} + public bool SameAddress { get; set; } + public DateTime SubmittedAt { get; set; } + } +} diff --git a/src/Promenade.Models/Keys/SiteSetting.cs b/src/Promenade.Models/Keys/SiteSetting.cs index 5fad53415..ca7beb6b2 100644 --- a/src/Promenade.Models/Keys/SiteSetting.cs +++ b/src/Promenade.Models/Keys/SiteSetting.cs @@ -7,9 +7,17 @@ namespace SiteSetting Justification = "No reason to compare these site setting keys")] public struct Card { + public const string AcceptedCounties = "Card.AcceptedCounties"; + public const string CardRenewalSegment = "Card.CardRenewalSegment"; public const string EmployeeCardSegment = "Card.EmployeeCardSegment"; public const string EmployeeCardThanksPage = "Card.EmployeeCardThanksPage"; + public const string ExpirationCutoffDays = "Card.ExpirationCutoffDays"; + public const string ForgotPasswordLink = "Card.ForgotPasswordLink"; + public const string JuvenilePatronCodes = "Card.JuvenilePatronCodes"; + public const string NoAddressSegment = "Card.NoAddressSegment"; + public const string SubmittedSegment = "Card.SubmittedSegment"; + public const string VerifyAddressSegment = "Card.VerifyAddressSegment"; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", diff --git a/src/Promenade.Service/CardRenewalService.cs b/src/Promenade.Service/CardRenewalService.cs new file mode 100644 index 000000000..4eedb6a6c --- /dev/null +++ b/src/Promenade.Service/CardRenewalService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ocuda.Promenade.Models.Entities; +using Ocuda.Promenade.Service.Abstract; +using Ocuda.Promenade.Service.Interfaces.Repositories; +using Ocuda.Utility.Abstract; + +namespace Ocuda.Promenade.Service +{ + public class CardRenewalService : BaseService + { + private ICardRenewalRequestRepository _cardRenewalRequestRepository; + + public CardRenewalService(ILogger logger, + IDateTimeProvider dateTimeProvider, + ICardRenewalRequestRepository cardRenewalRequestRepository) + : base(logger, dateTimeProvider) + { + ArgumentNullException.ThrowIfNull(cardRenewalRequestRepository); + + _cardRenewalRequestRepository = cardRenewalRequestRepository; + } + + public async Task CreateRequestAsync(CardRenewalRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + request.SubmittedAt = _dateTimeProvider.Now; + await _cardRenewalRequestRepository.AddSaveAsync(request); + } + + public async Task GetPendingRequestAsync(int patronId) + { + return await _cardRenewalRequestRepository.GetPendingRequestAsync(patronId); + } + } +} diff --git a/src/Promenade.Service/Interfaces/Repositories/ICardRenewalRequestRepository.cs b/src/Promenade.Service/Interfaces/Repositories/ICardRenewalRequestRepository.cs new file mode 100644 index 000000000..bba1b865e --- /dev/null +++ b/src/Promenade.Service/Interfaces/Repositories/ICardRenewalRequestRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Ocuda.Promenade.Models.Entities; + +namespace Ocuda.Promenade.Service.Interfaces.Repositories +{ + public interface ICardRenewalRequestRepository : IGenericRepository + { + Task AddSaveAsync(CardRenewalRequest request); + Task GetPendingRequestAsync(int patronId); + } +} diff --git a/src/Promenade.Web/Startup.cs b/src/Promenade.Web/Startup.cs index d38364f19..1b5faf740 100644 --- a/src/Promenade.Web/Startup.cs +++ b/src/Promenade.Web/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Clc.Polaris.Api.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; @@ -20,6 +21,7 @@ using Microsoft.Net.Http.Headers; using Ocuda.i18n; using Ocuda.i18n.RouteConstraint; +using Ocuda.PolarisHelper; using Ocuda.Promenade.Controllers; using Ocuda.Promenade.Data; using Ocuda.Promenade.Service; @@ -373,6 +375,8 @@ string cacheDiscriminator _.LowercaseUrls = true; }); + services.Configure(_config.GetSection(PapiSettings.SECTION_NAME)); + // service facades services.AddScoped(typeof(Controllers.ServiceFacades.Controller<>)); services.AddScoped(typeof(Controllers.ServiceFacades.PageController)); @@ -382,6 +386,7 @@ string cacheDiscriminator services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddHttpClient(); @@ -392,6 +397,8 @@ string cacheDiscriminator // repositories services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); // promenade services + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Promenade.Web/Views/CardRenewal/Index.cshtml b/src/Promenade.Web/Views/CardRenewal/Index.cshtml new file mode 100644 index 000000000..fea16e84a --- /dev/null +++ b/src/Promenade.Web/Views/CardRenewal/Index.cshtml @@ -0,0 +1,65 @@ +@model Ocuda.Promenade.Controllers.ViewModels.CardRenewal.IndexViewModel + +@if (Model?.SegmentText != null) +{ + if (!string.IsNullOrEmpty(Model.SegmentText.Header)) + { +

@Model.SegmentText.Header

+ } + if (!string.IsNullOrEmpty(Model.SegmentText.Text)) + { +
@Html.Raw(Model.SegmentText.Text)
+ } +} +else +{ +

@Localizer[Promenade.RenewYourCard]

+} + +@if (ViewData.ModelState[nameof(Model.Invalid)]?.ValidationState + == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) +{ +
+
+
+ + +
+
+
+} + +
diff --git a/src/Promenade.Web/Views/CardRenewal/Juvenile.cshtml b/src/Promenade.Web/Views/CardRenewal/Juvenile.cshtml new file mode 100644 index 000000000..810a1f08a --- /dev/null +++ b/src/Promenade.Web/Views/CardRenewal/Juvenile.cshtml @@ -0,0 +1,21 @@ +@model Ocuda.Promenade.Controllers.ViewModels.CardRenewal.JuvenileViewModel + +
+
+
+ + +
+ +
+
+
+
diff --git a/src/Promenade.Web/Views/CardRenewal/Submitted.cshtml b/src/Promenade.Web/Views/CardRenewal/Submitted.cshtml new file mode 100644 index 000000000..475deba36 --- /dev/null +++ b/src/Promenade.Web/Views/CardRenewal/Submitted.cshtml @@ -0,0 +1,14 @@ +@model Ocuda.Promenade.Controllers.ViewModels.CardRenewal.SubmittedViewModel + +@if (Model?.SegmentText != null) +{ + if (!string.IsNullOrEmpty(Model.SegmentText.Header)) + { +

@Model.SegmentText.Header

+ } + @Html.Raw(Model.SegmentText.Text) +} +else +{ +

@Localizer[Promenade.CardRenewalSubmitted]

+} \ No newline at end of file diff --git a/src/Promenade.Web/Views/CardRenewal/VerifyAddress.cshtml b/src/Promenade.Web/Views/CardRenewal/VerifyAddress.cshtml new file mode 100644 index 000000000..0de65b4b4 --- /dev/null +++ b/src/Promenade.Web/Views/CardRenewal/VerifyAddress.cshtml @@ -0,0 +1,86 @@ +@model Ocuda.Promenade.Controllers.ViewModels.CardRenewal.VerifyAddressViewModel + +@if (Model?.HeaderSegmentText != null) +{ + if (!string.IsNullOrEmpty(Model.HeaderSegmentText.Header)) + { +

@Model.HeaderSegmentText.Header

+ } + if (!string.IsNullOrEmpty(Model.HeaderSegmentText.Text)) + { +
@Html.Raw(Model.HeaderSegmentText.Text)
+ } +} +else +{ +

@Localizer[Promenade.VerifyYourAddress]

+} + +
+
+
+ +
+
+ @if (Model.Addresses.Count > 0) + { +
+ @Localizer[Promenade.CardRenewalSameAddress] + @foreach (var address in Model.Addresses) + { +
+
@address.StreetOne
+ @if (!string.IsNullOrWhiteSpace(address.StreetTwo)) + { +
@address.StreetTwo
+ } + @address.City + @address.PostalCode +
County: @address.County
+
+ } +
+
+ + +
+ } + else + { + @if (Model?.HeaderSegmentText != null) + { + @Html.Raw(Model.NoAddressSegmentText.Text) + } + else + { + @Localizer[Promenade.CardRenewalNoAddress] + } +
+ +
+ } +
+
+
+
+
\ No newline at end of file diff --git a/src/Utility/Keys/Cache.cs b/src/Utility/Keys/Cache.cs index 28b2b3f24..11f1f3be5 100644 --- a/src/Utility/Keys/Cache.cs +++ b/src/Utility/Keys/Cache.cs @@ -2,6 +2,11 @@ { public static class Cache { + /// + /// Cached Polaris patron codes + /// + public static readonly string PolarisPatronCodes = "patroncodes"; + /// /// Date and time of last expired slide purge /// diff --git a/src/i18n/Keys/Promenade.cs b/src/i18n/Keys/Promenade.cs index cddf5ba32..b38150cbd 100644 --- a/src/i18n/Keys/Promenade.cs +++ b/src/i18n/Keys/Promenade.cs @@ -13,6 +13,14 @@ public static class Promenade public const string ButtonSubmit = "Submit"; public const string ButtonSubscribe = "Subscribe"; public const string ButtonUseMyLocation = "Use my location"; + public const string CardRenewalInvalidCutoff = "Thanks for your submission! Your card has more than {0} days until it expires, please come back to renew it any time after {1}"; + public const string CardRenewalInvalidLogin = "You have entered an invalid card number or password"; + public const string CardRenewalNoAddress = "With no valid address on file your renewal request may not be eligible for a full renewal."; + public const string CardRenewalSameAddress = "Do you still live at an address listed below?"; + public const string CardRenewalSameAddressNo = "No, I have moved"; + public const string CardRenewalSameAddressYes = "Yes, this is my address"; + public const string CardRenewalSessionTimeout = "Unfortunately your session has timed out. Please resubmit your barcode and password."; + public const string CardRenewalSubmitted = "Renewal request submitted"; public const string ConnectSocialMedia = "Connect with us on social media!"; public const string ConnectSocialOn = "Connect with us on {0}"; public const string ContactInformation = "Contact Information"; @@ -99,14 +107,17 @@ public static class Promenade public const string PromptEmployeeNumber = "Employee Number"; public const string PromptExperience = "Why do you want to volunteer? Describe any previous volunteer experience."; public const string PromptFirstName = "First Name"; + public const string PromptGuardianBarcode = "Parent/Guardian Barcode"; public const string PromptGuardianEmail = "Parent/Guardian Email"; public const string PromptGuardianName = "Parent/Guardian Name"; public const string PromptGuardianPhone = "Parent/Guardian Phone"; public const string PromptLanguage = "Language"; public const string PromptLastName = "Last Name"; - public const string PromptLibraryCardNumber = "Library Card#"; + public const string PromptLibraryCardNumber = "Library Card Number"; + public const string PromptLibraryCardPlease = "Please enter your library card number"; public const string PromptName = "Name"; public const string PromptNotes = "Notes"; + public const string PromptPassword = "Password"; public const string PromptPhone = "Phone"; public const string PromptRequestedDate = "Requested date"; public const string PromptRequestedDateAndTime = "Requested date and time"; @@ -116,6 +127,8 @@ public static class Promenade public const string PromptVolunteerRegularity = "Are you interested in regular volunteer work or certain number of hours?"; public const string PromptWeeklyAvailability = "Weekly Availability"; public const string PromptZipCode = "Zip Code"; + public const string RenewCardForgotPassword = "Forgot your password?"; + public const string RenewYourCard = "Renew your card"; public const string RequiredField = "You must supply a value for: {0}"; public const string RequiredFieldItem = "The {0} field is required."; public const string ScheduleAppointmentDetails = "Here are the details of your appointment:"; @@ -134,6 +147,7 @@ public static class Promenade public const string SignUpForEmailNewsletter = "Sign up for our email newsletter"; public const string SpecialHours = "Special Hours"; public const string TodaysHours = "Today's Hours: {0}"; + public const string VerifyYourAddress = "Verify your address"; public const string ViewShowNotes = "View show notes"; public const string VisitHomePage = "Visit home page"; public const string VolunteerPageTitle = "Volunteer"; From a0ba81cd4e06464b2bafbc7a8772806898a805bf Mon Sep 17 00:00:00 2001 From: Dan Wilcox Date: Thu, 15 Jan 2026 15:16:20 -0700 Subject: [PATCH 03/13] Add further card renewal code --- ocuda.sln | 6 - src/Models/AddressLookupResult.cs | 13 + src/Models/Customer.cs | 1 + src/Models/CustomerBlock.cs | 8 + src/Ops.Controllers/ApiController.cs | 64 ++ .../Areas/Services/CardRenewalController.cs | 189 +++--- .../CardRenewal/DetailsViewModel.cs | 18 +- .../ViewModels/CardRenewal/IndexViewModel.cs | 1 + src/Ops.Controllers/Ops.Controllers.csproj | 2 +- .../Ops/CardRenewalResultRepository.cs | 9 + src/Ops.Models/Defaults/SiteSettings.cs | 22 +- .../Entities/CardRenewalResponse.cs | 3 +- src/Ops.Models/Keys/SiteSetting.cs | 3 +- src/Ops.Service/CardRenewalService.cs | 84 +-- .../ICardRenewalResultRepository.cs | 1 + .../Ops/Services/ICardRenewalService.cs | 3 +- .../Ops/Services/ISiteSettingService.cs | 1 + src/Ops.Service/Keys/CardRenewal.cs | 8 +- .../Models/CardRenewal/ProcessResult.cs | 9 +- src/Ops.Service/SiteSettingService.cs | 14 + .../Services/Views/CardRenewal/Details.cshtml | 366 +++++++----- .../Services/Views/CardRenewal/Index.cshtml | 27 +- src/Ops.Web/Startup.cs | 8 +- src/PolarisHelper/IPolarisHelper.cs | 17 +- src/PolarisHelper/PolarisContext.cs | 9 + src/PolarisHelper/PolarisHelper.cs | 175 +++++- src/PolarisHelper/PolarisHelper.csproj | 2 + .../CardRenewalController.cs | 548 +++++++++++++----- .../CardRenewal/JuvenileViewModel.cs | 2 + .../CardRenewal/SubmittedViewModel.cs | 10 - .../CardRenewal/VerifyAddressViewModel.cs | 4 +- .../Promenade/CardRenewalRequestRepository.cs | 4 +- src/Promenade.Models/Defaults/SiteSetting.cs | 144 +++-- .../Entities/CardRenewalRequest.cs | 17 +- src/Promenade.Models/Keys/SiteSetting.cs | 30 +- src/Promenade.Models/Promenade.Models.csproj | 1 + src/Promenade.Service/CardRenewalService.cs | 13 +- .../ICardRenewalRequestRepository.cs | 2 +- src/Promenade.Web/Startup.cs | 3 - .../Views/CardRenewal/Index.cshtml | 9 +- .../Views/CardRenewal/Juvenile.cshtml | 14 + .../Views/CardRenewal/NotConfigured.cshtml | 14 + .../Views/CardRenewal/Submitted.cshtml | 13 +- .../Views/CardRenewal/UnableToRenew.cshtml | 14 + .../Views/CardRenewal/VerifyAddress.cshtml | 21 +- src/i18n/Keys/Promenade.cs | 12 +- 46 files changed, 1396 insertions(+), 542 deletions(-) create mode 100644 src/Models/AddressLookupResult.cs create mode 100644 src/Models/CustomerBlock.cs create mode 100644 src/PolarisHelper/PolarisContext.cs delete mode 100644 src/Promenade.Controllers/ViewModels/CardRenewal/SubmittedViewModel.cs create mode 100644 src/Promenade.Web/Views/CardRenewal/NotConfigured.cshtml create mode 100644 src/Promenade.Web/Views/CardRenewal/UnableToRenew.cshtml diff --git a/ocuda.sln b/ocuda.sln index 072884408..983002e77 100644 --- a/ocuda.sln +++ b/ocuda.sln @@ -91,8 +91,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Models", "src\Models\Models EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolarisHelper", "src\PolarisHelper\PolarisHelper.csproj", "{E11E3598-BE6D-C9D8-D480-4EE48E7F4120}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Clc.Polaris.Api", "..\polaris-api-csharp\src\polaris-api-csharp\Clc.Polaris.Api.csproj", "{85306281-EC61-C4E5-8FC4-3CF8E64C83D5}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -179,10 +177,6 @@ Global {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Debug|Any CPU.Build.0 = Debug|Any CPU {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Release|Any CPU.ActiveCfg = Release|Any CPU {E11E3598-BE6D-C9D8-D480-4EE48E7F4120}.Release|Any CPU.Build.0 = Release|Any CPU - {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85306281-EC61-C4E5-8FC4-3CF8E64C83D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Models/AddressLookupResult.cs b/src/Models/AddressLookupResult.cs new file mode 100644 index 000000000..f408da017 --- /dev/null +++ b/src/Models/AddressLookupResult.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Ocuda.Models +{ + public class AddressLookupResult + { + public string Error { get; set; } + public IEnumerable Residents { get; set; } + public string PostalCode { get; set; } + public string StreetAddress1 { get; set; } + public bool Success { get; set; } + } +} diff --git a/src/Models/Customer.cs b/src/Models/Customer.cs index aff02ee52..326d98f98 100644 --- a/src/Models/Customer.cs +++ b/src/Models/Customer.cs @@ -9,6 +9,7 @@ public class Customer public DateTime? AddressVerificationDate { get; set; } public DateTime? BirthDate { get; set; } public string BlockingNotes { get; set; } + public double ChargeBalance { get; set; } public int CustomerCodeId { get; set; } public string CustomerIdNumber { get; set; } public string EmailAddress { get; set; } diff --git a/src/Models/CustomerBlock.cs b/src/Models/CustomerBlock.cs new file mode 100644 index 000000000..ebd64a3f7 --- /dev/null +++ b/src/Models/CustomerBlock.cs @@ -0,0 +1,8 @@ +namespace Ocuda.Models +{ + public class CustomerBlock + { + public int? BlockId { get; set; } + public string Description { get; set; } + } +} diff --git a/src/Ops.Controllers/ApiController.cs b/src/Ops.Controllers/ApiController.cs index 8ff0b41d5..f0558125e 100644 --- a/src/Ops.Controllers/ApiController.cs +++ b/src/Ops.Controllers/ApiController.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; +using System.Web; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Ocuda.Models; using Ocuda.Ops.Controllers.Areas.ContentManagement; using Ocuda.Ops.Models; using Ocuda.Ops.Models.Entities; @@ -10,6 +15,7 @@ using Ocuda.Ops.Service.Interfaces.Ops.Services; using Ocuda.Utility.Exceptions; using Ocuda.Utility.Keys; +using Org.BouncyCastle.Utilities.Encoders; using Serilog.Context; namespace Ocuda.Ops.Controllers @@ -20,36 +26,47 @@ public class ApiController : Controller private readonly IApiKeyService _apiKeyService; private readonly IAuthorizationService _authorizationService; private readonly IDigitalDisplayService _digitalDisplayService; + private readonly HttpClient _httpClient; private readonly ILdapService _ldapService; private readonly ILogger _logger; private readonly IPermissionGroupService _permissionGroupService; + private readonly ISiteSettingService _siteSettingService; private readonly IUserService _userService; public ApiController(IApiKeyService apiKeyService, IAuthorizationService authorizationService, IDigitalDisplayService digitalDisplayService, + HttpClient httpClient, ILdapService ldapService, ILogger logger, IPermissionGroupService permissionGroupService, + ISiteSettingService siteSettingService, IUserService userService) { ArgumentNullException.ThrowIfNull(apiKeyService); ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(digitalDisplayService); + ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(ldapService); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(permissionGroupService); + ArgumentNullException.ThrowIfNull(siteSettingService); ArgumentNullException.ThrowIfNull(userService); _apiKeyService = apiKeyService; _authorizationService = authorizationService; _digitalDisplayService = digitalDisplayService; + _httpClient = httpClient; _ldapService = ldapService; _logger = logger; _permissionGroupService = permissionGroupService; + _siteSettingService = siteSettingService; _userService = userService; } + public static string Name + { get { return "Api"; } } + private static JsonResponse ErrorJobResult(string message) { return new JsonResponse @@ -60,6 +77,53 @@ private static JsonResponse ErrorJobResult(string message) }; } + public async Task AddressLookup(string address, string zip) + { + var addressLookupPath = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .AddressLookupUrl); + + var queryParams = new Dictionary + { + { nameof(address), HttpUtility.UrlEncode(address) }, + { nameof(zip), HttpUtility.UrlEncode(zip) } + }; + var parameterString = string.Join('&', queryParams.Select(_ => $"{_.Key}={_.Value}")); + + var queryUri = new UriBuilder(addressLookupPath) { Query = parameterString }.Uri; + + using var response = await _httpClient.GetAsync(queryUri); + + if (response.IsSuccessStatusCode) + { + using var responseStream = await response.Content.ReadAsStreamAsync(); + + try + { + return await JsonSerializer.DeserializeAsync( + responseStream, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + catch (JsonException jex) + { + _logger.LogError(jex, "Error decoding JSON: {ErrorMessage}", jex.Message); + } + } + else + { + _logger.LogError("Address lookup returned status code {StatusCode} for parameters {Parameters}", + response.StatusCode, + parameterString); + } + + return null; + } + #region Slide Upload [HttpPost("[action]")] diff --git a/src/Ops.Controllers/Areas/Services/CardRenewalController.cs b/src/Ops.Controllers/Areas/Services/CardRenewalController.cs index bf70502af..f41a3d1f4 100644 --- a/src/Ops.Controllers/Areas/Services/CardRenewalController.cs +++ b/src/Ops.Controllers/Areas/Services/CardRenewalController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; @@ -12,6 +11,7 @@ using Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal; using Ocuda.Ops.Controllers.Filters; using Ocuda.Ops.Models; +using Ocuda.Ops.Models.Entities; using Ocuda.Ops.Service.Filters; using Ocuda.Ops.Service.Interfaces.Ops.Services; using Ocuda.Ops.Service.Interfaces.Promenade.Services; @@ -24,22 +24,28 @@ namespace Ocuda.Ops.Controllers.Areas.Services [Route("[area]/[controller]")] public class CardRenewalController : BaseController { + private const string leapPatronRecordsPath = "/records"; + private readonly ICardRenewalRequestService _cardRenewalRequestService; private readonly ICardRenewalService _cardRenewalService; + private readonly ILanguageService _languageService; private readonly IPolarisHelper _polarisHelper; public CardRenewalController(ServiceFacades.Controller context, ICardRenewalRequestService cardRenewalRequestService, ICardRenewalService cardRenewalService, + ILanguageService languageService, IPolarisHelper polarisHelper) : base(context) { ArgumentNullException.ThrowIfNull(cardRenewalRequestService); ArgumentNullException.ThrowIfNull(cardRenewalService); + ArgumentNullException.ThrowIfNull(languageService); ArgumentNullException.ThrowIfNull(polarisHelper); _cardRenewalRequestService = cardRenewalRequestService; _cardRenewalService = cardRenewalService; + _languageService = languageService; _polarisHelper = polarisHelper; } @@ -50,6 +56,11 @@ public static string Name [RestoreModelState] public async Task Details(int id) { + if (!_polarisHelper.IsConfigured) + { + return RedirectToAction(nameof(Index)); + } + var request = await _cardRenewalRequestService.GetRequestAsync(id); if (request == null) @@ -57,27 +68,31 @@ public async Task Details(int id) return RedirectToAction(nameof(Index)); } - var patronData = _polarisHelper.GetPatronDataOverride(request.Barcode); + request.Language = await _languageService.GetActiveByIdAsync(request.LanguageId); + + var customer = _polarisHelper.GetCustomerDataOverride(request.Barcode); var viewModel = new DetailsViewModel { - AddressLookupPath = await _siteSettingService.GetSettingStringAsync(Models - .Keys - .SiteSetting - .CardRenewal - .AddressLookupUrl), - AssessorLookupPath = await _siteSettingService.GetSettingStringAsync(Models + AddressLookupUrlSet = !string.IsNullOrWhiteSpace( + await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .AddressLookupUrl)), + AssessorLookupUrl = await _siteSettingService.GetSettingStringAsync(Models .Keys .SiteSetting .CardRenewal .AssessorLookupUrl), - PatronCode = await _polarisHelper.GetPatronCodeNameAsync(patronData.PatronCodeID), - PatronData = patronData, - PatronName = $"{patronData.NameFirst} {patronData.NameLast}", + Customer = customer, + CustomerCode = await _polarisHelper + .GetCustomerCodeNameAsync(customer.CustomerCodeId), + CustomerName = $"{customer.NameFirst} {customer.NameLast}", Request = request }; - if (!viewModel.Request.ProcessedAt.HasValue) + if (!request.ProcessedAt.HasValue) { var responses = await _cardRenewalService.GetAvailableResponsesAsync(); viewModel.ResponseList = responses.Select(_ => new SelectListItem @@ -88,8 +103,40 @@ public async Task Details(int id) } else { - viewModel.Result = await _cardRenewalService - .GetResultForRequestAsync(request.Id); + var result = await _cardRenewalService.GetResultForRequestAsync(request.Id); + result.ResponseText = CommonMark.CommonMarkConverter.Convert(result.ResponseText); + viewModel.Result = result; + } + + var blocks = await _polarisHelper.GetCustomerBlocksAsync(customer.Id); + if (blocks.Count > 0) + { + var ignoredBlocks = await _siteSettingService.GetSettingStringAsync(Models + .Keys + .SiteSetting + .CardRenewal + .IgnoredBlockIds); + if (!string.IsNullOrWhiteSpace(ignoredBlocks)) + { + var ignoredBlockIdList = ignoredBlocks + .Split(',', StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries) + .ToList(); + + foreach (var ignoredBlock in ignoredBlockIdList) + { + if (int.TryParse(ignoredBlock, out int ignoredBlockId)) + { + blocks.RemoveAll(_ => _.BlockId == ignoredBlockId); + } + else + { + _logger.LogError($"Invalid ignored block id '{ignoredBlock}'"); + } + } + } + + viewModel.CustomerBlocks = blocks; } var acceptedCounty = await _siteSettingService.GetSettingStringAsync(Models @@ -97,55 +144,34 @@ public async Task Details(int id) .SiteSetting .CardRenewal .AcceptedCounty); - if (!string.IsNullOrWhiteSpace(acceptedCounty)) { viewModel.AcceptedCounty = acceptedCounty; - viewModel.InCounty = patronData.PatronAddresses.Any(_ => + viewModel.InCounty = customer.Addresses.Any(_ => string.Equals(_.County, acceptedCounty, StringComparison.OrdinalIgnoreCase)); } - var juvenilePatronCodes = await _siteSettingService.GetSettingStringAsync(Models + var chargeLimit = await _siteSettingService.GetSettingDoubleAsync(Models .Keys .SiteSetting .CardRenewal - .JuvenilePatronCodes); - - if (!string.IsNullOrWhiteSpace(juvenilePatronCodes)) + .ChargesLimit); + if (chargeLimit >= 0 && customer.ChargeBalance >= chargeLimit) { - var patronCodeList = juvenilePatronCodes - .Split(",", StringSplitOptions.RemoveEmptyEntries - | StringSplitOptions.TrimEntries) - .ToList(); - foreach (var patronCode in patronCodeList) - { - int patronCodeId; + viewModel.OverChargesLimit = true; + } - if (int.TryParse(patronCode, out patronCodeId)) - { - if (patronData.PatronCodeID == patronCodeId) - { - viewModel.IsJuvenile = true; - - if (patronData.BirthDate.HasValue - && patronData.BirthDate.Value != DateTime.MinValue) - { - DateTime today = DateTime.Today; - var age = today.Year - patronData.BirthDate.Value.Year; - if (patronData.BirthDate.Value > today.AddYears(-age)) - { - age--; - } - viewModel.PatronAge = age; - } - } - } - else - { - _logger.LogError($"Invalid juvenile patron code id '{patronCode}'"); - } + if (!string.IsNullOrWhiteSpace(request.GuardianName) && customer.BirthDate.HasValue + && customer.BirthDate.Value != DateTime.MinValue) + { + DateTime today = DateTime.Today; + var age = today.Year - customer.BirthDate.Value.Year; + if (customer.BirthDate.Value > today.AddYears(-age)) + { + age--; } + viewModel.CustomerAge = age; } var leapPatronUrl = await _siteSettingService.GetSettingStringAsync(Models @@ -153,46 +179,63 @@ public async Task Details(int id) .SiteSetting .CardRenewal .LeapPatronUrl); - if (!string.IsNullOrWhiteSpace(leapPatronUrl)) { - viewModel.LeapPath = leapPatronUrl + request.PatronId; + viewModel.LeapPath = leapPatronUrl + request.CustomerId + leapPatronRecordsPath; } return View(viewModel); } [HttpPost("[action]/{id}")] + [SaveModelState] public async Task Details(DetailsViewModel viewModel) { ArgumentNullException.ThrowIfNull(viewModel); + if (!_polarisHelper.IsConfigured) + { + return RedirectToAction(nameof(Index)); + } + if (ModelState.IsValid) { try { - var request = await _cardRenewalRequestService - .GetRequestAsync(viewModel.RequestId); + var processResult = await _cardRenewalService.ProcessRequestAsync( + viewModel.RequestId, + viewModel.ResponseId.Value, + viewModel.ResponseText, + viewModel.CustomerName); - if (request.ProcessedAt.HasValue) + if (!processResult.EmailSent) { - _logger.LogError($"Attempted to process request {request.Id} which has already been processed."); + ShowAlertDanger($"There was an error sending the email for request {viewModel.RequestId}"); } + if (processResult.Type == CardRenewalResponse.ResponseType.Accept) + { + ShowAlertSuccess($"Request {viewModel.RequestId} has been successfully processed and the record has been updated in Polaris!"); - var result = _polarisHelper.RenewPatronRegistration( - request.Barcode, - request.Email); - - await _cardRenewalService.ProcessRequestAsync( - request.Id, - viewModel.ResponseId.Value, - viewModel.ResponseText, - viewModel.PatronName); + if (processResult.EmailNotUpdated) + { + ShowAlertWarning("Email was not able to be updated"); + return RedirectToAction(nameof(Details), new { viewModel.RequestId }); + } + } + else if (processResult.Type == CardRenewalResponse.ResponseType.Partial) + { + ShowAlertSuccess($"Request {viewModel.RequestId} has been successfully processed, be sure to update the record in Polaris!"); + } + else + { + ShowAlertSuccess($"Request {viewModel.RequestId} has been successfully processed"); + } + return RedirectToAction(nameof(Index)); } catch (OcudaException ex) { - + ShowAlertDanger($"Unable to process card renewal request: {ex.Message}"); } } @@ -203,17 +246,17 @@ await _cardRenewalService.ProcessRequestAsync( } [HttpPost("[action]")] - public async Task Discard(int id) + public async Task Discard(int requestId) { try { - await _cardRenewalService.DiscardRequestAsync(id); - ShowAlertSuccess($"Request {id} has been discarded"); + await _cardRenewalService.DiscardRequestAsync(requestId); + ShowAlertSuccess($"Request {requestId} has been discarded"); } catch (OcudaException ex) { ShowAlertDanger($"Unable to discard request: {ex.Message}"); - return RedirectToAction(nameof(Details), new { id }); + return RedirectToAction(nameof(Details), new { requestId }); } return RedirectToAction(nameof(Index)); @@ -259,6 +302,7 @@ public async Task Index(int? page, bool processed) var viewModel = new IndexViewModel { + APIConfigured = _polarisHelper.IsConfigured, CardRequests = requests.Data, CurrentPage = page.Value, IsProcessed = processed, @@ -270,6 +314,11 @@ public async Task Index(int? page, bool processed) { viewModel.PendingCount = await _cardRenewalRequestService.GetRequestCountAsync(false); viewModel.ProcessedCount = viewModel.ItemCount; + + foreach (var request in viewModel.CardRequests) + { + request.Accepted = await _cardRenewalService.IsRequestAccepted(request.Id); + } } else { diff --git a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs index 48b20d2f9..e5b359291 100644 --- a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs +++ b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/DetailsViewModel.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using Clc.Polaris.Api.Models; using Microsoft.AspNetCore.Mvc.Rendering; +using Ocuda.Models; using Ocuda.Ops.Models.Entities; using Ocuda.Promenade.Models.Entities; @@ -14,18 +14,18 @@ public class DetailsViewModel public CardRenewalRequest Request { get; set; } public CardRenewalResult Result { get; set; } - public PatronData PatronData { get; set; } + public List CustomerBlocks { get; set; } + public Customer Customer { get; set; } public string AcceptedCounty { get; set; } - public string AddressLookupPath { get; set; } - public string AssessorLookupPath { get; set; } + public bool AddressLookupUrlSet { get; set; } + public string AssessorLookupUrl { get; set; } + public int? CustomerAge { get; set; } + public string CustomerCode { get; set; } + public string CustomerName { get; set; } public bool InCounty { get; set; } - public bool IsJuvenile { get; set; } public string LeapPath { get; set; } - public int? PatronAge { get; set; } - public string PatronCode { get; set; } - public string PatronName { get; set; } + public bool OverChargesLimit { get; set; } public IEnumerable ResponseList { get; set; } - public int RequestId { get; set; } [DisplayName("Response")] diff --git a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs index ca729bc0b..6f4e9cd4f 100644 --- a/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs +++ b/src/Ops.Controllers/Areas/Services/ViewModels/CardRenewal/IndexViewModel.cs @@ -7,6 +7,7 @@ namespace Ocuda.Ops.Controllers.Areas.Services.ViewModels.CardRenewal public class IndexViewModel : PaginateModel { public ICollection CardRequests { get; set; } + public bool APIConfigured { get; set; } public bool IsProcessed { get; set; } public int PendingCount { get; set; } public int ProcessedCount { get; set; } diff --git a/src/Ops.Controllers/Ops.Controllers.csproj b/src/Ops.Controllers/Ops.Controllers.csproj index 79ebc663a..3dc985c13 100644 --- a/src/Ops.Controllers/Ops.Controllers.csproj +++ b/src/Ops.Controllers/Ops.Controllers.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Ops.Data/Ops/CardRenewalResultRepository.cs b/src/Ops.Data/Ops/CardRenewalResultRepository.cs index 581edc08b..c5d1e9cff 100644 --- a/src/Ops.Data/Ops/CardRenewalResultRepository.cs +++ b/src/Ops.Data/Ops/CardRenewalResultRepository.cs @@ -23,5 +23,14 @@ public async Task GetForRequestAsync(int requestId) .Where(_ => _.CardRenewalRequestId == requestId) .SingleOrDefaultAsync(); } + + public async Task GetRequestResponseTypeAsync( + int requestId) + { + return await DbSet.AsNoTracking() + .Where(_ => _.CardRenewalRequestId == requestId) + .Select(_ => _.CardRenewalResponse.Type) + .SingleOrDefaultAsync(); + } } } diff --git a/src/Ops.Models/Defaults/SiteSettings.cs b/src/Ops.Models/Defaults/SiteSettings.cs index cb333410a..6954fe4b0 100644 --- a/src/Ops.Models/Defaults/SiteSettings.cs +++ b/src/Ops.Models/Defaults/SiteSettings.cs @@ -39,9 +39,18 @@ public static class SiteSettings }, new SiteSetting { - Id = Keys.SiteSetting.CardRenewal.JuvenilePatronCodes, - Name = "Juvenile patron codes", - Description = "Juvenile patron code ids, comma delimited", + Id = Keys.SiteSetting.CardRenewal.ChargesLimit, + Name = "Charges Limit", + Description = "Charges amount when a warning starts being shown", + Category = "CardRenewal", + Value = "-1", + Type = SiteSettingType.Double + }, + new SiteSetting + { + Id = Keys.SiteSetting.CardRenewal.IgnoredBlockIds, + Name = "Ignored block ids", + Description = "Ids of blocks that won't be included, comma delimited", Category = "CardRenewal", Value = "", Type = SiteSettingType.StringNullable @@ -49,12 +58,13 @@ public static class SiteSettings new SiteSetting { Id = Keys.SiteSetting.CardRenewal.LeapPatronUrl, - Name = "Leap patron record url", - Description = "Leap patron record url with scheme, host and path", + Name = "Leap patron records url", + Description = "Leap patron records url with scheme, host and path", Category = "CardRenewal", Value = "", Type = SiteSettingType.StringNullable }, + #endregion CardRenwal @@ -229,7 +239,7 @@ public static class SiteSettings Name = "Email template id", Description = "Email template id to use when sending a notificaton about a new incident report, 0 is disabled", Category = "Incident", - Value = "0", + Value = "-1", Type = SiteSettingType.Int }, new SiteSetting diff --git a/src/Ops.Models/Entities/CardRenewalResponse.cs b/src/Ops.Models/Entities/CardRenewalResponse.cs index a8842b14d..ccd08f720 100644 --- a/src/Ops.Models/Entities/CardRenewalResponse.cs +++ b/src/Ops.Models/Entities/CardRenewalResponse.cs @@ -25,7 +25,8 @@ public class CardRenewalResponse : Abstract.BaseEntity public enum ResponseType { Accept, - Deny + Deny, + Partial } } } diff --git a/src/Ops.Models/Keys/SiteSetting.cs b/src/Ops.Models/Keys/SiteSetting.cs index 7e7fcaf35..a6c5d7a8d 100644 --- a/src/Ops.Models/Keys/SiteSetting.cs +++ b/src/Ops.Models/Keys/SiteSetting.cs @@ -7,7 +7,8 @@ public static class CardRenewal public static readonly string AcceptedCounty = "CardRenewal.AcceptedCounty"; public static readonly string AddressLookupUrl = "CardRenewal.AddressLookupUrl"; public static readonly string AssessorLookupUrl = "CardRenewal.AssessorLookupUrl"; - public static readonly string JuvenilePatronCodes = "CardRenewal.JuvenilePatronCodes"; + public static readonly string ChargesLimit = "CardRenewal.ChargesLimit"; + public static readonly string IgnoredBlockIds = "CardRenewal.IgnoredBlockIds"; public static readonly string LeapPatronUrl = "CardRenewal.LeapPatronUrl"; } diff --git a/src/Ops.Service/CardRenewalService.cs b/src/Ops.Service/CardRenewalService.cs index 40432a532..c049d5a10 100644 --- a/src/Ops.Service/CardRenewalService.cs +++ b/src/Ops.Service/CardRenewalService.cs @@ -100,8 +100,7 @@ public async Task DiscardRequestAsync(int id) { throw new OcudaException("Request does not exist"); } - - if (request.ProcessedAt.HasValue) + else if (request.ProcessedAt.HasValue) { throw new OcudaException("Request has already been processed"); } @@ -175,16 +174,26 @@ public async Task> GetResponsesAsync() return await _cardRenewalResponseRepository.GetAllAsync(); } - public async Task GetResultForRequestAsync(int requestId) { return await _cardRenewalResultRepository.GetForRequestAsync(requestId); } + public async Task IsRequestAccepted(int requestId) + { + var responseType = await _cardRenewalResultRepository + .GetRequestResponseTypeAsync(requestId); + + return responseType == CardRenewalResponse.ResponseType.Accept; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", + "CA1031:Do not catch general exception types", + Justification = "Show end user error message rather than exception")] public async Task ProcessRequestAsync(int requestId, int responseId, string responseText, - string patronName) + string customerName) { var request = await _cardRenewalRequestRepository .GetByIdAsync(requestId); @@ -193,13 +202,16 @@ public async Task ProcessRequestAsync(int requestId, throw new OcudaException("Request has already been processed."); } - var processResult = new ProcessResult(); - var response = await _cardRenewalResponseRepository.FindAsync(responseId); + var processResult = new ProcessResult + { + Type = response.Type + }; + if (response.Type == CardRenewalResponse.ResponseType.Accept) { - var renewResult = _polarisHelper.RenewPatronRegistration( + var renewResult = _polarisHelper.RenewCustomerRegistration( request.Barcode, request.Email); @@ -215,7 +227,7 @@ public async Task ProcessRequestAsync(int requestId, var tags = new Dictionary { { Keys.CardRenewal.CustomerBarcode, request.Barcode }, - { Keys.CardRenewal.CustomerName, patronName } + { Keys.CardRenewal.CustomerName, customerName } }; var emailDetails = await _emailService.GetDetailsAsync(response.EmailSetupId.Value, @@ -223,14 +235,40 @@ public async Task ProcessRequestAsync(int requestId, tags, responseText); - emailDetails.ToEmailAddress = request.Email; - emailDetails.ToName = patronName; + var now = _dateTimeProvider.Now; + + request.ProcessedAt = now; + _cardRenewalRequestRepository.Update(request); + + var result = new CardRenewalResult + { + CardRenewalRequestId = request.Id, + CardRenewalResponseId = response.Id, + CreatedAt = now, + CreatedBy = GetCurrentUserId(), + ResponseText = emailDetails.BodyText + }; - EmailRecord sentEmail = null; + await _cardRenewalResultRepository.AddAsync(result); + await _cardRenewalResultRepository.SaveAsync(); + await _cardRenewalRequestRepository.SaveAsync(); + + emailDetails.ToEmailAddress = request.Email; + emailDetails.ToName = customerName; try { - sentEmail = await _emailService.SendAsync(emailDetails); + var sentEmail = await _emailService.SendAsync(emailDetails); + if (sentEmail != null) + { + processResult.EmailSent = true; + } + else + { + _logger.LogWarning("Card renewal email (setup {EmailSetupId}) failed sending to {EmailTo}", + response.EmailSetupId.Value, + request.Email); + } } catch (Exception ex) { @@ -239,30 +277,8 @@ public async Task ProcessRequestAsync(int requestId, response.EmailSetupId.Value, emailDetails.ToEmailAddress, ex.Message); - - throw new OcudaException("Unable to send email."); - } - - if (sentEmail != null) - { - var now = _dateTimeProvider.Now; - - request.ProcessedAt = now; - _cardRenewalRequestRepository.Update(request); - - var result = new CardRenewalResult - { - CardRenewalRequestId = request.Id, - CreatedAt = now, - CreatedBy = GetCurrentUserId(), - ResponseText = sentEmail.BodyText - }; - - await _cardRenewalResultRepository.AddAsync(result); - //await _cardRenewalResponseRepository.SaveAsync(); } - return processResult; } diff --git a/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs index 014cc74d1..b81c9c499 100644 --- a/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs +++ b/src/Ops.Service/Interfaces/Ops/Repositories/ICardRenewalResultRepository.cs @@ -6,5 +6,6 @@ namespace Ocuda.Ops.Service.Interfaces.Ops.Repositories public interface ICardRenewalResultRepository : IOpsRepository { Task GetForRequestAsync(int requestId); + Task GetRequestResponseTypeAsync(int requestId); } } diff --git a/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs b/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs index b4ef0323a..d3073dfe4 100644 --- a/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs +++ b/src/Ops.Service/Interfaces/Ops/Services/ICardRenewalService.cs @@ -15,10 +15,11 @@ public interface ICardRenewalService Task> GetResponsesAsync(); Task GetResponseTextAsync(int responseId, int languageId); Task GetResultForRequestAsync(int requestId); + Task IsRequestAccepted(int requestId); Task ProcessRequestAsync(int requestId, int responseId, string responseText, - string patronName); + string customerName); Task UpdateResponseAsync(CardRenewalResponse response); Task UpdateResponseSortOrderAsync(int id, bool increase); } diff --git a/src/Ops.Service/Interfaces/Ops/Services/ISiteSettingService.cs b/src/Ops.Service/Interfaces/Ops/Services/ISiteSettingService.cs index fae8fcf5d..381f0d4cb 100644 --- a/src/Ops.Service/Interfaces/Ops/Services/ISiteSettingService.cs +++ b/src/Ops.Service/Interfaces/Ops/Services/ISiteSettingService.cs @@ -9,6 +9,7 @@ public interface ISiteSettingService Task EnsureSiteSettingsExistAsync(int sysadminId); Task> GetAllAsync(); Task GetSettingBoolAsync(string key); + Task GetSettingDoubleAsync(string key); Task GetSettingIntAsync(string key); Task GetSettingStringAsync(string key); Task UpdateAsync(string key, string value); diff --git a/src/Ops.Service/Keys/CardRenewal.cs b/src/Ops.Service/Keys/CardRenewal.cs index 01bf676e4..bb6bc40b9 100644 --- a/src/Ops.Service/Keys/CardRenewal.cs +++ b/src/Ops.Service/Keys/CardRenewal.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Ocuda.Ops.Service.Keys +namespace Ocuda.Ops.Service.Keys { public class CardRenewal { diff --git a/src/Ops.Service/Models/CardRenewal/ProcessResult.cs b/src/Ops.Service/Models/CardRenewal/ProcessResult.cs index 1c64fd34c..9e3b6329f 100644 --- a/src/Ops.Service/Models/CardRenewal/ProcessResult.cs +++ b/src/Ops.Service/Models/CardRenewal/ProcessResult.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using static Ocuda.Ops.Models.Entities.CardRenewalResponse; namespace Ocuda.Ops.Service.Models.CardRenewal { public class ProcessResult { - public bool APIRenew { get; set; } public bool EmailNotUpdated { get; set; } + public bool EmailSent { get; set; } + public ResponseType Type { get; set; } } } diff --git a/src/Ops.Service/SiteSettingService.cs b/src/Ops.Service/SiteSettingService.cs index 60c68fd3f..9372d844f 100644 --- a/src/Ops.Service/SiteSettingService.cs +++ b/src/Ops.Service/SiteSettingService.cs @@ -90,6 +90,20 @@ public async Task GetSettingBoolAsync(string key) } } + public async Task GetSettingDoubleAsync(string key) + { + var settingValue = await GetSettingValueAsync(key); + + if (double.TryParse(settingValue, out double result)) + { + return result; + } + else + { + throw new OcudaException($"Invalid value for double setting {key}: {settingValue}"); + } + } + public async Task GetSettingIntAsync(string key) { var settingValue = await GetSettingValueAsync(key); diff --git a/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml b/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml index 4e94d1bd8..f113dca38 100644 --- a/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml +++ b/src/Ops.Web/Areas/Services/Views/CardRenewal/Details.cshtml @@ -6,7 +6,7 @@ Record id - @Model.Request.PatronId + @Model.Request.CustomerId @if (!string.IsNullOrEmpty(Model.LeapPath)) { @@ -19,13 +19,17 @@ Request barcode @Model.Request.Barcode + + Request language + @Model.Request.Language.Description + First name - @Model.PatronData.NameFirst + @Model.Customer.NameFirst Last name - @Model.PatronData.NameLast + @Model.Customer.NameLast Email address @@ -33,11 +37,11 @@ Expiration date - @Model.PatronData.ExpirationDate.ToShortDateString() + @Model.Customer.ExpirationDate?.ToShortDateString() - + Address check date - @Model.PatronData.AddrCheckDate?.ToShortDateString() + @Model.Customer.AddressVerificationDate?.ToShortDateString() At the same address @@ -52,18 +56,18 @@ } Patron code - @Model.PatronCode + @Model.CustomerCode - @if (Model.IsJuvenile) + @if (!string.IsNullOrWhiteSpace(Model.Request.GuardianName)) { - @if (Model.PatronAge.HasValue) + @if (Model.CustomerAge.HasValue) { - + } @@ -76,33 +80,45 @@ - + - +
Age@Model.PatronAge@Model.CustomerAge
Polaris guardian's name@Model.PatronData.User3@Model.Customer.UserDefinedField3
Polaris guardian's barcode@Model.PatronData.User2@Model.Customer.UserDefinedField2
}
- + - + - + - + - + - + - + - + - + - +
Charges@Model.PatronData.ChargeBalance.ToString("C")@Model.Customer.ChargeBalance.ToString("C")
Has system blocks@(Model.PatronData.PatronSystemBlocks.Any() ? "Yes" : "No")@(Model.Customer.IsBlocked ? "Yes" : "No")
BlocksTODO + @if (Model.CustomerBlocks?.Count > 0) + { + foreach (var block in Model.CustomerBlocks) + { +
@block.Description
+ } + } + else + { + @:None + } +
Blocking notes - @if (Model.PatronData.PatronNotes?.BlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) + @if (Model.Customer.BlockingNotes?.Length > Model.MaxNotesDisplayLength) { @(!string.IsNullOrWhiteSpace(Model.PatronData.PatronNotes?.BlockingStatusNotes) ? new string(Model.PatronData.PatronNotes?.BlockingStatusNotes.Take(Model.MaxNotesDisplayLength).ToArray()) : "None") + @if (!string.IsNullOrWhiteSpace(Model.Customer.BlockingNotes)) + { + @Model.Customer.BlockingNotes.Substring(0, Model.MaxNotesDisplayLength) + } + else + { + @:None + } +
Non-blocking notes - @if (Model.PatronData.PatronNotes?.NonBlockingStatusNotes?.Length > Model.MaxNotesDisplayLength) + @if (Model.Customer.Notes?.Length > Model.MaxNotesDisplayLength) { @(!string.IsNullOrWhiteSpace(Model.PatronData.PatronNotes?.NonBlockingStatusNotes) ? new string(Model.PatronData.PatronNotes?.NonBlockingStatusNotes.Take(Model.MaxNotesDisplayLength).ToArray()) : "None") + @if (!string.IsNullOrWhiteSpace(Model.Customer.Notes)) + { + @Model.Customer.Notes.Substring(0, Model.MaxNotesDisplayLength) + } + else + { + @:None + } +
Proof of address@Model.PatronData.User4@Model.Customer.UserDefinedField4
@if (Model.Request.ProcessedAt.HasValue) @@ -168,7 +202,7 @@
-
+
-
- - - -
-
-
- - -
+@if (!Model.Request.ProcessedAt.HasValue) +{ + + + -
- - -
+
+
+
+ + +
-
-
- Replacement Keys – {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerBarcode}}, {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerName}} +
+ +
-
+
+
+ Replacement Keys – {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerBarcode}}, {{@Ocuda.Ops.Service.Keys.CardRenewal.CustomerName}} +
+
-
- - - Back +
+ + + Back +
-
- + -
- + + -