From f1527a97f1d51b18a7b307a2845bcc6c970ce260 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 5 Nov 2025 17:38:29 +0530 Subject: [PATCH] Added the recurring payment Details API --- .../ViewModels/Expenses/BasicExpenseVM.cs | 9 ++ .../Expenses/BasicPaymentRequestVM.cs | 9 ++ .../Expenses/BasicRecurringPaymentVM.cs | 13 ++ .../Expenses/PaymentRequestDetailsVM.cs | 2 +- .../ViewModels/Expenses/PaymentRequestVM.cs | 2 +- .../Expenses/RecurringPaymentDetailsVM.cs | 36 +++++ .../Controllers/ExpenseController.cs | 8 + .../MappingProfiles/MappingProfile.cs | 15 ++ Marco.Pms.Services/Service/ExpensesService.cs | 150 ++++++++++++++++-- .../ServiceInterfaces/IExpensesService.cs | 1 + 10 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 Marco.Pms.Model/ViewModels/Expenses/BasicExpenseVM.cs create mode 100644 Marco.Pms.Model/ViewModels/Expenses/BasicPaymentRequestVM.cs create mode 100644 Marco.Pms.Model/ViewModels/Expenses/BasicRecurringPaymentVM.cs create mode 100644 Marco.Pms.Model/ViewModels/Expenses/RecurringPaymentDetailsVM.cs diff --git a/Marco.Pms.Model/ViewModels/Expenses/BasicExpenseVM.cs b/Marco.Pms.Model/ViewModels/Expenses/BasicExpenseVM.cs new file mode 100644 index 0000000..6c6b0fc --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Expenses/BasicExpenseVM.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.ViewModels.Expenses +{ + public class BasicExpenseVM + { + public Guid Id { get; set; } + public string? ExpenseUId { get; set; } + public double Amount { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Expenses/BasicPaymentRequestVM.cs b/Marco.Pms.Model/ViewModels/Expenses/BasicPaymentRequestVM.cs new file mode 100644 index 0000000..6c39e4d --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Expenses/BasicPaymentRequestVM.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.ViewModels.Expenses +{ + public class BasicPaymentRequestVM + { + public Guid Id { get; set; } + public string? PaymentRequestUID { get; set; } + public double Amount { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Expenses/BasicRecurringPaymentVM.cs b/Marco.Pms.Model/ViewModels/Expenses/BasicRecurringPaymentVM.cs new file mode 100644 index 0000000..803f404 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Expenses/BasicRecurringPaymentVM.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.ViewModels.Expenses +{ + public class BasicRecurringPaymentVM + { + public Guid Id { get; set; } + public string? RecurringPaymentUID { get; set; } + public double Amount { get; set; } + public bool IsVariable { get; set; } + } +} + + + diff --git a/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestDetailsVM.cs b/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestDetailsVM.cs index 1766f03..dd82632 100644 --- a/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestDetailsVM.cs +++ b/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestDetailsVM.cs @@ -18,7 +18,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses public double? TaxAmount { get; set; } public DateTime DueDate { get; set; } public BasicProjectVM? Project { get; set; } - public string? RecurringPaymentUID { get; set; } + public BasicRecurringPaymentVM? RecurringPayment { get; set; } public ExpensesCategoryMasterVM? ExpenseCategory { get; set; } public ExpensesStatusMasterVM? ExpenseStatus { get; set; } public string? PaidTransactionId { get; set; } diff --git a/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestVM.cs b/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestVM.cs index 06e4d11..754c0b7 100644 --- a/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestVM.cs +++ b/Marco.Pms.Model/ViewModels/Expenses/PaymentRequestVM.cs @@ -10,7 +10,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses public Guid Id { get; set; } public string? Title { get; set; } public string? Description { get; set; } - public string? RecurringPaymentUID { get; set; } + public BasicRecurringPaymentVM? RecurringPayment { get; set; } public string? PaymentRequestUID { get; set; } public string? Payee { get; set; } public CurrencyMaster? Currency { get; set; } diff --git a/Marco.Pms.Model/ViewModels/Expenses/RecurringPaymentDetailsVM.cs b/Marco.Pms.Model/ViewModels/Expenses/RecurringPaymentDetailsVM.cs new file mode 100644 index 0000000..ba70d3c --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Expenses/RecurringPaymentDetailsVM.cs @@ -0,0 +1,36 @@ +using Marco.Pms.Model.Expenses; +using Marco.Pms.Model.Master; +using Marco.Pms.Model.TenantModels; +using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Master; +using Marco.Pms.Model.ViewModels.Projects; + +namespace Marco.Pms.Model.ViewModels.Expenses +{ + public class RecurringPaymentDetailsVM + { + public Guid Id { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? RecurringPaymentUID { get; set; } + public string? Payee { get; set; } + public List? NotifyTo { get; set; } + public CurrencyMaster? Currency { get; set; } + public double Amount { get; set; } + public DateTime StrikeDate { get; set; } + public DateTime? LatestPRGeneratedAt { get; set; } + public BasicProjectVM? Project { get; set; } + public int PaymentBufferDays { get; set; } + public int NumberOfIteration { get; set; } + public List? PaymentRequests { get; set; } + public ExpensesCategoryMasterVM? ExpenseCategory { get; set; } + public RecurringPaymentStatus? Status { get; set; } + public PLAN_FREQUENCY Frequency { get; set; } + public bool IsVariable { get; set; } + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + public BasicEmployeeVM? CreatedBy { get; set; } + public DateTime? UpdatedAt { get; set; } + public BasicEmployeeVM? UpdatedBy { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index d78b5ea..219e75f 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -222,6 +222,14 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet("get/recurring-payment/details/{id?}")] + public async Task GetRecurringPaymentDetailsAsync(Guid? id, [FromQuery] string? recurringPaymentUId) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.GetRecurringPaymentDetailsAsync(id, recurringPaymentUId, loggedInEmployee, tenantId); + return StatusCode(response.StatusCode, response); + } + [HttpPost("recurring-payment/create")] public async Task CreateRecurringPayment([FromBody] CreateRecurringTemplateDto model) { diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index e657982..9fe5b46 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -263,6 +263,10 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap() + .ForMember( + dest => dest.PaymentRequestUID, + opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); CreateMap(); #endregion @@ -272,6 +276,17 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap() + .ForMember( + dest => dest.RecurringPaymentUID, + opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")) + .ForMember( + dest => dest.NotifyTo, + opt => opt.MapFrom(src => new List())); + CreateMap() + .ForMember( + dest => dest.RecurringPaymentUID, + opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); #endregion diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index e0864e2..d4acc49 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -1314,8 +1314,8 @@ namespace Marco.Pms.Services.Service { var result = _mapper.Map(pr); result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}"; - if (pr.RecurringPayment != null) - result.RecurringPaymentUID = $"{pr.RecurringPayment.UIDPrefix}/{pr.RecurringPayment.UIDPostfix:D5}"; + //if (pr.RecurringPayment != null) + // result.RecurringPaymentUID = $"{pr.RecurringPayment.UIDPrefix}/{pr.RecurringPayment.UIDPostfix:D5}"; return result; }).ToList(); @@ -1404,7 +1404,7 @@ namespace Marco.Pms.Services.Service if (!hasViewSelfPermission && !hasViewAllPermission && !hasReviewPermission && !hasApprovePermission && !hasProcessPermission) { _logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to view payment requests.", loggedInEmployee.Id); - return ApiResponse.SuccessResponse(new List(), "You do not have permission to view any payment request.", 200); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view any payment request.", 200); } // Query payment request with all necessary navigation properties and validation constraints @@ -1440,7 +1440,7 @@ namespace Marco.Pms.Services.Service { _logger.LogWarning("Access DENIED: Employee {EmployeeId} lacks permission to view PaymentRequest {PaymentRequestId} created by another employee.", loggedInEmployee.Id, paymentRequest.Id); - return ApiResponse.SuccessResponse(new List(), "You do not have permission to view this payment request.", 200); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view this payment request.", 200); } // Concurrently fetch next possible statuses and related permissions @@ -1534,8 +1534,8 @@ namespace Marco.Pms.Services.Service // Map main response model and populate additional fields var response = _mapper.Map(paymentRequest); response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}"; - if (paymentRequest.RecurringPayment != null) - response.RecurringPaymentUID = $"{paymentRequest.RecurringPayment.UIDPrefix}/{paymentRequest.RecurringPayment.UIDPostfix:D5}"; + //if (paymentRequest.RecurringPayment != null) + // response.RecurringPaymentUID = $"{paymentRequest.RecurringPayment.UIDPrefix}/{paymentRequest.RecurringPayment.UIDPostfix:D5}"; response.Attachments = attachmentVMs; response.NextStatus = nextStatuses; response.UpdateLogs = updateLogs.Select(ul => @@ -1970,6 +1970,7 @@ namespace Marco.Pms.Services.Service .Include(pr => pr.ExpenseCategory) .FirstOrDefaultAsync(pr => pr.Id == model.PaymentRequestId && + !pr.IsAdvancePayment && pr.ProjectId.HasValue && pr.ExpenseCategoryId.HasValue && pr.PaidById.HasValue && @@ -2514,6 +2515,137 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); } } + public async Task> GetRecurringPaymentDetailsAsync(Guid? id, string? recurringPaymentUId, Employee loggedInEmployee, Guid tenantId) + { + _logger.LogInfo("Start GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId} with Id: {Id}, UID: {UID}", + loggedInEmployee.Id, tenantId, id ?? Guid.Empty, recurringPaymentUId ?? ""); + + try + { + // Validate input: require at least one identifier + if (!id.HasValue && string.IsNullOrWhiteSpace(recurringPaymentUId)) + { + _logger.LogWarning("Invalid parameters: Both Id and RecurringPaymentUID are null or empty."); + return ApiResponse.ErrorResponse("At least one parameter (Id or RecurringPaymentUID) must be provided.", "Invalid argument.", 400); + } + + // Concurrent permission checks for view-self, view-all, and manage recurring payments + var hasViewSelfPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id); + }); + + var hasViewAllPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id); + }); + + var hasManagePermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id); + }); + + await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask, hasManagePermissionTask); + + bool hasViewSelfPermission = hasViewSelfPermissionTask.Result; + bool hasViewAllPermission = hasViewAllPermissionTask.Result; + bool hasManagePermission = hasManagePermissionTask.Result; + + // Deny access if user lacks all relevant permissions + if (!hasViewSelfPermission && !hasViewAllPermission && !hasManagePermission) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to view recurring payments.", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view any recurring payment.", 200); + } + + // Query recurring payment by Id or UID with navigation and tenant checks + var recurringPayment = await _context.RecurringPayments + .Include(rp => rp.Currency) + .Include(rp => rp.Project) + .Include(rp => rp.ExpenseCategory) + .Include(rp => rp.Status) + .Include(rp => rp.CreatedBy).ThenInclude(e => e!.JobRole) + .Include(rp => rp.UpdatedBy).ThenInclude(e => e!.JobRole) + .Where(rp => + (rp.Id == id || (rp.UIDPrefix + "/" + rp.UIDPostfix.ToString().PadLeft(5, '0')) == recurringPaymentUId) && + rp.TenantId == tenantId && + rp.Currency != null && + rp.ExpenseCategory != null && + rp.Status != null && + rp.CreatedBy != null && + rp.CreatedBy.JobRole != null) + .FirstOrDefaultAsync(); + + if (recurringPayment == null) + { + _logger.LogWarning("Recurring Payment not found: Id={Id}, UID={UID}, TenantId={TenantId}", id ?? Guid.Empty, recurringPaymentUId ?? "N/A", tenantId); + return ApiResponse.ErrorResponse("Recurring Payment not found.", "Recurring payment not found.", 404); + } + + // If user has only view-self permission and the recurring payment belongs to another employee, deny access + bool selfCheck = hasViewSelfPermission && !hasViewAllPermission && !hasManagePermission && + recurringPayment.CreatedById != loggedInEmployee.Id; + + if (selfCheck) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} lacks permission to view RecurringPayment {RecurringPaymentId} created by another employee.", + loggedInEmployee.Id, recurringPayment.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view this recurring payment.", 200); + } + + // Concurrently fetch employees notified on this recurring payment and relevant active payment requests + var employeeTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + var emails = recurringPayment.NotifyTo.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return await context.Employees + .Include(e => e.JobRole) + .Where(e => emails.Contains(e.Email) && e.TenantId == tenantId && e.IsActive) + .ToListAsync(); + }); + + var paymentRequestTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.PaymentRequests + .Where(pr => pr.RecurringPaymentId == recurringPayment.Id && pr.TenantId == tenantId && pr.IsActive) + .Select(pr => _mapper.Map(pr)) + .ToListAsync(); + }); + + await Task.WhenAll(employeeTask, paymentRequestTask); + + var employees = employeeTask.Result; + var paymentRequests = paymentRequestTask.Result; + + // Map main response DTO and enrich with notification employees and payment requests + var response = _mapper.Map(recurringPayment); + response.NotifyTo = _mapper.Map>(employees); + response.PaymentRequests = paymentRequests; + + _logger.LogInfo("Recurring payment details fetched successfully for RecurringPaymentId: {RecurringPaymentId} by EmployeeId: {EmployeeId}", + recurringPayment.Id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Recurring payment details fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GetRecurringPaymentDetailsAsync for TenantId={TenantId}, EmployeeId={EmployeeId}: {Message}", tenantId, loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while fetching the recurring payment details.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); + } + } + public async Task> CreateRecurringPaymentAsync(CreateRecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("Start CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", loggedInEmployee.Id, tenantId); @@ -3370,9 +3502,3 @@ namespace Marco.Pms.Services.Service #endregion } } - -// Ensure the endDate is included if it matches the frequency -//if (dates.Last() != endDate && (endDate - strikeDate).Ticks % frequency.Ticks == 0) -//{ -// dates.Add(endDate); -//} \ No newline at end of file diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index c64e191..f2ce9b5 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -30,6 +30,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #region =================================================================== Recurring Payment Functions =================================================================== Task> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); + Task> GetRecurringPaymentDetailsAsync(Guid? id, string? recurringPaymentUId, Employee loggedInEmployee, Guid tenantId); Task> CreateRecurringPaymentAsync(CreateRecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId); Task> EditRecurringPaymentAsync(Guid id, UpdateRecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId); Task> PaymentRequestConversionAsync(List RecurringTemplateIds, Employee loggedInEmployee, Guid tenantId);