All checks were successful
deploy / deploy (push) Successful in 1m17s
Magic link flow on contact page: customer enters email, gets a time-limited signed link, clicks through to /orders showing all their paid orders and full detail pages with thumbnails and product links. - OrderLookupController generates/verifies Phoenix.Token signed links - Contact LiveView handles lookup_orders + reset_tracking events - Orders and OrderDetail LiveViews gated by session email - Order detail shows thumbnails, links to products still available - .themed-button gets base padding/font-weight so all usages are consistent - order-summary-card sticky scoped to .cart-grid (was leaking to orders list) - 27 new tests (1095 total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
279 lines
8.2 KiB
Elixir
279 lines
8.2 KiB
Elixir
defmodule Berrypod.OrdersTest do
|
|
use Berrypod.DataCase, async: false
|
|
|
|
import Ecto.Query
|
|
import Mox
|
|
import Berrypod.OrdersFixtures
|
|
|
|
alias Berrypod.Orders
|
|
alias Berrypod.Providers.MockProvider
|
|
|
|
setup :verify_on_exit!
|
|
|
|
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
|
|
|
|
describe "submit_to_provider/1" do
|
|
setup do
|
|
Application.put_env(:berrypod, :provider_modules, %{
|
|
"printify" => MockProvider
|
|
})
|
|
|
|
on_exit(fn -> Application.delete_env(:berrypod, :provider_modules) end)
|
|
:ok
|
|
end
|
|
|
|
test "submits order and sets fulfilment status" do
|
|
{order, _variant, _product, _conn} = paid_order_with_products_fixture()
|
|
|
|
expect(MockProvider, :submit_order, fn _conn, order_data ->
|
|
assert order_data.order_number == order.order_number
|
|
assert is_list(order_data.line_items)
|
|
assert hd(order_data.line_items).quantity == 1
|
|
{:ok, %{provider_order_id: "pfy_123"}}
|
|
end)
|
|
|
|
assert {:ok, updated} = Orders.submit_to_provider(order)
|
|
assert updated.fulfilment_status == "submitted"
|
|
assert updated.provider_order_id == "pfy_123"
|
|
assert updated.submitted_at != nil
|
|
assert updated.fulfilment_error == nil
|
|
end
|
|
|
|
test "is idempotent when already submitted" do
|
|
{order, _variant, _product, _conn} = submitted_order_fixture()
|
|
|
|
# No mock expectations — provider should not be called
|
|
assert {:ok, ^order} = Orders.submit_to_provider(order)
|
|
end
|
|
|
|
test "sets failed status on provider error" do
|
|
{order, _variant, _product, _conn} = paid_order_with_products_fixture()
|
|
|
|
expect(MockProvider, :submit_order, fn _conn, _data ->
|
|
{:error, {500, %{"message" => "Server error"}}}
|
|
end)
|
|
|
|
assert {:error, {500, _}} = Orders.submit_to_provider(order)
|
|
|
|
updated = Orders.get_order(order.id)
|
|
assert updated.fulfilment_status == "failed"
|
|
assert updated.fulfilment_error =~ "Provider API error (500)"
|
|
end
|
|
end
|
|
|
|
describe "refresh_fulfilment_status/1" do
|
|
setup do
|
|
Application.put_env(:berrypod, :provider_modules, %{
|
|
"printify" => MockProvider
|
|
})
|
|
|
|
on_exit(fn -> Application.delete_env(:berrypod, :provider_modules) end)
|
|
:ok
|
|
end
|
|
|
|
test "updates tracking info from provider" do
|
|
{order, _variant, _product, _conn} = submitted_order_fixture()
|
|
|
|
expect(MockProvider, :get_order_status, fn _conn, pid ->
|
|
assert pid == order.provider_order_id
|
|
|
|
{:ok,
|
|
%{
|
|
status: "shipped",
|
|
provider_status: "shipped",
|
|
tracking_number: "TRACK123",
|
|
tracking_url: "https://track.example.com/TRACK123"
|
|
}}
|
|
end)
|
|
|
|
assert {:ok, updated} = Orders.refresh_fulfilment_status(order)
|
|
assert updated.fulfilment_status == "shipped"
|
|
assert updated.tracking_number == "TRACK123"
|
|
assert updated.tracking_url == "https://track.example.com/TRACK123"
|
|
assert updated.shipped_at != nil
|
|
end
|
|
|
|
test "no-op when no provider_order_id" do
|
|
order = order_fixture(payment_status: "paid")
|
|
assert is_nil(order.provider_order_id)
|
|
|
|
assert {:ok, ^order} = Orders.refresh_fulfilment_status(order)
|
|
end
|
|
end
|
|
|
|
describe "update_fulfilment/2" do
|
|
test "updates fulfilment fields" do
|
|
order = order_fixture()
|
|
|
|
assert {:ok, updated} =
|
|
Orders.update_fulfilment(order, %{
|
|
fulfilment_status: "submitted",
|
|
provider_order_id: "test_123"
|
|
})
|
|
|
|
assert updated.fulfilment_status == "submitted"
|
|
assert updated.provider_order_id == "test_123"
|
|
end
|
|
|
|
test "validates fulfilment status inclusion" do
|
|
order = order_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Orders.update_fulfilment(order, %{fulfilment_status: "bogus"})
|
|
|
|
assert errors_on(changeset).fulfilment_status != []
|
|
end
|
|
end
|
|
|
|
describe "list_submitted_orders/0" do
|
|
test "returns orders with submitted or processing status" do
|
|
{submitted, _v, _p, _c} = submitted_order_fixture()
|
|
|
|
{processing, _v2, _p2, _c2} = submitted_order_fixture()
|
|
|
|
{:ok, processing} =
|
|
Orders.update_fulfilment(processing, %{fulfilment_status: "processing"})
|
|
|
|
_unfulfilled = order_fixture(payment_status: "paid")
|
|
|
|
orders = Orders.list_submitted_orders()
|
|
ids = Enum.map(orders, & &1.id)
|
|
|
|
assert submitted.id in ids
|
|
assert processing.id in ids
|
|
assert length(orders) == 2
|
|
end
|
|
|
|
test "returns empty list when no submitted orders" do
|
|
order_fixture()
|
|
assert Orders.list_submitted_orders() == []
|
|
end
|
|
end
|
|
|
|
describe "get_order_by_number/1" do
|
|
test "finds order by order number" do
|
|
order = order_fixture()
|
|
found = Orders.get_order_by_number(order.order_number)
|
|
assert found.id == order.id
|
|
end
|
|
|
|
test "returns nil for unknown order number" do
|
|
assert is_nil(Orders.get_order_by_number("SS-000000-XXXX"))
|
|
end
|
|
end
|
|
|
|
describe "list_orders_by_email/1" do
|
|
test "returns paid orders for the given email" do
|
|
order = order_fixture(%{customer_email: "buyer@example.com", payment_status: "paid"})
|
|
|
|
results = Orders.list_orders_by_email("buyer@example.com")
|
|
ids = Enum.map(results, & &1.id)
|
|
|
|
assert order.id in ids
|
|
end
|
|
|
|
test "excludes pending and failed orders" do
|
|
order_fixture(%{customer_email: "buyer@example.com"})
|
|
order_fixture(%{customer_email: "buyer@example.com", payment_status: "failed"})
|
|
|
|
assert Orders.list_orders_by_email("buyer@example.com") == []
|
|
end
|
|
|
|
test "excludes other customers' orders" do
|
|
order_fixture(%{customer_email: "other@example.com", payment_status: "paid"})
|
|
|
|
assert Orders.list_orders_by_email("buyer@example.com") == []
|
|
end
|
|
|
|
test "is case-insensitive" do
|
|
order = order_fixture(%{customer_email: "Buyer@Example.COM", payment_status: "paid"})
|
|
|
|
results = Orders.list_orders_by_email("buyer@example.com")
|
|
ids = Enum.map(results, & &1.id)
|
|
|
|
assert order.id in ids
|
|
end
|
|
|
|
test "preloads items" do
|
|
order_fixture(%{customer_email: "buyer@example.com", payment_status: "paid"})
|
|
|
|
[order] = Orders.list_orders_by_email("buyer@example.com")
|
|
assert Ecto.assoc_loaded?(order.items)
|
|
end
|
|
|
|
test "returns newest first" do
|
|
old = order_fixture(%{customer_email: "buyer@example.com", payment_status: "paid"})
|
|
new = order_fixture(%{customer_email: "buyer@example.com", payment_status: "paid"})
|
|
|
|
# Force different inserted_at by updating order records
|
|
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
|
earlier = DateTime.add(now, -60, :second)
|
|
|
|
Berrypod.Repo.update_all(
|
|
from(o in Berrypod.Orders.Order, where: o.id == ^old.id),
|
|
set: [inserted_at: earlier]
|
|
)
|
|
|
|
[first, second] = Orders.list_orders_by_email("buyer@example.com")
|
|
assert first.id == new.id
|
|
assert second.id == old.id
|
|
end
|
|
end
|
|
end
|