mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 06:58:57 +00:00
* 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>
86 lines
2.8 KiB
C#
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);
|
|
}
|
|
}
|