Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions JobFlow.API/Controllers/EmailController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,41 @@ public async Task<IActionResult> SendContactForm(
? Ok(new { message = "Contact form submitted." })
: StatusCode(500, new { message = "Failed to submit." });
}

[HttpPost]
[Route("waitlist-signup")]
public async Task<IActionResult> 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." });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}
3 changes: 2 additions & 1 deletion JobFlow.Business/Notifications/Enums/EmailTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public enum EmailTemplate
InvoiceReminder = 6,
OnTheWayNotification = 4,
ArrivalNotification = 5,
EmployeeInvite = 7
EmployeeInvite = 7,
WaitlistConfirmation = 8
}
6 changes: 6 additions & 0 deletions JobFlow.Business/Notifications/NotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ Task SendClientJobRescheduledNotificationAsync(

// Employee notifications
Task SendEmployeeInviteNotificationAsync(EmployeeInvite invite);

// Public / marketing notifications
Task SendWaitlistSignupNotificationAsync(string email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal static class BrevoListIds
{
public const int Newsletter = 3;
public const int TrialUsers = 4;
public const int Waitlist = 5;
}

[SingletonService]
Expand Down
1 change: 1 addition & 0 deletions JobFlow.Tests/FollowUpAutomationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading