Added an API to update purchase invoice
This commit is contained in:
parent
3dce559de2
commit
34c5ac9c25
@ -10,6 +10,7 @@
|
|||||||
public required string ShippingAddress { get; set; }
|
public required string ShippingAddress { get; set; }
|
||||||
public string? PurchaseOrderNumber { get; set; }
|
public string? PurchaseOrderNumber { get; set; }
|
||||||
public DateTime? PurchaseOrderDate { get; set; }
|
public DateTime? PurchaseOrderDate { get; set; }
|
||||||
|
public Guid? StatusId { get; set; }
|
||||||
public required Guid SupplierId { get; set; }
|
public required Guid SupplierId { get; set; }
|
||||||
public string? ProformaInvoiceNumber { get; set; }
|
public string? ProformaInvoiceNumber { get; set; }
|
||||||
public DateTime? ProformaInvoiceDate { get; set; }
|
public DateTime? ProformaInvoiceDate { get; set; }
|
||||||
@ -26,6 +27,6 @@
|
|||||||
public double? TransportCharges { get; set; }
|
public double? TransportCharges { get; set; }
|
||||||
public required double TotalAmount { get; set; }
|
public required double TotalAmount { get; set; }
|
||||||
public DateTime? PaymentDueDate { get; set; } // Defaults to 40 days from the invoice date
|
public DateTime? PaymentDueDate { get; set; } // Defaults to 40 days from the invoice date
|
||||||
public List<InvoiceAttachmentDto>? Attachments { get; set; }
|
public List<InvoiceAttachmentDto> Attachments { get; set; } = new List<InvoiceAttachmentDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
using AutoMapper;
|
||||||
|
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
|
using Microsoft.AspNetCore.JsonPatch;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
@ -12,16 +15,20 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly IPurchaseInvoiceService _purchaseInvoiceService;
|
private readonly IPurchaseInvoiceService _purchaseInvoiceService;
|
||||||
private readonly ISignalRService _signalR;
|
private readonly ISignalRService _signalR;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
|
|
||||||
public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService, ISignalRService signalR)
|
public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService, ISignalRService signalR, IServiceScopeFactory serviceScopeFactory)
|
||||||
{
|
{
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_purchaseInvoiceService = purchaseInvoiceService;
|
_purchaseInvoiceService = purchaseInvoiceService;
|
||||||
tenantId = _userHelper.GetTenantId();
|
tenantId = _userHelper.GetTenantId();
|
||||||
_signalR = signalR;
|
_signalR = signalR;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region =================================================================== Purchase Invoice Functions ===================================================================
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a list of purchase invoices based on search string, filter, activity status, page size, and page number.
|
/// Retrieves a list of purchase invoices based on search string, filter, activity status, page size, and page number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -90,5 +97,36 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
// Return the HTTP response
|
// Return the HTTP response
|
||||||
return StatusCode(response.StatusCode, response);
|
return StatusCode(response.StatusCode, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPatch("edit/{id}")]
|
||||||
|
public async Task<IActionResult> EditPurchaseInvoice(Guid id, [FromBody] JsonPatchDocument<PurchaseInvoiceDto> patchDoc, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var existingPurchaseInvoice = await _purchaseInvoiceService.GetPurchaseInvoiceByIdAsync(id, tenantId, ct);
|
||||||
|
if (existingPurchaseInvoice == null)
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Invalid purchase invoice ID", "Invalid purchase invoice ID", 404));
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
|
||||||
|
var modelToPatch = mapper.Map<PurchaseInvoiceDto>(existingPurchaseInvoice);
|
||||||
|
|
||||||
|
// Apply the JSON Patch document to the DTO and check model state validity
|
||||||
|
patchDoc.ApplyTo(modelToPatch, ModelState);
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Validation failed", "Provided patch document values are invalid", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _purchaseInvoiceService.UpdatePurchaseInvoiceAsync(id, existingPurchaseInvoice, modelToPatch, loggedInEmployee, tenantId, ct);
|
||||||
|
return StatusCode(response.StatusCode, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -628,6 +628,9 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.PaymentDueDate,
|
dest => dest.PaymentDueDate,
|
||||||
opt => opt.MapFrom(src => src.PaymentDueDate.HasValue ? src.PaymentDueDate : DateTime.UtcNow.AddDays(40)));
|
opt => opt.MapFrom(src => src.PaymentDueDate.HasValue ? src.PaymentDueDate : DateTime.UtcNow.AddDays(40)));
|
||||||
|
|
||||||
|
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDto>();
|
||||||
|
|
||||||
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceListVM>()
|
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceListVM>()
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.PurchaseInvoiceUId,
|
dest => dest.PurchaseInvoiceUId,
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
|
using Marco.Pms.Helpers.Utility;
|
||||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Filters;
|
using Marco.Pms.Model.Filters;
|
||||||
|
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||||
using Marco.Pms.Model.OrganizationModel;
|
using Marco.Pms.Model.OrganizationModel;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
using Marco.Pms.Model.PurchaseInvoice;
|
using Marco.Pms.Model.PurchaseInvoice;
|
||||||
@ -41,136 +43,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
_s3Service = s3Service;
|
_s3Service = s3Service;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
#region =================================================================== Purchase Invoice Functions ===================================================================
|
||||||
//public async Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
|
||||||
// Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
|
||||||
//{
|
|
||||||
// _logger.LogInfo("GetPurchaseInvoiceListAsync called for TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
|
||||||
|
|
||||||
// await using var _context = await _dbContextFactory.CreateDbContextAsync(ct);
|
|
||||||
|
|
||||||
// //var purchaseInvoices = _context.PurchaseInvoiceDetails
|
|
||||||
// var query = _context.PurchaseInvoiceDetails
|
|
||||||
// .Include(pid => pid.Organization)
|
|
||||||
// .Include(pid => pid.Supplier)
|
|
||||||
// .Include(pid => pid.Status)
|
|
||||||
// .Where(pid => pid.IsActive == isActive && pid.TenantId == tenantId);
|
|
||||||
|
|
||||||
// var advanceFilter = TryDeserializeFilter(filter);
|
|
||||||
|
|
||||||
// query = query.ApplyCustomFilters(advanceFilter, "CreatedAt");
|
|
||||||
|
|
||||||
// if (advanceFilter != null)
|
|
||||||
// {
|
|
||||||
// if (advanceFilter.Filters != null)
|
|
||||||
// {
|
|
||||||
// query = query.ApplyListFilters(advanceFilter.Filters);
|
|
||||||
// }
|
|
||||||
// if (advanceFilter.DateFilter != null)
|
|
||||||
// {
|
|
||||||
// query = query.ApplyDateFilter(advanceFilter.DateFilter);
|
|
||||||
// }
|
|
||||||
// if (advanceFilter.SearchFilters != null)
|
|
||||||
// {
|
|
||||||
// var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName" || f.Column != "Project").ToList();
|
|
||||||
// if (invoiceSearchFilter.Any())
|
|
||||||
// {
|
|
||||||
// query = query.ApplySearchFilters(invoiceSearchFilter);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
|
|
||||||
// {
|
|
||||||
// query = query.ApplyGroupByFilters(advanceFilter.GroupByColumn);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// bool isProjectFilter = false;
|
|
||||||
|
|
||||||
// var infraProjectTask = Task.Run(async () =>
|
|
||||||
// {
|
|
||||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
|
|
||||||
// var infraProjectsQuery = context.Projects.Where(p => p.TenantId == tenantId);
|
|
||||||
|
|
||||||
// if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
|
||||||
// {
|
|
||||||
// var projectSearchFilter = advanceFilter.SearchFilters
|
|
||||||
// .Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
|
||||||
// .Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
|
||||||
// .ToList();
|
|
||||||
// if (projectSearchFilter.Any())
|
|
||||||
// {
|
|
||||||
// infraProjectsQuery = infraProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
|
||||||
// isProjectFilter = true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return await infraProjectsQuery.Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// var serviceProjectTask = Task.Run(async () =>
|
|
||||||
// {
|
|
||||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
|
|
||||||
// var serviceProjectsQuery = context.ServiceProjects.Where(sp => sp.TenantId == tenantId);
|
|
||||||
|
|
||||||
// if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
|
||||||
// {
|
|
||||||
// var projectSearchFilter = advanceFilter.SearchFilters
|
|
||||||
// .Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
|
||||||
// .Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
|
||||||
// .ToList();
|
|
||||||
// if (projectSearchFilter.Any())
|
|
||||||
// {
|
|
||||||
// serviceProjectsQuery = serviceProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
|
||||||
// isProjectFilter = true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return await serviceProjectsQuery.Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
|
||||||
|
|
||||||
// var projects = infraProjectTask.Result;
|
|
||||||
// projects.AddRange(serviceProjectTask.Result);
|
|
||||||
|
|
||||||
// if (isProjectFilter)
|
|
||||||
// {
|
|
||||||
// var projectIds = projects.Select(p => p.Id).ToList();
|
|
||||||
// query = query.Where(pid => projectIds.Contains(pid.ProjectId));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var totalCount = await query.CountAsync(ct);
|
|
||||||
|
|
||||||
// var purchaseInvoices = await query
|
|
||||||
// .Skip((pageNumber - 1) * pageSize)
|
|
||||||
// .Take(pageSize)
|
|
||||||
// .ToListAsync();
|
|
||||||
|
|
||||||
// var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
|
||||||
|
|
||||||
// var response = purchaseInvoices.Select(pi =>
|
|
||||||
// {
|
|
||||||
// var result = _mapper.Map<PurchaseInvoiceListVM>(pi);
|
|
||||||
// result.Project = projects.FirstOrDefault(p => p.Id == pi.ProjectId);
|
|
||||||
// return result;
|
|
||||||
// }).ToList();
|
|
||||||
|
|
||||||
// var pagedResult = new
|
|
||||||
// {
|
|
||||||
// CurrentPage = pageNumber,
|
|
||||||
// PageSize = pageSize,
|
|
||||||
// TotalPages = totalPages,
|
|
||||||
// TotalCount = totalCount,
|
|
||||||
// HasPrevious = pageNumber > 1,
|
|
||||||
// HasNext = pageNumber < totalPages,
|
|
||||||
// Data = response
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return ApiResponse<object>.SuccessResponse(pagedResult, "Invoice list fetched successfully", 200);
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a paged list of purchase invoices for a given tenant with support for
|
/// Retrieves a paged list of purchase invoices for a given tenant with support for
|
||||||
/// advanced filters, project-based search across Infra and Service projects, and
|
/// advanced filters, project-based search across Infra and Service projects, and
|
||||||
@ -611,7 +484,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
// Generate Invoice ID early for S3 folder structure
|
// Generate Invoice ID early for S3 folder structure
|
||||||
var newInvoiceId = Guid.NewGuid();
|
var newInvoiceId = Guid.NewGuid();
|
||||||
|
|
||||||
if (model.Attachments?.Any() == true)
|
if (model.Attachments.Any())
|
||||||
{
|
{
|
||||||
var batchId = Guid.NewGuid();
|
var batchId = Guid.NewGuid();
|
||||||
|
|
||||||
@ -761,7 +634,281 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<object>> UpdatePurchaseInvoiceAsync(Guid id, PurchaseInvoiceDetails purchaseInvoice, PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Validate input arguments and log warnings for invalid cases.
|
||||||
|
if (id == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync called with empty invoice Id. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse(
|
||||||
|
"Invalid invoice identifier",
|
||||||
|
"The purchase invoice identifier cannot be empty.",
|
||||||
|
400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync called with empty tenant Id. InvoiceId: {InvoiceId}, EmployeeId: {EmployeeId}", id, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse(
|
||||||
|
"Invalid tenant identifier",
|
||||||
|
"The tenant identifier cannot be empty.",
|
||||||
|
400);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInfo("Starting UpdatePurchaseInvoiceAsync. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", id, tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||||
|
await using var transaction = await context.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Scoped helper service for update logs.
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||||
|
|
||||||
|
// 1. Validate existence of Project (Infra or Service).
|
||||||
|
var infraProject = await context.Projects
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
|
||||||
|
.Select(p => _mapper.Map<BasicProjectVM>(p))
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
BasicProjectVM? projectVm;
|
||||||
|
if (infraProject == null)
|
||||||
|
{
|
||||||
|
var serviceProject = await context.ServiceProjects
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(sp => sp.Id == model.ProjectId && sp.IsActive && sp.TenantId == tenantId)
|
||||||
|
.Select(p => _mapper.Map<BasicProjectVM>(p))
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (serviceProject == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync failed: Project {ProjectId} not found for Tenant {TenantId}", model.ProjectId, tenantId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Project not found", "The specified project does not exist.", 404);
|
||||||
|
}
|
||||||
|
projectVm = serviceProject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
projectVm = infraProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate Organization.
|
||||||
|
var organization = await context.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == model.OrganizationId && o.IsActive, ct);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync failed: Organization {OrganizationId} not found or inactive.", model.OrganizationId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Organization not found", "The selected organization is invalid or inactive.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate Supplier.
|
||||||
|
var supplier = await context.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == model.SupplierId && o.IsActive, ct);
|
||||||
|
|
||||||
|
if (supplier == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync failed: Supplier {SupplierId} not found or inactive.", model.SupplierId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Supplier not found", "The selected supplier is invalid or inactive.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate PurchaseInvoiceStatus.
|
||||||
|
var status = await context.PurchaseInvoiceStatus
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == model.StatusId, ct);
|
||||||
|
|
||||||
|
if (status == null)
|
||||||
|
{
|
||||||
|
_logger.LogError(null, "UpdatePurchaseInvoiceAsync critical: Missing required purchase invoice status ID {StatusId}.", model.StatusId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse(
|
||||||
|
"System configuration error",
|
||||||
|
"Required purchase invoice status configuration is missing in the system.",
|
||||||
|
500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save previous state for audit/logging.
|
||||||
|
var existingEntityBson = updateLogHelper.EntityToBsonDocument(purchaseInvoice);
|
||||||
|
|
||||||
|
// Map updated fields from DTO to entity.
|
||||||
|
_mapper.Map(model, purchaseInvoice);
|
||||||
|
purchaseInvoice.UpdatedAt = DateTime.UtcNow;
|
||||||
|
purchaseInvoice.UpdatedById = loggedInEmployee.Id;
|
||||||
|
|
||||||
|
context.PurchaseInvoiceDetails.Update(purchaseInvoice);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// 5. Handle attachments update.
|
||||||
|
var newAttachments = model.Attachments.Where(a => a.IsActive).ToList();
|
||||||
|
var deleteAttachmentIds = model.Attachments
|
||||||
|
.Where(a => a.DocumentId.HasValue && !a.IsActive)
|
||||||
|
.Select(a => a.DocumentId!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (newAttachments.Any())
|
||||||
|
{
|
||||||
|
var batchId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Validate attachment types.
|
||||||
|
var typeIds = newAttachments.Select(a => a.InvoiceAttachmentTypeId).Distinct().ToList();
|
||||||
|
var validTypes = await context.InvoiceAttachmentTypes
|
||||||
|
.Where(iat => typeIds.Contains(iat.Id))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var invalidTypeIds = typeIds.Except(validTypes.Select(t => t.Id)).ToList();
|
||||||
|
if (invalidTypeIds.Any())
|
||||||
|
{
|
||||||
|
foreach (var invalidId in invalidTypeIds)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync failed: Invalid attachment type ID {AttachmentTypeId}.", invalidId);
|
||||||
|
}
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid attachment types", $"One or more attachment types are invalid: {string.Join(", ", invalidTypeIds)}", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
var preparedDocuments = new List<Document>();
|
||||||
|
var preparedAttachments = new List<PurchaseInvoiceAttachment>();
|
||||||
|
|
||||||
|
// Process each new attachment.
|
||||||
|
foreach (var attachment in newAttachments)
|
||||||
|
{
|
||||||
|
// Validate base64 data presence.
|
||||||
|
var base64Data = attachment.Base64Data?.Split(',').LastOrDefault();
|
||||||
|
if (string.IsNullOrWhiteSpace(base64Data))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdatePurchaseInvoiceAsync failed: Attachment '{FileName}' contains no data.", attachment.FileName ?? "<unnamed>");
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid attachment", $"Attachment '{attachment.FileName ?? "<unnamed>"}' contains no valid data.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine content type with fallback.
|
||||||
|
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
|
||||||
|
var safeFileType = string.IsNullOrEmpty(fileType) ? "application/octet-stream" : fileType;
|
||||||
|
|
||||||
|
var fileName = attachment.FileName ?? _s3Service.GenerateFileName(safeFileType, tenantId, "invoice");
|
||||||
|
var objectKey = $"tenant-{tenantId}/PurchaseInvoice/{id}/{fileName}";
|
||||||
|
|
||||||
|
// Upload file to S3 asynchronously.
|
||||||
|
await _s3Service.UploadFileAsync(base64Data, safeFileType, objectKey);
|
||||||
|
|
||||||
|
var documentId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Prepare Document entity.
|
||||||
|
preparedDocuments.Add(new Document
|
||||||
|
{
|
||||||
|
Id = documentId,
|
||||||
|
BatchId = batchId,
|
||||||
|
UploadedById = loggedInEmployee.Id,
|
||||||
|
FileName = fileName,
|
||||||
|
ContentType = attachment.ContentType ?? safeFileType,
|
||||||
|
S3Key = objectKey,
|
||||||
|
FileSize = attachment.FileSize,
|
||||||
|
UploadedAt = DateTime.UtcNow,
|
||||||
|
TenantId = tenantId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare PurchaseInvoiceAttachment entity.
|
||||||
|
preparedAttachments.Add(new PurchaseInvoiceAttachment
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
InvoiceAttachmentTypeId = attachment.InvoiceAttachmentTypeId,
|
||||||
|
PurchaseInvoiceId = id,
|
||||||
|
DocumentId = documentId,
|
||||||
|
UploadedAt = DateTime.UtcNow,
|
||||||
|
UploadedById = loggedInEmployee.Id,
|
||||||
|
TenantId = tenantId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add batched uploaded documents and attachments.
|
||||||
|
context.Documents.AddRange(preparedDocuments);
|
||||||
|
context.PurchaseInvoiceAttachments.AddRange(preparedAttachments);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete attachments marked for removal.
|
||||||
|
if (deleteAttachmentIds.Any())
|
||||||
|
{
|
||||||
|
await DeleteAttachemnts(deleteAttachmentIds, ct);
|
||||||
|
_logger.LogInfo("Deleted {Count} attachments from PurchaseInvoiceId {InvoiceId} for TenantId {TenantId}", deleteAttachmentIds.Count, id, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
// Push audit log entry asynchronously for traceability.
|
||||||
|
await updateLogHelper.PushToUpdateLogsAsync(
|
||||||
|
new UpdateLogsObject
|
||||||
|
{
|
||||||
|
EntityId = id.ToString(),
|
||||||
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
|
OldObject = existingEntityBson,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
},
|
||||||
|
"PurchaseInvoiceModificationLog");
|
||||||
|
|
||||||
|
_logger.LogInfo("Purchase invoice updated successfully. InvoiceId: {InvoiceId}, TenantId: {TenantId}, UpdatedById: {UserId}", id, tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(model, "Purchase invoice updated successfully.", 200);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(null, "UpdatePurchaseInvoiceAsync operation cancelled. InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Request cancelled", "The update operation was cancelled by the client or the server.", 499);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Unexpected error during update of purchase invoice. InvoiceId: {InvoiceId}, TenantId: {TenantId}, UserId: {UserId}", id, tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Update failed", "An unexpected error occurred while updating the purchase invoice. Please try again later.", 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected error during update of purchase invoice. InvoiceId: {InvoiceId}, TenantId: {TenantId}, UserId: {UserId}", id, tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Update failed", "An unexpected error occurred while updating the purchase invoice. Please try again later.", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//public async Task<ApiResponse> DeletePurchaseInvoiceAsync(Guid id, Guid tenantId, CancellationToken ct = default)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
|
public async Task<PurchaseInvoiceDetails?> GetPurchaseInvoiceByIdAsync(Guid id, Guid tenantId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var readContext = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||||
|
var purchaseInvoice = await readContext.PurchaseInvoiceDetails
|
||||||
|
.Where(e => e.Id == id && e.TenantId == tenantId && e.IsActive)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (purchaseInvoice == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Purchase Invoice not found. ID: {Id}, TenantID: {TenantId}", id, tenantId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInfo("Purchase Invoice found. ID: {Id}, TenantID: {TenantId}", id, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return purchaseInvoice;
|
||||||
|
}
|
||||||
private AdvanceFilter? TryDeserializeFilter(string? filter)
|
private AdvanceFilter? TryDeserializeFilter(string? filter)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(filter))
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
@ -816,8 +963,54 @@ namespace Marco.Pms.Services.Service
|
|||||||
private async Task<BasicProjectVM?> LoadServiceProjectAsync(Guid projectId, Guid tenantId)
|
private async Task<BasicProjectVM?> LoadServiceProjectAsync(Guid projectId, Guid tenantId)
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await context.ServiceProjects.Where(sp => sp.Id == projectId && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).FirstOrDefaultAsync();
|
return await context.ServiceProjects.AsNoTracking().Where(sp => sp.Id == projectId && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAttachemnts(List<Guid> documentIds, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var attachmentTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var attachments = await context.PurchaseInvoiceAttachments.AsNoTracking().Where(ba => documentIds.Contains(ba.DocumentId)).ToListAsync(ct);
|
||||||
|
|
||||||
|
context.PurchaseInvoiceAttachments.RemoveRange(attachments);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
});
|
||||||
|
var documentsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||||
|
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var documents = await context.Documents.AsNoTracking().Where(ba => documentIds.Contains(ba.Id)).ToListAsync(ct);
|
||||||
|
|
||||||
|
if (documents.Any())
|
||||||
|
{
|
||||||
|
context.Documents.RemoveRange(documents);
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
List<S3DeletionObject> deletionObject = new List<S3DeletionObject>();
|
||||||
|
foreach (var document in documents)
|
||||||
|
{
|
||||||
|
deletionObject.Add(new S3DeletionObject
|
||||||
|
{
|
||||||
|
Key = document.S3Key
|
||||||
|
});
|
||||||
|
if (!string.IsNullOrWhiteSpace(document.ThumbS3Key) && document.ThumbS3Key != document.S3Key)
|
||||||
|
{
|
||||||
|
deletionObject.Add(new S3DeletionObject
|
||||||
|
{
|
||||||
|
Key = document.ThumbS3Key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _updateLogHelper.PushToS3DeletionAsync(deletionObject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(attachmentTask, documentsTask);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.PurchaseInvoice;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
||||||
|
|
||||||
@ -7,9 +8,23 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
{
|
{
|
||||||
public interface IPurchaseInvoiceService
|
public interface IPurchaseInvoiceService
|
||||||
{
|
{
|
||||||
|
#region =================================================================== Purchase Invoice Functions ===================================================================
|
||||||
Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
||||||
Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
Task<ApiResponse<PurchaseInvoiceDetailsVM>> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
Task<ApiResponse<PurchaseInvoiceDetailsVM>> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
Task<ApiResponse<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
Task<ApiResponse<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
|
Task<ApiResponse<object>> UpdatePurchaseInvoiceAsync(Guid id, PurchaseInvoiceDetails purchaseInvoice, PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
Task<PurchaseInvoiceDetails?> GetPurchaseInvoiceByIdAsync(Guid id, Guid tenantId, CancellationToken ct);
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user