From a4714d544060e8493573df28f8edaed9f50c575f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 1 Dec 2025 12:37:35 +0530 Subject: [PATCH] Added an API to deactivate or activate the purchase invoice --- .../Controllers/PurchaseInvoiceController.cs | 20 ++- .../Service/PurchaseInvoiceService.cs | 163 +++++++++++++++++- .../IPurchaseInvoiceService.cs | 1 + 3 files changed, 180 insertions(+), 4 deletions(-) diff --git a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs index 87fd5bd..80b2d58 100644 --- a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs +++ b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs @@ -122,6 +122,24 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpDelete("delete/{id}")] + public async Task DeletePurchaseInvoice(Guid id, CancellationToken ct, [FromQuery] bool isActive = false) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _purchaseInvoiceService.DeletePurchaseInvoiceAsync(id, isActive, loggedInEmployee, tenantId, ct); + if (response.Success) + { + var notification = new + { + LoggedInUserId = loggedInEmployee.Id, + Keyword = "Purchase_Invoice", + Response = response.Data + }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + #endregion #region =================================================================== Delivery Challan Functions =================================================================== @@ -186,7 +204,7 @@ namespace Marco.Pms.Services.Controllers var notification = new { LoggedInUserId = loggedInEmployee.Id, - Keyword = "Delivery_Challan", + Keyword = "Purchase_Invoice_Payment", Response = response.Data }; await _signalR.SendNotificationAsync(notification); diff --git a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs index d0f7ab4..34b618c 100644 --- a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs @@ -340,7 +340,7 @@ namespace Marco.Pms.Services.Service .FirstOrDefaultAsync(ct); // 3. Validation: Handle Not Found immediately - if (purchaseInvoice == null || !purchaseInvoice.IsActive) + if (purchaseInvoice == null) { _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); @@ -930,7 +930,164 @@ namespace Marco.Pms.Services.Service } } - //public async Task DeletePurchaseInvoiceAsync(Guid id, Guid tenantId, CancellationToken ct = default) + public async Task> DeletePurchaseInvoice(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId, CancellationToken ct) + { + // Check if the employee has the necessary permissions + var deletePermission = await HasPermissionAsync(PermissionsMaster.DeletePurchaseInvoice, loggedInEmployee.Id); + if (!deletePermission) + { + _logger.LogWarning("DeletePurchaseInvoiceAsync failed: EmployeeId {EmployeeId} does not have permission.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Permission denied", "You do not have permission to delete this invoice.", 403); + } + + await using var context = await _dbContextFactory.CreateDbContextAsync(ct); + + var purchaseInvoice = await context.PurchaseInvoiceDetails.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct); + if (purchaseInvoice == null) + { + _logger.LogWarning("DeletePurchaseInvoiceAsync failed: InvoiceId {InvoiceId} not found.", id); + return ApiResponse.ErrorResponse("Invoice not found", "The invoice with the specified ID was not found.", 404); + } + + using var scope = _serviceScopeFactory.CreateScope(); + var updateLogHelper = scope.ServiceProvider.GetRequiredService(); + + var existingEntityBson = updateLogHelper.EntityToBsonDocument(purchaseInvoice); + + purchaseInvoice.IsActive = isActive; + + await context.SaveChangesAsync(ct); + + await updateLogHelper.PushToUpdateLogsAsync( + new UpdateLogsObject + { + EntityId = id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = existingEntityBson, + UpdatedAt = DateTime.UtcNow + }, + "PurchaseInvoiceModificationLog"); + + return ApiResponse.SuccessResponse(new { }, "Invoice deleted successfully.", 200); + } + + /// + /// Soft-deletes or restores a Purchase Invoice by toggling its active flag, + /// with permission checks, audit logging, and structured logging suitable + /// for enterprise-grade observability. + /// + /// The Purchase Invoice identifier. + /// + /// Indicates the new active state: + /// false = mark as deleted/inactive (soft delete), + /// true = restore/reactivate. + /// + /// The currently logged-in employee performing the operation. + /// Tenant identifier to enforce multi-tenant isolation. + /// Cancellation token for cooperative cancellation. + /// + /// Standardized with operation result or error details. + /// + public async Task> DeletePurchaseInvoiceAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId, CancellationToken ct) + { + // Guard clause: validate invoice identifier. + if (id == Guid.Empty) + { + _logger.LogWarning("DeletePurchaseInvoiceAsync called with empty InvoiceId. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Invoice reference is required.", "DeletePurchaseInvoiceAsync received an empty invoice Id.", 400); + } + + try + { + // Step 1: Permission check for the current employee. + var hasDeletePermission = await HasPermissionAsync(PermissionsMaster.DeletePurchaseInvoice, loggedInEmployee.Id); + + if (!hasDeletePermission) + { + _logger.LogWarning("DeletePurchaseInvoiceAsync permission denied. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("You do not have permission to modify this invoice.", "DeletePurchaseInvoiceAsync failed due to missing DeletePurchaseInvoice permission.", + 403); + } + + // Step 2: Create a short-lived DbContext for this operation. + await using var context = await _dbContextFactory.CreateDbContextAsync(ct); + + // Step 3: Retrieve the invoice scoped to the current tenant. + var purchaseInvoice = await context.PurchaseInvoiceDetails + .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct); + + if (purchaseInvoice == null) + { + _logger.LogWarning( + "DeletePurchaseInvoiceAsync failed: Invoice not found. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("Invoice not found.", $"Purchase invoice not found for Id: {id}, TenantId: {tenantId}.", 404); + } + + // Step 4: Create a scoped helper for MongoDB update logs/audit trail. + using var scope = _serviceScopeFactory.CreateScope(); + var updateLogHelper = scope.ServiceProvider.GetRequiredService(); + + // Capture the existing state for audit logging before modification. + var existingEntityBson = updateLogHelper.EntityToBsonDocument(purchaseInvoice); + + // Step 5: Apply the soft-delete or restore operation. + purchaseInvoice.IsActive = isActive; + + // Persist changes with cancellation support. + var rowsAffected = await context.SaveChangesAsync(ct); + + if (rowsAffected <= 0) + { + _logger.LogError(null, "DeletePurchaseInvoiceAsync failed to persist changes. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Failed to update the invoice status.", "DeletePurchaseInvoiceAsync SaveChangesAsync returned 0 rows affected.", 500); + } + + // Step 6: Push audit log to MongoDB (non-critical but important for traceability). + await updateLogHelper.PushToUpdateLogsAsync( + new UpdateLogsObject + { + EntityId = id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = existingEntityBson, + UpdatedAt = DateTime.UtcNow + }, + "PurchaseInvoiceModificationLog"); + + _logger.LogInfo("DeletePurchaseInvoiceAsync completed successfully. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}, NewIsActive: {IsActive}", + id, tenantId, loggedInEmployee.Id, isActive); + + var action = isActive ? "restored" : "deleted"; + + return ApiResponse.SuccessResponse(new { InvoiceId = id, IsActive = isActive }, $"Invoice has been {action} successfully.", 200); + } + catch (OperationCanceledException) + { + // Explicit cancellation handling to avoid misclassification as an error. + _logger.LogError(null, "DeletePurchaseInvoiceAsync operation was canceled. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("The operation was canceled.", "DeletePurchaseInvoiceAsync was canceled by the caller.", 499); + } + catch (DbUpdateException dbEx) + { + // Database-related error with structured logging. + _logger.LogError(dbEx, "Database update error in DeletePurchaseInvoiceAsync. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("An error occurred while updating the invoice.", "Database update exception occurred in DeletePurchaseInvoiceAsync.", 500); + } + catch (Exception ex) + { + // Catch-all for any unexpected failures. + _logger.LogError(ex, "Unexpected error in DeletePurchaseInvoiceAsync. InvoiceId: {InvoiceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("An unexpected error occurred while updating the invoice status.", "Unhandled exception in DeletePurchaseInvoiceAsync.", 500); + } + } + #endregion @@ -1073,7 +1230,7 @@ namespace Marco.Pms.Services.Service // Note: We project only what we need or map later to avoid EF translation issues with complex Mappers. var purchaseInvoiceEntity = await context.PurchaseInvoiceDetails .AsNoTracking() - .FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.IsActive && pid.TenantId == tenantId, ct); + .FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.TenantId == tenantId, ct); if (purchaseInvoiceEntity == null) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs index a1e2e9c..eeba2a5 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs @@ -15,6 +15,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); Task> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); Task> UpdatePurchaseInvoiceAsync(Guid id, PurchaseInvoiceDetails purchaseInvoice, PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); + Task> DeletePurchaseInvoiceAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); #endregion