From dc1c8eb4b3fe809c0ad6e435f4cd442922f2ad6f Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Mon, 18 May 2026 19:22:59 -0400 Subject: [PATCH] feat(api): [AB#266] add en-route notify and address fallback Added POST /assignments/{id}/en-route endpoint for client en-route notifications and fixed assignment DTO to fall back to client address when assignment has no address set. Changes: [1] Added POST en-route endpoint in AssignmentController calling NotifyEnRouteAsync [2] Added NotifyEnRouteAsync in AssignmentService to send client en-route notification [3] Added client address fallback in MapToDtoAsync when assignment address fields are empty [4] Added BuildClientJobTrackingEnRoute notification message builder [5] Added SendClientJobTrackingEnRouteNotificationAsync to notification service [6] Updated interfaces and test stub for new notification method References: [1] [JobFlow.API/Controllers/AssignmentController.cs:126](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.API/Controllers/AssignmentController.cs#L126) [2] [JobFlow.Business/Services/AssignmentService.cs:349](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.Business/Services/AssignmentService.cs#L349) [3] [JobFlow.Business/Services/AssignmentService.cs:425](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.Business/Services/AssignmentService.cs#L425) [4] [JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs:213](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs#L213) [5] [JobFlow.Business/Notifications/NotificationService.cs:135](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.Business/Notifications/NotificationService.cs#L135) [6] [JobFlow.Tests/FollowUpAutomationServiceTests.cs:275](https://github.com/Katharix/JobFlow.API/blob/fix/mobile-bugs-backend/JobFlow.Tests/FollowUpAutomationServiceTests.cs#L275) --- .../Controllers/AssignmentController.cs | 13 ++++++++ .../Builders/INotificationMessageBuilder.cs | 1 + .../Builders/NotificationMessageBuilder.cs | 15 ++++++++++ .../Notifications/NotificationService.cs | 6 ++++ .../Services/AssignmentService.cs | 30 +++++++++++++++++++ .../ServiceInterfaces/IAssignmentService.cs | 2 ++ .../ServiceInterfaces/INotificationService.cs | 1 + .../FollowUpAutomationServiceTests.cs | 1 + 8 files changed, 69 insertions(+) diff --git a/JobFlow.API/Controllers/AssignmentController.cs b/JobFlow.API/Controllers/AssignmentController.cs index 5ac1e35..cb359c0 100644 --- a/JobFlow.API/Controllers/AssignmentController.cs +++ b/JobFlow.API/Controllers/AssignmentController.cs @@ -122,5 +122,18 @@ public async Task UpdateNotes(Guid id, [FromBody] UpdateAssignmen return Ok(result.Value); } + // Notify the client that the worker is en route + [HttpPost("{id:guid}/en-route")] + public async Task NotifyEnRoute(Guid id) + { + var organizationId = HttpContext.GetOrganizationId(); + + var result = await _assignmentService.NotifyEnRouteAsync(organizationId, id); + if (result.IsFailure) + return BadRequest(result.Error); + + return Ok(new { success = true }); + } + } diff --git a/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs b/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs index 5fbc440..fcde948 100644 --- a/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs +++ b/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs @@ -26,6 +26,7 @@ NotificationMessage BuildClientJobRescheduled( NotificationMessage BuildClientJobTrackingEta(OrganizationClient client, Job job, int etaMinutes); NotificationMessage BuildClientJobTrackingArrival(OrganizationClient client, Job job); + NotificationMessage BuildClientJobTrackingEnRoute(OrganizationClient client, Job job); NotificationMessage BuildEmployeeInvite(EmployeeInvite invite); diff --git a/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs b/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs index 38af0a3..9496aee 100644 --- a/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs +++ b/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs @@ -210,6 +210,21 @@ public NotificationMessage BuildClientJobTrackingArrival(OrganizationClient clie }; } + public NotificationMessage BuildClientJobTrackingEnRoute(OrganizationClient client, Job job) + { + return new NotificationMessage + { + Email = client.EmailAddress, + Phone = client.PhoneNumber, + Name = client.FirstName, + Subject = $"Your worker is on the way for {job.Title}", + Body = + $"Hello {client.ClientFullName()},\n\nYour JobFlow worker is on the way for your job: {job.Title}.", + Sms = $"Your JobFlow worker is on the way for {job.Title}. ", + TemplateId = EmailTemplate.Default + }; + } + public NotificationMessage BuildEmployeeInvite(EmployeeInvite invite) { var link = $"{baseUrl}/i/{invite.ShortCode}"; diff --git a/JobFlow.Business/Notifications/NotificationService.cs b/JobFlow.Business/Notifications/NotificationService.cs index 6021a32..1951dfa 100644 --- a/JobFlow.Business/Notifications/NotificationService.cs +++ b/JobFlow.Business/Notifications/NotificationService.cs @@ -132,6 +132,12 @@ public async Task SendClientJobTrackingArrivalNotificationAsync(OrganizationClie await SendNotificationAsync(message); } + public async Task SendClientJobTrackingEnRouteNotificationAsync(OrganizationClient client, Job job) + { + var message = _builder.BuildClientJobTrackingEnRoute(client, job); + await SendNotificationAsync(message); + } + public async Task SendEmployeeInviteNotificationAsync(EmployeeInvite invite) { var message = _builder.BuildEmployeeInvite(invite); diff --git a/JobFlow.Business/Services/AssignmentService.cs b/JobFlow.Business/Services/AssignmentService.cs index 8cadd73..39b278f 100644 --- a/JobFlow.Business/Services/AssignmentService.cs +++ b/JobFlow.Business/Services/AssignmentService.cs @@ -346,6 +346,26 @@ public async Task> GetAssignmentByIdAsync( return Result.Success(await MapToDtoAsync(organizationId, assignment)); } + public async Task NotifyEnRouteAsync(Guid organizationId, Guid assignmentId) + { + var assignment = await _assignments.Query() + .Include(a => a.Job) + .ThenInclude(j => j.OrganizationClient) + .FirstOrDefaultAsync(a => + a.Id == assignmentId && + a.Job.OrganizationClient.OrganizationId == organizationId); + + if (assignment == null) + return Result.Failure(AssignmentErrors.NotFound); + + var job = assignment.Job; + if (job?.OrganizationClient == null) + return Result.Failure(Error.NotFound("Client.NotFound", "The client for this assignment could not be found.")); + + await _notificationService.SendClientJobTrackingEnRouteNotificationAsync(job.OrganizationClient, job); + return Result.Success(); + } + private async Task MapToDtoAsync(Guid organizationId, Assignment assignment) { var labelMapResult = await _workflowSettings.GetJobLifecycleLabelMapAsync(organizationId); @@ -397,6 +417,16 @@ private AssignmentDto MapToDto(Assignment assignment, Dictionary>> GetAssignmentsAsync(Guid organizationId, DateTime start, DateTime end); Task> GetAssignmentByIdAsync(Guid organizationId, Guid assignmentId); + + Task NotifyEnRouteAsync(Guid organizationId, Guid assignmentId); } \ No newline at end of file diff --git a/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs b/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs index 0b49373..998f174 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs @@ -26,6 +26,7 @@ Task SendClientJobRescheduledNotificationAsync( Task SendClientPaymentReceivedNotificationAsync(OrganizationClient client, Invoice invoice); Task SendClientJobTrackingEtaNotificationAsync(OrganizationClient client, Job job, int etaMinutes); Task SendClientJobTrackingArrivalNotificationAsync(OrganizationClient client, Job job); + Task SendClientJobTrackingEnRouteNotificationAsync(OrganizationClient client, Job job); Task SendClientEstimateSentNotificationAsync(OrganizationClient client, Estimate estimate); Task SendClientEstimateFollowUpNotificationAsync(OrganizationClient client, Estimate estimate, string message); Task SendOrganizationEstimateRevisionRequestedNotificationAsync(Organization organization, OrganizationClient client, Estimate estimate, string revisionMessage); diff --git a/JobFlow.Tests/FollowUpAutomationServiceTests.cs b/JobFlow.Tests/FollowUpAutomationServiceTests.cs index dadb0fd..e507996 100644 --- a/JobFlow.Tests/FollowUpAutomationServiceTests.cs +++ b/JobFlow.Tests/FollowUpAutomationServiceTests.cs @@ -272,6 +272,7 @@ private sealed class NoOpNotificationService : INotificationService public Task SendClientPaymentReceivedNotificationAsync(OrganizationClient client, Invoice invoice) => Task.CompletedTask; public Task SendClientJobTrackingEtaNotificationAsync(OrganizationClient client, Job job, int etaMinutes) => Task.CompletedTask; public Task SendClientJobTrackingArrivalNotificationAsync(OrganizationClient client, Job job) => Task.CompletedTask; + public Task SendClientJobTrackingEnRouteNotificationAsync(OrganizationClient client, Job job) => Task.CompletedTask; public Task SendClientEstimateSentNotificationAsync(OrganizationClient client, Estimate estimate) => Task.CompletedTask; public Task SendClientEstimateFollowUpNotificationAsync(OrganizationClient client, Estimate estimate, string message) => Task.CompletedTask; public Task SendOrganizationEstimateRevisionRequestedNotificationAsync(Organization organization, OrganizationClient client, Estimate estimate, string revisionMessage) => Task.CompletedTask;