From 3dce559de2d9caa7b8c99dd10d1083a8f164edaf Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 26 Nov 2025 18:02:28 +0530 Subject: [PATCH] Added an API to get details of the purchase invoice --- .../PurchaseInvoiceAttachmentVM.cs | 14 ++ .../PurchaseInvoiceDetailsVM.cs | 44 ++++++ .../Controllers/PurchaseInvoiceController.cs | 13 ++ .../MappingProfiles/MappingProfile.cs | 15 ++ .../Service/PurchaseInvoiceService.cs | 132 ++++++++++++++++++ .../IPurchaseInvoiceService.cs | 1 + 6 files changed, 219 insertions(+) create mode 100644 Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceAttachmentVM.cs create mode 100644 Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceDetailsVM.cs diff --git a/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceAttachmentVM.cs b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceAttachmentVM.cs new file mode 100644 index 0000000..815dcd3 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceAttachmentVM.cs @@ -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; } + } +} diff --git a/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceDetailsVM.cs b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceDetailsVM.cs new file mode 100644 index 0000000..6b23b43 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceDetailsVM.cs @@ -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? Attachments { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs index 34adeaa..26c1f5e 100644 --- a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs +++ b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs @@ -46,6 +46,19 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet("details/{id}")] + public async Task 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); + } + /// /// Creates a purchase invoice. diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 3389281..2b0a06f 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -632,6 +632,21 @@ namespace Marco.Pms.Services.MappingProfiles .ForMember( dest => dest.PurchaseInvoiceUId, opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); + CreateMap() + .ForMember( + dest => dest.PurchaseInvoiceUId, + opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); + + CreateMap() + .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 } diff --git a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs index 5287ff2..697af23 100644 --- a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs @@ -398,6 +398,120 @@ namespace Marco.Pms.Services.Service } } + /// + /// Retrieves the details of a specific purchase invoice, including project details and S3 attachment links. + /// + /// The unique identifier of the Purchase Invoice. + /// The employee requesting the data. + /// The tenant identifier for data isolation. + /// Cancellation token for async operations. + /// A wrapped response containing the Purchase Invoice View Model. + public async Task> 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.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.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(purchaseInvoice); + response.Project = project; + + if (attachments.Count > 0) + { + response.Attachments = attachments.Select(a => + { + var result = _mapper.Map(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(); + } + + _logger.LogInfo("Successfully fetched Purchase Invoice details. InvoiceId: {InvoiceId}", id); + + return ApiResponse.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.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.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing your request. Please contact support.", + 500); + } + } + /// /// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage. /// @@ -687,5 +801,23 @@ namespace Marco.Pms.Services.Service return advanceFilter; } + /// + /// Helper method to load infrastructure project by id. + /// + private async Task 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(p)).FirstOrDefaultAsync(); + } + + /// + /// Helper method to load service project by id. + /// + private async Task 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(sp)).FirstOrDefaultAsync(); + } + } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs index 62a81fe..554da76 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs @@ -9,6 +9,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces { Task> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); + Task> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); Task> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); } }