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:
parent
e6f8d7fa2a
commit
02cdc810f2
@ -10,6 +10,41 @@ defmodule SimpleshopTheme.Orders do
|
|||||||
alias SimpleshopTheme.Repo
|
alias SimpleshopTheme.Repo
|
||||||
alias SimpleshopTheme.Orders.{Order, OrderItem}
|
alias SimpleshopTheme.Orders.{Order, OrderItem}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Lists orders, optionally filtered by payment status.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:status` - filter by payment_status ("paid", "pending", "failed", "refunded")
|
||||||
|
Pass nil or "all" to return all orders.
|
||||||
|
|
||||||
|
Returns orders sorted by newest first, with items preloaded.
|
||||||
|
"""
|
||||||
|
def list_orders(opts \\ []) do
|
||||||
|
status = opts[:status]
|
||||||
|
|
||||||
|
Order
|
||||||
|
|> maybe_filter_status(status)
|
||||||
|
|> order_by([o], desc: o.inserted_at)
|
||||||
|
|> preload(:items)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_filter_status(query, nil), do: query
|
||||||
|
defp maybe_filter_status(query, "all"), do: query
|
||||||
|
defp maybe_filter_status(query, status), do: where(query, [o], o.payment_status == ^status)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a map of payment_status => count for all orders.
|
||||||
|
"""
|
||||||
|
def count_orders_by_status do
|
||||||
|
Order
|
||||||
|
|> group_by(:payment_status)
|
||||||
|
|> select([o], {o.payment_status, count(o.id)})
|
||||||
|
|> Repo.all()
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates an order with line items from hydrated cart data.
|
Creates an order with line items from hydrated cart data.
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<.link href={~p"/admin/theme"}>Theme</.link>
|
<.link href={~p"/admin/theme"}>Theme</.link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/admin/orders"}>Orders</.link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<.link href={~p"/admin/settings"}>Credentials</.link>
|
<.link href={~p"/admin/settings"}>Credentials</.link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
175
lib/simpleshop_theme_web/live/admin_live/order_show.ex
Normal file
175
lib/simpleshop_theme_web/live/admin_live/order_show.ex
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
defmodule SimpleshopThemeWeb.AdminLive.OrderShow do
|
||||||
|
use SimpleshopThemeWeb, :live_view
|
||||||
|
|
||||||
|
alias SimpleshopTheme.Orders
|
||||||
|
alias SimpleshopTheme.Cart
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
|
case Orders.get_order(id) do
|
||||||
|
nil ->
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Order not found")
|
||||||
|
|> push_navigate(to: ~p"/admin/orders")
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
|
||||||
|
order ->
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, order.order_number)
|
||||||
|
|> assign(:order, order)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
|
<.header>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/admin/orders"}
|
||||||
|
class="text-sm font-normal text-base-content/60 hover:underline"
|
||||||
|
>
|
||||||
|
← Orders
|
||||||
|
</.link>
|
||||||
|
<div class="flex items-center gap-3 mt-1">
|
||||||
|
<span class="text-2xl font-bold">{@order.order_number}</span>
|
||||||
|
<.status_badge status={@order.payment_status} />
|
||||||
|
</div>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<div class="grid gap-6 mt-6 lg:grid-cols-2">
|
||||||
|
<%!-- order info --%>
|
||||||
|
<div class="card bg-base-100 shadow-sm border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-base">Order details</h3>
|
||||||
|
<.list>
|
||||||
|
<:item title="Date">{format_date(@order.inserted_at)}</:item>
|
||||||
|
<:item title="Customer">{@order.customer_email || "—"}</:item>
|
||||||
|
<:item title="Payment status">
|
||||||
|
<.status_badge status={@order.payment_status} />
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.stripe_payment_intent_id} title="Stripe payment">
|
||||||
|
<code class="text-xs">{@order.stripe_payment_intent_id}</code>
|
||||||
|
</:item>
|
||||||
|
<:item title="Currency">{String.upcase(@order.currency)}</:item>
|
||||||
|
</.list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- shipping address --%>
|
||||||
|
<div class="card bg-base-100 shadow-sm border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-base">Shipping address</h3>
|
||||||
|
<%= if @order.shipping_address != %{} do %>
|
||||||
|
<.list>
|
||||||
|
<:item :if={@order.shipping_address["name"]} title="Name">
|
||||||
|
{@order.shipping_address["name"]}
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.shipping_address["line1"]} title="Address">
|
||||||
|
{@order.shipping_address["line1"]}
|
||||||
|
<span :if={@order.shipping_address["line2"]}>
|
||||||
|
<br />{@order.shipping_address["line2"]}
|
||||||
|
</span>
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.shipping_address["city"]} title="City">
|
||||||
|
{@order.shipping_address["city"]}
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.shipping_address["state"]} title="State">
|
||||||
|
{@order.shipping_address["state"]}
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.shipping_address["postal_code"]} title="Postcode">
|
||||||
|
{@order.shipping_address["postal_code"]}
|
||||||
|
</:item>
|
||||||
|
<:item :if={@order.shipping_address["country"]} title="Country">
|
||||||
|
{@order.shipping_address["country"]}
|
||||||
|
</:item>
|
||||||
|
</.list>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-base-content/60 text-sm">No shipping address provided</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- line items --%>
|
||||||
|
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-base">Items</h3>
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Variant</th>
|
||||||
|
<th class="text-right">Qty</th>
|
||||||
|
<th class="text-right">Unit price</th>
|
||||||
|
<th class="text-right">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr :for={item <- @order.items}>
|
||||||
|
<td>{item.product_name}</td>
|
||||||
|
<td>{item.variant_title}</td>
|
||||||
|
<td class="text-right">{item.quantity}</td>
|
||||||
|
<td class="text-right">{Cart.format_price(item.unit_price)}</td>
|
||||||
|
<td class="text-right">{Cart.format_price(item.unit_price * item.quantity)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-right font-medium">Subtotal</td>
|
||||||
|
<td class="text-right font-medium">{Cart.format_price(@order.subtotal)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="text-lg">
|
||||||
|
<td colspan="4" class="text-right font-bold">Total</td>
|
||||||
|
<td class="text-right font-bold">{Cart.format_price(@order.total)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_badge(assigns) do
|
||||||
|
{bg, text, ring, icon} =
|
||||||
|
case assigns.status do
|
||||||
|
"paid" ->
|
||||||
|
{"bg-green-50", "text-green-700", "ring-green-600/20", "hero-check-circle-mini"}
|
||||||
|
|
||||||
|
"pending" ->
|
||||||
|
{"bg-amber-50", "text-amber-700", "ring-amber-600/20", "hero-clock-mini"}
|
||||||
|
|
||||||
|
"failed" ->
|
||||||
|
{"bg-red-50", "text-red-700", "ring-red-600/20", "hero-x-circle-mini"}
|
||||||
|
|
||||||
|
"refunded" ->
|
||||||
|
{"bg-zinc-50", "text-zinc-600", "ring-zinc-500/10", "hero-arrow-uturn-left-mini"}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{"bg-zinc-50", "text-zinc-600", "ring-zinc-500/10", "hero-question-mark-circle-mini"}
|
||||||
|
end
|
||||||
|
|
||||||
|
assigns = assign(assigns, bg: bg, text: text, ring: ring, icon: icon)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<span class={[
|
||||||
|
"inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||||
|
@bg,
|
||||||
|
@text,
|
||||||
|
@ring
|
||||||
|
]}>
|
||||||
|
<.icon name={@icon} class="size-3" /> {@status}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_date(datetime) do
|
||||||
|
Calendar.strftime(datetime, "%d %b %Y %H:%M")
|
||||||
|
end
|
||||||
|
end
|
||||||
162
lib/simpleshop_theme_web/live/admin_live/orders.ex
Normal file
162
lib/simpleshop_theme_web/live/admin_live/orders.ex
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
defmodule SimpleshopThemeWeb.AdminLive.Orders do
|
||||||
|
use SimpleshopThemeWeb, :live_view
|
||||||
|
|
||||||
|
alias SimpleshopTheme.Orders
|
||||||
|
alias SimpleshopTheme.Cart
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
counts = Orders.count_orders_by_status()
|
||||||
|
orders = Orders.list_orders()
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Orders")
|
||||||
|
|> assign(:status_filter, "all")
|
||||||
|
|> assign(:status_counts, counts)
|
||||||
|
|> assign(:order_count, length(orders))
|
||||||
|
|> stream(:orders, orders)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("filter", %{"status" => status}, socket) do
|
||||||
|
orders = Orders.list_orders(status: status)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:status_filter, status)
|
||||||
|
|> assign(:order_count, length(orders))
|
||||||
|
|> stream(:orders, orders, reset: true)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
|
<.header>
|
||||||
|
Orders
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-6 mb-4 flex-wrap">
|
||||||
|
<.filter_tab
|
||||||
|
status="all"
|
||||||
|
label="All"
|
||||||
|
count={total_count(@status_counts)}
|
||||||
|
active={@status_filter}
|
||||||
|
/>
|
||||||
|
<.filter_tab
|
||||||
|
status="paid"
|
||||||
|
label="Paid"
|
||||||
|
count={@status_counts["paid"]}
|
||||||
|
active={@status_filter}
|
||||||
|
/>
|
||||||
|
<.filter_tab
|
||||||
|
status="pending"
|
||||||
|
label="Pending"
|
||||||
|
count={@status_counts["pending"]}
|
||||||
|
active={@status_filter}
|
||||||
|
/>
|
||||||
|
<.filter_tab
|
||||||
|
status="failed"
|
||||||
|
label="Failed"
|
||||||
|
count={@status_counts["failed"]}
|
||||||
|
active={@status_filter}
|
||||||
|
/>
|
||||||
|
<.filter_tab
|
||||||
|
status="refunded"
|
||||||
|
label="Refunded"
|
||||||
|
count={@status_counts["refunded"]}
|
||||||
|
active={@status_filter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<.table
|
||||||
|
:if={@order_count > 0}
|
||||||
|
id="orders"
|
||||||
|
rows={@streams.orders}
|
||||||
|
row_item={fn {_id, order} -> order end}
|
||||||
|
row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end}
|
||||||
|
>
|
||||||
|
<:col :let={order} label="Order">{order.order_number}</:col>
|
||||||
|
<:col :let={order} label="Date">{format_date(order.inserted_at)}</:col>
|
||||||
|
<:col :let={order} label="Customer">{order.customer_email || "—"}</:col>
|
||||||
|
<:col :let={order} label="Total">{Cart.format_price(order.total)}</:col>
|
||||||
|
<:col :let={order} label="Status"><.status_badge status={order.payment_status} /></:col>
|
||||||
|
</.table>
|
||||||
|
|
||||||
|
<div :if={@order_count == 0} class="text-center py-12 text-base-content/60">
|
||||||
|
<.icon name="hero-inbox" class="size-12 mx-auto mb-4" />
|
||||||
|
<p class="text-lg font-medium">No orders yet</p>
|
||||||
|
<p class="text-sm mt-1">Orders will appear here once customers check out.</p>
|
||||||
|
</div>
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_tab(assigns) do
|
||||||
|
count = assigns[:count] || 0
|
||||||
|
active = assigns.active == assigns.status
|
||||||
|
|
||||||
|
assigns = assign(assigns, count: count, active: active)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<button
|
||||||
|
phx-click="filter"
|
||||||
|
phx-value-status={@status}
|
||||||
|
class={[
|
||||||
|
"btn btn-sm",
|
||||||
|
@active && "btn-primary",
|
||||||
|
!@active && "btn-ghost"
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{@label}
|
||||||
|
<span :if={@count > 0} class="badge badge-sm ml-1">{@count}</span>
|
||||||
|
</button>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_badge(assigns) do
|
||||||
|
{bg, text, ring, icon} =
|
||||||
|
case assigns.status do
|
||||||
|
"paid" ->
|
||||||
|
{"bg-green-50", "text-green-700", "ring-green-600/20", "hero-check-circle-mini"}
|
||||||
|
|
||||||
|
"pending" ->
|
||||||
|
{"bg-amber-50", "text-amber-700", "ring-amber-600/20", "hero-clock-mini"}
|
||||||
|
|
||||||
|
"failed" ->
|
||||||
|
{"bg-red-50", "text-red-700", "ring-red-600/20", "hero-x-circle-mini"}
|
||||||
|
|
||||||
|
"refunded" ->
|
||||||
|
{"bg-zinc-50", "text-zinc-600", "ring-zinc-500/10", "hero-arrow-uturn-left-mini"}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{"bg-zinc-50", "text-zinc-600", "ring-zinc-500/10", "hero-question-mark-circle-mini"}
|
||||||
|
end
|
||||||
|
|
||||||
|
assigns = assign(assigns, bg: bg, text: text, ring: ring, icon: icon)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<span class={[
|
||||||
|
"inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||||
|
@bg,
|
||||||
|
@text,
|
||||||
|
@ring
|
||||||
|
]}>
|
||||||
|
<.icon name={@icon} class="size-3" /> {@status}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_date(datetime) do
|
||||||
|
Calendar.strftime(datetime, "%d %b %Y %H:%M")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp total_count(counts) do
|
||||||
|
counts |> Map.values() |> Enum.sum()
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -117,6 +117,8 @@ defmodule SimpleshopThemeWeb.Router do
|
|||||||
live "/admin/providers", ProviderLive.Index, :index
|
live "/admin/providers", ProviderLive.Index, :index
|
||||||
live "/admin/providers/new", ProviderLive.Form, :new
|
live "/admin/providers/new", ProviderLive.Form, :new
|
||||||
live "/admin/providers/:id/edit", ProviderLive.Form, :edit
|
live "/admin/providers/:id/edit", ProviderLive.Form, :edit
|
||||||
|
live "/admin/orders", AdminLive.Orders, :index
|
||||||
|
live "/admin/orders/:id", AdminLive.OrderShow, :show
|
||||||
live "/admin/settings", AdminLive.Settings, :index
|
live "/admin/settings", AdminLive.Settings, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
65
test/simpleshop_theme/orders_test.exs
Normal file
65
test/simpleshop_theme/orders_test.exs
Normal 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
|
||||||
119
test/simpleshop_theme_web/live/admin_live/orders_test.exs
Normal file
119
test/simpleshop_theme_web/live/admin_live/orders_test.exs
Normal 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
|
||||||
42
test/support/fixtures/orders_fixtures.ex
Normal file
42
test/support/fixtures/orders_fixtures.ex
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user