using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using ProposalSystem.Application.DTOs; using ProposalSystem.Application.Interfaces; using ProposalSystem.Domain.Entities; using ProposalSystem.Infrastructure.Data; namespace ProposalSystem.Api.Controllers; [ApiController] [Route("api/proposals/{proposalId:guid}")] [Authorize] public class FilesController : ControllerBase { private readonly ProposalDbContext _db; private readonly IS3Service _s3; private readonly IJobPublisher _jobPublisher; private readonly IAuditService _audit; private readonly IConfiguration _config; public FilesController( ProposalDbContext db, IS3Service s3, IJobPublisher jobPublisher, IAuditService audit, IConfiguration config) { _db = db; _s3 = s3; _jobPublisher = jobPublisher; _audit = audit; _config = config; } [HttpPost("attachments")] public async Task> UploadAttachment( Guid proposalId, [FromQuery] string fileName, CancellationToken ct) { var proposal = await _db.Proposals.FindAsync(new object[] { proposalId }, ct); if (proposal == null) return NotFound(); var extension = Path.GetExtension(fileName).ToLowerInvariant(); if (extension != ".pdf") return BadRequest(new { error = "Only PDF files are accepted" }); var s3Key = $"vendors/{proposalId}/{Guid.NewGuid()}{extension}"; var bucket = _config["UPLOADS_BUCKET"]!; var url = await _s3.GeneratePresignedUploadUrlAsync(bucket, s3Key, "application/pdf"); var vendorProposal = new VendorProposal { Id = Guid.NewGuid(), ProposalId = proposalId, FileName = fileName, S3Key = s3Key, UploadedAt = DateTime.UtcNow, ProcessingStatus = ProcessingStatus.Pending, }; _db.VendorProposals.Add(vendorProposal); await _db.SaveChangesAsync(ct); await _jobPublisher.PublishAsync("pdf-extract", new { proposalId, s3Key, vendorProposalId = vendorProposal.Id }, ct); return Ok(new PresignedUploadResponse(url, s3Key, DateTime.UtcNow.AddMinutes(15))); } [HttpGet("pdf")] [Authorize(Roles = "admins,sysadmins")] public async Task> GetPdf(Guid proposalId, CancellationToken ct) { var pdf = await _db.GeneratedPdfs .Where(p => p.ProposalId == proposalId) .OrderByDescending(p => p.Revision) .FirstOrDefaultAsync(ct); if (pdf == null) { await _jobPublisher.PublishAsync("pdf-generate", new { proposalId }, ct); return Accepted(new { message = "PDF generation queued" }); } var bucket = _config["GENERATED_BUCKET"]!; var url = await _s3.GeneratePresignedDownloadUrlAsync(bucket, pdf.S3Key, 60); await _audit.LogAsync(AuditAction.Download, proposalId, $"Downloaded rev {pdf.Revision}", ct); return Ok(new PdfDownloadResponse(url, DateTime.UtcNow.AddMinutes(60))); } [HttpGet("pdf/{revision:int}")] [Authorize(Roles = "admins,sysadmins")] public async Task> GetPdfRevision( Guid proposalId, int revision, CancellationToken ct) { var pdf = await _db.GeneratedPdfs .FirstOrDefaultAsync(p => p.ProposalId == proposalId && p.Revision == revision, ct); if (pdf == null) return NotFound(); var bucket = _config["GENERATED_BUCKET"]!; var url = await _s3.GeneratePresignedDownloadUrlAsync(bucket, pdf.S3Key, 60); await _audit.LogAsync(AuditAction.Download, proposalId, $"Downloaded rev {revision}", ct); return Ok(new PdfDownloadResponse(url, DateTime.UtcNow.AddMinutes(60))); } [HttpGet("vendors")] [Authorize(Roles = "admins,sysadmins")] public async Task>> GetVendors( Guid proposalId, CancellationToken ct) { var entities = await _db.VendorProposals .Where(v => v.ProposalId == proposalId) .OrderByDescending(v => v.UploadedAt) .ToListAsync(ct); var vendors = entities.Select(v => new VendorProposalResponse( v.Id, v.VendorName, v.FileName, v.TotalVendorCost, v.ProcessingStatus.ToString(), string.IsNullOrEmpty(v.ExtractedData) ? null : JsonSerializer.Deserialize(v.ExtractedData) )).ToList(); return Ok(vendors); } }