add activity log with order timeline and global feed
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:
jamey
2026-03-01 15:09:08 +00:00
parent b235219aee
commit 580a7203c9
23 changed files with 1716 additions and 54 deletions

View File

@@ -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

View File

@@ -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 ---

View File

@@ -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