berrypod/priv/repo/migrations/20260301133937_create_activity_log.exs
jamey 580a7203c9
All checks were successful
deploy / deploy (push) Successful in 4m22s
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

103 lines
3.2 KiB
Elixir

defmodule Berrypod.Repo.Migrations.CreateActivityLog do
use Ecto.Migration
def up do
create table(:activity_log, primary_key: false) do
add :id, :binary_id, primary_key: true
add :event_type, :string, null: false
add :level, :string, null: false, default: "info"
add :order_id, references(:orders, type: :binary_id, on_delete: :nilify_all)
add :payload, :map, default: %{}
add :message, :string, null: false
add :resolved_at, :utc_datetime
add :occurred_at, :utc_datetime, null: false
timestamps(type: :utc_datetime)
end
create index(:activity_log, [:order_id])
create index(:activity_log, [:occurred_at])
create index(:activity_log, [:level, :resolved_at])
# Backfill from existing orders
flush()
backfill_from_orders()
end
def down do
drop table(:activity_log)
end
defp backfill_from_orders do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
# order.created for all paid orders
execute("""
INSERT INTO activity_log (id, event_type, level, order_id, payload, message, occurred_at, inserted_at, updated_at)
SELECT
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
'order.created',
'info',
id,
'{}',
'Order placed',
inserted_at,
'#{now}',
'#{now}'
FROM orders
WHERE payment_status = 'paid'
""")
# order.submitted
execute("""
INSERT INTO activity_log (id, event_type, level, order_id, payload, message, occurred_at, inserted_at, updated_at)
SELECT
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
'order.submitted',
'info',
id,
'{}',
'Submitted to provider',
submitted_at,
'#{now}',
'#{now}'
FROM orders
WHERE submitted_at IS NOT NULL
""")
# order.shipped
execute("""
INSERT INTO activity_log (id, event_type, level, order_id, payload, message, occurred_at, inserted_at, updated_at)
SELECT
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
'order.shipped',
'info',
id,
'{}',
'Shipped',
shipped_at,
'#{now}',
'#{now}'
FROM orders
WHERE shipped_at IS NOT NULL
""")
# order.delivered
execute("""
INSERT INTO activity_log (id, event_type, level, order_id, payload, message, occurred_at, inserted_at, updated_at)
SELECT
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
'order.delivered',
'info',
id,
'{}',
'Delivered',
delivered_at,
'#{now}',
'#{now}'
FROM orders
WHERE delivered_at IS NOT NULL
""")
end
end