From b30369baa580ce97f47cd36f9850cc1c9a82e2be Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 13 Oct 2025 18:35:57 +0530 Subject: [PATCH] Added the new API to create invoice --- Marco.Pms.Model/Dtos/Collection/InvoiceDto.cs | 18 ++ .../ViewModels/Collection/InvoiceListVM.cs | 23 +++ .../Controllers/CollectionController.cs | 175 ++++++++++++++++++ .../MappingProfiles/MappingProfile.cs | 8 + 4 files changed, 224 insertions(+) create mode 100644 Marco.Pms.Model/Dtos/Collection/InvoiceDto.cs create mode 100644 Marco.Pms.Model/ViewModels/Collection/InvoiceListVM.cs create mode 100644 Marco.Pms.Services/Controllers/CollectionController.cs diff --git a/Marco.Pms.Model/Dtos/Collection/InvoiceDto.cs b/Marco.Pms.Model/Dtos/Collection/InvoiceDto.cs new file mode 100644 index 0000000..b2dbeed --- /dev/null +++ b/Marco.Pms.Model/Dtos/Collection/InvoiceDto.cs @@ -0,0 +1,18 @@ +using Marco.Pms.Model.Utilities; + +namespace Marco.Pms.Model.Dtos.Collection +{ + public class InvoiceDto + { + public Guid? Id { get; set; } + public required string Title { get; set; } + public string? Description { get; set; } + public required string InvoiceNumber { get; set; } + public required Guid ProjectId { get; set; } + public required DateTime InvoiceDate { get; set; } + public required DateTime ClientSubmitedDate { get; set; } + public required DateTime ExceptedPaymentDate { get; set; } + public required double Amount { get; set; } + public List? Attachments { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Collection/InvoiceListVM.cs b/Marco.Pms.Model/ViewModels/Collection/InvoiceListVM.cs new file mode 100644 index 0000000..adff529 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Collection/InvoiceListVM.cs @@ -0,0 +1,23 @@ +using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Projects; + +namespace Marco.Pms.Model.ViewModels.Collection +{ + public class InvoiceListVM + { + public Guid Id { get; set; } + public string Title { get; set; } = default!; + public string Description { get; set; } = default!; + public string InvoiceNumber { get; set; } = default!; + public BasicProjectVM? Project { get; set; } + public DateTime InvoiceDate { get; set; } + public DateTime ClientSubmitedDate { get; set; } + public DateTime ExceptedPaymentDate { get; set; } + public double Amount { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; set; } + public BasicEmployeeVM? CreatedBy { get; set; } + public DateTime? UpdatedAt { get; set; } + public BasicEmployeeVM? UpdatedBy { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/CollectionController.cs b/Marco.Pms.Services/Controllers/CollectionController.cs new file mode 100644 index 0000000..f2b8c0c --- /dev/null +++ b/Marco.Pms.Services/Controllers/CollectionController.cs @@ -0,0 +1,175 @@ +using AutoMapper; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Collection; +using Marco.Pms.Model.DocumentManager; +using Marco.Pms.Model.Dtos.Collection; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Collection; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Service; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Controllers +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] + public class CollectionController : ControllerBase + { + private readonly IDbContextFactory _dbContextFactory; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly UserHelper _userHelper; + private readonly S3UploadService _s3Service; + private readonly IMapper _mapper; + private readonly ILoggingService _logger; + private readonly Guid tenantId; + public CollectionController(IDbContextFactory dbContextFactory, + IServiceScopeFactory serviceScopeFactory, + S3UploadService s3Service, + UserHelper userhelper, + ILoggingService logger, + IMapper mapper) + { + _dbContextFactory = dbContextFactory; + _serviceScopeFactory = serviceScopeFactory; + _userHelper = userhelper; + _s3Service = s3Service; + _mapper = mapper; + _logger = logger; + tenantId = userhelper.GetTenantId(); + } + + [HttpPost("invoice/create")] + public async Task CreateInvoiceAsync(InvoiceDto model) + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("Starting invoice creation for ProjectId: {ProjectId} by EmployeeId: {EmployeeId}", + model.ProjectId, loggedInEmployee.Id); + + // Validate date sequence + if (model.InvoiceDate.Date > model.ClientSubmitedDate.Date) + { + _logger.LogWarning("Invoice date {InvoiceDate} is later than client submitted date {ClientSubmitedDate}", + model.InvoiceDate, model.ClientSubmitedDate); + return BadRequest(ApiResponse.ErrorResponse( + "Invoice date is later than client submitted date", + "Invoice date is later than client submitted date", 400)); + } + if (model.ClientSubmitedDate.Date > model.ExceptedPaymentDate.Date) + { + _logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than expected payment date {ExpectedPaymentDate}", + model.ClientSubmitedDate, model.ExceptedPaymentDate); + return BadRequest(ApiResponse.ErrorResponse( + "Client submitted date is later than expected payment date", + "Client submitted date is later than expected payment date", 400)); + } + + // 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)); + } + + // Begin transaction scope with async flow support + await using var transaction = await context.Database.BeginTransactionAsync(); + var invoice = new Invoice(); + try + { + // Map and create invoice + invoice = _mapper.Map(model); + invoice.IsActive = true; + invoice.CreatedAt = DateTime.UtcNow; + invoice.CreatedById = loggedInEmployee.Id; + invoice.TenantId = tenantId; + + context.Invoices.Add(invoice); + await context.SaveChangesAsync(); // Save to generate invoice.Id + + // Handle attachments + var documents = new List(); + var invoiceAttachments = new List(); + if (model.Attachments?.Any() == true) + { + var batchId = Guid.NewGuid(); + + foreach (var attachment in model.Attachments) + { + string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; + if (string.IsNullOrWhiteSpace(base64)) + { + _logger.LogWarning("Base64 data is missing for attachment {FileName}", attachment.FileName ?? ""); + return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Image data missing", 400)); + } + + var fileType = _s3Service.GetContentTypeFromBase64(base64); + var fileName = _s3Service.GenerateFileName(fileType, tenantId, "invoice"); + var objectKey = $"tenant-{tenantId}/Project/{model.ProjectId}/Invoice/{fileName}"; + + await _s3Service.UploadFileAsync(base64, fileType, objectKey); + + var document = new Document + { + Id = Guid.NewGuid(), + BatchId = batchId, + UploadedById = loggedInEmployee.Id, + FileName = attachment.FileName ?? fileName, + ContentType = attachment.ContentType, + S3Key = objectKey, + FileSize = attachment.FileSize, + UploadedAt = DateTime.UtcNow, + TenantId = tenantId + }; + documents.Add(document); + + var invoiceAttachment = new InvoiceAttachment + { + InvoiceId = invoice.Id, + DocumentId = document.Id, + TenantId = tenantId + }; + invoiceAttachments.Add(invoiceAttachment); + } + + context.Documents.AddRange(documents); + context.InvoiceAttachments.AddRange(invoiceAttachments); + await context.SaveChangesAsync(); // Save attachments and mappings + } + + // Commit transaction + await transaction.CommitAsync(); + _logger.LogInfo("Invoice {InvoiceId} created successfully with {AttachmentCount} attachments.", + invoice.Id, documents.Count); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + _logger.LogError(ex, "Transaction rolled back during invoice creation for ProjectId {ProjectId}", model.ProjectId); + return StatusCode(500, ApiResponse.ErrorResponse( + "Transaction failed: " + ex.Message, + "An error occurred while creating the invoice", 500)); + } + + // Build response + var response = _mapper.Map(invoice); + response.Project = _mapper.Map(project); + response.CreatedBy = _mapper.Map(loggedInEmployee); + + return Ok(ApiResponse.SuccessResponse(response, "Invoice Created Successfully", 201)); + } + + + } +} diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index fa1ea71..c8bbc67 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,9 +1,11 @@ using AutoMapper; using Marco.Pms.Model.AppMenu; +using Marco.Pms.Model.Collection; using Marco.Pms.Model.Directory; using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.AppMenu; +using Marco.Pms.Model.Dtos.Collection; using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.Employees; @@ -26,6 +28,7 @@ using Marco.Pms.Model.Projects; using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Collection; using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.Employee; @@ -255,6 +258,11 @@ namespace Marco.Pms.Services.MappingProfiles #endregion + #region ======================================================= Collection ======================================================= + CreateMap(); + CreateMap(); + #endregion + #region ======================================================= Master ======================================================= CreateMap();