Added an API to get details of the purchase invoice

This commit is contained in:
ashutosh.nehete 2025-11-26 18:02:28 +05:30
parent 886a32b3e3
commit 3dce559de2
6 changed files with 219 additions and 0 deletions

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -46,6 +46,19 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response); 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> /// <summary>
/// Creates a purchase invoice. /// Creates a purchase invoice.

View File

@ -632,6 +632,21 @@ namespace Marco.Pms.Services.MappingProfiles
.ForMember( .ForMember(
dest => dest.PurchaseInvoiceUId, dest => dest.PurchaseInvoiceUId,
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); 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 #endregion
} }

View File

@ -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> /// <summary>
/// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage. /// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage.
/// </summary> /// </summary>
@ -687,5 +801,23 @@ namespace Marco.Pms.Services.Service
return advanceFilter; 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();
}
} }
} }

View File

@ -9,6 +9,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{ {
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<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); Task<ApiResponse<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
} }
} }