proposal-system/api/src/ProposalSystem.Infrastructure/Services/SimilarProposalService.cs
Adam Moussa 80e36bfb9a Fix Phase 3 audit findings: polish, operational maturity, and remaining FIX items
Addresses 29 FIX-severity findings and accessibility/code-quality NITs
from the 2026-05-20 audit. Key changes:

- Add confirmation dialogs for Mark as Sent and Create Revision (FIX-17)
- Restrict S3 CORS from wildcard to specific origins (FIX-38)
- Add API Gateway throttling at 100 rps / 50 burst (FIX-39)
- Separate vendor upload from SQS extraction trigger (FIX-04/05)
- Copy TotalBidAmount on proposal revision (FIX-11)
- Add CI paths-ignore and concurrency group (FIX-47)
- Add post-deploy health check (FIX-46)
- Fix N+1 query, Guid.Empty FK, pagination bounds, role sync (FIX-01/02/07/09)
- Fix dashboard OOM, status transitions, audit error handling (FIX-03/06/12)
- Fix frontend date filters, error display, currency formatting (FIX-14/16/18-23)
- Remove AOSS dashboard public access, skip empty AI suggestions (FIX-41/45)
- Add aria-labels, document.title management, deduplicate constants
- Restrict CORS localhost to development, log invalid API key attempts

[skip deploy]
2026-05-20 19:35:13 -04:00

88 lines
2.9 KiB
C#

using Microsoft.EntityFrameworkCore;
using ProposalSystem.Application.DTOs;
using ProposalSystem.Application.Interfaces;
using ProposalSystem.Domain.Entities;
using ProposalSystem.Infrastructure.Data;
namespace ProposalSystem.Infrastructure.Services;
public class SimilarProposalService : ISimilarProposalService
{
private readonly ProposalDbContext _db;
private readonly ICurrentUserService _currentUser;
public SimilarProposalService(ProposalDbContext db, ICurrentUserService currentUser)
{
_db = db;
_currentUser = currentUser;
}
public async Task<IReadOnlyList<SimilarProposalResponse>> GetSimilarProposalsAsync(
Guid proposalId,
CancellationToken ct = default)
{
var references = await _db.SimilarProposalReferences
.Where(r => r.ProposalId == proposalId)
.OrderByDescending(r => r.SimilarityScore)
.Take(5)
.ToListAsync(ct);
if (references.Count == 0)
return [];
var libraryItemIds = references.Select(r => r.ReferencedLibraryItemId).ToList();
var proposals = await _db.Proposals
.Include(p => p.LineItems)
.Where(p => libraryItemIds.Contains(p.ProposalNumber))
.ToListAsync(ct);
var proposalsByNumber = proposals.ToDictionary(p => p.ProposalNumber);
var results = new List<SimilarProposalResponse>();
foreach (var reference in references)
{
if (!proposalsByNumber.TryGetValue(reference.ReferencedLibraryItemId, out var referencedProposal))
continue;
results.Add(new SimilarProposalResponse(
referencedProposal.ProposalNumber,
referencedProposal.CustomerName,
referencedProposal.ServiceCategory.ToString(),
reference.SimilarityScore,
referencedProposal.TotalBidAmount,
referencedProposal.LineItems
.OrderBy(li => li.SortOrder)
.Select(li => new SimilarLineItemResponse(
li.Description,
li.Quantity,
li.Unit,
li.UnitPrice,
li.TotalPrice
))
.ToList()
));
}
return results;
}
public async Task AddReferenceAsync(
Guid proposalId,
CreateSimilarReferenceRequest request,
CancellationToken ct = default)
{
var reference = new SimilarProposalReference
{
Id = Guid.NewGuid(),
ProposalId = proposalId,
ReferencedLibraryItemId = request.ReferencedLibraryItemId,
SimilarityScore = request.SimilarityScore,
ReferencedAt = DateTime.UtcNow,
ReferencedById = _currentUser.UserId,
};
_db.SimilarProposalReferences.Add(reference);
await _db.SaveChangesAsync(ct);
}
}