simpleshop_theme/lib/simpleshop_theme/webhooks.ex
jamey 0af8997623 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>
2026-02-08 10:17:19 +00:00

120 lines
3.4 KiB
Elixir

defmodule SimpleshopTheme.Webhooks do
@moduledoc """
Handles incoming webhook events from POD providers.
"""
alias SimpleshopTheme.Orders
alias SimpleshopTheme.Orders.OrderNotifier
alias SimpleshopTheme.Products
alias SimpleshopTheme.Sync.ProductSyncWorker
alias SimpleshopTheme.Webhooks.ProductDeleteWorker
require Logger
@doc """
Handles a Printify webhook event.
Returns :ok or {:ok, job} on success, {:error, reason} on failure.
"""
# --- Product events ---
def handle_printify_event("product:updated", %{"id" => _product_id}) do
enqueue_product_sync()
end
def handle_printify_event("product:publish:started", %{"id" => _product_id}) do
enqueue_product_sync()
end
def handle_printify_event("product:deleted", %{"id" => product_id}) do
ProductDeleteWorker.enqueue(product_id)
end
# --- Order events ---
def handle_printify_event("order:sent-to-production", resource) do
with {:ok, order} <- find_order_from_resource(resource) do
Orders.update_fulfilment(order, %{
fulfilment_status: "processing",
provider_status: "in-production"
})
end
end
def handle_printify_event("order:shipment:created", resource) do
shipment = extract_shipment(resource)
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
def handle_printify_event("order:shipment:delivered", resource) do
with {:ok, order} <- find_order_from_resource(resource) do
Orders.update_fulfilment(order, %{
fulfilment_status: "delivered",
provider_status: "delivered",
delivered_at: DateTime.utc_now() |> DateTime.truncate(:second)
})
end
end
# --- Catch-all ---
def handle_printify_event("shop:disconnected", _resource) do
Logger.warning("Printify shop disconnected - manual intervention needed")
:ok
end
def handle_printify_event(event_type, _resource) do
Logger.info("Ignoring unhandled Printify event: #{event_type}")
:ok
end
# --- Private helpers ---
defp enqueue_product_sync do
case Products.get_provider_connection_by_type("printify") do
nil -> {:error, :no_connection}
conn -> ProductSyncWorker.enqueue(conn.id)
end
end
# Printify order webhooks include external_id (our order_number) in the resource
defp find_order_from_resource(%{"external_id" => external_id}) when is_binary(external_id) do
case Orders.get_order_by_number(external_id) do
nil ->
Logger.warning("Order webhook: no order found for external_id=#{external_id}")
{:error, :order_not_found}
order ->
{:ok, order}
end
end
defp find_order_from_resource(resource) do
Logger.warning("Order webhook: missing external_id in resource #{inspect(resource)}")
{:error, :missing_external_id}
end
defp extract_shipment(resource) do
shipments = resource["shipments"] || []
shipment = List.last(shipments) || %{}
%{
tracking_number: shipment["tracking_number"],
tracking_url: shipment["tracking_url"]
}
end
end