add activity log with order timeline and global feed
All checks were successful
deploy / deploy (push) Successful in 4m22s
All checks were successful
deploy / deploy (push) Successful in 4m22s
Single activity_log table powering two views: chronological timeline on each order detail page (replacing the old fulfilment card) and a global feed at /admin/activity with tabs, category filters, search, and pagination. Real-time via PubSub — new entries appear instantly, nav badge updates across all admin pages. Instrumented across all event points: Stripe webhooks, order notifier, submission worker, fulfilment status worker, product sync worker, and Oban exhausted-job telemetry. Contextual action buttons (retry submission, retry sync, dismiss) with Oban unique constraints to prevent double-enqueue. 90-day pruning via cron. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ defmodule Berrypod.Orders.FulfilmentStatusWorker do
|
||||
|
||||
use Oban.Worker, queue: :sync, max_attempts: 1
|
||||
|
||||
alias Berrypod.Orders
|
||||
alias Berrypod.{ActivityLog, Orders}
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -38,6 +38,8 @@ defmodule Berrypod.Orders.FulfilmentStatusWorker do
|
||||
Logger.info(
|
||||
"Order #{order.order_number} status: #{order.fulfilment_status} → #{updated.fulfilment_status}"
|
||||
)
|
||||
|
||||
log_status_change(order, updated)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -46,4 +48,42 @@ defmodule Berrypod.Orders.FulfilmentStatusWorker do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp log_status_change(order, updated) do
|
||||
{event_type, message, level, payload} =
|
||||
case updated.fulfilment_status do
|
||||
"processing" ->
|
||||
{"order.production", "In production", "info", %{}}
|
||||
|
||||
"shipped" ->
|
||||
{"order.shipped", "Shipped#{tracking_suffix(updated)}", "info",
|
||||
%{tracking_number: updated.tracking_number, tracking_url: updated.tracking_url}}
|
||||
|
||||
"delivered" ->
|
||||
{"order.delivered", "Delivered", "info", %{}}
|
||||
|
||||
"failed" ->
|
||||
{"order.submission_failed",
|
||||
"Fulfilment failed: #{updated.fulfilment_error || "unknown error"}", "error",
|
||||
%{error: updated.fulfilment_error}}
|
||||
|
||||
"cancelled" ->
|
||||
{"order.cancelled", "Order cancelled", "warning", %{}}
|
||||
|
||||
other ->
|
||||
{"order.status_changed", "Status changed to #{other}", "info", %{}}
|
||||
end
|
||||
|
||||
ActivityLog.log_event(event_type, message,
|
||||
level: level,
|
||||
order_id: order.id,
|
||||
payload: payload
|
||||
)
|
||||
end
|
||||
|
||||
defp tracking_suffix(%{tracking_number: num}) when is_binary(num) and num != "" do
|
||||
" — tracking: #{num}"
|
||||
end
|
||||
|
||||
defp tracking_suffix(_), do: ""
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Berrypod.Orders.OrderNotifier do
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias Berrypod.Cart
|
||||
alias Berrypod.{ActivityLog, Cart}
|
||||
alias Berrypod.Mailer
|
||||
|
||||
require Logger
|
||||
@@ -39,7 +39,21 @@ defmodule Berrypod.Orders.OrderNotifier do
|
||||
==============================
|
||||
"""
|
||||
|
||||
deliver(order.customer_email, subject, body)
|
||||
result = deliver(order.customer_email, subject, body)
|
||||
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
ActivityLog.log_event(
|
||||
"order.email.confirmation_sent",
|
||||
"Order confirmation sent to #{order.customer_email}",
|
||||
order_id: order.id
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -89,7 +103,22 @@ defmodule Berrypod.Orders.OrderNotifier do
|
||||
==============================
|
||||
"""
|
||||
|
||||
deliver(order.customer_email, subject, body)
|
||||
result = deliver(order.customer_email, subject, body)
|
||||
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
ActivityLog.log_event(
|
||||
"order.email.shipping_sent",
|
||||
"Shipping notification sent to #{order.customer_email}",
|
||||
order_id: order.id,
|
||||
payload: %{tracking_number: order.tracking_number}
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -116,7 +145,21 @@ defmodule Berrypod.Orders.OrderNotifier do
|
||||
==============================
|
||||
"""
|
||||
|
||||
deliver(cart.customer_email, "You left something behind", body)
|
||||
result = deliver(cart.customer_email, "You left something behind", body)
|
||||
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
ActivityLog.log_event(
|
||||
"abandoned_cart.email_sent",
|
||||
"Recovery email sent to #{cart.customer_email}",
|
||||
payload: %{email: cart.customer_email}
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# --- Private ---
|
||||
|
||||
@@ -7,14 +7,14 @@ defmodule Berrypod.Orders.OrderSubmissionWorker do
|
||||
Retries up to 3 times with backoff for transient failures.
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :checkout, max_attempts: 3
|
||||
use Oban.Worker, queue: :checkout, max_attempts: 3, unique: [period: 60]
|
||||
|
||||
alias Berrypod.Orders
|
||||
alias Berrypod.{ActivityLog, Orders}
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"order_id" => order_id}}) do
|
||||
def perform(%Oban.Job{args: %{"order_id" => order_id}} = job) do
|
||||
case Orders.get_order(order_id) do
|
||||
nil ->
|
||||
Logger.warning("Order submission: order #{order_id} not found")
|
||||
@@ -39,10 +39,30 @@ defmodule Berrypod.Orders.OrderSubmissionWorker do
|
||||
"Order #{updated.order_number} submitted to provider (#{updated.provider_order_id})"
|
||||
)
|
||||
|
||||
ActivityLog.log_event(
|
||||
"order.submitted",
|
||||
"Submitted to provider (#{updated.provider_order_id})",
|
||||
order_id: order.id,
|
||||
payload: %{provider_order_id: updated.provider_order_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Order #{order.order_number} submission failed: #{inspect(reason)}")
|
||||
|
||||
ActivityLog.log_event(
|
||||
"order.submission_failed",
|
||||
"Submission failed: #{inspect(reason)}",
|
||||
level: "error",
|
||||
order_id: order.id,
|
||||
payload: %{
|
||||
error: inspect(reason),
|
||||
attempt: job.attempt,
|
||||
max_attempts: job.max_attempts
|
||||
}
|
||||
)
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user