berrypod/test/simpleshop_theme/shipping_test.exs
jamey 5c2f70ce44 add shipping costs with live exchange rates and country detection
Shipping rates fetched from Printify during product sync, converted to
GBP at sync time using frankfurter.app ECB exchange rates with 5%
buffer. Cached in shipping_rates table per blueprint/provider/country.

Cart page shows shipping estimate with country selector (detected from
Accept-Language header, persisted in cookie). Stripe Checkout includes
shipping_options for UK domestic and international delivery. Order
shipping_cost extracted from Stripe on payment.

ScheduledSyncWorker runs every 6 hours via Oban cron to keep rates
and exchange rates fresh. REST_OF_THE_WORLD fallback covers unlisted
countries. 780 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:48:00 +00:00

311 lines
8.3 KiB
Elixir

defmodule SimpleshopTheme.ShippingTest do
use SimpleshopTheme.DataCase, async: false
alias SimpleshopTheme.Shipping
alias SimpleshopTheme.Shipping.ShippingRate
import SimpleshopTheme.ProductsFixtures
describe "upsert_rates/2" do
test "inserts new rates" do
conn = provider_connection_fixture()
rates = [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
},
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "US",
first_item_cost: 650,
additional_item_cost: 150,
currency: "USD"
}
]
assert {:ok, 2} = Shipping.upsert_rates(conn.id, rates)
assert Repo.aggregate(ShippingRate, :count) == 2
end
test "replaces existing rates on conflict" do
conn = provider_connection_fixture()
rates = [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
}
]
{:ok, 1} = Shipping.upsert_rates(conn.id, rates)
updated_rates = [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 550,
additional_item_cost: 200,
currency: "GBP"
}
]
{:ok, 1} = Shipping.upsert_rates(conn.id, updated_rates)
assert Repo.aggregate(ShippingRate, :count) == 1
rate = Repo.one(ShippingRate)
assert rate.first_item_cost == 550
assert rate.additional_item_cost == 200
end
end
describe "get_rate/3" do
test "returns rate when it exists" do
conn = provider_connection_fixture()
{:ok, _} =
Shipping.upsert_rates(conn.id, [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
}
])
rate = Shipping.get_rate(100, 200, "GB")
assert rate.first_item_cost == 450
assert rate.country_code == "GB"
end
test "returns nil when no rate exists" do
assert Shipping.get_rate(999, 999, "XX") == nil
end
end
describe "calculate_for_cart/2" do
test "returns {:ok, 0} for empty cart" do
assert {:ok, 0} = Shipping.calculate_for_cart([], "GB")
end
test "calculates shipping for single item" do
conn = provider_connection_fixture()
product =
product_fixture(%{
provider_connection: conn,
provider_data: %{"blueprint_id" => 100, "print_provider_id" => 200}
})
variant = product_variant_fixture(%{product: product})
{:ok, _} =
Shipping.upsert_rates(conn.id, [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
}
])
cart_items = [%{variant_id: variant.id, quantity: 1}]
assert {:ok, 450} = Shipping.calculate_for_cart(cart_items, "GB")
end
test "calculates shipping for multiple quantities" do
conn = provider_connection_fixture()
product =
product_fixture(%{
provider_connection: conn,
provider_data: %{"blueprint_id" => 100, "print_provider_id" => 200}
})
variant = product_variant_fixture(%{product: product})
{:ok, _} =
Shipping.upsert_rates(conn.id, [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
}
])
cart_items = [%{variant_id: variant.id, quantity: 3}]
# first_item_cost + 2 * additional_item_cost = 450 + 200 = 650
assert {:ok, 650} = Shipping.calculate_for_cart(cart_items, "GB")
end
test "returns error when no rates found" do
conn = provider_connection_fixture()
product =
product_fixture(%{
provider_connection: conn,
provider_data: %{"blueprint_id" => 100, "print_provider_id" => 200}
})
variant = product_variant_fixture(%{product: product})
cart_items = [%{variant_id: variant.id, quantity: 1}]
assert {:error, :rates_not_found} = Shipping.calculate_for_cart(cart_items, "XX")
end
test "groups items by print provider" do
conn = provider_connection_fixture()
product1 =
product_fixture(%{
provider_connection: conn,
provider_data: %{"blueprint_id" => 100, "print_provider_id" => 200}
})
product2 =
product_fixture(%{
provider_connection: conn,
provider_data: %{"blueprint_id" => 101, "print_provider_id" => 200}
})
variant1 = product_variant_fixture(%{product: product1})
variant2 = product_variant_fixture(%{product: product2})
{:ok, _} =
Shipping.upsert_rates(conn.id, [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
},
%{
blueprint_id: 101,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 500,
additional_item_cost: 150,
currency: "GBP"
}
])
cart_items = [
%{variant_id: variant1.id, quantity: 1},
%{variant_id: variant2.id, quantity: 1}
]
# Same provider: highest first_item_cost (500) + 1 additional item cost
assert {:ok, cost} = Shipping.calculate_for_cart(cart_items, "GB")
# 500 (highest first item) + one additional item cost (100 or 150)
assert cost in [600, 650]
end
end
describe "upsert_rates/3 with exchange rates" do
test "converts rates to GBP at sync time with buffer" do
conn = provider_connection_fixture()
exchange_rates = %{"USD" => 0.80}
rates = [
%{
blueprint_id: 100,
print_provider_id: 1,
country_code: "US",
first_item_cost: 1000,
additional_item_cost: 500,
currency: "USD"
}
]
{:ok, 1} = Shipping.upsert_rates(conn.id, rates, exchange_rates)
rate = Shipping.get_rate(100, 1, "US")
assert rate.currency == "GBP"
# 1000 USD cents * 0.80 rate * 1.05 buffer, ceil'd
assert rate.first_item_cost == 841
assert rate.additional_item_cost == 421
end
test "leaves GBP rates unconverted" do
conn = provider_connection_fixture()
exchange_rates = %{"USD" => 0.80}
rates = [
%{
blueprint_id: 100,
print_provider_id: 1,
country_code: "GB",
first_item_cost: 500,
additional_item_cost: 200,
currency: "GBP"
}
]
{:ok, 1} = Shipping.upsert_rates(conn.id, rates, exchange_rates)
rate = Shipping.get_rate(100, 1, "GB")
assert rate.currency == "GBP"
assert rate.first_item_cost == 500
assert rate.additional_item_cost == 200
end
end
describe "list_available_countries/0" do
test "returns distinct country codes" do
conn = provider_connection_fixture()
{:ok, _} =
Shipping.upsert_rates(conn.id, [
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 450,
additional_item_cost: 100,
currency: "GBP"
},
%{
blueprint_id: 100,
print_provider_id: 200,
country_code: "US",
first_item_cost: 650,
additional_item_cost: 150,
currency: "USD"
},
%{
blueprint_id: 101,
print_provider_id: 200,
country_code: "GB",
first_item_cost: 500,
additional_item_cost: 100,
currency: "GBP"
}
])
countries = Shipping.list_available_countries()
assert "GB" in countries
assert "US" in countries
assert length(countries) == 2
end
end
end