From bbe36ed535d19f391c33f4ca24034cc4f4e8e8fd Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 26 Nov 2025 12:07:11 +0530 Subject: [PATCH] Added an API create the Purchase invoice --- .../PurchaseInvoice/InvoiceAttachmentDto.cs | 14 + .../PurchaseInvoice/PurchaseInvoiceDto.cs | 31 ++ .../PurchaseInvoiceAttachment.cs | 12 + .../PurchaseInvoice/PurchaseInvoiceDetails.cs | 12 + .../PurchaseInvoice/PurchaseInvoiceListVM.cs | 18 ++ .../Controllers/PurchaseInvoiceController.cs | 42 +++ .../MappingProfiles/MappingProfile.cs | 16 + Marco.Pms.Services/Program.cs | 1 + .../Service/PurchaseInvoiceService.cs | 289 ++++++++++++++++++ .../IPurchaseInvoiceService.cs | 12 + 10 files changed, 447 insertions(+) create mode 100644 Marco.Pms.Model/Dtos/PurchaseInvoice/InvoiceAttachmentDto.cs create mode 100644 Marco.Pms.Model/Dtos/PurchaseInvoice/PurchaseInvoiceDto.cs create mode 100644 Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceListVM.cs create mode 100644 Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs create mode 100644 Marco.Pms.Services/Service/PurchaseInvoiceService.cs create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs diff --git a/Marco.Pms.Model/Dtos/PurchaseInvoice/InvoiceAttachmentDto.cs b/Marco.Pms.Model/Dtos/PurchaseInvoice/InvoiceAttachmentDto.cs new file mode 100644 index 0000000..2fedf48 --- /dev/null +++ b/Marco.Pms.Model/Dtos/PurchaseInvoice/InvoiceAttachmentDto.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.Dtos.PurchaseInvoice +{ + public class InvoiceAttachmentDto + { + public Guid? DocumentId { get; set; } + public required Guid InvoiceAttachmentTypeId { get; set; } + public required string FileName { get; set; } // Name of the file (e.g., "image1.png") + public string? Base64Data { get; set; } // Base64-encoded string of the file + public string? ContentType { get; set; } // MIME type (e.g., "image/png", "application/pdf") + public long FileSize { get; set; } // File size in bytes + public string? Description { get; set; } // Optional: Description or purpose of the file + public required bool IsActive { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/PurchaseInvoice/PurchaseInvoiceDto.cs b/Marco.Pms.Model/Dtos/PurchaseInvoice/PurchaseInvoiceDto.cs new file mode 100644 index 0000000..0688aac --- /dev/null +++ b/Marco.Pms.Model/Dtos/PurchaseInvoice/PurchaseInvoiceDto.cs @@ -0,0 +1,31 @@ +namespace Marco.Pms.Model.Dtos.PurchaseInvoice +{ + public class PurchaseInvoiceDto + { + public required string Title { get; set; } + public required string Description { get; set; } + public required Guid ProjectId { get; set; } + public required Guid OrganizationId { get; set; } + public required string BillingAddress { get; set; } + public required string ShippingAddress { get; set; } + public string? PurchaseOrderNumber { get; set; } + public DateTime? PurchaseOrderDate { get; set; } + public required Guid SupplierId { get; set; } + public string? ProformaInvoiceNumber { get; set; } + public DateTime? ProformaInvoiceDate { get; set; } + public double? ProformaInvoiceAmount { get; set; } + public string? InvoiceNumber { get; set; } + public DateTime? InvoiceDate { get; set; } + public string? EWayBillNumber { get; set; } + public DateTime? EWayBillDate { get; set; } + public string? InvoiceReferenceNumber { get; set; } + public string? AcknowledgmentNumber { get; set; } + public DateTime? AcknowledgmentDate { get; set; } + public required double BaseAmount { get; set; } + public required double TaxAmount { get; set; } + public double? TransportCharges { get; set; } + public required double TotalAmount { get; set; } + public DateTime? PaymentDueDate { get; set; } // Defaults to 40 days from the invoice date + public List? Attachments { get; set; } + } +} diff --git a/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceAttachment.cs b/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceAttachment.cs index a82c4d6..486f7a9 100644 --- a/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceAttachment.cs +++ b/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceAttachment.cs @@ -28,6 +28,18 @@ namespace Marco.Pms.Model.PurchaseInvoice [ForeignKey("PurchaseInvoiceId")] public PurchaseInvoiceDetails? PurchaseInvoice { get; set; } + /// + /// Gets or sets the unique identifier for the type of the invoice attachment. + /// + public Guid InvoiceAttachmentTypeId { get; set; } + + /// + /// Gets or sets the type of the invoice attachment. + /// + [ValidateNever] + [ForeignKey("InvoiceAttachmentTypeId")] + public InvoiceAttachmentType? InvoiceAttachmentType { get; set; } + /// /// Gets or sets the unique identifier for the document. /// diff --git a/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceDetails.cs b/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceDetails.cs index bd5a890..9307f5d 100644 --- a/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceDetails.cs +++ b/Marco.Pms.Model/PurchaseInvoice/PurchaseInvoiceDetails.cs @@ -57,6 +57,18 @@ namespace Marco.Pms.Model.PurchaseInvoice [ForeignKey("OrganizationId")] public Organization? Organization { get; set; } + /// + /// Gets or sets the status of the detail. + /// + public Guid StatusId { get; set; } + + /// + /// Gets or sets the status of the detail. + /// + [ValidateNever] + [ForeignKey("StatusId")] + public PurchaseInvoiceStatus? Status { get; set; } + /// /// Gets or sets the billing address of the detail. /// diff --git a/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceListVM.cs b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceListVM.cs new file mode 100644 index 0000000..ba8ef19 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/PurchaseInvoice/PurchaseInvoiceListVM.cs @@ -0,0 +1,18 @@ +using Marco.Pms.Model.PurchaseInvoice; +using Marco.Pms.Model.ViewModels.Organization; +using Marco.Pms.Model.ViewModels.Projects; + +namespace Marco.Pms.Model.ViewModels.PurchaseInvoice +{ + public class PurchaseInvoiceListVM + { + public Guid Id { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? PurchaseInvoiceUId { get; set; } + public BasicProjectVM? Project { get; set; } + public BasicOrganizationVm? Supplier { get; set; } + public PurchaseInvoiceStatus? Status { get; set; } + public double TotalAmount { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs new file mode 100644 index 0000000..907e39d --- /dev/null +++ b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs @@ -0,0 +1,42 @@ +using Marco.Pms.Model.Dtos.PurchaseInvoice; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using Microsoft.AspNetCore.Mvc; + +namespace Marco.Pms.Services.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class PurchaseInvoiceController : ControllerBase + { + private readonly UserHelper _userHelper; + private readonly IPurchaseInvoiceService _purchaseInvoiceService; + private readonly Guid tenantId; + + public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService) + { + _userHelper = userHelper; + _purchaseInvoiceService = purchaseInvoiceService; + tenantId = _userHelper.GetTenantId(); + } + + /// + /// Creates a purchase invoice. + /// + /// The purchase invoice model. + /// The cancellation token. + /// The HTTP response with the purchase invoice status and data. + [HttpPost("create")] + public async Task CreatePurchaseInvoice([FromBody] PurchaseInvoiceDto model, CancellationToken ct) + { + // Get the currently logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Create a purchase invoice using the purchase invoice service + var response = await _purchaseInvoiceService.CreatePurchaseInvoiceAsync(model, loggedInEmployee, tenantId, ct); + + // Return the HTTP response with the purchase invoice status and data + return StatusCode(response.StatusCode, response); + } + } +} diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 767e2f9..3389281 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -14,6 +14,7 @@ using Marco.Pms.Model.Dtos.Expenses.Masters; using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Organization; using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Dtos.PurchaseInvoice; using Marco.Pms.Model.Dtos.ServiceProject; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; @@ -28,6 +29,7 @@ using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.OrganizationModel; using Marco.Pms.Model.Projects; +using Marco.Pms.Model.PurchaseInvoice; using Marco.Pms.Model.ServiceProject; using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels.MongoDBModel; @@ -42,6 +44,7 @@ using Marco.Pms.Model.ViewModels.Expenses.Masters; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Organization; using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Model.ViewModels.PurchaseInvoice; using Marco.Pms.Model.ViewModels.ServiceProject; using Marco.Pms.Model.ViewModels.Tenant; @@ -618,6 +621,19 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); #endregion + + #region ======================================================= Purchase Invoice ======================================================= + + CreateMap() + .ForMember( + dest => dest.PaymentDueDate, + opt => opt.MapFrom(src => src.PaymentDueDate.HasValue ? src.PaymentDueDate : DateTime.UtcNow.AddDays(40))); + CreateMap() + .ForMember( + dest => dest.PurchaseInvoiceUId, + opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}")); + + #endregion } } } \ No newline at end of file diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index f15dba7..bc54c3f 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -192,6 +192,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #endregion #region Helpers diff --git a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs new file mode 100644 index 0000000..874cc54 --- /dev/null +++ b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs @@ -0,0 +1,289 @@ +using AutoMapper; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.DocumentManager; +using Marco.Pms.Model.Dtos.PurchaseInvoice; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.OrganizationModel; +using Marco.Pms.Model.PurchaseInvoice; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Organization; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Model.ViewModels.PurchaseInvoice; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Service +{ + public class PurchaseInvoiceService : IPurchaseInvoiceService + { + private readonly IDbContextFactory _dbContextFactory; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILoggingService _logger; + private readonly S3UploadService _s3Service; + private readonly IMapper _mapper; + + private readonly Guid DraftInvoiceStatusId = Guid.Parse("8a5ef25e-3c9e-45de-add9-6b1c1df54381"); + public PurchaseInvoiceService(IDbContextFactory dbContextFactory, + IServiceScopeFactory serviceScopeFactory, + ILoggingService logger, + S3UploadService s3Service, + IMapper mapper) + { + _dbContextFactory = dbContextFactory; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + _s3Service = s3Service; + _mapper = mapper; + } + + /// + /// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage. + /// + /// The invoice data transfer object. + /// The current user context. + /// The tenant identifier. + /// Cancellation token. + /// The created invoice view model wrapped in an API response. + public async Task> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct = default) + { + // 1. INPUT VALIDATION + if (model == null) throw new ArgumentNullException(nameof(model)); + + // Scoped variables to hold validation results to avoid fetching them again later + BasicProjectVM? projectVm = null; + Organization? organization = null; + Organization? supplier = null; + PurchaseInvoiceStatus? status = null; + + try + { + _logger.LogInfo("Initiating Purchase Invoice creation for ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId); + + // 2. DATA VALIDATION (Fail-Fast Strategy) + // We use a single Context instance here for read-only validation. + // It is more efficient than opening 5 parallel connections. + await using var readContext = await _dbContextFactory.CreateDbContextAsync(ct); + + // A. Validate Project (Check Infra, if null check Service) + // Optimized: Only fetch what is needed using Select first + var infraProject = await readContext.Projects + .AsNoTracking() + .Where(p => p.Id == model.ProjectId && p.TenantId == tenantId) + .Select(p => _mapper.Map(p)) + .FirstOrDefaultAsync(ct); + + if (infraProject == null) + { + var serviceProject = await readContext.ServiceProjects + .AsNoTracking() + .Where(sp => sp.Id == model.ProjectId && sp.IsActive && sp.TenantId == tenantId) + .Select(p => _mapper.Map(p)) + .FirstOrDefaultAsync(ct); + + if (serviceProject == null) + { + _logger.LogWarning("CreatePurchaseInvoice failed: Project {ProjectId} not found for Tenant {TenantId}", model.ProjectId, tenantId); + return ApiResponse.ErrorResponse("Project not found", "The specified project does not exist.", 404); + } + projectVm = serviceProject; + } + else + { + projectVm = infraProject; + } + + // B. Validate Organization + organization = await readContext.Organizations + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == model.OrganizationId && o.IsActive, ct); + + if (organization == null) + { + _logger.LogWarning("CreatePurchaseInvoice failed: Organization {OrganizationId} not found.", model.OrganizationId); + return ApiResponse.ErrorResponse("Organization not found", "The selected organization is invalid.", 404); + } + + // C. Validate Supplier + supplier = await readContext.Organizations + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == model.SupplierId && o.IsActive, ct); + + if (supplier == null) + { + _logger.LogWarning("CreatePurchaseInvoice failed: Supplier {SupplierId} not found.", model.SupplierId); + return ApiResponse.ErrorResponse("Supplier not found", "The selected supplier is invalid.", 404); + } + + // D. Validate Status + status = await readContext.PurchaseInvoiceStatus + .AsNoTracking() + .FirstOrDefaultAsync(s => s.Id == DraftInvoiceStatusId, ct); + + if (status == null) + { + _logger.LogWarning("CreatePurchaseInvoice critical: Default 'Draft' status ID {StatusId} is missing from DB.", DraftInvoiceStatusId); + return ApiResponse.ErrorResponse("System Error", "Default invoice status configuration is missing.", 500); + } + + // 3. PREPARE S3 UPLOADS (Optimistic Upload Pattern) + // We upload files BEFORE opening the DB transaction to prevent locking the DB during slow network I/O. + // If DB save fails, we will trigger a cleanup in the catch block. + var uploadedS3Keys = new List(); // Keep track for rollback + var preparedDocuments = new List(); + var preparedAttachments = new List(); + + // Generate Invoice ID early for S3 folder structure + var newInvoiceId = Guid.NewGuid(); + + if (model.Attachments?.Any() == true) + { + var batchId = Guid.NewGuid(); + + // Fetch Attachment Types + var typeIds = model.Attachments.Select(a => a.InvoiceAttachmentTypeId).ToList(); + var types = await readContext.InvoiceAttachmentTypes.Where(iat => typeIds.Contains(iat.Id)).ToListAsync(ct); + + foreach (var attachment in model.Attachments) + { + // Validate Type + if (!types.Any(t => t.Id == attachment.InvoiceAttachmentTypeId)) + { + _logger.LogWarning("CreatePurchaseInvoice failed: Attachment type {InvoiceAttachmentTypeId} is invalid.", attachment.InvoiceAttachmentTypeId); + return ApiResponse.ErrorResponse("Invalid Attachment", $"Attachment type {attachment.InvoiceAttachmentTypeId} is invalid.", 400); + } + + // Validate Base64 + var base64Data = attachment.Base64Data?.Split(',').LastOrDefault(); + if (string.IsNullOrWhiteSpace(base64Data)) + { + _logger.LogWarning("CreatePurchaseInvoice failed: Attachment {FileName} contains no data.", attachment.FileName ?? ""); + return ApiResponse.ErrorResponse("Invalid Attachment", $"Attachment {attachment.FileName} contains no data.", 400); + } + + // Process Metadata + var fileType = _s3Service.GetContentTypeFromBase64(base64Data); + // Use default extension if fileType extraction fails, prevents crashing + var safeFileType = string.IsNullOrEmpty(fileType) ? "application/octet-stream" : fileType; + var fileName = attachment.FileName ?? _s3Service.GenerateFileName(safeFileType, tenantId, "invoice"); + var objectKey = $"tenant-{tenantId}/PurchaseInvoice/{newInvoiceId}/{fileName}"; + + // Perform Upload + await _s3Service.UploadFileAsync(base64Data, safeFileType, objectKey); + uploadedS3Keys.Add(objectKey); // Track for rollback + + // Prepare Entities + var documentId = Guid.NewGuid(); + preparedDocuments.Add(new Document + { + Id = documentId, + BatchId = batchId, + UploadedById = loggedInEmployee.Id, + FileName = fileName, + ContentType = attachment.ContentType ?? safeFileType, + S3Key = objectKey, + FileSize = attachment.FileSize, + UploadedAt = DateTime.UtcNow, + TenantId = tenantId + }); + + preparedAttachments.Add(new PurchaseInvoiceAttachment + { + Id = Guid.NewGuid(), + InvoiceAttachmentTypeId = attachment.InvoiceAttachmentTypeId, + PurchaseInvoiceId = newInvoiceId, + DocumentId = documentId, + UploadedAt = DateTime.UtcNow, + UploadedById = loggedInEmployee.Id, + TenantId = tenantId + }); + } + } + + // 4. TRANSACTIONAL PERSISTENCE + await using var writeContext = await _dbContextFactory.CreateDbContextAsync(ct); + + // Use ExecutionStrategy for transient failure resiliency (e.g. cloud DB hiccups) + var strategy = writeContext.Database.CreateExecutionStrategy(); + + return await strategy.ExecuteAsync(async () => + { + await using var transaction = await writeContext.Database.BeginTransactionAsync(ct); + try + { + // A. UID Generation + // Note: In high concurrency, "Max + 1" can cause duplicates. + // Ideally, lock the table or use a DB Sequence. + string uIDPrefix = $"PUR/{DateTime.Now:MMyy}"; + + var lastInvoice = await writeContext.PurchaseInvoiceDetails + .Where(e => e.UIDPrefix == uIDPrefix && e.TenantId == tenantId) // Ensure Tenant Check + .OrderByDescending(e => e.UIDPostfix) + .Select(x => x.UIDPostfix) // Select only what we need + .FirstOrDefaultAsync(ct); + + int uIDPostfix = (lastInvoice == 0 ? 0 : lastInvoice) + 1; + + // B. Map & Add Invoice + var purchaseInvoice = _mapper.Map(model); + purchaseInvoice.Id = newInvoiceId; // Set the ID we generated earlier + purchaseInvoice.UIDPrefix = uIDPrefix; + purchaseInvoice.UIDPostfix = uIDPostfix; + purchaseInvoice.StatusId = status.Id; + purchaseInvoice.CreatedAt = DateTime.UtcNow; + purchaseInvoice.CreatedById = loggedInEmployee.Id; + purchaseInvoice.IsActive = true; + purchaseInvoice.TenantId = tenantId; + + writeContext.PurchaseInvoiceDetails.Add(purchaseInvoice); + + // C. Add Documents if any + if (preparedDocuments.Any()) + { + writeContext.Documents.AddRange(preparedDocuments); + writeContext.PurchaseInvoiceAttachments.AddRange(preparedAttachments); + } + + await writeContext.SaveChangesAsync(ct); + await transaction.CommitAsync(ct); + + _logger.LogInfo("Purchase Invoice created successfully. ID: {InvoiceId}, UID: {UID}", purchaseInvoice.Id, $"{uIDPrefix}-{uIDPostfix}"); + + // D. Prepare Response + var response = _mapper.Map(purchaseInvoice); + response.Status = status; + response.Project = projectVm; + response.Supplier = _mapper.Map(supplier); + + return ApiResponse.SuccessResponse(response, "Purchase invoice created successfully", 200); + } + catch (DbUpdateException ex) + { + await transaction.RollbackAsync(ct); + + // 5. COMPENSATION (S3 Rollback) + // If DB failed, we must delete the orphaned files from S3 to save cost and storage. + if (uploadedS3Keys != null && uploadedS3Keys.Any()) + { + _logger.LogInfo("Rolling back S3 uploads for failed invoice creation."); + // Fire and forget cleanup, or await if strict consistency is required + foreach (var key in uploadedS3Keys) + { + try { await _s3Service.DeleteFileAsync(key); } + catch (Exception s3Ex) { _logger.LogError(s3Ex, "Failed to cleanup S3 file {Key} during rollback", key); } + } + } + _logger.LogError(ex, "Failed to create purchase invoice for Tenant {TenantId}. Error: {Message}", tenantId, ex.Message); + + return ApiResponse.ErrorResponse("Creation Failed", "An unexpected error occurred while processing your request.", 500); + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create purchase invoice for Tenant {TenantId}. Error: {Message}", tenantId, ex.Message); + return ApiResponse.ErrorResponse("Creation Failed", "An unexpected error occurred while processing your request.", 500); + } + } + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs new file mode 100644 index 0000000..adb935b --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs @@ -0,0 +1,12 @@ +using Marco.Pms.Model.Dtos.PurchaseInvoice; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.PurchaseInvoice; + +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface IPurchaseInvoiceService + { + Task> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); + } +}