From 1fa2e3264ba039576bd856a61041430055537f3b Mon Sep 17 00:00:00 2001 From: Oscar Inostroza Date: Fri, 21 Nov 2025 13:50:44 -0700 Subject: [PATCH] FAMS3-4292 fmep inbound support for new options --- .../Mapping/Contructors.cs | 15 +- .../DynamicsAdapter.Web/Mapping/Resolvers.cs | 3 + .../SearchAgency/AgencyRequestController.cs | 83 +++- .../SearchAgency/AgencyRequestService.cs | 23 +- .../searchApi.openapi.json | 18 +- .../Error/DynamicsApiErrorLogger.cs | 30 ++ .../SearchApiRequestService.cs | 409 +++++++++++++----- .../SearchRequest/SSG_SearchRequest.cs | 16 + .../SearchRequest/SearchRequestService.cs | 175 ++++---- .../BcGov.Fams3.SearchApi.Contracts.csproj | 4 +- .../Person/Agency.cs | 9 +- .../Notifier/SearchRequestNotifier.cs | 103 +++-- .../SearchRequest.Adaptor/Program.cs | 2 +- 13 files changed, 655 insertions(+), 235 deletions(-) create mode 100644 app/DynamicsAdapter/Fams3Adapter.Dynamics/Error/DynamicsApiErrorLogger.cs diff --git a/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Contructors.cs b/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Contructors.cs index 35fcad515..ffec64d52 100644 --- a/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Contructors.cs +++ b/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Contructors.cs @@ -10,7 +10,11 @@ public class Contructors { public static SearchRequestEntity ConstructSearchRequestEntity(SearchRequestOrdered src) { - if (src?.Person?.Agency?.RequestId == null) throw new ArgumentNullException("SearchRequestOrdered.Person, Agency or RequestID are not allowed Null."); + if (src?.Person?.Agency?.RequestId == null) + { + throw new ArgumentNullException("SearchRequestOrdered.Person, Agency or RequestID are not allowed Null."); + } + SearchRequestEntity entity = new SearchRequestEntity(); #region agency part entity.AgentEmail = src.Person.Agency.Email; @@ -25,12 +29,14 @@ public static SearchRequestEntity ConstructSearchRequestEntity(SearchRequestOrde else entity.AgentFax = agencyFax?.PhoneNumber + " -" + agencyFax?.Extension; } + if (src.Person.Agency.Agent != null) { Name agentName = src.Person.Agency.Agent; entity.AgentFirstName = agentName.FirstName; entity.AgentLastName = agentName.LastName; } + if (src.Person.Agency.InformationRequested != null) { foreach (InformationRequested info in src.Person.Agency.InformationRequested) @@ -55,9 +61,14 @@ public static SearchRequestEntity ConstructSearchRequestEntity(SearchRequestOrde case InformationRequested.DateOfDeath: entity.DateOfDeathRequested = true; break; case InformationRequested.IA: entity.IAStatusRequested = true; break; case InformationRequested.SafetyConcern: entity.SafetyConcernRequested = true; break; + case InformationRequested.T1taxform: entity.T1taxform = true; break; + case InformationRequested.NoticeofAssessment: entity.NoticeofAssessment = true; break; + case InformationRequested.NoticeofReassessment: entity.NoticeofReassessment = true; break; + case InformationRequested.FinancialOtherIncome: entity.FinancialOtherIncome = true; break; }; } } + if (src.Person?.Agency.RequestId != null) { if (src.Person.Agency.RequestId.Length >= 30) @@ -81,7 +92,6 @@ public static SearchRequestEntity ConstructSearchRequestEntity(SearchRequestOrde } #endregion - if (src.Person.Addresses != null) { Address applicantAddress = src.Person.Addresses.FirstOrDefault
(m => m.Owner == OwnerType.Applicant); @@ -104,6 +114,7 @@ public static SearchRequestEntity ConstructSearchRequestEntity(SearchRequestOrde { entity.ApplicantPhoneNumber = src.Person.Phones.FirstOrDefault(m => m.Owner == OwnerType.Applicant)?.PhoneNumber; } + if (src.Person.Identifiers != null) { entity.ApplicantSIN = src.Person.Identifiers.FirstOrDefault( diff --git a/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Resolvers.cs b/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Resolvers.cs index 0911cccf0..30b42ecf4 100644 --- a/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Resolvers.cs +++ b/app/DynamicsAdapter/DynamicsAdapter.Web/Mapping/Resolvers.cs @@ -180,6 +180,9 @@ public Agency Resolve(SSG_SearchApiRequest source, PersonSearchRequest destinati ReasonCode = source.SearchRequest?.SearchReason?.ReasonCode switch { "EnfPayAgr" => SearchReasonCode.EnfPayAgr, + "ApplReasCd1" => SearchReasonCode.ApplReasCd1, + "ApplReasCd2" => SearchReasonCode.ApplReasCd2, + "ApplReasCd3" => SearchReasonCode.ApplReasCd3, "ChngAccAgr" => SearchReasonCode.ChngAccAgr, "ChngCustAg" => SearchReasonCode.ChngCustAg, "AstRecpAgy" => SearchReasonCode.AstRecpAgy, diff --git a/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestController.cs b/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestController.cs index e07a54299..55870da3a 100644 --- a/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestController.cs +++ b/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestController.cs @@ -43,58 +43,109 @@ ISearchRequestRegister register [OpenApiTag("Agency Search Request API")] public async Task CreateSearchRequest(string requestId, [FromBody]SearchRequestOrdered searchRequestOrdered) { - using (LogContext.PushProperty("RequestRef", $"{requestId}")) - using (LogContext.PushProperty("AgencyCode", $"{searchRequestOrdered?.Person?.Agency?.Code}")) + using (LogContext.PushProperty("RequestRef", requestId ?? "(null)")) + using (LogContext.PushProperty("AgencyCode", searchRequestOrdered?.Person?.Agency?.Code)) { - _logger.LogInformation("Get CreateSearchRequest"); - _logger.LogDebug(JsonConvert.SerializeObject(searchRequestOrdered)); - if (string.IsNullOrEmpty(requestId)) return BadRequest(new { ReasonCode = "error", Message = "requestId cannot be empty." }); - if (searchRequestOrdered == null) return BadRequest(new { ReasonCode = "error", Message = "SearchRequestOrdered cannot be empty." }); - if (searchRequestOrdered.Action != RequestAction.NEW) return BadRequest(new { ReasonCode = "error", Message = "CreateSearchRequest should only get NEW request." }); + _logger.LogInformation("➡️ Start CreateSearchRequest for RequestId: {RequestId}", requestId); + + if (string.IsNullOrEmpty(requestId)) + { + _logger.LogWarning("❌ requestId is missing."); + return BadRequest(new { ReasonCode = "error", Message = "requestId cannot be empty." }); + } + + if (searchRequestOrdered == null) + { + _logger.LogWarning("❌ SearchRequestOrdered payload is missing."); + return BadRequest(new { ReasonCode = "error", Message = "SearchRequestOrdered cannot be empty." }); + } + + if (searchRequestOrdered.Action != RequestAction.NEW) + { + _logger.LogWarning( + "❌ CreateSearchRequest should only receive NEW action, received: {Action}", + searchRequestOrdered.Action); + + return BadRequest(new + { + ReasonCode = "error", + Message = "CreateSearchRequest should only get NEW request." + }); + } SSG_SearchRequest createdSearchRequest = null; try { + _logger.LogDebug("Processing SearchRequestOrdered via AgencyRequestService..."); createdSearchRequest = await _agencyRequestService.ProcessSearchRequestOrdered(searchRequestOrdered); + if (createdSearchRequest == null) + { + _logger.LogError("❌ _agencyRequestService returned NULL for ProcessSearchRequestOrdered"); return StatusCode(StatusCodes.Status500InternalServerError); + } - _logger.LogInformation("SearchRequest is created successfully."); - + _logger.LogInformation( + "SearchRequest created successfully. SearchRequestId: {SearchRequestId}", + createdSearchRequest.SearchRequestId); } catch (AgencyRequestException ex) { - _logger.LogInformation(ex.Message); + _logger.LogError(ex, + "❌ AgencyRequestException while creating SearchRequest. Reason: {Reason}", + ex.Message); return StatusCode(StatusCodes.Status500InternalServerError, new { ReasonCode = ex.Message, Message = ex.InnerException?.Message }); } catch (Exception ex) { + _logger.LogError(ex, "❌ Unexpected error while creating SearchRequest."); + SSG_SearchRequest createdSR = _agencyRequestService.GetSSGSearchRequest(); if (createdSR != null) { + _logger.LogWarning("Rolling back: cancelling partially created SSG_SearchRequest {SearchRequestId}", createdSR.SearchRequestId); await _agencyRequestService.SystemCancelSSGSearchRequest(createdSR); } - if( ex is Simple.OData.Client.WebRequestException) + + if( ex is Simple.OData.Client.WebRequestException webEx) { - _logger.LogError(((Simple.OData.Client.WebRequestException)ex).RequestUri?.AbsoluteUri); - _logger.LogError(((Simple.OData.Client.WebRequestException)ex).Response); + _logger.LogError("OData RequestUri: {Uri}", webEx.RequestUri?.AbsoluteUri); + _logger.LogError("OData Response: {Response}", webEx.Response); } - _logger.LogError(ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, new { ReasonCode = ex.Message, Message = ex.InnerException?.Message }); } //try to submit to queue and then get EstimatedDate and positionInQueue try { + _logger.LogDebug( + "Submitting SearchRequest {SearchRequestId} to queue...", + createdSearchRequest.SearchRequestId); + await _agencyRequestService.SubmitSearchRequestToQueue(createdSearchRequest.SearchRequestId); createdSearchRequest = await _agencyRequestService.RefreshSearchRequest(createdSearchRequest.SearchRequestId); - }catch(Exception e) + + _logger.LogInformation( + "SearchRequest submitted to queue successfully. SearchRequestId: {SearchRequestId}", + createdSearchRequest.SearchRequestId); + }catch(Exception ex) { - _logger.LogError(e, "submit to queue or get current search request failed."); + _logger.LogError(ex, + "❌ Failed to submit SearchRequest {SearchRequestId} to queue.", + createdSearchRequest.SearchRequestId); //default value, in case there is error, we still can return accept event. createdSearchRequest.EstimatedCompletionDate = DateTime.UtcNow.AddMonths(3); createdSearchRequest.QueuePosition = 9999; } + + _logger.LogInformation( + "Returning SearchRequestSaved event for RequestId {RequestId}", + requestId); + _logger.LogDebug( + "🏁 End CreateSearchRequest for RequestId: {RequestId}", + requestId); + return Ok(BuildSearchRequestSaved_Create(createdSearchRequest, searchRequestOrdered)); } } diff --git a/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestService.cs b/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestService.cs index aefce0194..d45a9537e 100644 --- a/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestService.cs +++ b/app/DynamicsAdapter/DynamicsAdapter.Web/SearchAgency/AgencyRequestService.cs @@ -61,6 +61,14 @@ public AgencyRequestService(ISearchRequestService searchRequestService, ILogger< public async Task ProcessSearchRequestOrdered(SearchRequestOrdered searchRequestOrdered) { + _logger.LogDebug("➡️ Start ProcessSearchRequestOrdered for RequestId {RequestId}", searchRequestOrdered.RequestId); + + if (searchRequestOrdered == null) + { + _logger.LogError("❌ SearchRequestOrdered cannot be null."); + throw new ArgumentNullException(nameof(searchRequestOrdered)); + } + _personSought = searchRequestOrdered.Person; var cts = new CancellationTokenSource(); _cancellationToken = cts.Token; @@ -68,9 +76,15 @@ public async Task ProcessSearchRequestOrdered(SearchRequestOr SearchRequestEntity searchRequestEntity = _mapper.Map(searchRequestOrdered); searchRequestEntity.CreatedByApi = true; searchRequestEntity.SendNotificationOnCreation = true; + _uploadedSearchRequest = await _searchRequestService.CreateSearchRequest(searchRequestEntity, cts.Token); - if (_uploadedSearchRequest == null) return null; - _logger.LogInformation("Create Search Request successfully"); + if (_uploadedSearchRequest == null) + { + _logger.LogWarning("⚠️ CreateSearchRequest returned null"); + return null; + } + + _logger.LogInformation("Created base Search Request successfully. FileId: {FileId}", _uploadedSearchRequest?.FileId); PersonEntity personEntity = _mapper.Map(_personSought); personEntity.SearchRequest = _uploadedSearchRequest; @@ -78,7 +92,7 @@ public async Task ProcessSearchRequestOrdered(SearchRequestOr personEntity.IsCreatedByAgency = true; personEntity.IsPrimary = true; _uploadedPerson = await _searchRequestService.SavePerson(personEntity, _cancellationToken); - _logger.LogInformation("Create Person successfully"); + _logger.LogInformation("Created Person successfully"); await UploadIdentifiers(); await UploadAddresses(); @@ -88,6 +102,9 @@ public async Task ProcessSearchRequestOrdered(SearchRequestOr await UploadRelatedApplicant(_uploadedSearchRequest.ApplicantFirstName, _uploadedSearchRequest.ApplicantLastName); await UploadAliases(); await UploadSafetyConcern(); + + _logger.LogDebug("🏁 End ProcessSearchRequestOrdered for RequestId {RequestId}", searchRequestOrdered.RequestId); + return _uploadedSearchRequest; } diff --git a/app/DynamicsAdapter/DynamicsAdapter.Web/searchApi.openapi.json b/app/DynamicsAdapter/DynamicsAdapter.Web/searchApi.openapi.json index 5c7479be6..42255ac4e 100644 --- a/app/DynamicsAdapter/DynamicsAdapter.Web/searchApi.openapi.json +++ b/app/DynamicsAdapter/DynamicsAdapter.Web/searchApi.openapi.json @@ -497,6 +497,9 @@ "ChngPayAgr", "EnfPayAgND", "ChildAbduc", + "ApplReasCd1", + "ApplReasCd2", + "ApplReasCd3", "AstRecpAgy", "Unknown", "Other" @@ -517,6 +520,9 @@ "ChngPayAgr", "EnfPayAgND", "ChildAbduc", + "ApplReasCd1", + "ApplReasCd2", + "ApplReasCd3", "AstRecpAgy", "Unknown", "Other" @@ -536,7 +542,11 @@ "Carceration", "DateOfDeath", "IA", - "SafetyConcern" + "SafetyConcern", + "T1taxform", + "NoticeofAssessment", + "NoticeofReassessment", + "FinancialOtherIncome" ], "enum": [ "Asset", @@ -549,7 +559,11 @@ "Carceration", "DateOfDeath", "IA", - "SafetyConcern" + "SafetyConcern", + "T1taxform", + "NoticeofAssessment", + "NoticeofReassessment", + "FinancialOtherIncome" ] }, "Name": { diff --git a/app/DynamicsAdapter/Fams3Adapter.Dynamics/Error/DynamicsApiErrorLogger.cs b/app/DynamicsAdapter/Fams3Adapter.Dynamics/Error/DynamicsApiErrorLogger.cs new file mode 100644 index 000000000..192448e86 --- /dev/null +++ b/app/DynamicsAdapter/Fams3Adapter.Dynamics/Error/DynamicsApiErrorLogger.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using Simple.OData.Client; +using System; + +namespace Fams3Adapter.Dynamics.Error +{ + public static class DynamicsApiErrorLogger + { + public static void LogDynamicsError(Exception ex, ILogger logger) + { + logger.LogError(ex, "❌ Dynamics operation failed"); + + var inner = ex.InnerException; + while (inner != null) + { + logger.LogError("⛔ InnerException: {Message}", inner.Message); + inner = inner.InnerException; + } + + if (ex is WebRequestException webEx && webEx.Response != null) + { + try + { + logger.LogError("📝 OData Response Content:\n{Response}", webEx.Response); + } + catch { } + } + } + } +} \ No newline at end of file diff --git a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchApiRequest/SearchApiRequestService.cs b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchApiRequest/SearchApiRequestService.cs index 421862e28..263d04919 100644 --- a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchApiRequest/SearchApiRequestService.cs +++ b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchApiRequest/SearchApiRequestService.cs @@ -7,7 +7,9 @@ using Fams3Adapter.Dynamics.SearchApiEvent; using Fams3Adapter.Dynamics.SearchRequest; using Fams3Adapter.Dynamics.Types; +using Fams3Adapter.Dynamics.Error; using Simple.OData.Client; +using Microsoft.Extensions.Logging; using Entry = System.Collections.Generic.Dictionary; @@ -46,11 +48,13 @@ public class SearchApiRequestService : ISearchApiRequestService { private readonly IODataClient _oDataClient; + private readonly ILogger _logger; // private readonly SearchApiConfiguration _searchApiConfiguration; - public SearchApiRequestService(IODataClient oDataClient) + public SearchApiRequestService(IODataClient oDataClient, ILogger logger) { this._oDataClient = oDataClient; + _logger = logger; } /// @@ -63,37 +67,52 @@ public async Task> GetAllReadyForSearchAsync( SSG_DataProvider[] dataProviders, string availableDataPartners) { - List providers = dataProviders.ToList(); - int readyForSearchCode = SearchApiRequestStatusReason.ReadyForSearch.Value; - List results = new List(); + try + { + List providers = dataProviders.ToList(); + int readyForSearchCode = SearchApiRequestStatusReason.ReadyForSearch.Value; + List results = new List(); - //todo: we need to change to use following code, but ODataClient 4 has problems with expand, curent implemented code is a workaround - //ref: https://powerusers.microsoft.com/t5/Power-Apps-Ideas/Web-API-Implement-expand-on-collections/idi-p/221291 + _logger.LogDebug("➡️ Start GetAllReadyForSearchAsync..."); - IEnumerable searchApiRequests = await _oDataClient.For() - .Select(x => x.SearchApiRequestId) - .Filter(x => x.StatusCode == readyForSearchCode) - .FindEntriesAsync(cancellationToken); + //todo: we need to change to use following code, but ODataClient 4 has problems with expand, curent implemented code is a workaround + //ref: https://powerusers.microsoft.com/t5/Power-Apps-Ideas/Web-API-Implement-expand-on-collections/idi-p/221291 - foreach (SSG_SearchApiRequest request in searchApiRequests) - { - SSG_SearchApiRequest searchApiRequest = await _oDataClient.For() - .Key(request.SearchApiRequestId) - .Expand(x => x.Identifiers) - .Expand(x => x.DataProviders) - .Expand(x => x.SearchRequest) - .FindEntryAsync(cancellationToken); - if (searchApiRequest.SearchRequest != null) + IEnumerable searchApiRequests = await _oDataClient.For() + .Select(x => x.SearchApiRequestId) + .Filter(x => x.StatusCode == readyForSearchCode) + .FindEntriesAsync(cancellationToken); + + foreach (SSG_SearchApiRequest request in searchApiRequests) { - searchApiRequest.SearchRequest = await _oDataClient.For() - .Key(searchApiRequest.SearchRequest.SearchRequestId) - .Expand(x => x.SearchReason).FindEntryAsync(cancellationToken); + SSG_SearchApiRequest searchApiRequest = await _oDataClient.For() + .Key(request.SearchApiRequestId) + .Expand(x => x.Identifiers) + .Expand(x => x.DataProviders) + .Expand(x => x.SearchRequest) + .FindEntryAsync(cancellationToken); + if (searchApiRequest.SearchRequest != null) + { + searchApiRequest.SearchRequest = await _oDataClient.For() + .Key(searchApiRequest.SearchRequest.SearchRequestId) + .Expand(x => x.SearchReason).FindEntryAsync(cancellationToken); + } + searchApiRequest.IsFailed = false; + UpdateProviderInfo(providers, searchApiRequest, availableDataPartners); + results.Add(searchApiRequest); } - searchApiRequest.IsFailed = false; - UpdateProviderInfo(providers, searchApiRequest, availableDataPartners); - results.Add(searchApiRequest); + _logger.LogDebug("🏁 End GetAllReadyForSearchAsync with {Count} results.", results.Count); + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to retrieve SearchApiRequests ready for search. AvailableDataPartners: {Partners}", + availableDataPartners); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + // rethrow to preserve existing behavior + throw; } - return results; } private static void UpdateProviderInfo(List providers, SSG_SearchApiRequest searchApiRequest, string availableDataPartners) @@ -137,15 +156,37 @@ private static void UpdateProviderInfo(List providers, SSG_Sea public async Task GetLinkedSearchRequestIdAsync(Guid searchApiRequestId, CancellationToken cancellationToken) { - if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) throw new ArgumentNullException(nameof(searchApiRequestId)); + if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) + { + throw new ArgumentNullException(nameof(searchApiRequestId)); + } - var result = await _oDataClient - .For() - .Key(searchApiRequestId) - .Select(x => x.SearchRequestId) - .FindEntryAsync(cancellationToken); + _logger.LogDebug("➡️ Start GetLinkedSearchRequestIdAsync for SearchApiRequestId {Id}", searchApiRequestId); - return result.SearchRequestId; + try + { + SSG_SearchApiRequest result = await _oDataClient + .For() + .Key(searchApiRequestId) + .Select(x => x.SearchRequestId) + .FindEntryAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End GetLinkedSearchRequestIdAsync for SearchApiRequestId {Id} → SearchRequestId {SearchRequestId}", + searchApiRequestId, + result?.SearchRequestId); + + return result.SearchRequestId; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to retrieve linked SearchRequestId for SearchApiRequestId {Id}", + searchApiRequestId); + + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; // Re-throw so calling code can handle it + } } /// @@ -156,45 +197,162 @@ public async Task GetLinkedSearchRequestIdAsync(Guid searchApiRequestId, /// public async Task MarkInProgress(Guid searchApiRequestId, CancellationToken cancellationToken) { - if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) throw new ArgumentNullException(nameof(searchApiRequestId)); + if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) + { + throw new ArgumentNullException(nameof(searchApiRequestId)); + } + try + { + _logger.LogDebug("➡️ Start MarkInProgress for SearchApiRequestId {Id}", searchApiRequestId); - return await _oDataClient - .For() - .Key(searchApiRequestId) - .Set(new Entry { { Keys.DYNAMICS_STATUS_CODE_FIELD, SearchApiRequestStatusReason.InProgress.Value } }) - .UpdateEntryAsync(cancellationToken); - } + SSG_SearchApiRequest updatedRequest = await _oDataClient + .For() + .Key(searchApiRequestId) + .Set(new Entry + { + { Keys.DYNAMICS_STATUS_CODE_FIELD, SearchApiRequestStatusReason.InProgress.Value } + }) + .UpdateEntryAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End MarkInProgress for SearchApiRequestId {Id} → StatusCode {StatusCode}", + searchApiRequestId, + updatedRequest?.StatusCode); + return updatedRequest; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to mark SearchApiRequestId {Id} as InProgress", + searchApiRequestId); - public async Task AddEventAsync(Guid searchApiRequestId, SSG_SearchApiEvent searchApiEvent, + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + + throw; // rethrow for upstream handling + } + } + + public async Task AddEventAsync( + Guid searchApiRequestId, + SSG_SearchApiEvent searchApiEvent, CancellationToken cancellationToken) { - if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) throw new ArgumentNullException(nameof(searchApiRequestId)); + if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) + { + throw new ArgumentNullException(nameof(searchApiRequestId)); + } - searchApiEvent.SearchApiRequest = new SSG_SearchApiRequest() { SearchApiRequestId = searchApiRequestId}; + try + { + _logger.LogDebug( + "➡️ Start AddEventAsync for SearchApiRequestId {Id} with EventType {EventType}", + searchApiRequestId, + searchApiEvent?.EventType); - return await this._oDataClient.For().Set(searchApiEvent).InsertEntryAsync(cancellationToken); + searchApiEvent.SearchApiRequest = new SSG_SearchApiRequest + { + SearchApiRequestId = searchApiRequestId + }; + + SSG_SearchApiEvent insertedEvent = await _oDataClient + .For() + .Set(searchApiEvent) + .InsertEntryAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End AddEventAsync for SearchApiRequestId {Id} → EventId {EventId}", + searchApiRequestId, + insertedEvent?.Id); + + return insertedEvent; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to add event for SearchApiRequestId {Id} with EventType {EventType}", + searchApiRequestId, + searchApiEvent?.EventType); + + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; // rethrow to allow upstream handling + } } - public async Task> GetEventsAsync(Guid searchApiRequestId, CancellationToken cancellationToken) + public async Task> GetEventsAsync( + Guid searchApiRequestId, + CancellationToken cancellationToken) { - if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) throw new ArgumentNullException(nameof(searchApiRequestId)); - IEnumerable events = await this._oDataClient.For() - .Filter(m=>m.SearchApiRequest.SearchApiRequestId==searchApiRequestId) - .FindEntriesAsync(cancellationToken); + if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) + { + throw new ArgumentNullException(nameof(searchApiRequestId)); + } - return events; + try + { + _logger.LogDebug("➡️ Start GetEventsAsync for SearchApiRequestId {Id}", searchApiRequestId); + + IEnumerable events = await _oDataClient + .For() + .Filter(m => m.SearchApiRequest.SearchApiRequestId == searchApiRequestId) + .FindEntriesAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End GetEventsAsync for SearchApiRequestId {Id} → {Count} events retrieved", + searchApiRequestId, + events?.Count() ?? 0); + + return events; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to retrieve events for SearchApiRequestId {Id}", + searchApiRequestId); + + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; // rethrow so upstream code can handle + } } - public async Task MarkComplete(Guid searchApiRequestId, CancellationToken cancellationToken) + public async Task MarkComplete( + Guid searchApiRequestId, + CancellationToken cancellationToken) { - if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) throw new ArgumentNullException(nameof(searchApiRequestId)); + if (searchApiRequestId == default || searchApiRequestId == Guid.Empty) + { + throw new ArgumentNullException(nameof(searchApiRequestId)); + } + + try + { + _logger.LogDebug("➡️ Start MarkComplete for SearchApiRequestId {Id}", searchApiRequestId); - return await _oDataClient - .For() - .Key(searchApiRequestId) - .Set(new Entry { { Keys.DYNAMICS_STATUS_CODE_FIELD, SearchApiRequestStatusReason.Complete.Value } }) - .UpdateEntryAsync(cancellationToken); + SSG_SearchApiRequest updatedRequest = await _oDataClient + .For() + .Key(searchApiRequestId) + .Set(new Entry + { + { Keys.DYNAMICS_STATUS_CODE_FIELD, SearchApiRequestStatusReason.Complete.Value } + }) + .UpdateEntryAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End MarkComplete for SearchApiRequestId {Id} → StatusCode {StatusCode}", + searchApiRequestId, + updatedRequest?.StatusCode); + + return updatedRequest; + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Failed to mark SearchApiRequestId {Id} as Complete", + searchApiRequestId); + + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; // rethrow for upstream handling + } } /// @@ -202,52 +360,82 @@ public async Task MarkComplete(Guid searchApiRequestId, Ca /// /// /// - public async Task> GetAllValidFailedSearchRequest(CancellationToken cancellationToken, SSG_DataProvider[] dataProviders) + public async Task> GetAllValidFailedSearchRequest( + CancellationToken cancellationToken, + SSG_DataProvider[] dataProviders) { - - List results = new List(); //todo: we need to change to use following code, but ODataClient 4 has problems with expand, curent implemented code is a workaround //ref: https://powerusers.microsoft.com/t5/Power-Apps-Ideas/Web-API-Implement-expand-on-collections/idi-p/221291 - foreach (SSG_DataProvider provider in dataProviders) + try { - string adaptorName = provider.AdaptorName; - int noOfDaysToRetry = provider.NumberOfDaysToRetry; - int allRetryDone = NullableBooleanType.No.Value; - IEnumerable searchApiRequests = await _oDataClient.For() - .Select(x => x.SearchAPIRequestId) - .Filter(x => x.NumberOfFailures > 0) - .Filter(x => x.AdaptorName == adaptorName) - .Filter(x => x.NumberOfFailures < noOfDaysToRetry) - .Filter(x => x.AllRetriesDone == allRetryDone) - .FindEntriesAsync(cancellationToken); + _logger.LogDebug("➡️ Start GetAllValidFailedSearchRequest for {ProviderCount} providers", dataProviders?.Length ?? 0); - foreach (SSG_SearchapiRequestDataProvider request in searchApiRequests) + foreach (SSG_DataProvider provider in dataProviders) { + string adaptorName = provider.AdaptorName; + int noOfDaysToRetry = provider.NumberOfDaysToRetry; + int allRetryDone = NullableBooleanType.No.Value; + IEnumerable searchApiRequests = await _oDataClient.For() + .Select(x => x.SearchAPIRequestId) + .Filter(x => x.NumberOfFailures > 0) + .Filter(x => x.AdaptorName == adaptorName) + .Filter(x => x.NumberOfFailures < noOfDaysToRetry) + .Filter(x => x.AllRetriesDone == allRetryDone) + .FindEntriesAsync(cancellationToken); + + foreach (SSG_SearchapiRequestDataProvider request in searchApiRequests) + { - SSG_SearchApiRequest searchApiRequest = await _oDataClient.For() - .Key(request.SearchAPIRequestId) - .Expand(x => x.Identifiers) - .Expand(x => x.DataProviders) - .Expand(x => x.SearchRequest) - .FindEntryAsync(cancellationToken); - FilterAffectedDataProvider( provider, searchApiRequest); - searchApiRequest.IsFailed = true; - results.Add(searchApiRequest); - - - + SSG_SearchApiRequest searchApiRequest = await _oDataClient.For() + .Key(request.SearchAPIRequestId) + .Expand(x => x.Identifiers) + .Expand(x => x.DataProviders) + .Expand(x => x.SearchRequest) + .FindEntryAsync(cancellationToken); + FilterAffectedDataProvider( provider, searchApiRequest); + searchApiRequest.IsFailed = true; + results.Add(searchApiRequest); + } } + _logger.LogDebug( + "🏁 End GetAllValidFailedSearchRequest → {Count} failed requests retrieved", + results.Count); + + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, "❌ Failed to retrieve valid failed search requests"); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; } - return results; } public async Task> GetDataProvidersList(CancellationToken cancellationToken) { - return await _oDataClient.For() - .FindEntriesAsync(cancellationToken); + try + { + _logger.LogDebug("➡️ Start GetDataProvidersList"); + + IEnumerable dataProviders = await _oDataClient + .For() + .FindEntriesAsync(cancellationToken); + + _logger.LogDebug( + "🏁 End GetDataProvidersList → {Count} data providers retrieved", + dataProviders?.Count() ?? 0); + + return dataProviders; + } + catch (Exception ex) + { + _logger.LogError(ex, "❌ Failed to retrieve data providers list"); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; + } } private static void FilterAffectedDataProvider(SSG_DataProvider provider, SSG_SearchApiRequest searchApiRequest) @@ -261,23 +449,46 @@ private static void FilterAffectedDataProvider(SSG_DataProvider provider, SSG_Se searchApiRequest.DataProviders = list.ToArray(); } - public async Task GetSearchApiRequest(string searchRequestKey, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(searchRequestKey)) throw new ArgumentNullException(nameof(searchRequestKey)); - string[] strs = searchRequestKey.Split("_"); - if (strs.Length > 1) + if (string.IsNullOrEmpty(searchRequestKey)) { - string sequence = strs[1]; - IEnumerable searchApiRequests = await _oDataClient.For() - .Select(x => x.SearchApiRequestId) - .Filter(x => x.SequenceNumber == sequence) - .FindEntriesAsync(cancellationToken); + throw new ArgumentNullException(nameof(searchRequestKey)); + } + + try + { + _logger.LogDebug("➡️ Start GetSearchApiRequest for SearchRequestKey: {Key}", searchRequestKey); + + string[] parts = searchRequestKey.Split("_"); + SSG_SearchApiRequest result = null; + + if (parts.Length > 1) + { + string sequence = parts[1]; + + IEnumerable searchApiRequests = await _oDataClient + .For() + .Select(x => x.SearchApiRequestId) + .Filter(x => x.SequenceNumber == sequence) + .FindEntriesAsync(cancellationToken); + + result = searchApiRequests?.FirstOrDefault(); + } - return searchApiRequests?.FirstOrDefault(); + _logger.LogDebug( + "🏁 End GetSearchApiRequest → Found: {Found}, SearchRequestKey: {Key}", + result != null, + searchRequestKey); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "❌ Failed to retrieve SearchApiRequest for key {Key}", searchRequestKey); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); + throw; } - - return null; } } } \ No newline at end of file diff --git a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SSG_SearchRequest.cs b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SSG_SearchRequest.cs index 00d2e23b0..2455803f4 100644 --- a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SSG_SearchRequest.cs +++ b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SSG_SearchRequest.cs @@ -127,6 +127,22 @@ public class SearchRequestEntity [DisplayName("Info Requested Date of Death")] public bool DateOfDeathRequested { get; set; } + [JsonProperty("ssg_inforequestedtaxincomeinformation")] + [DisplayName("Info Requested T1 tax info")] + public bool T1taxform { get; set; } + + [JsonProperty("fams_inforequestednoticeofassessment")] + [DisplayName("Info Requested Notice of Assessment")] + public bool NoticeofAssessment { get; set; } + + [JsonProperty("fams_inforequestednoticeofreassessment")] + [DisplayName("Info Requested Notice of Reassessment")] + public bool NoticeofReassessment { get; set; } + + [JsonProperty("fams_inforequestedfinancialotherincome")] + [DisplayName("Info Requested Financial Other Income")] + public bool FinancialOtherIncome { get; set; } + [JsonProperty("ssg_inforequesteddriverslicense")] [DisplayName("Info Requested Driver's License")] public bool DriverLicenseRequested { get; set; } diff --git a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SearchRequestService.cs b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SearchRequestService.cs index 9a0da127b..1c72e6513 100644 --- a/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SearchRequestService.cs +++ b/app/DynamicsAdapter/Fams3Adapter.Dynamics/SearchRequest/SearchRequestService.cs @@ -20,6 +20,7 @@ using Fams3Adapter.Dynamics.ResultTransaction; using Fams3Adapter.Dynamics.SafetyConcern; using Fams3Adapter.Dynamics.Vehicle; +using Fams3Adapter.Dynamics.Error; using Microsoft.Extensions.Logging; using Simple.OData.Client; using System; @@ -125,7 +126,7 @@ public async Task CreateIdentifier(IdentifierEntity identifier, } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; // rethrow so caller can react } @@ -175,7 +176,7 @@ public async Task SavePerson(PersonEntity person, CancellationToken } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -199,7 +200,7 @@ public async Task CreatePhoneNumber(PhoneNumberEntity phone, Ca } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -229,7 +230,7 @@ public async Task CreateEmail(EmailEntity email, CancellationToken ca } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -255,7 +256,7 @@ public async Task CreateTransaction( } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -312,7 +313,7 @@ public async Task CreateAddress(AddressEntity address, Cancellation } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -350,7 +351,7 @@ public async Task CreateTaxIncomeInformation(TaxIncome } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; // allow caller to see failure too } @@ -400,7 +401,7 @@ public async Task CreateFinancialOtherIncome(Financia } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; // allow caller to see failure too } @@ -411,29 +412,6 @@ public async Task CreateFinancialOtherIncome(Financia return insertedRecord; } - private void LogDynamicsError(Exception ex) - { - _logger.LogError(ex, "❌ Dynamics insert failed"); - - var error = ex.InnerException; - while (error != null) - { - _logger.LogError("⛔ InnerException: {Message}", error.Message); - error = error.InnerException; - } - - // Attempt to extract OData error JSON if present - if (ex is WebRequestException webEx && webEx.Response != null) - { - try - { - var body = webEx.Response; - _logger.LogError("📝 OData Response Content:\n{Response}", body); - } - catch { } - } - } - private IEnumerable _taxCodes { get; set; } public async Task> GetTaxCodes(CancellationToken cancellationToken) { @@ -461,7 +439,7 @@ public async Task CreateName(AliasEntity name, CancellationToken can } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -496,7 +474,7 @@ public async Task CreateEmployment(EmploymentEntity employment, } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -522,7 +500,7 @@ public async Task CreateEmploymentContact(EmploymentConta } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -548,7 +526,7 @@ public async Task CreateRelatedPerson(RelatedPersonEntity relatedP } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -574,7 +552,7 @@ public async Task CreateBankInfo(BankingInformatio } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -601,7 +579,7 @@ public async Task CreateVehicle(VehicleEntity vehicle, Cancel } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -619,7 +597,7 @@ public async Task CreateVehicle(VehicleEntity vehicle, Cancel } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -645,7 +623,7 @@ public async Task CreateAssetOwner(AssetOwnerEntity owner, Cance } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -671,7 +649,7 @@ public async Task CreateOtherAsset(AssetOtherEntity otherAsset, } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -696,7 +674,7 @@ private async Task GetDuplicatedCompensation(Compensa } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -718,7 +696,7 @@ private async Task GetDuplicatedCompensation(Compensa } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -753,7 +731,7 @@ public async Task CreateCompensationClaim(Compensatio } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -789,7 +767,7 @@ public async Task CreateInsuranceClaim(ICBCClaimEntity insu } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -803,7 +781,7 @@ public async Task CreateInsuranceClaim(ICBCClaimEntity insu } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -825,7 +803,7 @@ public async Task CreateSimplePhoneNumber(SimplePhoneNumb } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -847,23 +825,33 @@ public async Task CreateInvolvedParty(InvolvedPartyEntity inv } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } public async Task CreateSearchRequest(SearchRequestEntity searchRequest, CancellationToken cancellationToken) { + _logger.LogDebug("➡️ Start base CreateSearchRequest"); + SearchRequestEntity linkedSearchRequest = await LinkSearchRequestRef(searchRequest, cancellationToken); + try { - return await this._oDataClient.For() + var result = await this._oDataClient.For() .Set(linkedSearchRequest) .InsertEntryAsync(cancellationToken); + + _logger.LogInformation("🏁 End Dynamics base SearchRequest created successfully. SearchRequestId: {SearchRequestId}, FileId: {FileId}", + result?.SearchRequestId, + result?.FileId); + + return result; } catch (Exception ex) { - LogDynamicsError(ex); + _logger.LogError(ex, "❌ Failed to create Dynamics SearchRequest."); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -885,7 +873,7 @@ public async Task CreateSafetyConcern(SafetyConcernEnti } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -918,7 +906,7 @@ public async Task CancelSearchRequest(string fileId, string c } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -942,7 +930,7 @@ public async Task GetCurrentSearchRequest(Guid searchRequestI } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -973,7 +961,7 @@ public async Task SystemCancelSearchRequest(SSG_SearchRequest } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1000,7 +988,7 @@ await _oDataClient } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); _logger.LogError(ex, "Failed to delete SearchRequest with FileId={FileId}", fileId); throw; } @@ -1053,7 +1041,7 @@ public async Task GetSearchRequest(string fileId, Cancellatio } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); _logger.LogError(ex, "Error retrieving SearchRequest with FileId={FileId}", fileId); throw; } @@ -1083,7 +1071,7 @@ public async Task GetPerson(Guid personId, CancellationToken cancell } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1107,7 +1095,7 @@ public async Task GetEmployment(Guid employmentId, CancellationT } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1125,7 +1113,7 @@ public async Task UpdateSearchRequest(Guid requestId, IDictio } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1150,7 +1138,7 @@ public async Task UpdatePerson(SSG_Person existedPerson, IDictionary } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1168,7 +1156,7 @@ public async Task UpdateRelatedPerson(Guid relatedPersonId, IDicti } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1185,7 +1173,7 @@ public async Task UpdateEmployment(Guid employmentId, IDictionar } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1202,7 +1190,7 @@ public async Task UpdateIdentifier(Guid identifierId, IDictionar } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1220,7 +1208,7 @@ public async Task UpdateSafetyConcern(Guid safetyId, ID } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1242,7 +1230,7 @@ public async Task CreateNotes(NotesEntity note, CancellationToken ca } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1260,7 +1248,7 @@ private async Task LinkSearchRequestRef(SearchRequestEntity } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } @@ -1280,15 +1268,46 @@ private async Task LinkSearchRequestRef(SearchRequestEntity public async Task GetSearchReason(string reasonCode, CancellationToken cancellationToken) { //find reasoncode + if (string.IsNullOrEmpty(reasonCode)) + { + throw new ArgumentNullException(nameof(reasonCode)); + } + + _logger.LogDebug( + "➡️ Start GetSearchReason for ReasonCode: {ReasonCode}", + reasonCode); + try { - return await _oDataClient.For() - .Filter(x => x.ReasonCode == reasonCode) - .FindEntryAsync(cancellationToken); + SSG_SearchRequestReason result = await _oDataClient + .For() + .Filter(x => x.ReasonCode == reasonCode) + .FindEntryAsync(cancellationToken); + + if (result == null) + { + _logger.LogWarning( + "⚠️ No SearchRequestReason found for ReasonCode: {ReasonCode}", + reasonCode); + } + else + { + _logger.LogDebug( + "Found SearchRequestReason for ReasonCode: {ReasonCode}: {@Result}", + reasonCode, + result); + } + + _logger.LogDebug("🏁 End GetSearchReason for ReasonCode: {ReasonCode}", reasonCode); + return result; } catch (Exception ex) { - LogDynamicsError(ex); + _logger.LogError(ex, + "❌ Error while retrieving SearchRequestReason for ReasonCode {ReasonCode}", + reasonCode); + + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1319,7 +1338,7 @@ public async Task GetSearchAgencyLocation(string locationCod } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); return null; } } @@ -1335,7 +1354,7 @@ public async Task GetEmploymentCountry(string countryText, Cancella } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1351,7 +1370,7 @@ public async Task GetEmploymentSubdivision(string subDiv } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1369,7 +1388,7 @@ await _oDataClient } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1394,7 +1413,7 @@ await _oDataClient } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1419,7 +1438,7 @@ public async Task> GetAutoCloseSearchRequestAsync } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1450,7 +1469,7 @@ private async Task FindDuplicatedPerson(PersonEntity newPerson) } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1469,7 +1488,7 @@ await _oDataClient } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } @@ -1490,7 +1509,7 @@ public async Task UpdateSearchRequestStatusAutoClosed(Guid se } catch (Exception ex) { - LogDynamicsError(ex); + DynamicsApiErrorLogger.LogDynamicsError(ex, _logger); throw; } } diff --git a/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/BcGov.Fams3.SearchApi.Contracts.csproj b/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/BcGov.Fams3.SearchApi.Contracts.csproj index b7f67c293..3789bf0a9 100644 --- a/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/BcGov.Fams3.SearchApi.Contracts.csproj +++ b/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/BcGov.Fams3.SearchApi.Contracts.csproj @@ -2,11 +2,11 @@ net8.0 - 1.1.2 + 1.1.3 PathFinder BcGov - 1.1.2 + 1.1.3 True false README.md diff --git a/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/Person/Agency.cs b/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/Person/Agency.cs index fa97eecd6..0e77960a6 100644 --- a/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/Person/Agency.cs +++ b/app/SearchApi/BcGov.Fams3.SearchApi.Contracts/Person/Agency.cs @@ -88,6 +88,9 @@ public enum SearchReasonCode ChngPayAgr, EnfPayAgND, ChildAbduc, + ApplReasCd1, + ApplReasCd2, + ApplReasCd3, AstRecpAgy, Unknown, Other, @@ -105,6 +108,10 @@ public enum InformationRequested Carceration, DateOfDeath, IA, - SafetyConcern + SafetyConcern, + T1taxform, + NoticeofAssessment, + NoticeofReassessment, + FinancialOtherIncome } } diff --git a/app/SearchApi/SearchRequest.Adaptor/Notifier/SearchRequestNotifier.cs b/app/SearchApi/SearchRequest.Adaptor/Notifier/SearchRequestNotifier.cs index 3045f9379..29e1fab87 100644 --- a/app/SearchApi/SearchRequest.Adaptor/Notifier/SearchRequestNotifier.cs +++ b/app/SearchApi/SearchRequest.Adaptor/Notifier/SearchRequestNotifier.cs @@ -61,9 +61,13 @@ private async Task NotifySearchRequestOrderedEvent( int retryTimes, int maxRetryTimes) { - var webHookName = "SearchRequest"; + if (searchRequestOrdered == null) + { + throw new ArgumentNullException(nameof(searchRequestOrdered)); + } - if (searchRequestOrdered == null) throw new ArgumentNullException(nameof(SearchRequestOrdered)); + const string webHookName = "SearchRequest"; + _logger.LogDebug("➡️ Start NotifySearchRequestOrderedEvent for RequestId: {RequestId} and WebHookName: {webHookName}", requestId, webHookName); string eventName = searchRequestOrdered.Action switch { @@ -76,29 +80,29 @@ private async Task NotifySearchRequestOrderedEvent( foreach (var webHook in _searchRequestOptions.WebHooks) { _logger.LogDebug( - $"The webHook {webHookName} notification is attempting to send {eventName} for {webHook.Name} webhook."); + $"The webHook {webHookName} notification is attempting to send {eventName} for {webHook.Name} webhook."); if (!URLHelper.TryCreateUri(webHook.Uri, eventName, $"{requestId}", out var endpoint)) { - _logger.LogWarning( - $"The webHook {webHookName} notification uri is not established or is not an absolute Uri for {webHook.Name}. Set the WebHook.Uri value on SearchApi.WebHooks settings."); + _logger.LogError( + "❌ Invalid webhook URI. WebHookName: {WebHookName}, ConfigName: {Name}. Please verify SearchRequestAdaptor.WebHooks settings.", + webHookName, + webHook.Name); throw new Exception($"The webHook {webHookName} notification uri is not established or is not an absolute Uri for {webHook.Name}."); } - using var request = new HttpRequestMessage(); - try { - StringContent content = new StringContent(JsonConvert.SerializeObject(searchRequestOrdered)); + using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); + var json = JsonConvert.SerializeObject(searchRequestOrdered); - content.Headers.ContentType = + request.Content = new StringContent(json); + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - request.Method = HttpMethod.Post; request.Headers.Accept.Add( System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); request.Headers.Add("X-ApiKey", _searchRequestOptions.ApiKeyForDynadaptor); - request.RequestUri = endpoint; + var response = await _httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) @@ -108,7 +112,7 @@ private async Task NotifySearchRequestOrderedEvent( string reason = await response.Content.ReadAsStringAsync(); _logger.LogError( $"The webHook {webHookName} notification has not executed status {eventName} successfully for {webHook.Name} webHook. The error code is {response.StatusCode.GetHashCode()}.Reason is {reason}."); - throw(new Exception($"The webHook {webHookName} notification has not executed status {eventName} successfully for {webHook.Name} webHook. The error code is {response.StatusCode.GetHashCode()}.")); + throw new Exception($"The webHook {webHookName} notification has not executed status {eventName} successfully for {webHook.Name} webHook. The error code is {response.StatusCode.GetHashCode()}."); } else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { @@ -186,62 +190,99 @@ await _searchRequestEventPublisher.PublishSearchRequestRejected( } catch (Exception exception) { - _logger.LogError(exception, exception.Message); + _logger.LogError(exception, $"Webhook {webHook.Name} failed: {exception.Message}"); throw; } } + _logger.LogDebug("🏁 End NotifySearchRequestOrderedEvent for RequestId: {RequestId}", requestId); } - private async Task NotifyNotificationAcknowledged(string requestId, NotificationAcknowledged notificationAck,CancellationToken cancellationToken) + private async Task NotifyNotificationAcknowledged( + string requestId, + NotificationAcknowledged notificationAck, + CancellationToken cancellationToken) { - var webHookName = "SearchRequest"; + if (string.IsNullOrEmpty(requestId)) + { + throw new ArgumentNullException(nameof(requestId)); + } + + if (notificationAck == null) + { + throw new ArgumentNullException(nameof(notificationAck)); + } - if (notificationAck == null) throw new ArgumentNullException(nameof(NotificationAcknowledged)); + const string webHookName = "SearchRequest"; + _logger.LogDebug("➡️ Starting NotifyNotificationAcknowledged for RequestId: {RequestId} and WebHookName: {webHookName}", requestId, webHookName); foreach (var webHook in _searchRequestOptions.WebHooks) { if (!URLHelper.TryCreateUri(webHook.Uri, "NotificationAcknowledged", $"{requestId}", out var endpoint)) { _logger.LogError( - $"The webHook {webHookName} notification uri is not established or is not an absolute Uri for {webHook.Name}. Set the WebHook.Uri value on SearchApi.WebHooks settings."); + "❌ Invalid webhook URI for RequestId {RequestId}. WebHookName: {WebHookName}, ConfigName: {Name}. Please verify SearchRequestAdaptor.WebHooks settings.", + requestId, + webHookName, + webHook.Name); return; } - using var request = new HttpRequestMessage(); + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpoint); try { - StringContent content = new StringContent(JsonConvert.SerializeObject(notificationAck)); - + string payload = JsonConvert.SerializeObject(notificationAck); + StringContent content = new StringContent(payload); content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - request.Method = HttpMethod.Post; request.Headers.Accept.Add( System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); request.Headers.Add("X-ApiKey", _searchRequestOptions.ApiKeyForDynadaptor); - request.RequestUri = endpoint; - var response = await _httpClient.SendAsync(request, cancellationToken); + request.Content = content; + + _logger.LogDebug("Posting notification to webhook: {Endpoint}", endpoint); + _logger.LogDebug( + "📨 Posting WebHook notification for RequestId {RequestId} to {Endpoint}. PayloadSize={PayloadSize} bytes", + requestId, + endpoint, + payload.Length); + HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { - _logger.LogError($"Message Failed { response.StatusCode}, {response.Content.ReadAsStringAsync()}"); + string errorBody = await response.Content.ReadAsStringAsync(); + _logger.LogError( + "❌ Webhook call failed for RequestId {RequestId}. WebHookName {WebHookName}, Endpoint {Endpoint}. Status={StatusCode}, Response={Body}", + requestId, + webHookName, + endpoint, + response.StatusCode, + errorBody); + if (response.StatusCode != System.Net.HttpStatusCode.InternalServerError && response.StatusCode != System.Net.HttpStatusCode.BadRequest) { - throw new Exception($"Message Failed {response.StatusCode}, {response.Content.ReadAsStringAsync()}"); + throw new Exception($"Message Failed {response.StatusCode}, {errorBody}"); } } - - _logger.LogInformation("get response successfully from webhook."); - + else + { + _logger.LogInformation( + "✅ Webhook acknowledged successfully for RequestId {RequestId}. Endpoint: {Endpoint}", + requestId, + endpoint); + } } catch (Exception exception) { - _logger.LogError($"NotifyNotificationAcknowledged {exception.Message}"); + _logger.LogError(exception, + "❌ NotifyNotificationAcknowledged failed for RequestId {RequestId}: {Message}", + requestId, + exception.Message); throw; } } + _logger.LogDebug("🏁 End NotifyNotificationAcknowledged for RequestId: {RequestId}", requestId); } } diff --git a/app/SearchApi/SearchRequest.Adaptor/Program.cs b/app/SearchApi/SearchRequest.Adaptor/Program.cs index a88ea8760..9047591aa 100644 --- a/app/SearchApi/SearchRequest.Adaptor/Program.cs +++ b/app/SearchApi/SearchRequest.Adaptor/Program.cs @@ -59,7 +59,7 @@ public static void Main(string[] args) var builder = Host.CreateDefaultBuilder(args) .UseSerilog((hostingContext, loggerConfiguration) => { - string serviceName = hostingContext.Configuration["JAEGER_SERVICE_NAME"]; + string serviceName = hostingContext.Configuration["JAEGER_SERVICE_NAME"] ?? "request-api"; loggerConfiguration .ReadFrom.Configuration(hostingContext.Configuration)