Added an API to deactivate or activate the purchase invoice

This commit is contained in:
ashutosh.nehete 2025-12-01 12:37:35 +05:30
parent 4b981b6c74
commit a4714d5440
3 changed files with 180 additions and 4 deletions

View File

@ -122,6 +122,24 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpDelete("delete/{id}")]
public async Task<IActionResult> 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);

View File

@ -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<PurchaseInvoiceDetailsVM>.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<ApiResponse> DeletePurchaseInvoiceAsync(Guid id, Guid tenantId, CancellationToken ct = default)
public async Task<ApiResponse<object>> 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<object>.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<object>.ErrorResponse("Invoice not found", "The invoice with the specified ID was not found.", 404);
}
using var scope = _serviceScopeFactory.CreateScope();
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
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<object>.SuccessResponse(new { }, "Invoice deleted successfully.", 200);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The Purchase Invoice identifier.</param>
/// <param name="isActive">
/// Indicates the new active state:
/// false = mark as deleted/inactive (soft delete),
/// true = restore/reactivate.
/// </param>
/// <param name="loggedInEmployee">The currently logged-in employee performing the operation.</param>
/// <param name="tenantId">Tenant identifier to enforce multi-tenant isolation.</param>
/// <param name="ct">Cancellation token for cooperative cancellation.</param>
/// <returns>
/// Standardized <see cref="ApiResponse{T}"/> with operation result or error details.
/// </returns>
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<UtilityMongoDBHelper>();
// 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<object>.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<object>.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<object>.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<object>.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<object>.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)
{

View File

@ -15,6 +15,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
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<object>> UpdatePurchaseInvoiceAsync(Guid id, PurchaseInvoiceDetails purchaseInvoice, PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<object>> DeletePurchaseInvoiceAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
#endregion