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>
220 lines
6.3 KiB
Elixir
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
|