add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
defmodule BerrypodWeb.Admin.ActivityTest do
|
|
|
|
|
use BerrypodWeb.ConnCase, async: false
|
|
|
|
|
|
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
|
import Berrypod.AccountsFixtures
|
|
|
|
|
import Berrypod.OrdersFixtures
|
|
|
|
|
|
|
|
|
|
alias Berrypod.ActivityLog
|
|
|
|
|
|
|
|
|
|
setup do
|
|
|
|
|
user = user_fixture()
|
|
|
|
|
%{user: user}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe "unauthenticated" do
|
|
|
|
|
test "redirects to login", %{conn: conn} do
|
|
|
|
|
{:error, redirect} = live(conn, ~p"/admin/activity")
|
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
|
|
|
assert path == ~p"/users/log-in"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe "activity page" do
|
|
|
|
|
setup %{conn: conn, user: user} do
|
|
|
|
|
conn = log_in_user(conn, user)
|
|
|
|
|
%{conn: conn}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "renders empty state", %{conn: conn} do
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
assert html =~ "Activity"
|
|
|
|
|
assert html =~ "No activity to show"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "renders activity entries", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("order.created", "Order placed")
|
2026-03-02 08:32:06 +00:00
|
|
|
ActivityLog.log_event("order.shipped", "Order shipped")
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
assert html =~ "Order placed"
|
2026-03-02 08:32:06 +00:00
|
|
|
assert html =~ "Order shipped"
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "links to order when order_id present", %{conn: conn} do
|
|
|
|
|
order = order_fixture(payment_status: "paid")
|
|
|
|
|
|
|
|
|
|
ActivityLog.log_event("order.created", "Order placed", order_id: order.id)
|
|
|
|
|
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
assert html =~ "View order"
|
|
|
|
|
assert html =~ ~p"/admin/orders/#{order}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "filters by attention tab", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("sync.completed", "Sync OK")
|
|
|
|
|
ActivityLog.log_event("order.submission_failed", "Submission failed", level: "error")
|
|
|
|
|
|
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
html = render_click(view, "tab", %{"tab" => "attention"})
|
|
|
|
|
|
|
|
|
|
assert html =~ "Submission failed"
|
|
|
|
|
refute html =~ "Sync OK"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "filters by category", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("sync.completed", "Sync done")
|
|
|
|
|
ActivityLog.log_event("order.created", "Order placed")
|
|
|
|
|
|
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
2026-03-02 08:32:06 +00:00
|
|
|
# Enable system events so sync.completed is visible
|
|
|
|
|
render_click(view, "toggle_system")
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
html = render_click(view, "category", %{"category" => "syncs"})
|
|
|
|
|
|
|
|
|
|
assert html =~ "Sync done"
|
|
|
|
|
refute html =~ "Order placed"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "searches by order number", %{conn: conn} do
|
|
|
|
|
order = order_fixture(payment_status: "paid")
|
|
|
|
|
_other_order = order_fixture(payment_status: "paid")
|
|
|
|
|
|
|
|
|
|
ActivityLog.log_event("order.created", "First order", order_id: order.id)
|
|
|
|
|
|
|
|
|
|
ActivityLog.log_event("sync.completed", "Sync done")
|
|
|
|
|
|
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
html =
|
|
|
|
|
render_submit(view, "search", %{"search" => %{"query" => order.order_number}})
|
|
|
|
|
|
|
|
|
|
assert html =~ "First order"
|
|
|
|
|
refute html =~ "Sync done"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "dismisses a warning entry", %{conn: conn} do
|
|
|
|
|
{:ok, entry} =
|
|
|
|
|
ActivityLog.log_event("order.cancelled", "Order cancelled", level: "warning")
|
|
|
|
|
|
|
|
|
|
{:ok, view, html} = live(conn, ~p"/admin/activity?tab=attention")
|
|
|
|
|
|
|
|
|
|
assert html =~ "Order cancelled"
|
|
|
|
|
assert html =~ "Dismiss"
|
|
|
|
|
|
|
|
|
|
render_click(view, "resolve", %{"id" => entry.id})
|
|
|
|
|
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity?tab=attention")
|
|
|
|
|
refute html =~ "Order cancelled"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "shows retry submission button for failed orders", %{conn: conn} do
|
|
|
|
|
order = order_fixture(payment_status: "paid")
|
|
|
|
|
|
|
|
|
|
ActivityLog.log_event("order.submission_failed", "Submission failed",
|
|
|
|
|
level: "error",
|
|
|
|
|
order_id: order.id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity?tab=attention")
|
|
|
|
|
|
|
|
|
|
assert html =~ "Retry submission"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "shows retry sync button for failed syncs", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("sync.failed", "Sync failed",
|
|
|
|
|
level: "error",
|
|
|
|
|
payload: %{connection_id: Ecto.UUID.generate()}
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-02 08:32:06 +00:00
|
|
|
{:ok, view, _html} = live(conn, ~p"/admin/activity?tab=attention")
|
|
|
|
|
|
|
|
|
|
# sync.failed is a system event, hidden by default
|
|
|
|
|
render_click(view, "toggle_system")
|
|
|
|
|
html = render(view)
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
|
|
|
|
|
assert html =~ "Retry sync"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "updates via PubSub", %{conn: conn} do
|
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
2026-03-02 08:32:06 +00:00
|
|
|
ActivityLog.log_event("order.created", "New order arrived")
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
|
|
|
|
|
html = render(view)
|
2026-03-02 08:32:06 +00:00
|
|
|
assert html =~ "New order arrived"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "system events hidden by default, shown after toggle", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("sync.completed", "Sync done")
|
|
|
|
|
ActivityLog.log_event("order.created", "Order placed")
|
|
|
|
|
|
|
|
|
|
{:ok, view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
# System events hidden by default
|
|
|
|
|
refute html =~ "Sync done"
|
|
|
|
|
assert html =~ "Order placed"
|
|
|
|
|
|
|
|
|
|
# Toggle shows system events
|
|
|
|
|
html = render_click(view, "toggle_system")
|
|
|
|
|
assert html =~ "Sync done"
|
|
|
|
|
assert html =~ "Order placed"
|
add activity log with order timeline and global feed
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>
2026-03-01 15:09:08 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe "nav badge" do
|
|
|
|
|
setup %{conn: conn, user: user} do
|
|
|
|
|
conn = log_in_user(conn, user)
|
|
|
|
|
%{conn: conn}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "shows badge when attention items exist", %{conn: conn} do
|
|
|
|
|
ActivityLog.log_event("order.submission_failed", "Something broke", level: "error")
|
|
|
|
|
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
# The nav badge should show the count
|
|
|
|
|
assert html =~ "admin-badge-warning"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "hides badge when no attention items", %{conn: conn} do
|
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/admin/activity")
|
|
|
|
|
|
|
|
|
|
# No badge should be rendered
|
|
|
|
|
refute html =~ "admin-badge-warning"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|