berrypod/test/simpleshop_theme/providers/printful_test.exs

484 lines
15 KiB
Elixir
Raw Normal View History

defmodule SimpleshopTheme.Providers.PrintfulTest do
use SimpleshopTheme.DataCase, async: true
alias SimpleshopTheme.Providers.Printful
describe "provider_type/0" do
test "returns printful" do
assert Printful.provider_type() == "printful"
end
end
describe "test_connection/1" do
test "returns error when no API key" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil
}
assert {:error, :no_api_key} = Printful.test_connection(conn)
end
end
describe "fetch_products/1" do
test "returns error when no store_id in config" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{}
}
assert {:error, :no_store_id} = Printful.fetch_products(conn)
end
test "returns error when no API key" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{"store_id" => "12345"}
}
assert {:error, :no_api_key} = Printful.fetch_products(conn)
end
end
describe "submit_order/2" do
test "returns error when no store_id in config" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{}
}
assert {:error, :no_store_id} = Printful.submit_order(conn, %{})
end
test "returns error when no API key" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{"store_id" => "12345"}
}
assert {:error, :no_api_key} = Printful.submit_order(conn, %{})
end
end
describe "get_order_status/2" do
test "returns error when no store_id in config" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{}
}
assert {:error, :no_store_id} = Printful.get_order_status(conn, "12345")
end
test "returns error when no API key" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{"store_id" => "12345"}
}
assert {:error, :no_api_key} = Printful.get_order_status(conn, "12345")
end
end
describe "fetch_shipping_rates/2" do
test "returns error when no API key" do
conn = %SimpleshopTheme.Products.ProviderConnection{
provider_type: "printful",
api_key_encrypted: nil,
config: %{"store_id" => "12345"}
}
assert {:error, :no_api_key} = Printful.fetch_shipping_rates(conn, [])
end
end
describe "extract_option_types/1" do
test "extracts color and size options" do
provider_data = %{
"options" => [
%{
"name" => "Color",
"type" => "color",
"values" => [
%{"title" => "Black"},
%{"title" => "Natural", "hex" => "#F5F5DC"}
]
},
%{
"name" => "Size",
"type" => "size",
"values" => [
%{"title" => "S"},
%{"title" => "M"},
%{"title" => "L"}
]
}
]
}
result = Printful.extract_option_types(provider_data)
assert length(result) == 2
[color_opt, size_opt] = result
assert color_opt.name == "Color"
assert color_opt.type == :color
assert length(color_opt.values) == 2
assert hd(color_opt.values).title == "Black"
assert Enum.at(color_opt.values, 1).hex == "#F5F5DC"
assert size_opt.name == "Size"
assert size_opt.type == :size
assert length(size_opt.values) == 3
end
test "returns empty list for nil provider_data" do
assert Printful.extract_option_types(nil) == []
end
test "returns empty list for provider_data without options" do
assert Printful.extract_option_types(%{}) == []
end
end
describe "product normalization" do
test "normalizes sync product response correctly" do
{sync_product, sync_variants} = printful_sync_product_response()
normalized = normalize_product(sync_product, sync_variants)
assert normalized.provider_product_id == "456789"
assert normalized.title == "PC Man T-Shirt"
assert normalized.description == ""
assert normalized.category == "Apparel"
# Images — one per unique colour from preview files
assert length(normalized.images) == 2
[img1, img2] = normalized.images
assert img1.position == 0
assert img2.position == 1
assert img1.alt == "Black"
assert img2.alt == "Natural"
# Variants
assert length(normalized.variants) == 4
[v1 | _] = normalized.variants
assert v1.provider_variant_id == "5001"
assert v1.title == "Black / S"
assert v1.price == 1350
assert v1.sku == "PCM-BK-S"
assert v1.is_enabled == true
assert v1.is_available == true
assert v1.options == %{"Color" => "Black", "Size" => "S"}
# Provider data
assert normalized.provider_data.catalog_product_id == 71
assert is_list(normalized.provider_data.options)
assert length(normalized.provider_data.catalog_variant_ids) == 4
end
test "handles variant with missing colour or size" do
sync_product = %{"id" => 1, "name" => "Test", "thumbnail_url" => nil}
sync_variants = [
%{
"id" => 100,
"color" => nil,
"size" => "M",
"retail_price" => "10.00",
"sku" => "T-M",
"synced" => true,
"availability_status" => "active",
"files" => [],
"product" => %{"product_id" => 1, "name" => "Test"},
"variant_id" => 200
}
]
normalized = normalize_product(sync_product, sync_variants)
assert length(normalized.variants) == 1
[v] = normalized.variants
assert v.title == "M"
assert v.options == %{"Size" => "M"}
end
test "parses price strings correctly" do
assert parse_price("13.50") == 1350
assert parse_price("0.99") == 99
assert parse_price("100.00") == 10000
assert parse_price(nil) == 0
assert parse_price(13.5) == 1350
end
end
describe "order status mapping" do
test "maps Printful statuses to internal statuses" do
assert map_order_status("draft") == "submitted"
assert map_order_status("pending") == "submitted"
assert map_order_status("inprocess") == "processing"
assert map_order_status("fulfilled") == "shipped"
assert map_order_status("shipped") == "shipped"
assert map_order_status("delivered") == "delivered"
assert map_order_status("canceled") == "cancelled"
assert map_order_status("failed") == "submitted"
assert map_order_status("onhold") == "submitted"
assert map_order_status("unknown_status") == "submitted"
end
end
# =============================================================================
# Test fixtures — replicate Printful API responses
# =============================================================================
defp printful_sync_product_response do
sync_product = %{
"id" => 456_789,
"name" => "PC Man T-Shirt",
"thumbnail_url" => "https://files.cdn.printful.com/thumb.png",
"synced" => 4
}
sync_variants = [
%{
"id" => 5001,
"color" => "Black",
"size" => "S",
"retail_price" => "13.50",
"sku" => "PCM-BK-S",
"synced" => true,
"availability_status" => "active",
"variant_id" => 4011,
"product" => %{
"product_id" => 71,
"variant_id" => 4011,
"name" => "Bella+Canvas 3001 Unisex Short Sleeve Jersey T-Shirt"
},
"files" => [
%{
"type" => "preview",
"preview_url" => "https://files.cdn.printful.com/preview-black.png",
"thumbnail_url" => "https://files.cdn.printful.com/thumb-black.png"
}
]
},
%{
"id" => 5002,
"color" => "Black",
"size" => "M",
"retail_price" => "13.50",
"sku" => "PCM-BK-M",
"synced" => true,
"availability_status" => "active",
"variant_id" => 4012,
"product" => %{
"product_id" => 71,
"variant_id" => 4012,
"name" => "Bella+Canvas 3001 Unisex Short Sleeve Jersey T-Shirt"
},
"files" => [
%{
"type" => "preview",
"preview_url" => "https://files.cdn.printful.com/preview-black.png",
"thumbnail_url" => "https://files.cdn.printful.com/thumb-black.png"
}
]
},
%{
"id" => 5003,
"color" => "Natural",
"size" => "S",
"retail_price" => "13.50",
"sku" => "PCM-NT-S",
"synced" => true,
"availability_status" => "active",
"variant_id" => 4013,
"product" => %{
"product_id" => 71,
"variant_id" => 4013,
"name" => "Bella+Canvas 3001 Unisex Short Sleeve Jersey T-Shirt"
},
"files" => [
%{
"type" => "preview",
"preview_url" => "https://files.cdn.printful.com/preview-natural.png",
"thumbnail_url" => "https://files.cdn.printful.com/thumb-natural.png"
}
]
},
%{
"id" => 5004,
"color" => "Natural",
"size" => "M",
"retail_price" => "13.50",
"sku" => "PCM-NT-M",
"synced" => true,
"availability_status" => "active",
"variant_id" => 4014,
"product" => %{
"product_id" => 71,
"variant_id" => 4014,
"name" => "Bella+Canvas 3001 Unisex Short Sleeve Jersey T-Shirt"
},
"files" => [
%{
"type" => "preview",
"preview_url" => "https://files.cdn.printful.com/preview-natural.png",
"thumbnail_url" => "https://files.cdn.printful.com/thumb-natural.png"
}
]
}
]
{sync_product, sync_variants}
end
# =============================================================================
# Test helpers — replicate private normalization functions
# =============================================================================
defp normalize_product(sync_product, sync_variants) do
images = extract_preview_images(sync_variants)
catalog_product_id = extract_catalog_product_id(sync_variants)
catalog_variant_ids = Enum.map(sync_variants, & &1["variant_id"]) |> Enum.reject(&is_nil/1)
%{
provider_product_id: to_string(sync_product["id"]),
title: sync_product["name"],
description: "",
category: extract_category(sync_variants),
images: images,
variants: Enum.map(sync_variants, &normalize_variant/1),
provider_data: %{
catalog_product_id: catalog_product_id,
catalog_variant_ids: catalog_variant_ids,
thumbnail_url: sync_product["thumbnail_url"],
options: build_option_types(sync_variants),
raw: %{sync_product: sync_product}
}
}
end
defp normalize_variant(sv) do
%{
provider_variant_id: to_string(sv["id"]),
title: build_variant_title(sv),
sku: sv["sku"],
price: parse_price(sv["retail_price"]),
cost: nil,
options: build_variant_options(sv),
is_enabled: sv["synced"] == true,
is_available: sv["availability_status"] == "active"
}
end
defp build_variant_title(sv) do
[sv["color"], sv["size"]] |> Enum.reject(&is_nil/1) |> Enum.join(" / ")
end
defp build_variant_options(sv) do
opts = %{}
opts = if sv["color"], do: Map.put(opts, "Color", sv["color"]), else: opts
if sv["size"], do: Map.put(opts, "Size", sv["size"]), else: opts
end
defp extract_preview_images(sync_variants) do
sync_variants
|> Enum.flat_map(fn sv ->
(sv["files"] || [])
|> Enum.filter(&(&1["type"] == "preview"))
|> Enum.map(fn file ->
%{src: file["preview_url"] || file["thumbnail_url"], color: sv["color"]}
end)
end)
|> Enum.uniq_by(& &1.color)
|> Enum.with_index()
|> Enum.map(fn {img, index} ->
%{src: img.src, position: index, alt: img.color}
end)
end
defp extract_catalog_product_id(sync_variants) do
Enum.find_value(sync_variants, 0, fn sv -> get_in(sv, ["product", "product_id"]) end)
end
defp build_option_types(sync_variants) do
colors =
sync_variants
|> Enum.map(& &1["color"])
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> Enum.map(fn c -> %{"title" => c} end)
sizes =
sync_variants
|> Enum.map(& &1["size"])
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> Enum.map(fn s -> %{"title" => s} end)
opts = []
opts =
if colors != [],
do: opts ++ [%{"name" => "Color", "type" => "color", "values" => colors}],
else: opts
if sizes != [],
do: opts ++ [%{"name" => "Size", "type" => "size", "values" => sizes}],
else: opts
end
defp extract_category(sync_variants) do
case sync_variants do
[sv | _] ->
product_name = get_in(sv, ["product", "name"]) || ""
categorize_from_name(product_name)
[] ->
nil
end
end
defp categorize_from_name(name) do
name_lower = String.downcase(name)
cond do
has_keyword?(name_lower, ~w[t-shirt tshirt shirt hoodie sweatshirt jogger]) -> "Apparel"
has_keyword?(name_lower, ~w[canvas poster print frame]) -> "Canvas Prints"
has_keyword?(name_lower, ~w[mug cup blanket pillow cushion]) -> "Homewares"
has_keyword?(name_lower, ~w[notebook journal]) -> "Stationery"
has_keyword?(name_lower, ~w[phone case bag tote hat cap]) -> "Accessories"
true -> "Apparel"
end
end
defp has_keyword?(text, keywords), do: Enum.any?(keywords, &String.contains?(text, &1))
defp parse_price(price) when is_binary(price) do
case Float.parse(price) do
{float, _} -> round(float * 100)
:error -> 0
end
end
defp parse_price(price) when is_number(price), do: round(price * 100)
defp parse_price(_), do: 0
defp map_order_status("draft"), do: "submitted"
defp map_order_status("pending"), do: "submitted"
defp map_order_status("inprocess"), do: "processing"
defp map_order_status("fulfilled"), do: "shipped"
defp map_order_status("shipped"), do: "shipped"
defp map_order_status("delivered"), do: "delivered"
defp map_order_status("canceled"), do: "cancelled"
defp map_order_status("failed"), do: "submitted"
defp map_order_status("onhold"), do: "submitted"
defp map_order_status(_), do: "submitted"
end