From cf51d4f37cd3399b0334f4939ccf8a48080edfa8 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 20 Aug 2025 16:07:00 +0530 Subject: [PATCH] Added the notification for expenses controller --- .../Controllers/AuthController.cs | 9 + Marco.Pms.Services/Service/ExpensesService.cs | 16 +- Marco.Pms.Services/Service/FirebaseService.cs | 169 +++++++++++++++++- .../ServiceInterfaces/IFirebaseService.cs | 3 + 4 files changed, 194 insertions(+), 3 deletions(-) diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 44390d8..8bba876 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -239,8 +239,17 @@ namespace MarcoBMS.Services.Controllers } else { + var oldFCMToken = exsistingFCMMapping.FcmToken; exsistingFCMMapping.FcmToken = loginDto.FcmToken; _logger.LogInfo("Updating FCM Token for employee {EmployeeId}", emp.Id); + _ = Task.Run(async () => + { + // --- Push Notification Section --- + // This section attempts to send a test push notification to the user's device. + // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. + await _firebase.SendLoginOnAnotherDeviceMessageAsync(oldFCMToken); + + }); } try { diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index cfcac91..8a907d4 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -596,6 +596,9 @@ namespace Marco.Pms.Services.Service /// An ApiResponse containing the updated expense details or an error. public async Task> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + // 1. Fetch Existing Expense with Related Entities (Single Query) var expense = await _context.Expenses .Include(e => e.ExpensesType) @@ -670,7 +673,6 @@ namespace Marco.Pms.Services.Service } else if (requiredPermissions.Any()) { - using var scope = _serviceScopeFactory.CreateScope(); var permissionService = scope.ServiceProvider.GetRequiredService(); foreach (var permission in requiredPermissions) { @@ -753,6 +755,18 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Expense was modified by another user. Please refresh and try again.", "Concurrency Error", 409); } + _ = Task.Run(async () => + { + // --- Push Notification Section --- + // This section attempts to send a test push notification to the user's device. + // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. + + var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + + await _firebase.SendExpenseMessageAsync(expense, name, tenantId); + + }); + // 10. Post-processing (audit log, cache, fetch next states) try { diff --git a/Marco.Pms.Services/Service/FirebaseService.cs b/Marco.Pms.Services/Service/FirebaseService.cs index 641b908..859d33f 100644 --- a/Marco.Pms.Services/Service/FirebaseService.cs +++ b/Marco.Pms.Services/Service/FirebaseService.cs @@ -6,7 +6,9 @@ using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Projects; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Service { @@ -14,6 +16,14 @@ namespace Marco.Pms.Services.Service { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; + + private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"); + private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b"); + private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8"); + private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"); + private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"); + private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"); + public FirebaseService(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory) { @@ -21,6 +31,22 @@ namespace Marco.Pms.Services.Service _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); } + public async Task SendLoginOnAnotherDeviceMessageAsync(string FcmToken) + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + + // List of device registration tokens to send the message to + var registrationTokens = new List { FcmToken }; + ; + + var notificationFirebase = new Notification + { + Title = "Login Alert", + Body = $"You has successfully logged in to the application from another device" + }; + + await SendMessageToMultipleDevicesAsync(registrationTokens, notificationFirebase); + } public async Task SendLoginMessageAsync(string name) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); @@ -1444,10 +1470,149 @@ namespace Marco.Pms.Services.Service } } - //Contact Controller - public async Task SendReviewExpenseMessageAsync(Expenses expenses, string name, bool IsExist, Guid tenantId) + //Expenses Controller + public async Task SendExpenseMessageAsync(Expenses expenses, string name, Guid tenantId) { + using var scope = _serviceScopeFactory.CreateScope(); + var _logger = scope.ServiceProvider.GetRequiredService(); + try + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + var nextStatusIds = await _context.ExpensesStatusMapping.Where(sm => sm.StatusId == expenses.StatusId).Select(sm => sm.NextStatusId).ToListAsync(); + + var requriedPermissionIds = await _context.StatusPermissionMapping.Where(sp => nextStatusIds.Contains(sp.StatusId)).Select(sp => sp.PermissionId).ToListAsync(); + requriedPermissionIds = requriedPermissionIds.Distinct().ToList(); + + + + var notificationEmployeeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var roleIds = await dbContext.RolePermissionMappings + .Where(rp => requriedPermissionIds.Contains(rp.FeaturePermissionId)) + .Select(rp => rp.ApplicationRoleId).ToListAsync(); + var employeeIds = await dbContext.EmployeeRoleMappings + .Where(er => + roleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .ToListAsync(); + return employeeIds; + }); + var dataEmployeeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var roleIds = await dbContext.RolePermissionMappings + .Where(rp => rp.FeaturePermissionId == PermissionsMaster.ExpenseViewAll) + .Select(rp => rp.ApplicationRoleId).ToListAsync(); + var employeeIds = await dbContext.EmployeeRoleMappings + .Where(er => + roleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .ToListAsync(); + return employeeIds; + }); + + await Task.WhenAll(notificationEmployeeTask, dataEmployeeTask); + + var notificationEmployeeIds = notificationEmployeeTask.Result; + var dataEmployeeIds = dataEmployeeTask.Result; + + Notification? notificationFirebase = null; + Notification? notificationCreatedBy = null; + if (expenses.StatusId == Review) + { + notificationFirebase = new Notification + { + Title = $"Expense Awaiting Your Review", + Body = $"An expense of amount \"{expenses.Amount}\" has been submitted by {name} and is awaiting your review." + }; + } + else if (expenses.StatusId == Approve) + { + notificationFirebase = new Notification + { + Title = $"Expense Awaiting Your Approval", + Body = $"An expense of amount \"{expenses.Amount}\" has been reviewed by {name} and is awaiting your approval." + }; + notificationCreatedBy = new Notification + { + Title = "Expense Reviewed", + Body = $"Your expense of amount \"{expenses.Amount}\" is reviewed by {name}." + }; + } + else if (expenses.StatusId == ProcessPending) + { + notificationFirebase = new Notification + { + Title = $"Expense Awaiting Your Payment", + Body = $"Expense of amount \"{expenses.Amount}\" has been approved by {name}. Please complete the payment." + }; + notificationCreatedBy = new Notification + { + Title = "Expense Approved", + Body = $"Your expense of amount \"{expenses.Amount}\" is approved by {name}." + }; + } + else if (expenses.StatusId == Processed) + { + notificationCreatedBy = new Notification + { + Title = "Expense Paid", + Body = $"Your expense of amount \"{expenses.Amount}\" has been processed and paid by {name}." + }; + } + else if (expenses.StatusId == RejectedByApprover || expenses.StatusId == RejectedByReviewer) + { + notificationCreatedBy = new Notification + { + Title = "Expense Rejected", + Body = $"Your expense of amount \"{expenses.Amount}\" has been rejected by {name}." + }; + } + + var data = new Dictionary() + { + { "Keyword", "Expenses_Modefied" }, + { "ExpenseId", expenses.Id.ToString() } + }; + + var registrationTokensForNotificationTask = Task.Run(async () => + { + if (notificationFirebase != null) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var registrationTokensForNotification = await dbContext.FCMTokenMappings.Where(ft => notificationEmployeeIds.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data); + } + }); + var registrationTokensForDataTask = Task.Run(async () => + { + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var registrationTokensForNotification = await dbContext.FCMTokenMappings.Where(ft => dataEmployeeIds.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForNotification, data); + + }); + var registrationTokensForCreatedByTask = Task.Run(async () => + { + if (notificationCreatedBy != null) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var registrationTokensForemployee = await dbContext.FCMTokenMappings.Where(ft => ft.EmployeeId == expenses.CreatedById).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForemployee, notificationCreatedBy, data); + } + }); + + await Task.WhenAll(registrationTokensForNotificationTask, registrationTokensForDataTask, registrationTokensForCreatedByTask); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while get data for sending notification"); + } } #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs index cedcb10..05cb30d 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs @@ -1,4 +1,5 @@ using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Projects; namespace Marco.Pms.Services.Service.ServiceInterfaces @@ -6,6 +7,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces public interface IFirebaseService { Task SendLoginMessageAsync(string name); + Task SendLoginOnAnotherDeviceMessageAsync(string FcmToken); Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, Guid tenantId); Task SendAssignTaskMessageAsync(Guid workItemId, string name, List teamMembers, Guid tenantId); Task SendReportTaskMessageAsync(Guid taskAllocationId, string name, Guid tenantId); @@ -19,5 +21,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task SendProjectAllocationMessageAsync(List projectAllocations, string name, Guid tenantId); Task SendModifyProjectMessageAsync(Project project, string name, bool IsExist, Guid tenantId); + Task SendExpenseMessageAsync(Expenses expenses, string name, Guid tenantId); } }