simpleshop_theme/test/simpleshop_theme/orders_test.exs
jamey 3e19887499 feat: add Printify order submission and fulfilment tracking
Submit paid orders to Printify via provider API with idempotent
guards, Stripe address mapping, and error handling. Track fulfilment
status through submitted → processing → shipped → delivered via
webhook-driven updates (primary) and Oban Cron polling fallback.

- 9 fulfilment fields on orders (status, provider IDs, tracking, timestamps)
- OrderSubmissionWorker with retry logic, auto-enqueued after Stripe payment
- FulfilmentStatusWorker polls every 30 mins for missed webhook events
- Printify order webhook handlers (sent-to-production, shipment, delivered)
- Admin UI: fulfilment column in table, fulfilment card with tracking info,
  submit/retry and refresh buttons on order detail
- Mox provider mocking for test isolation (Provider.for_type configurable)
- 33 new tests (555 total), verified against real Printify API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 09:51:51 +00:00

220 lines
6.3 KiB
Elixir

defmodule SimpleshopTheme.OrdersTest do
use SimpleshopTheme.DataCase, async: false
import Mox
import SimpleshopTheme.OrdersFixtures
alias SimpleshopTheme.Orders
alias SimpleshopTheme.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(:simpleshop_theme, :provider_modules, %{
"printify" => MockProvider
})
on_exit(fn -> Application.delete_env(:simpleshop_theme, :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(:simpleshop_theme, :provider_modules, %{
"printify" => MockProvider
})
on_exit(fn -> Application.delete_env(:simpleshop_theme, :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
end