feat: add order management admin with list and detail views

Admin UI at /admin/orders to view, filter, and inspect orders.
Adds list_orders/1 and count_orders_by_status/0 to the Orders
context, status filter tabs, clickable order table with streams,
and a detail page showing items, totals, and shipping address.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-07 21:59:14 +00:00
parent e6f8d7fa2a
commit 02cdc810f2
8 changed files with 603 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
defmodule SimpleshopTheme.OrdersTest do
use SimpleshopTheme.DataCase, async: false
alias SimpleshopTheme.Orders
import SimpleshopTheme.OrdersFixtures
describe "list_orders/1" do
test "returns all orders" do
order1 = order_fixture()
order2 = order_fixture()
orders = Orders.list_orders()
order_ids = Enum.map(orders, & &1.id)
assert order1.id in order_ids
assert order2.id in order_ids
assert length(orders) == 2
end
test "filters by payment status" do
_pending = order_fixture()
paid = order_fixture(payment_status: "paid")
_failed = order_fixture(payment_status: "failed")
orders = Orders.list_orders(status: "paid")
assert length(orders) == 1
assert hd(orders).id == paid.id
end
test "returns all when status is 'all'" do
order_fixture()
order_fixture(payment_status: "paid")
orders = Orders.list_orders(status: "all")
assert length(orders) == 2
end
test "preloads items" do
order_fixture()
[order] = Orders.list_orders()
assert Ecto.assoc_loaded?(order.items)
assert length(order.items) == 1
end
end
describe "count_orders_by_status/0" do
test "returns empty map when no orders" do
assert Orders.count_orders_by_status() == %{}
end
test "counts orders by status" do
order_fixture()
order_fixture()
order_fixture(payment_status: "paid")
order_fixture(payment_status: "failed")
counts = Orders.count_orders_by_status()
assert counts["pending"] == 2
assert counts["paid"] == 1
assert counts["failed"] == 1
end
end
end

View File

@@ -0,0 +1,119 @@
defmodule SimpleshopThemeWeb.AdminLive.OrdersTest do
use SimpleshopThemeWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
import SimpleshopTheme.OrdersFixtures
setup do
user = user_fixture()
%{user: user}
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/orders")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "order list" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders empty state when no orders", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ "No orders yet"
assert html =~ "Orders"
end
test "renders orders table", %{conn: conn} do
order = order_fixture(payment_status: "paid", customer_email: "test@shop.com")
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ order.order_number
assert html =~ "test@shop.com"
assert html =~ "paid"
end
test "filters by status", %{conn: conn} do
paid = order_fixture(payment_status: "paid")
_pending = order_fixture()
{:ok, view, _html} = live(conn, ~p"/admin/orders")
html = render_click(view, "filter", %{"status" => "paid"})
assert html =~ paid.order_number
end
test "navigates to order detail", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ ~p"/admin/orders/#{order}"
end
end
describe "order detail" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders order details", %{conn: conn} do
order = order_fixture(payment_status: "paid", customer_email: "buyer@example.com")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ order.order_number
assert html =~ "buyer@example.com"
assert html =~ "paid"
assert html =~ "Order details"
end
test "shows line items", %{conn: conn} do
order = order_fixture(product_name: "Cool T-shirt", variant_title: "Blue / XL")
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{order}")
assert html =~ "Cool T-shirt"
assert html =~ "Blue / XL"
end
test "shows shipping address", %{conn: conn} do
order = order_fixture(payment_status: "paid")
{:ok, updated_order} =
SimpleshopTheme.Orders.update_order(order, %{
shipping_address: %{
"name" => "Jane Doe",
"line1" => "42 Test Street",
"city" => "London",
"postal_code" => "SW1A 1AA",
"country" => "GB"
}
})
{:ok, _view, html} = live(conn, ~p"/admin/orders/#{updated_order}")
assert html =~ "Jane Doe"
assert html =~ "42 Test Street"
assert html =~ "London"
assert html =~ "SW1A 1AA"
end
test "redirects when order not found", %{conn: conn} do
fake_id = Ecto.UUID.generate()
{:error, {:live_redirect, %{to: "/admin/orders"}}} =
live(conn, ~p"/admin/orders/#{fake_id}")
end
end
end

View File

@@ -0,0 +1,42 @@
defmodule SimpleshopTheme.OrdersFixtures do
@moduledoc """
Test helpers for creating orders.
"""
alias SimpleshopTheme.Orders
def order_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{})
items = [
%{
variant_id: "var_#{System.unique_integer([:positive])}",
name: Map.get(attrs, :product_name, "Test product"),
variant: Map.get(attrs, :variant_title, "Red / Large"),
price: Map.get(attrs, :unit_price, 1999),
quantity: Map.get(attrs, :quantity, 1)
}
]
order_attrs = %{
items: items,
customer_email: Map.get(attrs, :customer_email, "customer@example.com"),
currency: Map.get(attrs, :currency, "gbp")
}
{:ok, order} = Orders.create_order(order_attrs)
case attrs[:payment_status] do
"paid" ->
{:ok, order} = Orders.mark_paid(order, "pi_test_#{System.unique_integer([:positive])}")
order
"failed" ->
{:ok, order} = Orders.mark_failed(order)
order
_ ->
order
end
end
end