Added the payment Request status Change API
This commit is contained in:
parent
a204efb133
commit
18480b94cd
File diff suppressed because one or more lines are too long
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_Payment_Infromation_In_Payment_request_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "NextStatusId",
|
||||
table: "StatusUpdateLogs",
|
||||
type: "char(36)",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "PaidAt",
|
||||
table: "PaymentRequests",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "PaidById",
|
||||
table: "PaymentRequests",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PaidTransactionId",
|
||||
table: "PaymentRequests",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_PaidById",
|
||||
table: "PaymentRequests",
|
||||
column: "PaidById");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PaymentRequests_Employees_PaidById",
|
||||
table: "PaymentRequests",
|
||||
column: "PaidById",
|
||||
principalTable: "Employees",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PaymentRequests_Employees_PaidById",
|
||||
table: "PaymentRequests");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PaymentRequests_PaidById",
|
||||
table: "PaymentRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NextStatusId",
|
||||
table: "StatusUpdateLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaidAt",
|
||||
table: "PaymentRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaidById",
|
||||
table: "PaymentRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaidTransactionId",
|
||||
table: "PaymentRequests");
|
||||
}
|
||||
}
|
||||
}
|
||||
7359
Marco.Pms.DataAccess/Migrations/20251103133736_Added_UIDPerfix_In_Expense_Table.Designer.cs
generated
Normal file
7359
Marco.Pms.DataAccess/Migrations/20251103133736_Added_UIDPerfix_In_Expense_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_UIDPerfix_In_Expense_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
|
||||
table: "AdvancePaymentTransactions");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UIDPostfix",
|
||||
table: "Expenses",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UIDPrefix",
|
||||
table: "Expenses",
|
||||
type: "longtext",
|
||||
nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "AdvancePaymentTransactions",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci",
|
||||
oldClrType: typeof(Guid),
|
||||
oldType: "char(36)")
|
||||
.OldAnnotation("Relational:Collation", "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
|
||||
table: "AdvancePaymentTransactions",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
|
||||
table: "AdvancePaymentTransactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UIDPostfix",
|
||||
table: "Expenses");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UIDPrefix",
|
||||
table: "Expenses");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "AdvancePaymentTransactions",
|
||||
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");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
|
||||
table: "AdvancePaymentTransactions",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2097,7 +2097,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
@ -2372,6 +2372,13 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<string>("TransactionId")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("UIDPostfix")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("UIDPrefix")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovedById");
|
||||
@ -2563,6 +2570,15 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<bool>("IsAdvancePayment")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("PaidAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid?>("PaidById")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("PaidTransactionId")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Payee")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
@ -2603,6 +2619,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("ExpenseStatusId");
|
||||
|
||||
b.HasIndex("PaidById");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("RecurringPaymentId");
|
||||
@ -3853,6 +3871,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<Guid>("EntityId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("NextStatusId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("StatusId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
@ -6219,9 +6240,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
@ -6469,6 +6488,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "PaidBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("PaidById");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
@ -6495,6 +6518,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("ExpenseStatus");
|
||||
|
||||
b.Navigation("PaidBy");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("RecurringPayment");
|
||||
|
||||
12
Marco.Pms.Model/Dtos/Expenses/PaymentRequestRecordDto.cs
Normal file
12
Marco.Pms.Model/Dtos/Expenses/PaymentRequestRecordDto.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Marco.Pms.Model.Dtos.Expenses
|
||||
{
|
||||
public class PaymentRequestRecordDto
|
||||
{
|
||||
public Guid PaymentRequestId { get; set; }
|
||||
public Guid StatusId { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public string? PaidTransactionId { get; set; }
|
||||
public DateTime? PaidAt { get; set; }
|
||||
public Guid? PaidById { get; set; }
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ namespace Marco.Pms.Model.Expenses
|
||||
public string FinanceUIdPrefix { get; set; } = default!;
|
||||
public int FinanceUIdPostfix { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
|
||||
@ -10,6 +10,8 @@ namespace Marco.Pms.Model.Expenses
|
||||
public class Expenses : TenantRelation
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string UIDPrefix { get; set; } = default!;
|
||||
public int UIDPostfix { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
|
||||
@ -43,6 +43,13 @@ namespace Marco.Pms.Model.Expenses
|
||||
[ValidateNever]
|
||||
[ForeignKey("ExpenseStatusId")]
|
||||
public ExpensesStatusMaster? ExpenseStatus { get; set; }
|
||||
public string? PaidTransactionId { get; set; }
|
||||
public DateTime? PaidAt { get; set; }
|
||||
public Guid? PaidById { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("PaidById")]
|
||||
public Employee? PaidBy { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
@ -9,6 +9,7 @@ namespace Marco.Pms.Model.Master
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid StatusId { get; set; }
|
||||
public Guid NextStatusId { get; set; }
|
||||
public Guid EntityId { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
@ -7,5 +7,6 @@
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public string S3Key { get; set; } = string.Empty;
|
||||
public string ThumbS3Key { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,12 +20,16 @@ namespace Marco.Pms.Model.ViewModels.Expenses
|
||||
public RecurringPayment? RecurringPayment { get; set; }
|
||||
public ExpensesCategoryMasterVM? ExpenseCategory { get; set; }
|
||||
public ExpensesStatusMasterVM? ExpenseStatus { get; set; }
|
||||
public string? PaidTransactionId { get; set; }
|
||||
public DateTime? PaidAt { get; set; }
|
||||
public BasicEmployeeVM? PaidBy { get; set; }
|
||||
public bool IsAdvancePayment { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
public List<ExpensesStatusMasterVM>? NextStatus { get; set; }
|
||||
public List<PaymentRequestUpdateLog>? UpdateLogs { get; set; }
|
||||
public List<PaymentRequestAttachmentVM>? Attachments { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Master;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.Expenses
|
||||
{
|
||||
public class PaymentRequestUpdateLog
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public ExpensesStatusMasterVM? Status { get; set; }
|
||||
public ExpensesStatusMasterVM? NextStatus { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
@ -172,6 +172,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("payment-request/action")]
|
||||
public async Task<IActionResult> ChangePaymentRequestStatus([FromBody] PaymentRequestRecordDto model)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _expensesService.ChangePaymentRequestStatusAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Payment_Request", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPut("payment-request/edit/{id}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(Guid id, [FromBody] PaymentRequestDto model)
|
||||
{
|
||||
|
||||
@ -6,6 +6,7 @@ using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Expenses;
|
||||
using Marco.Pms.Model.Filters;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.MongoDBModels.Employees;
|
||||
using Marco.Pms.Model.MongoDBModels.Expenses;
|
||||
@ -46,6 +47,7 @@ namespace Marco.Pms.Services.Service
|
||||
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");
|
||||
private static readonly Guid AdvancePayment = Guid.Parse("f67beee6-6763-4108-922c-03bd86b9178d");
|
||||
private static readonly string Collection = "ExpensesModificationLog";
|
||||
public ExpensesService(
|
||||
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
@ -549,9 +551,19 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
|
||||
|
||||
string uIDPrefix = $"EX/{DateTime.Now:MMyy}";
|
||||
|
||||
// Generate unique UID postfix based on existing requests for the current prefix
|
||||
var lastPR = await _context.Expenses.Where(pr => pr.UIDPrefix == uIDPrefix)
|
||||
.OrderByDescending(pr => pr.UIDPostfix)
|
||||
.FirstOrDefaultAsync();
|
||||
int uIDPostfix = lastPR == null ? 1 : (lastPR.UIDPostfix + 1);
|
||||
|
||||
// 3. Entity Creation
|
||||
var expense = _mapper.Map<Expenses>(dto);
|
||||
expense.ExpenseUId = $"EX-{currentexpenseUId}";
|
||||
expense.UIDPostfix = uIDPostfix;
|
||||
expense.UIDPrefix = uIDPrefix;
|
||||
expense.CreatedById = loggedInEmployee.Id;
|
||||
expense.CreatedAt = DateTime.UtcNow;
|
||||
expense.TenantId = tenantId;
|
||||
@ -642,6 +654,12 @@ namespace Marco.Pms.Services.Service
|
||||
expense.Id, expense.StatusId, model.StatusId);
|
||||
|
||||
// 2. Run Prerequisite Checks in Parallel (Status transition + Permissions)
|
||||
var processedStatusTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.ExpensesStatusMaster
|
||||
.FirstOrDefaultAsync(es => es.Id == Processed);
|
||||
});
|
||||
var statusTransitionTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -658,9 +676,10 @@ namespace Marco.Pms.Services.Service
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
|
||||
var statusTransition = await statusTransitionTask;
|
||||
var requiredPermissions = await targetStatusPermissionsTask;
|
||||
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask, processedStatusTask);
|
||||
var statusTransition = statusTransitionTask.Result;
|
||||
var requiredPermissions = targetStatusPermissionsTask.Result;
|
||||
var processedStatus = processedStatusTask.Result;
|
||||
|
||||
// 3. Validate Transition and Required Fields
|
||||
if (statusTransition == null)
|
||||
@ -710,10 +729,65 @@ namespace Marco.Pms.Services.Service
|
||||
// 5. Prepare for update (Audit snapshot)
|
||||
var expenseStateBeforeChange = _updateLogHelper.EntityToBsonDocument(expense);
|
||||
|
||||
// 6. Apply Status Transition
|
||||
expense.StatusId = statusTransition.NextStatusId;
|
||||
expense.Status = statusTransition.NextStatus;
|
||||
var expenseLogs = new List<ExpenseLog>
|
||||
{
|
||||
new ExpenseLog
|
||||
{
|
||||
ExpenseId = expense.Id,
|
||||
Action = $"Status changed to '{statusTransition.NextStatus?.Name}'",
|
||||
UpdatedById = loggedInEmployee.Id,
|
||||
UpdateAt = DateTime.UtcNow,
|
||||
Comment = model.Comment,
|
||||
TenantId = tenantId
|
||||
},
|
||||
};
|
||||
|
||||
// 6. Apply Status Transition
|
||||
if (model.StatusId == ProcessPending && expense.PaymentModeId == AdvancePayment)
|
||||
{
|
||||
expense.StatusId = Processed;
|
||||
expense.Status = processedStatus;
|
||||
expense.ProcessedById = loggedInEmployee.Id;
|
||||
|
||||
var lastTransaction = await _context.AdvancePaymentTransactions.OrderByDescending(apt => apt.CreatedAt).FirstOrDefaultAsync(apt => apt.TenantId == tenantId);
|
||||
double lastBalance = 0;
|
||||
if (lastTransaction != null)
|
||||
{
|
||||
lastBalance = lastTransaction.CurrentBalance;
|
||||
}
|
||||
|
||||
_context.AdvancePaymentTransactions.Add(new AdvancePaymentTransaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FinanceUIdPostfix = expense.UIDPostfix,
|
||||
FinanceUIdPrefix = expense.UIDPrefix,
|
||||
Title = expense.Description,
|
||||
ProjectId = expense.ProjectId,
|
||||
EmployeeId = expense.PaidById,
|
||||
Amount = 0 - expense.Amount,
|
||||
CurrentBalance = lastBalance - expense.Amount,
|
||||
CreatedAt = expense.TransactionDate,
|
||||
CreatedById = loggedInEmployee.Id,
|
||||
IsActive = true,
|
||||
TenantId = tenantId
|
||||
});
|
||||
|
||||
var expenseLog = new ExpenseLog
|
||||
{
|
||||
ExpenseId = expense.Id,
|
||||
Action = $"Status changed to '{processedStatus?.Name}'",
|
||||
UpdatedById = loggedInEmployee.Id,
|
||||
UpdateAt = DateTime.UtcNow,
|
||||
Comment = model.Comment,
|
||||
TenantId = tenantId
|
||||
};
|
||||
expenseLogs.Add(expenseLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
expense.StatusId = statusTransition.NextStatusId;
|
||||
expense.Status = statusTransition.NextStatus;
|
||||
}
|
||||
// Handle reviewer/approver/processor fields based on target StatusId (Guid)
|
||||
if (model.StatusId == Approve || model.StatusId == RejectedByReviewer)
|
||||
{
|
||||
@ -749,15 +823,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// 8. Add Expense Log Entry
|
||||
_context.ExpenseLogs.Add(new ExpenseLog
|
||||
{
|
||||
ExpenseId = expense.Id,
|
||||
Action = $"Status changed to '{statusTransition.NextStatus?.Name}'",
|
||||
UpdatedById = loggedInEmployee.Id,
|
||||
UpdateAt = DateTime.UtcNow,
|
||||
Comment = model.Comment,
|
||||
TenantId = tenantId
|
||||
});
|
||||
_context.ExpenseLogs.AddRange(expenseLogs);
|
||||
|
||||
// 9. Commit database transaction
|
||||
try
|
||||
@ -1383,6 +1449,14 @@ namespace Marco.Pms.Services.Service
|
||||
results.Add(mappedStatus);
|
||||
}
|
||||
|
||||
int index = results.FindIndex(ns => ns.DisplayName == "Reject");
|
||||
if (index > -1)
|
||||
{
|
||||
var item = results[index];
|
||||
results.RemoveAt(index);
|
||||
results.Insert(0, item);
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
@ -1404,16 +1478,45 @@ namespace Marco.Pms.Services.Service
|
||||
}).ToList();
|
||||
});
|
||||
|
||||
await Task.WhenAll(nextStatusTask, documentTask);
|
||||
var updateLogsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.StatusUpdateLogs.Include(sul => sul.UpdatedBy).Where(sul => sul.EntityId == paymentRequest.Id && sul.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(nextStatusTask, documentTask, updateLogsTask);
|
||||
|
||||
var nextStatuses = nextStatusTask.Result;
|
||||
var attachmentVMs = documentTask.Result;
|
||||
var updateLogs = updateLogsTask.Result;
|
||||
|
||||
var statusIds = updateLogs.Select(sul => sul.StatusId).ToList();
|
||||
statusIds.AddRange(updateLogs.Select(sul => sul.NextStatusId).ToList());
|
||||
|
||||
statusIds = statusIds.Distinct().ToList();
|
||||
|
||||
var status = await _context.ExpensesStatusMaster.Where(es => statusIds.Contains(es.Id)).ToListAsync();
|
||||
|
||||
// Map main response model and populate additional fields
|
||||
var response = _mapper.Map<PaymentRequestDetailsVM>(paymentRequest);
|
||||
response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}";
|
||||
response.Attachments = attachmentVMs;
|
||||
response.NextStatus = nextStatuses;
|
||||
response.UpdateLogs = updateLogs.Select(ul =>
|
||||
{
|
||||
var statusVm = status.FirstOrDefault(es => es.Id == ul.StatusId);
|
||||
var nextStatusVm = status.FirstOrDefault(es => es.Id == ul.NextStatusId);
|
||||
|
||||
return new PaymentRequestUpdateLog
|
||||
{
|
||||
Id = ul.Id,
|
||||
Comment = ul.Comment,
|
||||
Status = _mapper.Map<ExpensesStatusMasterVM>(statusVm),
|
||||
NextStatus = _mapper.Map<ExpensesStatusMasterVM>(nextStatusVm),
|
||||
UpdatedAt = ul.UpdatedAt,
|
||||
UpdatedBy = _mapper.Map<BasicEmployeeVM>(ul.UpdatedBy)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
_logger.LogInfo("Payment request details fetched successfully for PaymentRequestId: {PaymentRequestId}, EmployeeId: {EmployeeId}", paymentRequest.Id, loggedInEmployee.Id);
|
||||
|
||||
@ -1474,12 +1577,11 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Internal Exception Occured", ExceptionMapper(ex), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
_logger.LogInfo("Start CreatePaymentRequestAsync for EmployeeId: {EmployeeId} TenantId: {TenantId}", loggedInEmployee.Id, tenantId);
|
||||
|
||||
string uIDPrefix = $"PY/{DateTime.Now:MMyy}";
|
||||
string uIDPrefix = $"PR/{DateTime.Now:MMyy}";
|
||||
|
||||
try
|
||||
{
|
||||
@ -1615,6 +1717,194 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogInfo("End CreatePaymentRequestAsync for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
}
|
||||
}
|
||||
public async Task<ApiResponse<object>> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
|
||||
// 1. Fetch Existing Payment Request with Related Entities (Single Query)
|
||||
var paymentRequest = await _context.PaymentRequests
|
||||
.Include(pr => pr.Currency)
|
||||
.Include(pr => pr.Project)
|
||||
.Include(pr => pr.RecurringPayment)
|
||||
.Include(pr => pr.ExpenseCategory)
|
||||
.Include(pr => pr.ExpenseStatus)
|
||||
.Include(pr => pr.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.FirstOrDefaultAsync(pr =>
|
||||
pr.Id == model.PaymentRequestId &&
|
||||
pr.ExpenseStatusId != model.StatusId &&
|
||||
pr.TenantId == tenantId
|
||||
);
|
||||
|
||||
if (paymentRequest == null)
|
||||
{
|
||||
_logger.LogWarning("ChangeStatus: Payment Request not found or already at target status. payment RequestId={PaymentRequestId}, TenantId={TenantId}", model.PaymentRequestId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("payment Request not found or status is already set.", "payment Request not found", 404);
|
||||
}
|
||||
|
||||
_logger.LogInfo("ChangeStatus: Requested status change. PaymentRequestId={PaymentRequestId} FromStatus={FromStatusId} ToStatus={ToStatusId}",
|
||||
paymentRequest.Id, paymentRequest.ExpenseStatusId, model.StatusId);
|
||||
|
||||
// 2. Run Prerequisite Checks in Parallel (Status transition + Permissions)
|
||||
var statusTransitionTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.ExpensesStatusMapping
|
||||
.Include(m => m.NextStatus)
|
||||
.FirstOrDefaultAsync(m => m.StatusId == paymentRequest.ExpenseStatusId && m.NextStatusId == model.StatusId);
|
||||
});
|
||||
|
||||
var targetStatusPermissionsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.StatusPermissionMapping
|
||||
.Where(spm => spm.StatusId == paymentRequest.ExpenseStatusId)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(statusTransitionTask, targetStatusPermissionsTask);
|
||||
var statusTransition = await statusTransitionTask;
|
||||
var requiredPermissions = await targetStatusPermissionsTask;
|
||||
|
||||
// 3. Validate Transition and Required Fields
|
||||
if (statusTransition == null)
|
||||
{
|
||||
_logger.LogWarning("ChangeStatus: Invalid status transition. PaymentRequestId={PaymentRequestId}, FromStatus={FromStatus}, ToStatus={ToStatus}",
|
||||
paymentRequest.Id, paymentRequest.ExpenseStatusId, model.StatusId);
|
||||
return ApiResponse<object>.ErrorResponse("Status change is not permitted.", "Invalid Transition", 400);
|
||||
}
|
||||
|
||||
// Validate special logic for "Processed"
|
||||
if (statusTransition.NextStatusId == Processed &&
|
||||
(string.IsNullOrWhiteSpace(model.PaidTransactionId) ||
|
||||
!model.PaidAt.HasValue ||
|
||||
model.PaidById == null ||
|
||||
model.PaidById == Guid.Empty))
|
||||
{
|
||||
_logger.LogWarning("ChangeStatus: Missing payment fields for 'Processed'. PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
|
||||
return ApiResponse<object>.ErrorResponse("payment details are missing or invalid.", "Invalid Payment", 400);
|
||||
}
|
||||
|
||||
// 4. Permission Check (CreatedBy -> Reviewer bypass, else required permissions)
|
||||
bool hasPermission = false;
|
||||
if (model.StatusId == Review && paymentRequest.CreatedById == loggedInEmployee.Id)
|
||||
{
|
||||
hasPermission = true;
|
||||
}
|
||||
else if (requiredPermissions.Any())
|
||||
{
|
||||
var permissionIds = requiredPermissions.Select(p => p.PermissionId).ToList();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
hasPermission = await permissionService.HasPermissionAny(permissionIds, loggedInEmployee.Id) && model.StatusId != Review;
|
||||
|
||||
}
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("ChangeStatus: Permission denied. EmployeeId={EmployeeId} PaymentRequestId={PaymentRequestId} ToStatus={ToStatusId}",
|
||||
loggedInEmployee.Id, paymentRequest.Id, model.StatusId);
|
||||
return ApiResponse<object>.ErrorResponse("You do not have permission for this action.", "Access Denied", 403);
|
||||
}
|
||||
|
||||
// 5. Prepare for update (Audit snapshot)
|
||||
var paymentRequestStateBeforeChange = _updateLogHelper.EntityToBsonDocument(paymentRequest);
|
||||
|
||||
// 6. Apply Status Transition
|
||||
paymentRequest.ExpenseStatusId = statusTransition.NextStatusId;
|
||||
paymentRequest.ExpenseStatus = statusTransition.NextStatus;
|
||||
|
||||
|
||||
// 7. Add Reimbursement if applicable
|
||||
if (model.StatusId == Processed)
|
||||
{
|
||||
paymentRequest.PaidAt = model.PaidAt;
|
||||
paymentRequest.PaidById = model.PaidById;
|
||||
paymentRequest.PaidTransactionId = model.PaidTransactionId;
|
||||
|
||||
var lastTransaction = await _context.AdvancePaymentTransactions.OrderByDescending(apt => apt.CreatedAt).FirstOrDefaultAsync(apt => apt.TenantId == tenantId);
|
||||
double lastBalance = 0;
|
||||
if (lastTransaction != null)
|
||||
{
|
||||
lastBalance = lastTransaction.CurrentBalance;
|
||||
}
|
||||
|
||||
_context.AdvancePaymentTransactions.Add(new AdvancePaymentTransaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FinanceUIdPostfix = paymentRequest.UIDPostfix,
|
||||
FinanceUIdPrefix = paymentRequest.UIDPrefix,
|
||||
Title = paymentRequest.Title,
|
||||
ProjectId = paymentRequest.ProjectId,
|
||||
EmployeeId = paymentRequest.CreatedById,
|
||||
Amount = paymentRequest.Amount,
|
||||
CurrentBalance = lastBalance + paymentRequest.Amount,
|
||||
CreatedAt = model.PaidAt!.Value,
|
||||
CreatedById = loggedInEmployee.Id,
|
||||
IsActive = true,
|
||||
TenantId = tenantId
|
||||
});
|
||||
}
|
||||
|
||||
// 8. Add paymentRequest Log Entry
|
||||
_context.StatusUpdateLogs.Add(new StatusUpdateLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EntityId = paymentRequest.Id,
|
||||
StatusId = statusTransition.StatusId,
|
||||
NextStatusId = statusTransition.NextStatusId,
|
||||
UpdatedById = loggedInEmployee.Id,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Comment = model.Comment,
|
||||
TenantId = tenantId
|
||||
});
|
||||
|
||||
// 9. Commit database transaction
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("ChangeStatus: Status updated successfully. PaymentRequestId={PaymentRequestId} NewStatus={NewStatusId}", paymentRequest.Id, paymentRequest.ExpenseStatusId);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException ex)
|
||||
{
|
||||
_logger.LogError(ex, "ChangeStatus: Concurrency error. PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Payment Request 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(paymentRequest, name, tenantId);
|
||||
|
||||
//});
|
||||
|
||||
// 10. Post-processing (audit log, cache, fetch next states)
|
||||
try
|
||||
{
|
||||
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = paymentRequest.Id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
OldObject = paymentRequestStateBeforeChange,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
}, "PaymentRequestModificationLog");
|
||||
|
||||
// Prepare response
|
||||
var responseDto = _mapper.Map<PaymentRequestVM>(paymentRequest);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(responseDto, "Payment Request status chnaged successfully", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ChangeStatus: Post-operation error (e.g. audit logging). PaymentRequestId={PaymentRequestId}", paymentRequest.Id);
|
||||
var responseDto = _mapper.Map<PaymentRequestVM>(paymentRequest);
|
||||
return ApiResponse<object>.SuccessResponse(responseDto, "Status updated, but audit logging or cache update failed.");
|
||||
}
|
||||
}
|
||||
public async Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
_logger.LogInfo("Start EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}, EmployeeId: {EmployeeId}", id, loggedInEmployee.Id);
|
||||
@ -2023,7 +2313,8 @@ namespace Marco.Pms.Services.Service
|
||||
FileName = ba.Document.FileName,
|
||||
ContentType = ba.Document.ContentType,
|
||||
S3Key = ba.Document.S3Key,
|
||||
ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key
|
||||
ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key,
|
||||
FileSize = ba.Document.FileSize,
|
||||
}).ToList()
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> GetPayeeNameListAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetPaymentRequestFilterObjectAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user