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));
|
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)
|
if (employee == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
|
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
|
||||||
|
|||||||
@ -124,6 +124,38 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#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
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
|||||||
@ -635,6 +635,11 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.PurchaseInvoiceUId,
|
dest => dest.PurchaseInvoiceUId,
|
||||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
|
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>()
|
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDetailsVM>()
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.PurchaseInvoiceUId,
|
dest => dest.PurchaseInvoiceUId,
|
||||||
@ -651,6 +656,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
dest => dest.ContentType,
|
dest => dest.ContentType,
|
||||||
opt => opt.MapFrom(src => src.Document != null ? src.Document.ContentType : null));
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ using Marco.Pms.Model.Projects;
|
|||||||
using Marco.Pms.Model.PurchaseInvoice;
|
using Marco.Pms.Model.PurchaseInvoice;
|
||||||
using Marco.Pms.Model.ServiceProject;
|
using Marco.Pms.Model.ServiceProject;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
|
using Marco.Pms.Model.ViewModels.Activities;
|
||||||
using Marco.Pms.Model.ViewModels.Organization;
|
using Marco.Pms.Model.ViewModels.Organization;
|
||||||
using Marco.Pms.Model.ViewModels.Projects;
|
using Marco.Pms.Model.ViewModels.Projects;
|
||||||
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
||||||
@ -31,6 +32,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
private readonly Guid DraftInvoiceStatusId = Guid.Parse("8a5ef25e-3c9e-45de-add9-6b1c1df54381");
|
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,
|
public PurchaseInvoiceService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
ILoggingService logger,
|
ILoggingService logger,
|
||||||
@ -727,7 +729,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
if (status == null)
|
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(
|
return ApiResponse<object>.ErrorResponse(
|
||||||
"System configuration error",
|
"System configuration error",
|
||||||
@ -884,6 +886,172 @@ namespace Marco.Pms.Services.Service
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#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
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
|||||||
@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
#region =================================================================== Purchase Invoice History Functions ===================================================================
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user