proposal-system/api/src/ProposalSystem.Infrastructure/Services/SesEmailService.cs
Adam Moussa 1fca0fa978
feat: proposal delivery — email customers the PDF on Mark as Sent (#126)
* feat: proposal delivery — email customers the PDF on "Mark as Sent"

Makes the system's namesake feature real: marking a proposal Sent now emails the
customer an expiring link to the branded PDF, and customers are managed (with
contact emails) instead of hardcoded. v1 PR4.

API:
- Customer.ContactEmail + migration; Customer list/update endpoints. Search stays
  additive at GET /api/customers?query= (frozen-mobile + web compat); new paginated
  list at GET /api/customers/list (admin).
- IEmailService (SesEmailService v2 / DevEmailService, dev-gated). MarkSentAsync
  resolves the customer's email, presigns the latest PDF (7d), and sends via SES.
  Email/presign failures are caught + audited and NEVER roll back the Sent transition.
- Startup EF migration guarded by a Postgres advisory lock (concurrency-safe).

Infra:
- SES email identity (proposals@seahavenind.com); least-privilege ses:SendEmail/
  SendRawEmail scoped to the identity ARN + ses:FromAddress condition; SES_FROM_ADDRESS
  env. SES starts in sandbox — production access needed for unverified recipients.

Web:
- Customer management page (/admin/customers): list / create / edit incl. contact email.
- New-proposal form searches real customers (free-solo) instead of a hardcoded value.
- Mark-as-Sent dialog notes the PDF will be emailed to the customer.

GPT-4.1 cross-review (SES IAM): no BLOCK (ses:FromAddress condition applied).
Verified: api build + 121 tests; web tsc + 26 tests; infra tsc; ruff clean.

* Potential fix for pull request finding 'CodeQL / Exposure of private information'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'CodeQL / Exposure of private information'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'CodeQL / Exposure of private information'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-18 12:40:55 -04:00

86 lines
2.8 KiB
C#

using Amazon.SimpleEmailV2;
using Amazon.SimpleEmailV2.Model;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ProposalSystem.Application.Interfaces;
namespace ProposalSystem.Infrastructure.Services;
public class SesEmailService : IEmailService
{
private readonly IAmazonSimpleEmailServiceV2 _ses;
private readonly string _fromAddress;
private readonly ILogger<SesEmailService> _logger;
public SesEmailService(
IAmazonSimpleEmailServiceV2 ses,
IConfiguration config,
ILogger<SesEmailService> logger)
{
_ses = ses;
_fromAddress = config["SES_FROM_ADDRESS"]
?? throw new InvalidOperationException(
"SES_FROM_ADDRESS environment variable is required for email delivery.");
_logger = logger;
}
public async Task SendProposalDeliveryAsync(
string toEmail,
string proposalNumber,
string customerName,
string pdfUrl,
CancellationToken ct = default)
{
var subject = $"Your proposal {proposalNumber} from Sea Haven Industries";
var htmlBody = $@"
<html>
<body style=""font-family: Arial, sans-serif; color: #333;"">
<p>Dear {System.Net.WebUtility.HtmlEncode(customerName)},</p>
<p>Your proposal <strong>{System.Net.WebUtility.HtmlEncode(proposalNumber)}</strong> is ready for review.</p>
<p>
<a href=""{System.Net.WebUtility.HtmlEncode(pdfUrl)}"" style=""display: inline-block; padding: 10px 20px; background-color: #0066cc; color: #ffffff; text-decoration: none; border-radius: 4px;"">View Proposal PDF</a>
</p>
<p>This link will expire in 7 days. If you have any questions, please contact us directly.</p>
<p>Thank you,<br/>Sea Haven Industries</p>
</body>
</html>";
var textBody = $@"Dear {customerName},
Your proposal {proposalNumber} is ready for review.
View the proposal PDF at: {pdfUrl}
This link will expire in 7 days. If you have any questions, please contact us directly.
Thank you,
Sea Haven Industries";
var request = new SendEmailRequest
{
FromEmailAddress = _fromAddress,
Destination = new Destination
{
ToAddresses = new List<string> { toEmail },
},
Content = new EmailContent
{
Simple = new Message
{
Subject = new Content { Data = subject },
Body = new Body
{
Html = new Content { Data = htmlBody },
Text = new Content { Data = textBody },
},
},
},
};
await _ses.SendEmailAsync(request, ct);
_logger.LogInformation(
"Proposal delivery email sent for {ProposalNumber}",
proposalNumber);
}
}