Added an API to get details of the purchase invoice
This commit is contained in:
parent
886a32b3e3
commit
3dce559de2
@ -0,0 +1,14 @@
|
||||
using Marco.Pms.Model.PurchaseInvoice;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
|
||||
{
|
||||
public class PurchaseInvoiceAttachmentVM
|
||||
{
|
||||
public Guid DocumentId { get; set; }
|
||||
public InvoiceAttachmentType? InvoiceAttachmentType { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public string? ContentType { get; set; }
|
||||
public string? PreSignedUrl { get; set; }
|
||||
public string? ThumbPreSignedUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using Marco.Pms.Model.PurchaseInvoice;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
|
||||
{
|
||||
public class PurchaseInvoiceDetailsVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? PurchaseInvoiceUId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public BasicOrganizationVm? Organization { get; set; }
|
||||
public PurchaseInvoiceStatus? Status { get; set; }
|
||||
public string? BillingAddress { get; set; }
|
||||
public string? ShippingAddress { get; set; }
|
||||
public string? PurchaseOrderNumber { get; set; }
|
||||
public DateTime? PurchaseOrderDate { get; set; }
|
||||
public BasicOrganizationVm? Supplier { get; set; }
|
||||
public string? ProformaInvoiceNumber { get; set; }
|
||||
public DateTime? ProformaInvoiceDate { get; set; }
|
||||
public double? ProformaInvoiceAmount { get; set; }
|
||||
public string? InvoiceNumber { get; set; }
|
||||
public DateTime? InvoiceDate { get; set; }
|
||||
public string? EWayBillNumber { get; set; }
|
||||
public DateTime? EWayBillDate { get; set; }
|
||||
public string? InvoiceReferenceNumber { get; set; }
|
||||
public string? AcknowledgmentNumber { get; set; }
|
||||
public DateTime? AcknowledgmentDate { get; set; }
|
||||
public double BaseAmount { get; set; }
|
||||
public double TaxAmount { get; set; }
|
||||
public double? TransportCharges { get; set; }
|
||||
public double TotalAmount { get; set; }
|
||||
public DateTime PaymentDueDate { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
public List<PurchaseInvoiceAttachmentVM>? Attachments { get; set; }
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpGet("details/{id}")]
|
||||
public async Task<IActionResult> GetPurchaseInvoiceDetailsAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the currently logged-in employee
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Retrieve the purchase invoice details using the service
|
||||
var response = await _purchaseInvoiceService.GetPurchaseInvoiceDetailsAsync(id, loggedInEmployee, tenantId, cancellationToken);
|
||||
|
||||
// Return the response with the appropriate HTTP status code
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a purchase invoice.
|
||||
|
||||
@ -632,6 +632,21 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
.ForMember(
|
||||
dest => dest.PurchaseInvoiceUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
|
||||
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDetailsVM>()
|
||||
.ForMember(
|
||||
dest => dest.PurchaseInvoiceUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
|
||||
|
||||
CreateMap<PurchaseInvoiceAttachment, PurchaseInvoiceAttachmentVM>()
|
||||
.ForMember(
|
||||
dest => dest.DocumentId,
|
||||
opt => opt.MapFrom(src => src.DocumentId))
|
||||
.ForMember(
|
||||
dest => dest.FileName,
|
||||
opt => opt.MapFrom(src => src.Document != null ? src.Document.FileName : null))
|
||||
.ForMember(
|
||||
dest => dest.ContentType,
|
||||
opt => opt.MapFrom(src => src.Document != null ? src.Document.ContentType : null));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -398,6 +398,120 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the details of a specific purchase invoice, including project details and S3 attachment links.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the Purchase Invoice.</param>
|
||||
/// <param name="loggedInEmployee">The employee requesting the data.</param>
|
||||
/// <param name="tenantId">The tenant identifier for data isolation.</param>
|
||||
/// <param name="ct">Cancellation token for async operations.</param>
|
||||
/// <returns>A wrapped response containing the Purchase Invoice View Model.</returns>
|
||||
public async Task<ApiResponse<PurchaseInvoiceDetailsVM>> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||
{
|
||||
// 1. Structured Logging: Log entry with context
|
||||
_logger.LogInfo("Fetching Purchase Invoice details. InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// 2. Performance: Use AsNoTracking for read-only queries.
|
||||
// Use AsSplitQuery to avoid Cartesian explosion on multiple Includes.
|
||||
var purchaseInvoice = await context.PurchaseInvoiceDetails
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(pid => pid.Organization)
|
||||
.Include(pid => pid.Supplier)
|
||||
.Include(pid => pid.Status)
|
||||
.Include(pid => pid.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(pid => pid.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(pid => pid.Id == id && pid.TenantId == tenantId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
// 3. Validation: Handle Not Found immediately
|
||||
if (purchaseInvoice == null || !purchaseInvoice.IsActive)
|
||||
{
|
||||
_logger.LogWarning("Purchase Invoice not found or inactive. InvoiceId: {InvoiceId}", id);
|
||||
return ApiResponse<PurchaseInvoiceDetailsVM>.ErrorResponse("Purchase invoice not found", "The specified purchase invoice does not exist or has been deleted.", 404);
|
||||
}
|
||||
|
||||
// 4. Parallel Execution: Fetch Project details efficiently
|
||||
// Note: Assuming these methods return null if not found, rather than throwing.
|
||||
var infraProjectTask = LoadInfraProjectAsync(purchaseInvoice.ProjectId, tenantId);
|
||||
var serviceProjectTask = LoadServiceProjectAsync(purchaseInvoice.ProjectId, tenantId);
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
|
||||
// Safely retrieve results without blocking .Result
|
||||
var project = await infraProjectTask ?? await serviceProjectTask;
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
_logger.LogWarning("Data Inconsistency: Project not found for InvoiceId: {InvoiceId}, ProjectId: {ProjectId}", id, purchaseInvoice.ProjectId);
|
||||
return ApiResponse<PurchaseInvoiceDetailsVM>.ErrorResponse("Project not found", "The project associated with this invoice could not be found.", 404);
|
||||
}
|
||||
|
||||
// 5. Optimized Attachment Fetching
|
||||
var attachments = await context.PurchaseInvoiceAttachments
|
||||
.AsNoTracking()
|
||||
.Include(pia => pia.Document)
|
||||
.Include(pia => pia.InvoiceAttachmentType)
|
||||
.Where(pia =>
|
||||
pia.PurchaseInvoiceId == id &&
|
||||
pia.TenantId == tenantId &&
|
||||
pia.Document != null &&
|
||||
pia.InvoiceAttachmentType != null)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 6. Mapping & Transformation
|
||||
var response = _mapper.Map<PurchaseInvoiceDetailsVM>(purchaseInvoice);
|
||||
response.Project = project;
|
||||
|
||||
if (attachments.Count > 0)
|
||||
{
|
||||
response.Attachments = attachments.Select(a =>
|
||||
{
|
||||
var result = _mapper.Map<PurchaseInvoiceAttachmentVM>(a);
|
||||
|
||||
// Ensure S3 Key exists before generating URL to prevent SDK errors
|
||||
if (a.Document != null)
|
||||
{
|
||||
result.PreSignedUrl = _s3Service.GeneratePreSignedUrl(a.Document.S3Key);
|
||||
|
||||
// Fallback logic for thumbnail
|
||||
var thumbKey = !string.IsNullOrEmpty(a.Document.ThumbS3Key)
|
||||
? a.Document.ThumbS3Key
|
||||
: a.Document.S3Key;
|
||||
|
||||
result.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(thumbKey);
|
||||
}
|
||||
return result;
|
||||
}).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Attachments = new List<PurchaseInvoiceAttachmentVM>();
|
||||
}
|
||||
|
||||
_logger.LogInfo("Successfully fetched Purchase Invoice details. InvoiceId: {InvoiceId}", id);
|
||||
|
||||
return ApiResponse<PurchaseInvoiceDetailsVM>.SuccessResponse(response, "Purchase invoice details fetched successfully.", 200);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Handle request cancellation (e.g., user navigates away)
|
||||
_logger.LogWarning("Request was cancelled by the user. InvoiceId: {InvoiceId}", id);
|
||||
return ApiResponse<PurchaseInvoiceDetailsVM>.ErrorResponse("Request Cancelled", "The operation was cancelled.", 499);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 7. Global Error Handling
|
||||
_logger.LogError(ex, "An unhandled exception occurred while fetching Purchase Invoice. InvoiceId: {InvoiceId}", id);
|
||||
return ApiResponse<PurchaseInvoiceDetailsVM>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing your request. Please contact support.",
|
||||
500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage.
|
||||
/// </summary>
|
||||
@ -687,5 +801,23 @@ namespace Marco.Pms.Services.Service
|
||||
return advanceFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to load infrastructure project by id.
|
||||
/// </summary>
|
||||
private async Task<BasicProjectVM?> LoadInfraProjectAsync(Guid projectId, Guid tenantId)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.Where(p => p.Id == projectId && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to load service project by id.
|
||||
/// </summary>
|
||||
private async Task<BasicProjectVM?> LoadServiceProjectAsync(Guid projectId, Guid tenantId)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
{
|
||||
Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user