Made StatusId Nullable In StatusUpdateLog Table

This commit is contained in:
ashutosh.nehete 2025-11-08 15:19:58 +05:30
parent 72d03539ce
commit dc267b8f61
6 changed files with 7620 additions and 84 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Made_StatusId_Nullable_In_StatusUpdateLog_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "StatusId",
table: "StatusUpdateLogs",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci",
oldClrType: typeof(Guid),
oldType: "char(36)")
.OldAnnotation("Relational:Collation", "ascii_general_ci");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "StatusId",
table: "StatusUpdateLogs",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
collation: "ascii_general_ci",
oldClrType: typeof(Guid),
oldType: "char(36)",
oldNullable: true)
.OldAnnotation("Relational:Collation", "ascii_general_ci");
}
}
}

View File

@ -3971,7 +3971,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("NextStatusId") b.Property<Guid>("NextStatusId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid>("StatusId") b.Property<Guid?>("StatusId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")

View File

@ -8,7 +8,7 @@ namespace Marco.Pms.Model.Master
public class StatusUpdateLog : TenantRelation public class StatusUpdateLog : TenantRelation
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid StatusId { get; set; } public Guid? StatusId { get; set; }
public Guid NextStatusId { get; set; } public Guid NextStatusId { get; set; }
public Guid EntityId { get; set; } public Guid EntityId { get; set; }
public string? Comment { get; set; } public string? Comment { get; set; }

View File

@ -41,6 +41,7 @@ namespace Marco.Pms.Services.Service
private readonly CacheUpdateHelper _cache; private readonly CacheUpdateHelper _cache;
private readonly IMapper _mapper; private readonly IMapper _mapper;
//Expense Status
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"); private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"); 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 RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
@ -48,9 +49,12 @@ namespace Marco.Pms.Services.Service
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"); 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 ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"); private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
private static readonly Guid Done = Guid.Parse("b8586f67-dc19-49c3-b4af-224149efe1d3");
// Payment mode
private static readonly Guid AdvancePayment = Guid.Parse("f67beee6-6763-4108-922c-03bd86b9178d"); private static readonly Guid AdvancePayment = Guid.Parse("f67beee6-6763-4108-922c-03bd86b9178d");
// Recurring payment status
private static readonly Guid ActiveTemplateStatus = Guid.Parse("da462422-13b2-45cc-a175-910a225f6fc8"); private static readonly Guid ActiveTemplateStatus = Guid.Parse("da462422-13b2-45cc-a175-910a225f6fc8");
@ -262,18 +266,18 @@ namespace Marco.Pms.Services.Service
}).ToListAsync(); }).ToListAsync();
expenseVM = cacheList.Select(m => expenseVM = cacheList.Select(m =>
{
var response = _mapper.Map<ExpenseList>(m);
if (response.Status != null && (response.NextStatus?.Any() ?? false))
{ {
var response = _mapper.Map<ExpenseList>(m); response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault();
if (response.Status != null && (response.NextStatus?.Any() ?? false)) foreach (var status in response.NextStatus)
{ {
response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault(); status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
foreach (var status in response.NextStatus)
{
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
}
} }
return response; }
}).ToList(); return response;
}).ToList();
totalEntites = (int)totalCount; totalEntites = (int)totalCount;
} }
// 7. --- Return Final Success Response --- // 7. --- Return Final Success Response ---
@ -351,16 +355,16 @@ namespace Marco.Pms.Services.Service
var vm = _mapper.Map<ExpenseDetailsVM>(expenseDetails); var vm = _mapper.Map<ExpenseDetailsVM>(expenseDetails);
var permissionStatusMappingTask = Task.Run(async () => var permissionStatusMappingTask = Task.Run(async () =>
{ {
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.StatusPermissionMapping return await dbContext.StatusPermissionMapping
.GroupBy(ps => ps.StatusId) .GroupBy(ps => ps.StatusId)
.Select(g => new .Select(g => new
{ {
StatusId = g.Key, StatusId = g.Key,
PermissionIds = g.Select(ps => ps.PermissionId).ToList() PermissionIds = g.Select(ps => ps.PermissionId).ToList()
}).ToListAsync(); }).ToListAsync();
}); });
var expenseReimburseTask = Task.Run(async () => var expenseReimburseTask = Task.Run(async () =>
{ {
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -447,7 +451,7 @@ namespace Marco.Pms.Services.Service
PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(), PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(), CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(), Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
ExpensesCategory = expenses.Where(e => e.ExpenseCategory != null).Select(e => new { Id = e.ExpenseCategory!.Id, Name = e.ExpenseCategory.Name }).Distinct().ToList() ExpenseCategory = expenses.Where(e => e.ExpenseCategory != null).Select(e => new { Id = e.ExpenseCategory!.Id, Name = e.ExpenseCategory.Name }).Distinct().ToList()
}; };
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200); return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200);
} }
@ -835,15 +839,22 @@ namespace Marco.Pms.Services.Service
$"The sum of the base amount and tax amount ({totalAmount}) does not match the expected expense amount ({expense.Amount}).", $"The sum of the base amount and tax amount ({totalAmount}) does not match the expected expense amount ({expense.Amount}).",
400); 400);
} }
var result = ValidateTdsPercentage(model.TDSPercentage);
if (result != null)
{
return result;
}
expense.ProcessedById = loggedInEmployee.Id; expense.ProcessedById = loggedInEmployee.Id;
expense.BaseAmount = model.BaseAmount; expense.BaseAmount = model.BaseAmount;
expense.TaxAmount = model.TaxAmount; expense.TaxAmount = model.TaxAmount;
expense.TDSPercentage = model.TDSPercentage;
} }
// 7. Add Reimbursement if applicable // 7. Add Reimbursement if applicable
if (model.StatusId == Processed) if (model.StatusId == Processed)
{ {
expense.TDSPercentage = model.TDSPercentage;
var reimbursement = new ExpensesReimburse var reimbursement = new ExpensesReimburse
{ {
ReimburseTransactionId = model.ReimburseTransactionId!, ReimburseTransactionId = model.ReimburseTransactionId!,
@ -1339,8 +1350,6 @@ namespace Marco.Pms.Services.Service
{ {
var result = _mapper.Map<PaymentRequestVM>(pr); var result = _mapper.Map<PaymentRequestVM>(pr);
result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}"; result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}";
//if (pr.RecurringPayment != null)
// result.RecurringPaymentUID = $"{pr.RecurringPayment.UIDPrefix}/{pr.RecurringPayment.UIDPostfix:D5}";
return result; return result;
}).ToList(); }).ToList();
@ -1485,7 +1494,7 @@ namespace Marco.Pms.Services.Service
var nextStatuses = await context.ExpensesStatusMapping var nextStatuses = await context.ExpensesStatusMapping
.Include(esm => esm.NextStatus) .Include(esm => esm.NextStatus)
.Where(esm => esm.StatusId == paymentRequest.ExpenseStatusId && esm.NextStatus != null) .Where(esm => esm.StatusId == paymentRequest.ExpenseStatusId && esm.NextStatus != null && !(esm.NextStatusId == Done && paymentRequest.IsAdvancePayment))
.Select(esm => esm.NextStatus!) .Select(esm => esm.NextStatus!)
.ToListAsync(); .ToListAsync();
@ -1557,7 +1566,7 @@ namespace Marco.Pms.Services.Service
var attachmentVMs = documentTask.Result; var attachmentVMs = documentTask.Result;
var updateLogs = updateLogsTask.Result; var updateLogs = updateLogsTask.Result;
var statusIds = updateLogs.Select(sul => sul.StatusId).ToList(); var statusIds = updateLogs.Where(sul => sul.StatusId.HasValue).Select(sul => sul.StatusId!.Value).ToList();
statusIds.AddRange(updateLogs.Select(sul => sul.NextStatusId).ToList()); statusIds.AddRange(updateLogs.Select(sul => sul.NextStatusId).ToList());
statusIds = statusIds.Distinct().ToList(); statusIds = statusIds.Distinct().ToList();
@ -1746,7 +1755,7 @@ namespace Marco.Pms.Services.Service
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
EntityId = paymentRequest.Id, EntityId = paymentRequest.Id,
StatusId = Draft, //StatusId = Draft,
NextStatusId = Draft, NextStatusId = Draft,
UpdatedById = loggedInEmployee.Id, UpdatedById = loggedInEmployee.Id,
UpdatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
@ -2507,7 +2516,6 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Recurring Payment Functions =================================================================== #region =================================================================== Recurring Payment Functions ===================================================================
public async Task<ApiResponse<object>> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId)
{ {
_logger.LogInfo("Start GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}, PageNumber: {PageNumber}, PageSize: {PageSize}", _logger.LogInfo("Start GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}, PageNumber: {PageNumber}, PageSize: {PageSize}",
@ -2787,7 +2795,6 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("End GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); _logger.LogInfo("End GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id);
} }
} }
public async Task<ApiResponse<object>> CreateRecurringPaymentAsync(CreateRecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> CreateRecurringPaymentAsync(CreateRecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId)
{ {
_logger.LogInfo("Start CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", loggedInEmployee.Id, tenantId); _logger.LogInfo("Start CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", loggedInEmployee.Id, tenantId);
@ -3148,7 +3155,6 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("End EditRecurringPaymentAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); _logger.LogInfo("End EditRecurringPaymentAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id);
} }
} }
#endregion #endregion
#region =================================================================== Advance Payment Functions =================================================================== #region =================================================================== Advance Payment Functions ===================================================================
@ -3341,7 +3347,7 @@ namespace Marco.Pms.Services.Service
if ((!isRejected) || (isRejected && (loggedInEmployeeId == model.CreatedById || hasManagePermission))) if ((!isRejected) || (isRejected && (loggedInEmployeeId == model.CreatedById || hasManagePermission)))
{ {
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterMongoDB>>(statusMapping.NextStatus); response.NextStatus = statusMapping.NextStatus.Where(ns => ns != null && ns.Id != Done).Select(ns => _mapper.Map<ExpensesStatusMasterMongoDB>(ns)).ToList();
} }
} }
if (response.Status == null) if (response.Status == null)
@ -3675,77 +3681,107 @@ namespace Marco.Pms.Services.Service
await Task.WhenAll(attachmentTask, documentsTask); await Task.WhenAll(attachmentTask, documentsTask);
} }
/// <summary>
/// Determines if recurring payments are applicable based on frequency, iteration count, and date logic.
/// </summary>
/// <param name="numberOfIteration">Maximum allowed payment iterations.</param>
/// <param name="frequency">Frequency of recurring payments (e.g., Monthly, Quarterly).</param>
/// <param name="strikeDate">The starting date for recurring payments.</param>
/// <param name="latestPRGeneratedAt">The date of the latest payment receipt, if any.</param>
/// <returns>True if recurring payments are applicable; otherwise, false.</returns>
private static bool IsRecurringApplicable(int numberOfIteration, PLAN_FREQUENCY frequency, DateTime strikeDate, DateTime? latestPRGeneratedAt) private static bool IsRecurringApplicable(int numberOfIteration, PLAN_FREQUENCY frequency, DateTime strikeDate, DateTime? latestPRGeneratedAt)
{ {
List<DateTime> dates = new List<DateTime>(); // Validate input parameters
DateTime currentDate = strikeDate; if (numberOfIteration <= 0)
DateTime endDate = DateTime.UtcNow.Date;
switch (frequency)
{ {
case PLAN_FREQUENCY.MONTHLY: return false;
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddMonths(1);
}
break;
case PLAN_FREQUENCY.QUARTERLY:
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddMonths(3);
}
break;
case PLAN_FREQUENCY.HALF_YEARLY:
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddMonths(6);
}
break;
case PLAN_FREQUENCY.YEARLY:
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddYears(1);
}
break;
case PLAN_FREQUENCY.DAILY:
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddDays(1);
}
break;
case PLAN_FREQUENCY.WEEKLY:
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = currentDate.AddDays(7);
}
break;
} }
// Ensure strikeDate is in a consistent timezone (UTC or local as per your business logic)
var currentDate = strikeDate.Date;
var endDate = DateTime.UtcNow.Date;
// List to store generated dates for validation
var dates = new List<DateTime>();
// Define increment logic for each frequency
Func<DateTime, DateTime> incrementFunc = frequency switch
{
PLAN_FREQUENCY.MONTHLY => d => d.AddMonths(1),
PLAN_FREQUENCY.QUARTERLY => d => d.AddMonths(3),
PLAN_FREQUENCY.HALF_YEARLY => d => d.AddMonths(6),
PLAN_FREQUENCY.YEARLY => d => d.AddYears(1),
PLAN_FREQUENCY.DAILY => d => d.AddDays(1),
PLAN_FREQUENCY.WEEKLY => d => d.AddDays(7),
_ => d => d.AddDays(1)
};
// Return false if frequency is not supported
if (incrementFunc == null)
{
return false;
}
// Generate dates based on frequency until endDate
while (currentDate <= endDate)
{
dates.Add(currentDate);
currentDate = incrementFunc(currentDate);
}
// Validation: Must have at least one date and not exceed iteration count
if (!dates.Any() || dates.Count > numberOfIteration) if (!dates.Any() || dates.Count > numberOfIteration)
{ {
return false; return false;
} }
// Validation: Last generated date must match endDate
if (dates.Last() != endDate) if (dates.Last() != endDate)
{ {
return false; return false;
} }
if (latestPRGeneratedAt.HasValue && latestPRGeneratedAt.Value == endDate) // Validation: Latest payment receipt should not be on endDate
if (latestPRGeneratedAt.HasValue && latestPRGeneratedAt.Value.Date == endDate)
{ {
return false; return false;
} }
return true; return true;
} }
/// <summary>
/// Validates the TDS Percentage in the provided model.
/// </summary>
/// <param name="model">The input model containing TDS Percentage.</param>
/// <returns>Returns an error response if validation fails; otherwise, null.</returns>
private ApiResponse<object>? ValidateTdsPercentage(double? TDSPercentage)
{
// Check if TDSPercentage is present in the model
if (!TDSPercentage.HasValue)
{
return null; // No validation needed if TDSPercentage is not provided
}
var tdsValue = TDSPercentage.Value;
// Validate TDS Percentage range: must be between 0 and 100 inclusive
if (tdsValue < 0 || tdsValue > 100)
{
// Log a warning with structured logging for traceability
_logger.LogWarning("TDS Percentage validation failed. Provided value: {TdsValue} is outside the valid range (0 - 100).", tdsValue);
// Return a consistent and clear error response with HTTP status 400
return ApiResponse<object>.ErrorResponse(
"Invalid TDS Percentage value. Allowed range is 0 to 100 inclusive.",
"TDS Percentage value out of range.",
400);
}
return null; // Validation successful, no error
}
#endregion #endregion
} }
} }

View File

@ -10,18 +10,15 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
public interface IMasterService public interface IMasterService
{ {
#region =================================================================== Recurring Payment Status APIs =================================================================== #region =================================================================== Recurring Payment Status APIs ===================================================================
Task<ApiResponse<object>> GetRecurringPaymentStatusAsync(Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetRecurringPaymentStatusAsync(Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Currency APIs =================================================================== #region =================================================================== Currency APIs ===================================================================
Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Organization Type APIs =================================================================== #region =================================================================== Organization Type APIs ===================================================================
Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetOrganizationTypesAsync(Employee loggedInEmployee, Guid tenantId);
#endregion #endregion