Added An API to add delivery challan to purchase invoice
This commit is contained in:
parent
34c5ac9c25
commit
41feb58d45
11
Marco.Pms.Model/Dtos/PurchaseInvoice/DeliveryChallanDto.cs
Normal file
11
Marco.Pms.Model/Dtos/PurchaseInvoice/DeliveryChallanDto.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Marco.Pms.Model.Dtos.PurchaseInvoice
|
||||
{
|
||||
public class DeliveryChallanDto
|
||||
{
|
||||
public required string DeliveryChallanNumber { get; set; }
|
||||
public required DateTime DeliveryChallanDate { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public required Guid PurchaseInvoiceId { get; set; }
|
||||
public required InvoiceAttachmentDto Attachment { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
|
||||
{
|
||||
public class BasicPurchaseInvoiceVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? PurchaseInvoiceUId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
|
||||
{
|
||||
public class DeliveryChallanVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? DeliveryChallanNumber { get; set; }
|
||||
public DateTime DeliveryChallanDate { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public BasicPurchaseInvoiceVM? PurchaseInvoice { get; set; }
|
||||
public PurchaseInvoiceAttachmentVM? Attachment { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
|
||||
}
|
||||
|
||||
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId);
|
||||
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId);
|
||||
if (employee == null)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
|
||||
|
||||
@ -124,6 +124,38 @@ namespace Marco.Pms.Services.Controllers
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Adds a delivery challan.
|
||||
/// </summary>
|
||||
/// <param name="model">The delivery challan model.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The HTTP response for adding the delivery challan.</returns>
|
||||
[HttpPost("delivery-challan/create")]
|
||||
public async Task<IActionResult> AddDeliveryChallan([FromBody] DeliveryChallanDto model, CancellationToken ct)
|
||||
{
|
||||
// Get the currently logged-in employee
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Add the delivery challan using the purchase invoice service
|
||||
var response = await _purchaseInvoiceService.AddDeliveryChallanAsync(model, loggedInEmployee, tenantId, ct);
|
||||
|
||||
// If the addition is successful, send a notification to the SignalR service
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new
|
||||
{
|
||||
LoggedInUserId = loggedInEmployee.Id,
|
||||
Keyword = "Delivery_Challan",
|
||||
Response = response.Data
|
||||
};
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
|
||||
// Return the HTTP response
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
|
||||
@ -635,6 +635,11 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
.ForMember(
|
||||
dest => dest.PurchaseInvoiceUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
|
||||
|
||||
CreateMap<PurchaseInvoiceDetails, BasicPurchaseInvoiceVM>()
|
||||
.ForMember(
|
||||
dest => dest.PurchaseInvoiceUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
|
||||
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDetailsVM>()
|
||||
.ForMember(
|
||||
dest => dest.PurchaseInvoiceUId,
|
||||
@ -651,6 +656,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
dest => dest.ContentType,
|
||||
opt => opt.MapFrom(src => src.Document != null ? src.Document.ContentType : null));
|
||||
|
||||
CreateMap<DeliveryChallanDto, DeliveryChallanDetails>()
|
||||
.ForMember(
|
||||
dest => dest.Attachment,
|
||||
opt => opt.Ignore());
|
||||
|
||||
CreateMap<DeliveryChallanDetails, DeliveryChallanVM>();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.PurchaseInvoice;
|
||||
using Marco.Pms.Model.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
||||
@ -31,6 +32,7 @@ namespace Marco.Pms.Services.Service
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
private readonly Guid DraftInvoiceStatusId = Guid.Parse("8a5ef25e-3c9e-45de-add9-6b1c1df54381");
|
||||
private readonly Guid DeliveryChallanTypeId = Guid.Parse("ca294108-a586-4207-88c8-163b24305ddc");
|
||||
public PurchaseInvoiceService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILoggingService logger,
|
||||
@ -727,7 +729,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
if (status == null)
|
||||
{
|
||||
_logger.LogError(null, "UpdatePurchaseInvoiceAsync critical: Missing required purchase invoice status ID {StatusId}.", model.StatusId);
|
||||
_logger.LogError(null, "UpdatePurchaseInvoiceAsync critical: Missing required purchase invoice status ID {StatusId}.", model.StatusId ?? Guid.Empty);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"System configuration error",
|
||||
@ -884,6 +886,172 @@ namespace Marco.Pms.Services.Service
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||
public async Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||
{
|
||||
// 1. Input Validation - Fail Fast
|
||||
// Validate inputs before engaging expensive resources (DB/S3).
|
||||
if (model == null) throw new ArgumentNullException(nameof(model));
|
||||
|
||||
// Extract Base64 Data safely
|
||||
var base64Data = model.Attachment.Base64Data?.Split(',').LastOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(base64Data))
|
||||
{
|
||||
_logger.LogWarning("AddDeliveryChallan: Validation Failed - Attachment is empty. Tenant: {TenantId}, Invoice: {InvoiceId}", tenantId, model.PurchaseInvoiceId);
|
||||
return ApiResponse<DeliveryChallanVM>.ErrorResponse("Invalid Attachment", "The uploaded attachment contains no data.", 400);
|
||||
}
|
||||
|
||||
// Prepare S3 Metadata
|
||||
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
|
||||
var safeContentType = string.IsNullOrEmpty(fileType) ? "application/octet-stream" : fileType;
|
||||
// Use the sanitized file name or generate a new one to prevent path traversal or collision
|
||||
var fileName = !string.IsNullOrWhiteSpace(model.Attachment.FileName)
|
||||
? model.Attachment.FileName
|
||||
: _s3Service.GenerateFileName(safeContentType, tenantId, "invoice");
|
||||
|
||||
var objectKey = $"tenant-{tenantId}/PurchaseInvoice/{model.PurchaseInvoiceId}/{fileName}";
|
||||
|
||||
// Generate new IDs upfront to maintain referential integrity in code
|
||||
var documentId = Guid.NewGuid();
|
||||
var attachmentId = Guid.NewGuid();
|
||||
var deliveryChallanId = Guid.NewGuid();
|
||||
|
||||
// 2. Database Read Operations (Scoped Context)
|
||||
// We use a factory to create a short-lived context.
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Fetch Purchase Invoice - Use AsNoTracking for performance since we aren't modifying it here.
|
||||
// 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.TenantId == tenantId, ct);
|
||||
|
||||
if (purchaseInvoiceEntity == null)
|
||||
{
|
||||
_logger.LogWarning("AddDeliveryChallan: Purchase Invoice not found. Id: {InvoiceId}, Tenant: {TenantId}", model.PurchaseInvoiceId, tenantId);
|
||||
return ApiResponse<DeliveryChallanVM>.ErrorResponse("Not Found", "The specified Purchase Invoice does not exist.", 404);
|
||||
}
|
||||
|
||||
// Validate Attachment Type
|
||||
var invoiceAttachmentType = await context.InvoiceAttachmentTypes
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(iat => iat.Id == DeliveryChallanTypeId, ct);
|
||||
|
||||
if (invoiceAttachmentType == null)
|
||||
{
|
||||
_logger.LogError(null, "AddDeliveryChallan: Configuration Error - InvoiceAttachmentType {TypeId} missing.", DeliveryChallanTypeId);
|
||||
return ApiResponse<DeliveryChallanVM>.ErrorResponse("Configuration Error", "System configuration for Delivery Challan is missing.", 500);
|
||||
}
|
||||
|
||||
// 3. External Service Call (S3 Upload)
|
||||
// We upload BEFORE the DB transaction. If this fails, we return error.
|
||||
// If DB fails later, we must compensate (delete this file).
|
||||
try
|
||||
{
|
||||
await _s3Service.UploadFileAsync(base64Data, safeContentType, objectKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AddDeliveryChallan: S3 Upload failed. Key: {ObjectKey}", objectKey);
|
||||
return ApiResponse<DeliveryChallanVM>.ErrorResponse("Upload Failed", "Failed to upload the attachment to storage.", 502);
|
||||
}
|
||||
|
||||
// 4. Transactional Write Operations
|
||||
// Begin transaction for data consistency across multiple tables.
|
||||
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Entity 1: Document (Metadata)
|
||||
var document = new Document
|
||||
{
|
||||
Id = documentId,
|
||||
BatchId = Guid.NewGuid(), // Assuming single batch for this operation
|
||||
UploadedById = loggedInEmployee.Id,
|
||||
FileName = fileName,
|
||||
ContentType = model.Attachment.ContentType ?? safeContentType,
|
||||
S3Key = objectKey,
|
||||
FileSize = model.Attachment.FileSize, // Ensure this is calculated correctly in DTO or here
|
||||
UploadedAt = now,
|
||||
TenantId = tenantId
|
||||
};
|
||||
|
||||
// Entity 2: PurchaseInvoiceAttachment (Link)
|
||||
var newAttachment = new PurchaseInvoiceAttachment
|
||||
{
|
||||
Id = attachmentId,
|
||||
InvoiceAttachmentTypeId = DeliveryChallanTypeId,
|
||||
PurchaseInvoiceId = model.PurchaseInvoiceId,
|
||||
DocumentId = documentId,
|
||||
UploadedAt = now,
|
||||
UploadedById = loggedInEmployee.Id,
|
||||
TenantId = tenantId
|
||||
};
|
||||
|
||||
// Entity 3: DeliveryChallanDetails (Domain Data)
|
||||
var deliveryChallan = _mapper.Map<DeliveryChallanDetails>(model);
|
||||
deliveryChallan.Id = deliveryChallanId;
|
||||
deliveryChallan.AttachmentId = attachmentId;
|
||||
deliveryChallan.CreatedAt = now;
|
||||
deliveryChallan.CreatedById = loggedInEmployee.Id;
|
||||
deliveryChallan.TenantId = tenantId;
|
||||
|
||||
// Batch Add
|
||||
context.Documents.Add(document);
|
||||
context.PurchaseInvoiceAttachments.Add(newAttachment);
|
||||
context.DeliveryChallanDetails.Add(deliveryChallan);
|
||||
|
||||
// Execute DB changes - One round trip
|
||||
await context.SaveChangesAsync(ct);
|
||||
await transaction.CommitAsync(ct);
|
||||
|
||||
_logger.LogInfo("AddDeliveryChallan: Success. ChallanId: {ChallanId}, Tenant: {TenantId}", deliveryChallanId, tenantId);
|
||||
|
||||
// 5. Response Preparation
|
||||
// Map response objects. Ensure the VM matches the generic return type.
|
||||
var response = _mapper.Map<DeliveryChallanVM>(deliveryChallan);
|
||||
|
||||
// Manual mapping for complex nested objects if Automapper config is not set for deep linking
|
||||
response.PurchaseInvoice = _mapper.Map<BasicPurchaseInvoiceVM>(purchaseInvoiceEntity);
|
||||
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
||||
|
||||
response.Attachment = new PurchaseInvoiceAttachmentVM
|
||||
{
|
||||
DocumentId = document.Id,
|
||||
InvoiceAttachmentType = invoiceAttachmentType,
|
||||
FileName = document.FileName,
|
||||
ContentType = document.ContentType,
|
||||
// Generate URLs only when needed to keep response lightweight, or if they expire
|
||||
PreSignedUrl = _s3Service.GeneratePreSignedUrl(objectKey),
|
||||
ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(objectKey)
|
||||
};
|
||||
|
||||
return ApiResponse<DeliveryChallanVM>.SuccessResponse(response, "Delivery Challan added successfully.", 201); // 201 Created
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 6. Rollback & Compensation
|
||||
await transaction.RollbackAsync(ct);
|
||||
|
||||
_logger.LogError(ex, "AddDeliveryChallan: Database transaction failed. Rolling back. Tenant: {TenantId}", tenantId);
|
||||
|
||||
// Compensating Action: Delete the file from S3 since DB insert failed.
|
||||
// We run this in a fire-and-forget or background manner, or await it carefully so it doesn't hide the original exception.
|
||||
try
|
||||
{
|
||||
_logger.LogInfo("AddDeliveryChallan: Attempting to delete orphaned S3 file: {ObjectKey}", objectKey);
|
||||
await _s3Service.DeleteFileAsync(objectKey);
|
||||
}
|
||||
catch (Exception s3Ex)
|
||||
{
|
||||
// Just log this, don't throw, so we still return the original DB error to the user
|
||||
_logger.LogError(s3Ex, "AddDeliveryChallan: Failed to clean up orphaned S3 file: {ObjectKey}", objectKey);
|
||||
}
|
||||
|
||||
return ApiResponse<DeliveryChallanVM>.ErrorResponse("Processing Error", "An error occurred while saving the delivery challan.", 500);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
|
||||
@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user