feat: add transactional emails for order confirmation and shipping
Plain text emails via Swoosh OrderNotifier module. Order confirmation triggered from Stripe webhook after payment, shipping notification from Printify shipment webhook with polling fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Orders do
|
||||
|
||||
import Ecto.Query
|
||||
alias SimpleshopTheme.Repo
|
||||
alias SimpleshopTheme.Orders.{Order, OrderItem}
|
||||
alias SimpleshopTheme.Orders.{Order, OrderItem, OrderNotifier}
|
||||
alias SimpleshopTheme.Products
|
||||
alias SimpleshopTheme.Providers.Provider
|
||||
|
||||
@@ -233,7 +233,13 @@ defmodule SimpleshopTheme.Orders do
|
||||
}
|
||||
|> maybe_set_timestamp(order)
|
||||
|
||||
update_fulfilment(order, attrs)
|
||||
with {:ok, updated_order} <- update_fulfilment(order, attrs) do
|
||||
if attrs[:fulfilment_status] == "shipped" and order.fulfilment_status != "shipped" do
|
||||
OrderNotifier.deliver_shipping_notification(updated_order)
|
||||
end
|
||||
|
||||
{:ok, updated_order}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
130
lib/simpleshop_theme/orders/order_notifier.ex
Normal file
130
lib/simpleshop_theme/orders/order_notifier.ex
Normal file
@@ -0,0 +1,130 @@
|
||||
defmodule SimpleshopTheme.Orders.OrderNotifier do
|
||||
@moduledoc """
|
||||
Sends transactional emails for orders.
|
||||
|
||||
Order confirmation after payment, shipping notification when dispatched.
|
||||
"""
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias SimpleshopTheme.Cart
|
||||
alias SimpleshopTheme.Mailer
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Sends an order confirmation email after successful payment.
|
||||
|
||||
Skips silently if the order has no customer email.
|
||||
"""
|
||||
def deliver_order_confirmation(%{customer_email: nil}), do: {:ok, :no_email}
|
||||
def deliver_order_confirmation(%{customer_email: ""}), do: {:ok, :no_email}
|
||||
|
||||
def deliver_order_confirmation(order) do
|
||||
subject = "Order confirmed - #{order.order_number}"
|
||||
|
||||
body = """
|
||||
==============================
|
||||
|
||||
Thanks for your order!
|
||||
|
||||
Order: #{order.order_number}
|
||||
|
||||
#{format_items(order.items)}
|
||||
Total: #{Cart.format_price(order.total)}
|
||||
|
||||
#{format_shipping_address(order.shipping_address)}
|
||||
We'll send you another email when your order ships.
|
||||
|
||||
==============================
|
||||
"""
|
||||
|
||||
deliver(order.customer_email, subject, body)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends a shipping notification with tracking info.
|
||||
|
||||
Skips silently if the order has no customer email.
|
||||
"""
|
||||
def deliver_shipping_notification(%{customer_email: nil}), do: {:ok, :no_email}
|
||||
def deliver_shipping_notification(%{customer_email: ""}), do: {:ok, :no_email}
|
||||
|
||||
def deliver_shipping_notification(order) do
|
||||
subject = "Your order has shipped - #{order.order_number}"
|
||||
|
||||
body = """
|
||||
==============================
|
||||
|
||||
Good news! Your order #{order.order_number} is on its way.
|
||||
|
||||
#{format_tracking(order)}
|
||||
Thanks for shopping with us.
|
||||
|
||||
==============================
|
||||
"""
|
||||
|
||||
deliver(order.customer_email, subject, body)
|
||||
end
|
||||
|
||||
# --- Private ---
|
||||
|
||||
defp deliver(recipient, subject, body) do
|
||||
email =
|
||||
new()
|
||||
|> to(recipient)
|
||||
|> from({"SimpleshopTheme", "contact@example.com"})
|
||||
|> subject(subject)
|
||||
|> text_body(body)
|
||||
|
||||
case Mailer.deliver(email) do
|
||||
{:ok, _metadata} = result ->
|
||||
result
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.warning("Failed to send email to #{recipient}: #{inspect(reason)}")
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp format_items(items) when is_list(items) do
|
||||
items
|
||||
|> Enum.map_join("\n", fn item ->
|
||||
price = Cart.format_price(item.unit_price * item.quantity)
|
||||
" #{item.quantity}x #{item.product_name} (#{item.variant_title}) - #{price}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_items(_), do: ""
|
||||
|
||||
defp format_shipping_address(address) when is_map(address) and map_size(address) > 0 do
|
||||
lines =
|
||||
[
|
||||
address["name"],
|
||||
address["line1"],
|
||||
address["line2"],
|
||||
[address["city"], address["postal_code"]] |> Enum.reject(&is_nil/1) |> Enum.join(" "),
|
||||
address["state"],
|
||||
address["country"]
|
||||
]
|
||||
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|
||||
|> Enum.map_join("\n", &" #{&1}")
|
||||
|
||||
"Shipping to:\n#{lines}\n\n"
|
||||
end
|
||||
|
||||
defp format_shipping_address(_), do: ""
|
||||
|
||||
defp format_tracking(order) do
|
||||
cond do
|
||||
order.tracking_url not in [nil, ""] and order.tracking_number not in [nil, ""] ->
|
||||
"Tracking: #{order.tracking_number}\n#{order.tracking_url}\n\n"
|
||||
|
||||
order.tracking_number not in [nil, ""] ->
|
||||
"Tracking: #{order.tracking_number}\n\n"
|
||||
|
||||
true ->
|
||||
"Tracking details will follow once the carrier updates.\n\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ defmodule SimpleshopTheme.Webhooks do
|
||||
"""
|
||||
|
||||
alias SimpleshopTheme.Orders
|
||||
alias SimpleshopTheme.Orders.OrderNotifier
|
||||
alias SimpleshopTheme.Products
|
||||
alias SimpleshopTheme.Sync.ProductSyncWorker
|
||||
alias SimpleshopTheme.Webhooks.ProductDeleteWorker
|
||||
@@ -44,14 +45,17 @@ defmodule SimpleshopTheme.Webhooks do
|
||||
def handle_printify_event("order:shipment:created", resource) do
|
||||
shipment = extract_shipment(resource)
|
||||
|
||||
with {:ok, order} <- find_order_from_resource(resource) do
|
||||
Orders.update_fulfilment(order, %{
|
||||
fulfilment_status: "shipped",
|
||||
provider_status: "shipped",
|
||||
tracking_number: shipment.tracking_number,
|
||||
tracking_url: shipment.tracking_url,
|
||||
shipped_at: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
})
|
||||
with {:ok, order} <- find_order_from_resource(resource),
|
||||
{:ok, updated_order} <-
|
||||
Orders.update_fulfilment(order, %{
|
||||
fulfilment_status: "shipped",
|
||||
provider_status: "shipped",
|
||||
tracking_number: shipment.tracking_number,
|
||||
tracking_url: shipment.tracking_url,
|
||||
shipped_at: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
}) do
|
||||
OrderNotifier.deliver_shipping_notification(updated_order)
|
||||
{:ok, updated_order}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user