stripe and email after signup

This commit is contained in:
Graham McIntire 2026-03-06 10:07:07 -06:00
parent b3cff9afbb
commit f83213080f
No known key found for this signature in database
2 changed files with 209 additions and 0 deletions

View file

@ -0,0 +1,101 @@
defmodule Towerops.Billing.BillingNotifier do
@moduledoc """
Email notification service for billing events.
Sends emails for payment failures, subscription updates, and billing alerts.
"""
use Gettext, backend: ToweropsWeb.Gettext
import Swoosh.Email
import ToweropsWeb.GettextHelpers
alias Towerops.Mailer
alias Towerops.Organizations
require Logger
defp deliver(recipient, subject, body) do
from_address = Application.get_env(:towerops, :mailer_from, {"Towerops", "hi@towerops.net"})
email =
new()
|> to(recipient)
|> from(from_address)
|> subject(subject)
|> text_body(body)
case Mailer.deliver(email) do
{:ok, _metadata} ->
Logger.info("Billing email sent to #{recipient}: #{subject}")
{:ok, email}
{:error, reason} = error ->
Logger.error("Failed to send billing email to #{recipient}: #{inspect(reason)}")
error
end
end
@doc """
Deliver payment failure notification to organization owners and admins.
Sends an email to all organization owners and admins informing them of a payment
failure and providing a link to update their payment method.
"""
def deliver_payment_failed_notification(organization) do
# Get all owners and admins (people who can manage billing)
recipients =
organization.id
|> Organizations.list_organization_members()
|> Enum.filter(fn membership -> membership.role in [:owner, :admin] end)
|> Enum.map(fn membership -> membership.user end)
if Enum.empty?(recipients) do
Logger.warning("No owners or admins found for organization #{organization.slug} to notify about payment failure")
{:ok, []}
else
subject = t_email("Payment failed for %{org_name}", org_name: organization.name)
body =
t_email(
"""
Hi,
We were unable to process the payment for your %{org_name} subscription on Towerops.
Your subscription is still active, but we'll retry the payment in a few days. To avoid service interruption, please update your payment method as soon as possible.
You can update your payment method here:
%{billing_url}
If you have any questions or need assistance, please don't hesitate to reply to this email.
Thank you,
Towerops Team
""",
org_name: organization.name,
billing_url: billing_portal_url(organization)
)
results =
Enum.map(recipients, fn user ->
deliver(user.email, subject, body)
end)
# Return success if at least one email was sent
if Enum.any?(results, &match?({:ok, _}, &1)) do
{:ok, results}
else
{:error, :all_emails_failed}
end
end
end
defp billing_portal_url(organization) do
# Generate URL to billing settings page
# In production, this should use the configured app URL
base_url = Application.get_env(:towerops, :app_url, "https://towerops.net")
"#{base_url}/o/#{organization.slug}/settings#billing"
end
end

View file

@ -0,0 +1,108 @@
defmodule Towerops.Billing.BillingNotifierTest do
use Towerops.DataCase, async: true
import Swoosh.TestAssertions
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Billing.BillingNotifier
alias Towerops.Organizations
describe "deliver_payment_failed_notification/1" do
test "sends email to organization owner" do
user = user_fixture()
organization = organization_fixture(user.id)
assert {:ok, _results} = BillingNotifier.deliver_payment_failed_notification(organization)
assert_email_sent(fn email ->
assert email.to == [{"", user.email}]
assert email.subject =~ "Payment failed"
assert email.subject =~ organization.name
assert email.text_body =~ organization.name
assert email.text_body =~ "unable to process the payment"
assert email.text_body =~ "/o/#{organization.slug}/settings#billing"
end)
end
test "sends email to all owners and admins" do
owner = user_fixture()
admin = user_fixture(%{email: "admin@example.com"})
member = user_fixture(%{email: "member@example.com"})
viewer = user_fixture(%{email: "viewer@example.com"})
organization = organization_fixture(owner.id)
# Add admin, member, and viewer
{:ok, _} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: admin.id,
role: :admin
})
{:ok, _} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: member.id,
role: :member
})
{:ok, _} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: viewer.id,
role: :viewer
})
assert {:ok, results} = BillingNotifier.deliver_payment_failed_notification(organization)
# Should send 2 emails (owner + admin only, not member or viewer)
assert length(results) == 2
assert Enum.all?(results, &match?({:ok, _}, &1))
# Check that owner and admin received emails
assert_email_sent(fn email -> email.to == [{"", owner.email}] end)
assert_email_sent(fn email -> email.to == [{"", admin.email}] end)
end
test "returns ok with empty list when no owners or admins exist" do
# Create organization with only member (no owner somehow)
user = user_fixture()
organization = organization_fixture(user.id)
# Delete the owner membership
membership = Organizations.get_membership(organization.id, user.id)
Towerops.Repo.delete!(membership)
assert {:ok, []} = BillingNotifier.deliver_payment_failed_notification(organization)
# No emails should be sent
assert_no_email_sent()
end
test "includes billing portal URL in email" do
user = user_fixture()
organization = organization_fixture(user.id)
assert {:ok, _results} = BillingNotifier.deliver_payment_failed_notification(organization)
assert_email_sent(fn email ->
email.text_body =~ "/o/#{organization.slug}/settings#billing"
end)
end
test "email contains helpful instructions" do
user = user_fixture()
organization = organization_fixture(user.id)
assert {:ok, _results} = BillingNotifier.deliver_payment_failed_notification(organization)
assert_email_sent(fn email ->
assert email.text_body =~ "update your payment method"
assert email.text_body =~ "retry the payment"
assert email.text_body =~ "avoid service interruption"
end)
end
end
end