diff --git a/Marco.Pms.Services/Controllers/CollectionController.cs b/Marco.Pms.Services/Controllers/CollectionController.cs index 872baf2..89d9a52 100644 --- a/Marco.Pms.Services/Controllers/CollectionController.cs +++ b/Marco.Pms.Services/Controllers/CollectionController.cs @@ -1,8 +1,10 @@ using AutoMapper; using Marco.Pms.DataAccess.Data; +using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.Collection; using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.Collection; +using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Collection; @@ -45,6 +47,8 @@ namespace Marco.Pms.Services.Controllers tenantId = userhelper.GetTenantId(); } + #region =================================================================== Get Functions =================================================================== + [HttpGet("invoice/list")] public async Task GetInvoiceListAsync([FromQuery] string? searchString, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1 , [FromQuery] bool isActive = true, [FromQuery] bool isPending = false) @@ -221,6 +225,10 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(response, "Invoice details fetched successfully", 200)); } + #endregion + + #region =================================================================== Post Functions =================================================================== + [HttpPost("invoice/create")] public async Task CreateInvoiceAsync([FromBody] InvoiceDto model) { @@ -533,6 +541,7 @@ namespace Marco.Pms.Services.Controllers /// /// DTO containing InvoiceId and Comment text. /// 201 Created with comment details, or error codes for validation/invoice not found. + [HttpPost("invoice/add/comment")] public async Task AddCommentToInvoiceAsync([FromBody] InvoiceCommentDto model) { @@ -588,6 +597,194 @@ namespace Marco.Pms.Services.Controllers 201)); } + #endregion + + #region =================================================================== Put Functions =================================================================== + + /// + /// Updates an existing invoice if it exists, has no payments, and the model is valid. + /// + /// The unique identifier of the invoice to update. + /// The updated invoice data transfer object. + /// Success response on update, or appropriate error if validation fails. + + [HttpPut("invoice/edit/{id}")] + public async Task UpdateInvoiceAsync(Guid id, [FromBody] InvoiceDto model) + { + // Validate route and model ID consistency + if (!model.Id.HasValue || id != model.Id) + { + _logger.LogWarning("Invoice ID mismatch: route ID {RouteId} does not match model ID {ModelId}", id, model.Id ?? Guid.Empty); + return BadRequest(ApiResponse.ErrorResponse( + "Invalid invoice ID", + "The invoice ID in the URL does not match the ID in the request body.", + 400)); + } + + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + using var scope = _serviceScopeFactory.CreateScope(); + var _updateLogHelper = scope.ServiceProvider.GetRequiredService(); + + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Retrieve the invoice with tenant isolation + var invoice = await _context.Invoices + .FirstOrDefaultAsync(i => i.Id == id && i.TenantId == tenantId); + + if (invoice == null) + { + _logger.LogWarning("Invoice not found for ID {InvoiceId} and TenantId {TenantId}", id, tenantId); + return NotFound(ApiResponse.ErrorResponse( + "Invoice not found", + "The specified invoice does not exist for this tenant.", + 404)); + } + + // Fetch project + var project = await _context.Projects + .FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId); + if (project == null) + { + _logger.LogWarning("Project not found: ProjectId {ProjectId}, TenantId {TenantId}", + model.ProjectId, tenantId); + return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); + } + + // Prevent modification if any payment has already been received + var receivedPaymentExists = await _context.ReceivedInvoicePayments + .AnyAsync(rip => rip.InvoiceId == id && rip.TenantId == tenantId); + + if (receivedPaymentExists) + { + _logger.LogWarning("Update blocked: Payment already received for InvoiceId {InvoiceId}, TenantId {TenantId}", id, tenantId); + return BadRequest(ApiResponse.ErrorResponse( + "Invoice cannot be updated", + "This invoice has received payments and cannot be modified.", + 400)); + } + + try + { + var invoiceStateBeforeChange = _updateLogHelper.EntityToBsonDocument(invoice); + + // Map updated data to existing invoice entity + _mapper.Map(model, invoice); + invoice.UpdatedAt = DateTime.UtcNow; + invoice.UpdatedById = loggedInEmployee.Id; + + // Handle attachment updates if provided + if (model.Attachments?.Any() ?? false) + { + var inactiveDocumentIds = model.Attachments + .Where(a => !a.IsActive && a.DocumentId.HasValue) + .Select(a => a.DocumentId!.Value) + .ToList(); + + var newAttachments = model.Attachments + .Where(a => a.IsActive && !string.IsNullOrWhiteSpace(a.Base64Data)) + .ToList(); + + // Remove inactive attachments + if (inactiveDocumentIds.Any()) + { + var existingInvoiceAttachments = await _context.InvoiceAttachments + .AsNoTracking() + .Where(ia => inactiveDocumentIds.Contains(ia.DocumentId) && ia.TenantId == tenantId) + .ToListAsync(); + + _context.InvoiceAttachments.RemoveRange(existingInvoiceAttachments); + _logger.LogInfo("Removed {Count} inactive attachments for InvoiceId {InvoiceId}", existingInvoiceAttachments.Count, id); + } + + // Process and upload new attachments + if (newAttachments.Any()) + { + var batchId = Guid.NewGuid(); + var documents = new List(); + var invoiceAttachments = new List(); + + foreach (var attachment in newAttachments) + { + string base64Data = attachment.Base64Data?.Split(',').LastOrDefault() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(base64Data)) + { + _logger.LogWarning("Base64 data missing for attachment: {FileName}", attachment.FileName ?? "Unknown"); + return BadRequest(ApiResponse.ErrorResponse( + "Invalid attachment data", + "Base64 data is missing or malformed for one or more attachments.", + 400)); + } + + var contentType = _s3Service.GetContentTypeFromBase64(base64Data); + var fileName = _s3Service.GenerateFileName(contentType, tenantId, "invoice"); + var objectKey = $"tenant-{tenantId}/Project/{model.ProjectId}/Invoice/{fileName}"; + + // Upload file to S3 + await _s3Service.UploadFileAsync(base64Data, contentType, objectKey); + + // Create document record + var document = new Document + { + Id = Guid.NewGuid(), + BatchId = batchId, + UploadedById = loggedInEmployee.Id, + FileName = attachment.FileName ?? fileName, + ContentType = contentType, + S3Key = objectKey, + FileSize = attachment.FileSize, + UploadedAt = DateTime.UtcNow, + TenantId = tenantId + }; + documents.Add(document); + + // Link document to invoice + var invoiceAttachment = new InvoiceAttachment + { + InvoiceId = invoice.Id, + DocumentId = document.Id, + TenantId = tenantId + }; + invoiceAttachments.Add(invoiceAttachment); + } + + _context.Documents.AddRange(documents); + _context.InvoiceAttachments.AddRange(invoiceAttachments); + _logger.LogInfo("Added {Count} new attachments to InvoiceId {InvoiceId}", invoiceAttachments.Count, id); + } + } + + // Save all changes in a single transaction + await _context.SaveChangesAsync(); + + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = invoice.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = invoiceStateBeforeChange, + UpdatedAt = DateTime.UtcNow + }, "InvoiceModificationLog"); + + _logger.LogInfo("Invoice {InvoiceId} updated successfully by EmployeeId {EmployeeId}, TenantId {TenantId}", + invoice.Id, loggedInEmployee.Id, tenantId); + + // Build response + var response = _mapper.Map(invoice); + response.Project = _mapper.Map(project); + response.UpdatedBy = _mapper.Map(loggedInEmployee); + response.BalanceAmount = response.BasicAmount + response.TaxAmount; + + return Ok(ApiResponse.SuccessResponse(response, "Invoice updated successfully", 200)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while updating InvoiceId {InvoiceId}, TenantId {TenantId}", id, tenantId); + return StatusCode(500, ApiResponse.ErrorResponse( + "Internal server error", + "An unexpected error occurred while updating the invoice.", + 500)); + } + } /// /// Marks the specified invoice as completed if it exists and is not already completed. @@ -645,6 +842,10 @@ namespace Marco.Pms.Services.Controllers } } + #endregion + + #region =================================================================== Helper Functions =================================================================== + /// /// Loads invoice comments asynchronously with related metadata. /// @@ -686,5 +887,7 @@ namespace Marco.Pms.Services.Controllers .ToListAsync(); } + #endregion + } }