stripe and email after signup
This commit is contained in:
parent
b3cff9afbb
commit
f83213080f
2 changed files with 209 additions and 0 deletions
101
lib/towerops/billing/billing_notifier.ex
Normal file
101
lib/towerops/billing/billing_notifier.ex
Normal 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
|
||||
108
test/towerops/billing/billing_notifier_test.exs
Normal file
108
test/towerops/billing/billing_notifier_test.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue