proposal-system/api/src/ProposalSystem.Infrastructure/Services/SesEmailService.cs

87 lines
2.8 KiB
C#
Raw Normal View History

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
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);
}
}