diff --git a/ECC.Core.DataContext/Entities/Entities.cs b/ECC.Core.DataContext/Entities/Entities.cs index c4277a74..216a885f 100644 --- a/ECC.Core.DataContext/Entities/Entities.cs +++ b/ECC.Core.DataContext/Entities/Entities.cs @@ -54794,7 +54794,8 @@ public partial class Fields public const string ofm_contactyominame = "ofm_contactyominame"; public const string ofm_current_version = "ofm_current_version"; public const string ofm_current_versionname = "ofm_current_versionname"; - public const string ofm_duration = "ofm_duration"; + public const string ofm_duedate = "ofm_duedate"; + public const string ofm_duration = "ofm_duration"; public const string ofm_end_date = "ofm_end_date"; public const string ofm_facility = "ofm_facility"; public const string ofm_facility_survey_response = "ofm_facility_survey_response"; @@ -55220,8 +55221,24 @@ public string ofm_current_versionname } } } - - [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("ofm_duration")] + + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("ofm_duedate")] + public System.Nullable ofm_duedate + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue>("ofm_duedate"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.SetAttributeValue("ofm_duedate", value); + } + } + + + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("ofm_duration")] public string ofm_duration { [System.Diagnostics.DebuggerNonUserCode()] diff --git a/OFM.Infrastructure.Plugins/ProviderReports/SetDueDate.cs b/OFM.Infrastructure.Plugins/ProviderReports/SetDueDate.cs new file mode 100644 index 00000000..8dc24bc8 --- /dev/null +++ b/OFM.Infrastructure.Plugins/ProviderReports/SetDueDate.cs @@ -0,0 +1,97 @@ +using ECC.Core.DataContext; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace OFM.Infrastructure.Plugins.Provider_Reports +{ + /// + /// Plugin development guide: https://docs.microsoft.com/powerapps/developer/common-data-service/plug-ins + /// Best practices and guidance: https://docs.microsoft.com/powerapps/developer/common-data-service/best-practices/business-logic/ + /// + public class SetDueDate : PluginBase + { + public SetDueDate(string unsecureConfiguration, string secureConfiguration) + : base(typeof(SetDueDate)) + { + // TODO: Implement your custom configuration handling + // https://docs.microsoft.com/powerapps/developer/common-data-service/register-plug-in#set-configuration-data + } + + // Entry point for custom business logic execution + protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext) + { + if (localPluginContext == null) + { + throw new InvalidPluginExecutionException(nameof(localPluginContext), new Dictionary() { ["failedRecordId"] = localPluginContext.Target.Id.ToString() }); + } + + localPluginContext.Trace("Start SetDueDate Plug-in"); + + if (localPluginContext.Target.Contains(ECC.Core.DataContext.ofm_survey_response.Fields.ofm_survey_responseid) && !localPluginContext.Target.Contains(ECC.Core.DataContext.ofm_survey_response.Fields.ofm_current_version)) + { + // Getting latest data to get the value + + using (var crmContext = new DataverseContext(localPluginContext.PluginUserService)) + { + var currentDate = DateTime.UtcNow; + + localPluginContext.Trace($"CurrentDate {currentDate}"); + + //Set the duedate base on fiscal year and report month + + var fiscal_year_ref = localPluginContext.Target.GetAttributeValue(ofm_survey_response.Fields.ofm_fiscal_year); + var report_month_ref = localPluginContext.Target.GetAttributeValue(ofm_survey_response.Fields.ofm_reporting_month); + + + if (fiscal_year_ref != null && report_month_ref != null) + { + var fiscal_year = crmContext.ofm_fiscal_yearSet.Where(year => year.Id == fiscal_year_ref.Id).FirstOrDefault(); + var fiscal_year_start = fiscal_year.GetAttributeValue(ofm_fiscal_year.Fields.ofm_start_date); + var fiscal_year_end = fiscal_year.GetAttributeValue(ofm_fiscal_year.Fields.ofm_end_date); + + //converted to PST to compare + var fiscal_year_start_in_PST = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(fiscal_year_start, "Pacific Standard Time"); + var fiscal_year_end_in_PST = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(fiscal_year_end, "Pacific Standard Time"); + + localPluginContext.Trace($"fiscal_year_start_in_PST {fiscal_year_start_in_PST}"); + localPluginContext.Trace($"fiscal_year_end_in_PST {fiscal_year_end_in_PST}"); + + var report_month = crmContext.ofm_monthSet.Where(month => month.Id == report_month_ref.Id).FirstOrDefault(); + var report_month_name = report_month.GetAttributeValue(ofm_month.Fields.ofm_name); + + localPluginContext.Trace($"report_month_name {report_month_name}"); + + int month_num = DateTime.ParseExact(report_month_name, "MMMM", CultureInfo.CurrentCulture).Month; + var report_month_date = (fiscal_year_start_in_PST <= new DateTime(fiscal_year_start.Year, month_num, 01, 0, 0, 0) && fiscal_year_end_in_PST >= new DateTime(fiscal_year_start.Year, month_num, 01, 0, 0, 0)) ? new DateTime(fiscal_year_start.Year, month_num, 01, 23, 59, 0): new DateTime(fiscal_year_end.Year, month_num, 01, 23, 59, 0); + localPluginContext.Trace($"report_month_date {report_month_date}"); + + var duedateInPST = report_month_date.AddMonths(2).AddDays(-1); + + TimeZoneInfo PSTZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + + var duedateInUTC = TimeZoneInfo.ConvertTimeToUtc(duedateInPST, PSTZone); + + localPluginContext.Trace($"duedateInPST {duedateInPST}"); + localPluginContext.Trace($"duedateInUTC {duedateInUTC}"); + + //Update the duedate + var entity = new ofm_survey_response + { + Id = localPluginContext.Target.Id, + ofm_duedate = duedateInUTC + }; + UpdateRequest updateRequest = new UpdateRequest { Target = entity }; + crmContext.Execute(updateRequest); + } + + localPluginContext.Trace("Completed with no errors."); + } + } + } + } +} \ No newline at end of file diff --git a/OFM.Infrastructure.WebAPI/Extensions/SetupInfo.cs b/OFM.Infrastructure.WebAPI/Extensions/SetupInfo.cs index 33408d24..234a2af3 100644 --- a/OFM.Infrastructure.WebAPI/Extensions/SetupInfo.cs +++ b/OFM.Infrastructure.WebAPI/Extensions/SetupInfo.cs @@ -80,10 +80,10 @@ public static class Payments public static class FundingReports { public const Int16 CloneFundingReportResponseId = 600; - public const string CloneFundingReportResponseName = "Clone Funding Report Response"; + public const string CloneFundingReportResponseName = "Clone Provider Report Responses"; - public const Int16 GeneratePaymentLinesId = 505; - public const string GeneratePaymentLinesName = "Generate Payment Lines"; + public const Int16 CloseDuedReportsId = 605; + public const string CloseDuedReportsName = "Automatically Close Provider Reports at the Due Date"; } public static class Reporting { diff --git a/OFM.Infrastructure.WebAPI/Program.cs b/OFM.Infrastructure.WebAPI/Program.cs index 934d445c..71988eb6 100644 --- a/OFM.Infrastructure.WebAPI/Program.cs +++ b/OFM.Infrastructure.WebAPI/Program.cs @@ -67,6 +67,7 @@ services.AddScoped(); services.AddScoped(); services.AddScoped(); +services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/OFM.Infrastructure.WebAPI/Services/D365WebApi/D365WebApiService.cs b/OFM.Infrastructure.WebAPI/Services/D365WebApi/D365WebApiService.cs index 6fb9d0d3..0e1b0474 100644 --- a/OFM.Infrastructure.WebAPI/Services/D365WebApi/D365WebApiService.cs +++ b/OFM.Infrastructure.WebAPI/Services/D365WebApi/D365WebApiService.cs @@ -3,6 +3,7 @@ using OFM.Infrastructure.WebAPI.Messages; using OFM.Infrastructure.WebAPI.Models; using OFM.Infrastructure.WebAPI.Services.Processes; +using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -113,6 +114,7 @@ public async Task SendBatchMessageAsync(AZAppUser spn, List errors = []; + List results = []; if (batchResponse.IsSuccessStatusCode) batchResponse.HttpResponseMessages.ForEach(async res => @@ -120,6 +122,10 @@ public async Task SendBatchMessageAsync(AZAppUser spn, List()); + } } else { @@ -139,7 +145,7 @@ public async Task SendBatchMessageAsync(AZAppUser spn, List SendBulkEmailTemplateMessageAsync(AZAppUser spn, JsonObject contentBody, Guid? callerObjectId) diff --git a/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P600CloneFundingReportResponse.cs b/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P600CloneFundingReportResponse.cs index 00315b5c..2e0439ab 100644 --- a/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P600CloneFundingReportResponse.cs +++ b/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P600CloneFundingReportResponse.cs @@ -12,10 +12,12 @@ using System.Diagnostics; using System.Net; using System.Net.Http; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices.JavaScript; using System.Text.Json; using System.Text.Json.Nodes; using static OFM.Infrastructure.WebAPI.Extensions.Setup.Process; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace OFM.Infrastructure.WebAPI.Services.Processes.FundingReports; @@ -102,7 +104,7 @@ public async Task GetDataAsync() if (!response.IsSuccessStatusCode) { var responseBody = await response.Content.ReadAsStringAsync(); - _logger.LogError(CustomLogEvent.Process, "Failed to query email reminders with the server error {responseBody}", responseBody.CleanLog()); + _logger.LogError(CustomLogEvent.Process, "Failed to query question response with the server error {responseBody}", responseBody.CleanLog()); return await Task.FromResult(new ProcessData(string.Empty)); } @@ -114,7 +116,7 @@ public async Task GetDataAsync() { if (currentValue?.AsArray().Count == 0) { - _logger.LogInformation(CustomLogEvent.Process, "No emails found with query {requestUri}", RequestUri); + _logger.LogInformation(CustomLogEvent.Process, "No question response found with query {requestUri}", RequestUri); } d365Result = currentValue!; } @@ -157,6 +159,25 @@ public async Task RunProcessAsync(ID365AppUserService appUserService var newFundingReportResponse = await sendNewFundingReportResponseRequestResult.Content.ReadFromJsonAsync(); var newFundingReportResponseId = newFundingReportResponse?["ofm_survey_responseid"]; + //Update the original funding report version number by 1 + var orignialVersionNumber = (int)newFundingReportResponse?["ofm_version_number"]; + var newVersionNumber = orignialVersionNumber + 1; + + var updateFundingReportData = new JsonObject + { + {"ofm_version_number", newVersionNumber } + }; + + var updateFundingReportRequest = JsonSerializer.Serialize(updateFundingReportData); + + var updateFundingReportResult = await d365WebApiService.SendPatchRequestAsync(_appUserService.AZSystemAppUser, $"ofm_survey_responses({_processParams.FundingReport.FundingReportId})", updateFundingReportRequest); + + if (!updateFundingReportResult.IsSuccessStatusCode) + { + var responseBody = await updateFundingReportResult.Content.ReadAsStringAsync(); + _logger.LogError(CustomLogEvent.Process, "Failed to deactivate the record with the server error {responseBody}", responseBody.CleanLog()); + } + //Fetch Question Response from the funding report response var questionResponseData = await GetDataAsync(); @@ -175,6 +196,7 @@ public async Task RunProcessAsync(ID365AppUserService appUserService //Create a new questionResponse var newQuestionResponseRequest = new CreateRequest("ofm_question_responses", questionData); + newQuestionResponseRequest.Headers.Add("Prefer", "return=representation"); questionResponseRequestList.Add(newQuestionResponseRequest); } @@ -188,6 +210,56 @@ public async Task RunProcessAsync(ID365AppUserService appUserService return sendQuestionResponseError.SimpleProcessResult; } + + //Deactivate the copied funding report + var deactivateFundingReportData = new JsonObject + { + {"statuscode", 2 }, + {"statecode", 1 } + }; + + var deactivateFundingReportRequest = JsonSerializer.Serialize(deactivateFundingReportData); + + var deactivateFundingReportRequestResult = await d365WebApiService.SendPatchRequestAsync(_appUserService.AZSystemAppUser, $"ofm_survey_responses({newFundingReportResponseId})", deactivateFundingReportRequest); + + if (!deactivateFundingReportRequestResult.IsSuccessStatusCode) + { + var responseBody = await deactivateFundingReportRequestResult.Content.ReadAsStringAsync(); + _logger.LogError(CustomLogEvent.Process, "Failed to deactivate the record with the server error {responseBody}", responseBody.CleanLog()); + } + + //Deactivate the copied question responses + var createResult = sendquestionResponseRequestBatchResult.Result; + + if(createResult?.Count() > 0) + { + List deactivatedQuestionResponseRequestList = []; + + foreach (var res in createResult) + { + var deactivatedQuestionResponseId = res["ofm_question_responseid"].ToString(); + var deactivateQuestionResponseData = new JsonObject + { + {"statuscode", 2}, + {"statecode", 1 } + }; + deactivatedQuestionResponseRequestList.Add(new D365UpdateRequest(new EntityReference("ofm_question_responses", new Guid(deactivatedQuestionResponseId)), deactivateQuestionResponseData)); + + + } + + var deactivatedQuestionResponseRequestResult = await d365WebApiService.SendBatchMessageAsync(appUserService.AZSystemAppUser, deactivatedQuestionResponseRequestList, null); + + if (deactivatedQuestionResponseRequestResult.Errors.Any()) + { + var deactivatedQuestionResponseError = ProcessResult.Failure(ProcessId, deactivatedQuestionResponseRequestResult.Errors, deactivatedQuestionResponseRequestResult.TotalProcessed, deactivatedQuestionResponseRequestResult.TotalRecords); + _logger.LogError(CustomLogEvent.Process, "Failed to deactivate question response with an error: {error}", JsonValue.Create(deactivatedQuestionResponseError)!.ToString()); + + return deactivatedQuestionResponseError.SimpleProcessResult; + } + + } + } _logger.LogInformation(CustomLogEvent.Process, "A new copy of funding report response is created"); diff --git a/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P605CloseDuedReportsProvider.cs b/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P605CloseDuedReportsProvider.cs new file mode 100644 index 00000000..74f03746 --- /dev/null +++ b/OFM.Infrastructure.WebAPI/Services/Processes/FundingReports/P605CloseDuedReportsProvider.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.Options; +using OFM.Infrastructure.WebAPI.Extensions; +using OFM.Infrastructure.WebAPI.Messages; +using OFM.Infrastructure.WebAPI.Models; +using OFM.Infrastructure.WebAPI.Services.AppUsers; +using OFM.Infrastructure.WebAPI.Services.D365WebApi; +using System.Text.Json.Nodes; + +namespace OFM.Infrastructure.WebAPI.Services.Processes.FundingReports; + +public class P605CloseDuedReportsProvider : ID365ProcessProvider +{ + private readonly ID365AppUserService _appUserService; + private readonly ID365WebApiService _d365webapiservice; + private readonly D365AuthSettings _d365AuthSettings; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + private ProcessData? _data; + private ProcessParameter? _processParams; + + public P605CloseDuedReportsProvider(IOptionsSnapshot d365AuthSettings, ID365AppUserService appUserService, ID365WebApiService d365WebApiService, ILoggerFactory loggerFactory, TimeProvider timeProvider) + { + _appUserService = appUserService; + _d365webapiservice = d365WebApiService; + _d365AuthSettings = d365AuthSettings.Value; + _logger = loggerFactory.CreateLogger(LogCategory.Process); + _timeProvider = timeProvider; + } + + public Int16 ProcessId => Setup.Process.FundingReports.CloseDuedReportsId; + public string ProcessName => Setup.Process.FundingReports.CloseDuedReportsName; + public string RequestUri + { + get + { + // Note: Get the funding report response + var currentDateInUTC = DateTime.UtcNow; + //for reference only + var fetchXml = $""" + + + + + + + + + """; + + var requestUri = $""" + ofm_survey_responses?$filter=(Microsoft.Dynamics.CRM.On(PropertyName='ofm_duedate',PropertyValue='{currentDateInUTC}') and statecode eq 0) + """; + + return requestUri.CleanCRLF(); + } + } + + public async Task GetDataAsync() + { + _logger.LogDebug(CustomLogEvent.Process, "Calling GetData of {nameof}", nameof(P605CloseDuedReportsProvider)); + + if (_data is null) + { + _logger.LogDebug(CustomLogEvent.Process, "Getting with query {requestUri}", RequestUri); + + var response = await _d365webapiservice.SendRetrieveRequestAsync(_appUserService.AZSystemAppUser, RequestUri, isProcess: true); + + if (!response.IsSuccessStatusCode) + { + var responseBody = await response.Content.ReadAsStringAsync(); + _logger.LogError(CustomLogEvent.Process, "Failed to query reports with the server error {responseBody}", responseBody.CleanLog()); + + return await Task.FromResult(new ProcessData(string.Empty)); + } + + var jsonObject = await response.Content.ReadFromJsonAsync(); + + JsonNode d365Result = string.Empty; + if (jsonObject?.TryGetPropertyValue("value", out var currentValue) == true) + { + if (currentValue?.AsArray().Count == 0) + { + _logger.LogInformation(CustomLogEvent.Process, "No reports found with query {requestUri}", RequestUri); + } + d365Result = currentValue!; + } + + _data = new ProcessData(d365Result); + + _logger.LogDebug(CustomLogEvent.Process, "Query Result {_data}", _data.Data.ToJsonString(Setup.s_writeOptionsForLogs)); + } + + return await Task.FromResult(_data); + } + + public async Task RunProcessAsync(ID365AppUserService appUserService, ID365WebApiService d365WebApiService, ProcessParameter processParams) + { + + _processParams = processParams; + + var startTime = _timeProvider.GetTimestamp(); + var localData = await GetDataAsync(); + + if (localData.Data.AsArray().Count == 0) + { + _logger.LogInformation(CustomLogEvent.Process, "Close past due report process completed. No reports found."); + return ProcessResult.Completed(ProcessId).SimpleProcessResult; + } + + List requests = []; + + foreach (var report in localData.Data.AsArray()) + { + var reportId = report!["ofm_survey_responseid"]!.ToString(); + + var deactivateReportBody = new JsonObject + { + {"statuscode", 2 }, + {"statecode", 1 } + }; + + requests.Add(new D365UpdateRequest(new EntityReference("ofm_survey_responses", new Guid(reportId)), deactivateReportBody)); + } + + var batchResult = await d365WebApiService.SendBatchMessageAsync(appUserService.AZSystemAppUser, requests, null); + + var endTime = _timeProvider.GetTimestamp(); + + _logger.LogInformation(CustomLogEvent.Process, "Close past due report process finished in {totalElapsedTime} minutes", _timeProvider.GetElapsedTime(startTime, endTime).TotalMinutes); + + if (batchResult.Errors.Any()) + { + var result = ProcessResult.Failure(ProcessId, batchResult.Errors, batchResult.TotalProcessed, batchResult.TotalRecords); + + _logger.LogError(CustomLogEvent.Process, "Close past due report process finished with an error {error}", JsonValue.Create(result)!.ToJsonString()); + + return result.SimpleProcessResult; + } + + return ProcessResult.Completed(ProcessId).SimpleProcessResult; + } + +} \ No newline at end of file