Added skeleton for action and update API.
This commit is contained in:
parent
3b4b09783b
commit
839bc360f3
@ -16,7 +16,6 @@ namespace Marco.Pms.Model.Dtos.Expenses
|
|||||||
public required string SupplerName { get; set; }
|
public required string SupplerName { get; set; }
|
||||||
public required double Amount { get; set; }
|
public required double Amount { get; set; }
|
||||||
public int? NoOfPersons { get; set; } = 0;
|
public int? NoOfPersons { get; set; } = 0;
|
||||||
public required Guid StatusId { get; set; }
|
|
||||||
public bool PreApproved { get; set; } = false;
|
public bool PreApproved { get; set; } = false;
|
||||||
public required List<FileUploadModel> BillAttachments { get; set; }
|
public required List<FileUploadModel> BillAttachments { get; set; }
|
||||||
}
|
}
|
||||||
|
9
Marco.Pms.Model/Dtos/Expenses/ExpenseRecordDto.cs
Normal file
9
Marco.Pms.Model/Dtos/Expenses/ExpenseRecordDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Marco.Pms.Model.Dtos.Expenses
|
||||||
|
{
|
||||||
|
public class ExpenseRecordDto
|
||||||
|
{
|
||||||
|
public Guid ExpenseId { get; set; }
|
||||||
|
public Guid StatusId { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
}
|
23
Marco.Pms.Model/Dtos/Expenses/UpdateExpensesDto.cs
Normal file
23
Marco.Pms.Model/Dtos/Expenses/UpdateExpensesDto.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.Dtos.Expenses
|
||||||
|
{
|
||||||
|
public class UpdateExpensesDto
|
||||||
|
{
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
public required Guid ProjectId { get; set; }
|
||||||
|
public required Guid ExpensesTypeId { get; set; }
|
||||||
|
public required Guid PaymentModeId { get; set; }
|
||||||
|
public required Guid PaidById { get; set; }
|
||||||
|
public DateTime TransactionDate { get; set; } = DateTime.Now;
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
public required string Description { get; set; }
|
||||||
|
public string? Location { get; set; }
|
||||||
|
public string? GSTNumber { get; set; }
|
||||||
|
public required string SupplerName { get; set; }
|
||||||
|
public required double Amount { get; set; }
|
||||||
|
public int? NoOfPersons { get; set; } = 0;
|
||||||
|
public bool PreApproved { get; set; } = false;
|
||||||
|
public List<FileUploadModel>? BillAttachments { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ using Marco.Pms.Model.Expenses;
|
|||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.Expanses;
|
using Marco.Pms.Model.ViewModels.Expanses;
|
||||||
using Marco.Pms.Model.ViewModels.Master;
|
using Marco.Pms.Model.ViewModels.Master;
|
||||||
|
using Marco.Pms.Model.ViewModels.Projects;
|
||||||
using Marco.Pms.Services.Service;
|
using Marco.Pms.Services.Service;
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
@ -15,7 +16,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||||
|
|
||||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
{
|
{
|
||||||
@ -24,135 +24,37 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class ExpenseController : ControllerBase
|
public class ExpenseController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly PermissionServices _permission;
|
private readonly PermissionServices _permission;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly S3UploadService _s3Service;
|
private readonly S3UploadService _s3Service;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
public ExpenseController(
|
public ExpenseController(
|
||||||
|
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
UserHelper userHelper,
|
UserHelper userHelper,
|
||||||
PermissionServices permission,
|
PermissionServices permission,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
ILoggingService logger,
|
ILoggingService logger,
|
||||||
S3UploadService s3Service,
|
S3UploadService s3Service,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
_context = context;
|
_context = context;
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_permission = permission;
|
_permission = permission;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_s3Service = s3Service;
|
_s3Service = s3Service;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("list")]
|
|
||||||
public async Task<IActionResult> GetExpensesList1(string? filter, int pageSize = 20, int pageNumber = 1)
|
|
||||||
{
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
||||||
var loggedInEmployeeId = loggedInEmployee.Id;
|
|
||||||
|
|
||||||
List<Expenses>? expensesList = null;
|
|
||||||
var expensesListQuery = _context.Expenses
|
|
||||||
.Include(e => e.ExpensesType)
|
|
||||||
.Include(e => e.Project)
|
|
||||||
.Include(e => e.PaidBy)
|
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(e => e.PaymentMode)
|
|
||||||
.Include(e => e.Status)
|
|
||||||
.Include(e => e.CreatedBy)
|
|
||||||
.Where(e => e.TenantId == tenantId)
|
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
|
||||||
.Take(pageSize);
|
|
||||||
|
|
||||||
var HasViewAllPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
|
||||||
var HasViewSelfPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
|
||||||
|
|
||||||
if (HasViewSelfPermission)
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
|
||||||
}
|
|
||||||
else if (!HasViewAllPermission)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expanses list.", loggedInEmployeeId);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No Expense found for current user", 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpensesFilter? expenesFilter = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
||||||
expenesFilter = JsonSerializer.Deserialize<ExpensesFilter>(filter, options);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "[GetExpensesList] Failed to parse filter came from website or mobile");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
||||||
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
|
||||||
expenesFilter = JsonSerializer.Deserialize<ExpensesFilter>(unescapedJsonString, options);
|
|
||||||
}
|
|
||||||
catch (Exception ex1)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex1, "[GetExpensesList] Failed to parse filter came from postman");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectIds = expenesFilter?.ProjectIds;
|
|
||||||
var filterStatusIds = expenesFilter?.StatusIds;
|
|
||||||
var createdByIds = expenesFilter?.CreatedByIds;
|
|
||||||
var paidById = expenesFilter?.PaidById;
|
|
||||||
var startDate = expenesFilter?.StartDate;
|
|
||||||
var endDate = expenesFilter?.EndDate;
|
|
||||||
|
|
||||||
if (startDate != null && endDate != null)
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => e.CreatedAt.Date >= startDate && e.CreatedAt.Date <= endDate);
|
|
||||||
}
|
|
||||||
else if (projectIds != null && projectIds.Any())
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => projectIds.Contains(e.ProjectId));
|
|
||||||
}
|
|
||||||
else if (filterStatusIds != null && filterStatusIds.Any())
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => filterStatusIds.Contains(e.StatusId));
|
|
||||||
}
|
|
||||||
else if (createdByIds != null && createdByIds.Any() && HasViewAllPermission)
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => createdByIds.Contains(e.CreatedById));
|
|
||||||
}
|
|
||||||
else if (paidById != null && paidById.Any())
|
|
||||||
{
|
|
||||||
expensesListQuery = expensesListQuery.Where(e => paidById.Contains(e.PaidById));
|
|
||||||
}
|
|
||||||
|
|
||||||
expensesList = await expensesListQuery.ToListAsync();
|
|
||||||
|
|
||||||
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
|
||||||
|
|
||||||
var statusIds = expensesList.Select(e => e.StatusId).ToList();
|
|
||||||
|
|
||||||
var statusMappings = await _context.ExpensesStatusMapping
|
|
||||||
.Include(sm => sm.NextStatus)
|
|
||||||
.Where(sm => statusIds.Contains(sm.StatusId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var expense in response)
|
|
||||||
{
|
|
||||||
var statusMapping = statusMappings.Where(sm => sm.StatusId == expense.Status?.Id).Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus)).ToList();
|
|
||||||
expense.NextStatus = statusMapping;
|
|
||||||
|
|
||||||
}
|
|
||||||
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of expenses for you fetched successfully", 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a paginated list of expenses based on user permissions and optional filters.
|
/// Retrieves a paginated list of expenses based on user permissions and optional filters.
|
||||||
@ -161,7 +63,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
/// <param name="pageSize">The number of records to return per page.</param>
|
/// <param name="pageSize">The number of records to return per page.</param>
|
||||||
/// <param name="pageNumber">The page number to retrieve.</param>
|
/// <param name="pageNumber">The page number to retrieve.</param>
|
||||||
/// <returns>A paginated list of expenses.</returns>
|
/// <returns>A paginated list of expenses.</returns>
|
||||||
[HttpGet] // Assuming this is a GET endpoint
|
[HttpGet("list")] // Assuming this is a GET endpoint
|
||||||
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
|
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -180,8 +82,21 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
||||||
|
|
||||||
var hasViewAllPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
var hasViewSelfPermissionTask = Task.Run(async () =>
|
||||||
var hasViewSelfPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasViewAllPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
|
||||||
|
|
||||||
// 2. --- Build Base Query and Apply Permissions ---
|
// 2. --- Build Base Query and Apply Permissions ---
|
||||||
// Start with a base IQueryable. Filters will be chained onto this.
|
// Start with a base IQueryable. Filters will be chained onto this.
|
||||||
@ -196,12 +111,12 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||||
|
|
||||||
// Apply permission-based filtering BEFORE any other filters or pagination.
|
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||||
if (hasViewAllPermission)
|
if (hasViewAllPermissionTask.Result)
|
||||||
{
|
{
|
||||||
// User has 'View All' permission, no initial restriction on who created the expense.
|
// User has 'View All' permission, no initial restriction on who created the expense.
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
||||||
}
|
}
|
||||||
else if (hasViewSelfPermission)
|
else if (hasViewSelfPermissionTask.Result)
|
||||||
{
|
{
|
||||||
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
||||||
@ -241,7 +156,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||||
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermission)
|
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
||||||
{
|
{
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
||||||
}
|
}
|
||||||
@ -329,6 +244,433 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("details/{id}")]
|
||||||
|
public string Get(int id)
|
||||||
|
{
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("create")]
|
||||||
|
public async Task<IActionResult> CreateExpense1([FromBody] CreateExpensesDto dto)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var hasUploadPermission = await _permission.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
||||||
|
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
||||||
|
if (!hasUploadPermission || !hasProjectPermission)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} for uploading expense on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
||||||
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to Upload expenses for this project", 403));
|
||||||
|
}
|
||||||
|
var isExpensesTypeExist = await _context.ExpensesTypeMaster.AnyAsync(et => et.Id == dto.ExpensesTypeId);
|
||||||
|
if (!isExpensesTypeExist)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Expenses type not for ID: {ExpensesTypeId} when creating new expense", dto.ExpensesTypeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Expanses Type not found", "Expanses Type not found", 404));
|
||||||
|
}
|
||||||
|
var isPaymentModeExist = await _context.PaymentModeMatser.AnyAsync(et => et.Id == dto.PaymentModeId);
|
||||||
|
if (!isPaymentModeExist)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Payment Mode not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Payment Mode not found", "Payment Mode not found", 404));
|
||||||
|
}
|
||||||
|
var isStatusExist = await _context.ExpensesStatusMaster.AnyAsync(et => et.Id == Draft);
|
||||||
|
if (!isStatusExist)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Status not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Status not found", "Status not found", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
var expense = _mapper.Map<Expenses>(dto);
|
||||||
|
|
||||||
|
expense.CreatedById = loggedInEmployee.Id;
|
||||||
|
expense.CreatedAt = DateTime.UtcNow;
|
||||||
|
expense.TenantId = tenantId;
|
||||||
|
expense.IsActive = true;
|
||||||
|
expense.StatusId = Draft;
|
||||||
|
|
||||||
|
_context.Expenses.Add(expense);
|
||||||
|
|
||||||
|
Guid batchId = Guid.NewGuid();
|
||||||
|
foreach (var attachment in dto.BillAttachments)
|
||||||
|
{
|
||||||
|
if (!_s3Service.IsBase64String(attachment.Base64Data))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Image upload failed: Base64 data is missing While creating new expense entity for project {ProjectId} by employee {EmployeeId}", expense.ProjectId, expense.PaidById);
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
|
||||||
|
}
|
||||||
|
var base64 = attachment.Base64Data!.Contains(',')
|
||||||
|
? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..]
|
||||||
|
: attachment.Base64Data;
|
||||||
|
|
||||||
|
var fileType = _s3Service.GetContentTypeFromBase64(base64);
|
||||||
|
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
|
||||||
|
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
|
||||||
|
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while saving image to S3");
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot upload attachment to S3", new
|
||||||
|
{
|
||||||
|
message = ex.Message,
|
||||||
|
innerexcption = ex.InnerException?.Message,
|
||||||
|
stackTrace = ex.StackTrace,
|
||||||
|
source = ex.Source
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new Document
|
||||||
|
{
|
||||||
|
BatchId = batchId,
|
||||||
|
UploadedById = loggedInEmployee.Id,
|
||||||
|
FileName = attachment.FileName ?? "",
|
||||||
|
ContentType = attachment.ContentType ?? "",
|
||||||
|
S3Key = objectKey,
|
||||||
|
FileSize = attachment.FileSize,
|
||||||
|
UploadedAt = DateTime.UtcNow,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
_context.Documents.Add(document);
|
||||||
|
|
||||||
|
var billAttachement = new BillAttachments
|
||||||
|
{
|
||||||
|
DocumentId = document.Id,
|
||||||
|
ExpensesId = expense.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
_context.BillAttachments.Add(billAttachement);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Error occured while saving Expense, Document and bill attachment entity");
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
|
||||||
|
{
|
||||||
|
Message = dbEx.Message,
|
||||||
|
StackTrace = dbEx.StackTrace,
|
||||||
|
Source = dbEx.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = dbEx.InnerException?.Message,
|
||||||
|
StackTrace = dbEx.InnerException?.StackTrace,
|
||||||
|
Source = dbEx.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
_logger.LogInfo("Documents and attachments saved for Expense: {ExpenseId}", expense.Id);
|
||||||
|
|
||||||
|
return StatusCode(201, ApiResponse<object>.SuccessResponse(expense, "Expense created Successfully", 201));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new expense entry along with its bill attachments.
|
||||||
|
/// This operation is transactional and performs validations and file uploads in parallel for optimal performance.
|
||||||
|
/// Permission checks are also run in parallel using IServiceScopeFactory to ensure thread safety.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The data transfer object containing expense details and attachments.</param>
|
||||||
|
/// <returns>An IActionResult indicating the result of the creation operation.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateExpense([FromBody] CreateExpensesDto dto)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("CreateExpense for Project {ProjectId}", dto.ProjectId);
|
||||||
|
// The entire operation is wrapped in a transaction to ensure data consistency.
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
|
// 1. Authorization: Run permission checks in parallel using a service scope for each task.
|
||||||
|
// This is crucial for thread-safety as IPermissionService is a scoped service.
|
||||||
|
var hasUploadPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasProjectPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask);
|
||||||
|
|
||||||
|
if (!hasUploadPermissionTask.Result || !hasProjectPermissionTask.Result)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
||||||
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2. Validation: Check if prerequisite entities exist.
|
||||||
|
// The method now returns a tuple indicating success or failure.
|
||||||
|
// Each task creates its own DbContext instance from the factory, making the parallel calls thread-safe.
|
||||||
|
var projectGetTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var expenseTypeGetTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesTypeMaster.FirstOrDefaultAsync(et => et.Id == dto.ExpensesTypeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var paymentModeGetTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.PaymentModeMatser.FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var statusGetTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMaster.FirstOrDefaultAsync(es => es.Id == Draft);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(projectGetTask, expenseTypeGetTask, paymentModeGetTask, statusGetTask);
|
||||||
|
|
||||||
|
var project = await projectGetTask;
|
||||||
|
var expenseType = await expenseTypeGetTask;
|
||||||
|
var paymentMode = await paymentModeGetTask;
|
||||||
|
var status = await statusGetTask;
|
||||||
|
|
||||||
|
if (project == null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
|
||||||
|
_logger.LogWarning("Expense creation failed due to validation: Project with ID {ProjectId} not found.", dto.ProjectId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404));
|
||||||
|
}
|
||||||
|
else if (expenseType == null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
|
||||||
|
_logger.LogWarning("Expense creation failed due to validation: Expense Type with ID {ExpensesTypeId} not found.", dto.ExpensesTypeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Expense Type not found.", "Expense Type not found.", 404));
|
||||||
|
}
|
||||||
|
else if (paymentMode == null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
|
||||||
|
_logger.LogWarning("Expense creation failed due to validation: Payment Mode with ID {PaymentModeId} not found.", dto.PaymentModeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Payment Mode not found.", "Payment Mode not found.", 404));
|
||||||
|
}
|
||||||
|
else if (status == null)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
|
||||||
|
_logger.LogWarning("Expense creation failed due to validation: Status with ID {StatusId} not found.", Draft);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Status not found.", "Status not found.", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Entity Creation
|
||||||
|
var expense = _mapper.Map<Expenses>(dto);
|
||||||
|
expense.CreatedById = loggedInEmployee.Id;
|
||||||
|
expense.CreatedAt = DateTime.UtcNow;
|
||||||
|
expense.TenantId = tenantId;
|
||||||
|
expense.IsActive = true;
|
||||||
|
expense.StatusId = Draft;
|
||||||
|
|
||||||
|
_context.Expenses.Add(expense);
|
||||||
|
|
||||||
|
// 4. Process Attachments
|
||||||
|
if (dto.BillAttachments?.Any() ?? false)
|
||||||
|
{
|
||||||
|
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Database Commit
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
// 6. Transaction Commit
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
var response = _mapper.Map<ExpenseList>(expense);
|
||||||
|
|
||||||
|
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
||||||
|
response.Status = _mapper.Map<ExpensesStatusMasterVM>(status);
|
||||||
|
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
|
||||||
|
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
|
||||||
|
|
||||||
|
_logger.LogInfo("Successfully created Expense {ExpenseId} for Project {ProjectId}.", expense.Id, expense.ProjectId);
|
||||||
|
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Expense created successfully.", 201));
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex) // For invalid Base64 or other bad arguments.
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Invalid argument provided during expense creation for project {ProjectId}.", dto.ProjectId);
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Request Data", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 failure).
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId);
|
||||||
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes and uploads attachments in parallel, then adds the resulting entities to the main DbContext.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
|
||||||
|
{
|
||||||
|
var batchId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var processingTasks = attachments.Select(attachment => Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
|
||||||
|
throw new ArgumentException("Invalid or missing Base64 data for an attachment.");
|
||||||
|
|
||||||
|
var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
|
||||||
|
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
|
||||||
|
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
|
||||||
|
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
|
||||||
|
|
||||||
|
// Upload and create entities
|
||||||
|
await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);
|
||||||
|
_logger.LogInfo("Uploaded file to S3 with key: {ObjectKey}", objectKey);
|
||||||
|
|
||||||
|
return CreateAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
|
||||||
|
})).ToList();
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(processingTasks);
|
||||||
|
|
||||||
|
// This part is thread-safe as it runs after all parallel tasks are complete.
|
||||||
|
foreach (var (document, billAttachment) in results)
|
||||||
|
{
|
||||||
|
_context.Documents.Add(document);
|
||||||
|
_context.BillAttachments.Add(billAttachment);
|
||||||
|
}
|
||||||
|
_logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A private static helper method to create Document and BillAttachment entities.
|
||||||
|
/// </summary>
|
||||||
|
private static (Document document, BillAttachments billAttachment) CreateAttachmentEntities(
|
||||||
|
Guid batchId, Guid expenseId, Guid uploadedById, Guid tenantId, string s3Key, FileUploadModel attachmentDto)
|
||||||
|
{
|
||||||
|
var document = new Document
|
||||||
|
{
|
||||||
|
BatchId = batchId,
|
||||||
|
UploadedById = uploadedById,
|
||||||
|
FileName = attachmentDto.FileName ?? "",
|
||||||
|
ContentType = attachmentDto.ContentType ?? "",
|
||||||
|
S3Key = s3Key,
|
||||||
|
FileSize = attachmentDto.FileSize,
|
||||||
|
UploadedAt = DateTime.UtcNow,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
var billAttachment = new BillAttachments { Document = document, ExpensesId = expenseId, TenantId = tenantId };
|
||||||
|
return (document, billAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("action")]
|
||||||
|
public async Task<IActionResult> ChangeStatus([FromBody] ExpenseRecordDto model)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var exsitingExpenses = await _context.Expenses
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (exsitingExpenses == null)
|
||||||
|
{
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
exsitingExpenses.StatusId = model.StatusId;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException dbEx)
|
||||||
|
{
|
||||||
|
// --- Step 3: Handle Concurrency Conflicts ---
|
||||||
|
// This happens if another user modified the project after we fetched it.
|
||||||
|
_logger.LogError(dbEx, "Error occured while update status of expanse.");
|
||||||
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error occured while update status of expanse.", new
|
||||||
|
{
|
||||||
|
Message = dbEx.Message,
|
||||||
|
StackTrace = dbEx.StackTrace,
|
||||||
|
Source = dbEx.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = dbEx.InnerException?.Message,
|
||||||
|
StackTrace = dbEx.InnerException?.StackTrace,
|
||||||
|
Source = dbEx.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500));
|
||||||
|
}
|
||||||
|
var response = _mapper.Map<ExpenseList>(exsitingExpenses);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("edit/{id}")]
|
||||||
|
public async Task<IActionResult> UpdateExpanse(Guid id, [FromBody] UpdateExpensesDto model)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var exsitingExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == model.Id && e.TenantId == tenantId);
|
||||||
|
|
||||||
|
|
||||||
|
if (exsitingExpense == null)
|
||||||
|
{
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
|
||||||
|
}
|
||||||
|
_mapper.Map(model, exsitingExpense);
|
||||||
|
_context.Entry(exsitingExpense).State = EntityState.Modified;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException ex)
|
||||||
|
{
|
||||||
|
// --- Step 3: Handle Concurrency Conflicts ---
|
||||||
|
// This happens if another user modified the project after we fetched it.
|
||||||
|
_logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id);
|
||||||
|
return StatusCode(409, ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409));
|
||||||
|
}
|
||||||
|
var response = _mapper.Map<ExpenseList>(exsitingExpense);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("delete/{id}")]
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -373,153 +715,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return expenseFilter;
|
return expenseFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("details/{id}")]
|
#endregion
|
||||||
public string Get(int id)
|
|
||||||
{
|
|
||||||
return "value";
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("create")]
|
|
||||||
public async Task<IActionResult> Post([FromBody] CreateExpensesDto dto)
|
|
||||||
{
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
||||||
var hasUploadPermission = await _permission.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
|
||||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
|
||||||
if (!hasUploadPermission || !hasProjectPermission)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} for uploading expense on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
|
||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to Upload expenses for this project", 403));
|
|
||||||
}
|
|
||||||
var isExpensesTypeExist = await _context.ExpensesTypeMaster.AnyAsync(et => et.Id == dto.ExpensesTypeId);
|
|
||||||
if (!isExpensesTypeExist)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Expenses type not for ID: {ExpensesTypeId} when creating new expense", dto.ExpensesTypeId);
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Expanses Type not found", "Expanses Type not found", 404));
|
|
||||||
}
|
|
||||||
var isPaymentModeExist = await _context.PaymentModeMatser.AnyAsync(et => et.Id == dto.PaymentModeId);
|
|
||||||
if (!isPaymentModeExist)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Payment Mode not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Payment Mode not found", "Payment Mode not found", 404));
|
|
||||||
}
|
|
||||||
var isStatusExist = await _context.ExpensesStatusMaster.AnyAsync(et => et.Id == dto.StatusId);
|
|
||||||
if (!isStatusExist)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Status not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Status not found", "Status not found", 404));
|
|
||||||
}
|
|
||||||
var expense = new Expenses
|
|
||||||
{
|
|
||||||
ProjectId = dto.ProjectId,
|
|
||||||
ExpensesTypeId = dto.ExpensesTypeId,
|
|
||||||
PaymentModeId = dto.PaymentModeId,
|
|
||||||
PaidById = dto.PaidById,
|
|
||||||
CreatedById = loggedInEmployee.Id,
|
|
||||||
TransactionDate = dto.TransactionDate,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
TransactionId = dto.TransactionId,
|
|
||||||
Description = dto.Description,
|
|
||||||
Location = dto.Location,
|
|
||||||
GSTNumber = dto.GSTNumber,
|
|
||||||
SupplerName = dto.SupplerName,
|
|
||||||
Amount = dto.Amount,
|
|
||||||
NoOfPersons = dto.NoOfPersons,
|
|
||||||
StatusId = dto.StatusId,
|
|
||||||
PreApproved = dto.PreApproved,
|
|
||||||
IsActive = true,
|
|
||||||
TenantId = tenantId
|
|
||||||
};
|
|
||||||
_context.Expenses.Add(expense);
|
|
||||||
|
|
||||||
|
|
||||||
Guid batchId = Guid.NewGuid();
|
|
||||||
foreach (var attachment in dto.BillAttachments)
|
|
||||||
{
|
|
||||||
//if (!_s3Service.IsBase64String(attachment.Base64Data))
|
|
||||||
//{
|
|
||||||
// _logger.LogWarning("Image upload failed: Base64 data is missing While creating new expense entity for project {ProjectId} by employee {EmployeeId}", expense.ProjectId, expense.PaidById);
|
|
||||||
// return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
|
|
||||||
//}
|
|
||||||
var base64 = attachment.Base64Data!.Contains(',')
|
|
||||||
? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..]
|
|
||||||
: attachment.Base64Data;
|
|
||||||
|
|
||||||
var fileType = _s3Service.GetContentTypeFromBase64(base64);
|
|
||||||
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
|
|
||||||
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
|
|
||||||
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error occured while saving image to S3");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot upload attachment to S3", new
|
|
||||||
{
|
|
||||||
message = ex.Message,
|
|
||||||
innerexcption = ex.InnerException?.Message,
|
|
||||||
stackTrace = ex.StackTrace,
|
|
||||||
source = ex.Source
|
|
||||||
}, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
var document = new Document
|
|
||||||
{
|
|
||||||
BatchId = batchId,
|
|
||||||
UploadedById = loggedInEmployee.Id,
|
|
||||||
FileName = attachment.FileName ?? "",
|
|
||||||
ContentType = attachment.ContentType ?? "",
|
|
||||||
S3Key = objectKey,
|
|
||||||
//Base64Data = attachment.Base64Data,
|
|
||||||
FileSize = attachment.FileSize,
|
|
||||||
UploadedAt = DateTime.UtcNow,
|
|
||||||
TenantId = tenantId
|
|
||||||
};
|
|
||||||
_context.Documents.Add(document);
|
|
||||||
|
|
||||||
var billAttachement = new BillAttachments
|
|
||||||
{
|
|
||||||
DocumentId = document.Id,
|
|
||||||
ExpensesId = expense.Id,
|
|
||||||
TenantId = tenantId
|
|
||||||
};
|
|
||||||
_context.BillAttachments.Add(billAttachement);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException dbEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(dbEx, "Error occured while saving Expense, Document and bill attachment entity");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
|
|
||||||
{
|
|
||||||
Message = dbEx.Message,
|
|
||||||
StackTrace = dbEx.StackTrace,
|
|
||||||
Source = dbEx.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = dbEx.InnerException?.Message,
|
|
||||||
StackTrace = dbEx.InnerException?.StackTrace,
|
|
||||||
Source = dbEx.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 400));
|
|
||||||
}
|
|
||||||
_logger.LogInfo("Documents and attachments saved for Expense: {ExpenseId}", expense.Id);
|
|
||||||
|
|
||||||
return StatusCode(201, ApiResponse<object>.SuccessResponse(expense, "Expense created Successfully", 201));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPut("edit/{id}")]
|
|
||||||
public void Put(int id, [FromBody] string value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("delete/{id}")]
|
|
||||||
public void Delete(int id)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Marco.Pms.Model.Dtos.Expenses;
|
||||||
using Marco.Pms.Model.Dtos.Master;
|
using Marco.Pms.Model.Dtos.Master;
|
||||||
using Marco.Pms.Model.Dtos.Project;
|
using Marco.Pms.Model.Dtos.Project;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
@ -76,7 +77,8 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
#region ======================================================= Expenses =======================================================
|
#region ======================================================= Expenses =======================================================
|
||||||
|
|
||||||
CreateMap<Expenses, ExpenseList>();
|
CreateMap<Expenses, ExpenseList>();
|
||||||
|
CreateMap<CreateExpensesDto, Expenses>();
|
||||||
|
CreateMap<UpdateExpensesDto, Expenses>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
if (allowedFilesType == null || !allowedFilesType.Contains(fileType))
|
if (allowedFilesType == null || !allowedFilesType.Contains(fileType))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Unsupported file type. {FileType}", fileType);
|
||||||
throw new InvalidOperationException("Unsupported file type.");
|
throw new InvalidOperationException("Unsupported file type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user