diff --git a/JobFlow.API/Controllers/EmailController.cs b/JobFlow.API/Controllers/EmailController.cs index b0f6742..a0f352c 100644 --- a/JobFlow.API/Controllers/EmailController.cs +++ b/JobFlow.API/Controllers/EmailController.cs @@ -76,4 +76,41 @@ public async Task SendContactForm( ? Ok(new { message = "Contact form submitted." }) : StatusCode(500, new { message = "Failed to submit." }); } + + [HttpPost] + [Route("waitlist-signup")] + public async Task WaitlistSignup( + [FromBody] NewsletterSubscriptionRequest request, + [FromServices] IBrevoService brevoService, + [FromServices] INotificationService notificationService, + [FromServices] ICaptchaVerificationService captchaService, + CancellationToken cancellationToken) + { + var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString(); + + var verification = await captchaService.VerifyAsync( + request.CaptchaToken, + "waitlist-signup", + remoteIp, + cancellationToken); + + if (!verification.IsValid) + { + return BadRequest(new + { + message = "Turnstile validation failed.", + errors = verification.ErrorCodes + }); + } + + var added = await brevoService.AddContactAsync(request.Email, 5 /* BrevoListIds.Waitlist */); + if (!added) + return StatusCode(500, new { message = "Failed to join waitlist." }); + + // Fire-and-forget confirmation email — use CancellationToken.None so the task + // is not cancelled when the HTTP response completes. + _ = Task.Run(() => notificationService.SendWaitlistSignupNotificationAsync(request.Email), CancellationToken.None); + + return Ok(new { message = "Added to waitlist." }); + } } \ No newline at end of file diff --git a/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs b/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs index fcde948..db38d38 100644 --- a/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs +++ b/JobFlow.Business/Notifications/Builders/INotificationMessageBuilder.cs @@ -40,4 +40,6 @@ NotificationMessage BuildClientJobRescheduled( NotificationMessage BuildOrganizationClientJobUpdate(Organization organization, OrganizationClient client, Job job, string updateMessage); NotificationMessage BuildOrganizationClientPortalMagicLink(OrganizationClient client, string magicLink); + + NotificationMessage BuildWaitlistSignup(string email); } \ No newline at end of file diff --git a/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs b/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs index 9496aee..d49414f 100644 --- a/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs +++ b/JobFlow.Business/Notifications/Builders/NotificationMessageBuilder.cs @@ -457,4 +457,28 @@ private static string FormatScheduleRange(DateTimeOffset start, DateTimeOffset? return $"{localStart:MMM dd, yyyy h:mm tt} - {localEnd:MMM dd, yyyy h:mm tt}"; } + + public NotificationMessage BuildWaitlistSignup(string email) + { + return new NotificationMessage + { + Name = "Founding Member", + Email = email, + Subject = "You're on the JobFlow waitlist — your rate is reserved", + Body = """ + Hi there, + + You're officially on the JobFlow early-access waitlist. + + As a founding member, your $19/mo rate on the Go plan is reserved for you — + locked in for life, even when pricing increases at general availability. + + We'll reach out as soon as your spot is ready. In the meantime, feel free to + reply to this email with any questions. + + — The JobFlow Team + """, + TemplateId = EmailTemplate.WaitlistConfirmation + }; + } } \ No newline at end of file diff --git a/JobFlow.Business/Notifications/Enums/EmailTemplate.cs b/JobFlow.Business/Notifications/Enums/EmailTemplate.cs index 76db692..7661181 100644 --- a/JobFlow.Business/Notifications/Enums/EmailTemplate.cs +++ b/JobFlow.Business/Notifications/Enums/EmailTemplate.cs @@ -8,5 +8,6 @@ public enum EmailTemplate InvoiceReminder = 6, OnTheWayNotification = 4, ArrivalNotification = 5, - EmployeeInvite = 7 + EmployeeInvite = 7, + WaitlistConfirmation = 8 } \ No newline at end of file diff --git a/JobFlow.Business/Notifications/NotificationService.cs b/JobFlow.Business/Notifications/NotificationService.cs index 1951dfa..55c06dc 100644 --- a/JobFlow.Business/Notifications/NotificationService.cs +++ b/JobFlow.Business/Notifications/NotificationService.cs @@ -150,6 +150,12 @@ public async Task SendOrganizationClientPortalMagicLinkAsync(OrganizationClient await SendNotificationAsync(message); } + public async Task SendWaitlistSignupNotificationAsync(string email) + { + var message = _builder.BuildWaitlistSignup(email); + await SendNotificationAsync(message); + } + public async Task SendOrganizationSubscriptionPaymentFailedNotificationAsync(Organization org) { var message = _builder.BuildOrganizationSubscriptionFailed(org); diff --git a/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs b/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs index 998f174..1b84566 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/INotificationService.cs @@ -39,4 +39,7 @@ Task SendClientJobRescheduledNotificationAsync( // Employee notifications Task SendEmployeeInviteNotificationAsync(EmployeeInvite invite); + + // Public / marketing notifications + Task SendWaitlistSignupNotificationAsync(string email); } \ No newline at end of file diff --git a/JobFlow.Infrastructure/ExternalServices/Brevo/BrevoService.cs b/JobFlow.Infrastructure/ExternalServices/Brevo/BrevoService.cs index 6e01832..82f5559 100644 --- a/JobFlow.Infrastructure/ExternalServices/Brevo/BrevoService.cs +++ b/JobFlow.Infrastructure/ExternalServices/Brevo/BrevoService.cs @@ -16,6 +16,7 @@ internal static class BrevoListIds { public const int Newsletter = 3; public const int TrialUsers = 4; + public const int Waitlist = 5; } [SingletonService] diff --git a/JobFlow.Tests/FollowUpAutomationServiceTests.cs b/JobFlow.Tests/FollowUpAutomationServiceTests.cs index e507996..b2ddd33 100644 --- a/JobFlow.Tests/FollowUpAutomationServiceTests.cs +++ b/JobFlow.Tests/FollowUpAutomationServiceTests.cs @@ -283,5 +283,6 @@ private sealed class NoOpNotificationService : INotificationService public Task SendOrganizationClientJobUpdateNotificationAsync(Organization organization, OrganizationClient client, Job job, string updateMessage) => Task.CompletedTask; public Task SendOrganizationClientPortalMagicLinkAsync(OrganizationClient client, string magicLink) => Task.CompletedTask; public Task SendEmployeeInviteNotificationAsync(EmployeeInvite invite) => Task.CompletedTask; + public Task SendWaitlistSignupNotificationAsync(string email) => Task.CompletedTask; } }