feat: add Products context with provider integration (Phase 1)
Implement the schema foundation for syncing products from POD providers like Printify. This includes encrypted credential storage, product/variant schemas, and an Oban worker for background sync. New modules: - Vault: AES-256-GCM encryption for API keys - Products context: CRUD and sync operations for products - Provider behaviour: abstraction for POD provider implementations - ProductSyncWorker: Oban job for async product sync Schemas: ProviderConnection, Product, ProductImage, ProductVariant Also reorganizes Printify client to lib/simpleshop_theme/clients/ and mockup generator to lib/simpleshop_theme/mockups/ for better structure. 134 tests added covering all new functionality. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
250
test/support/fixtures/products_fixtures.ex
Normal file
250
test/support/fixtures/products_fixtures.ex
Normal file
@@ -0,0 +1,250 @@
|
||||
defmodule SimpleshopTheme.ProductsFixtures do
|
||||
@moduledoc """
|
||||
Test helpers for creating entities via the `SimpleshopTheme.Products` context.
|
||||
"""
|
||||
|
||||
alias SimpleshopTheme.Products
|
||||
|
||||
def unique_provider_product_id, do: "prov_#{System.unique_integer([:positive])}"
|
||||
def unique_slug, do: "product-#{System.unique_integer([:positive])}"
|
||||
def unique_variant_id, do: "var_#{System.unique_integer([:positive])}"
|
||||
|
||||
# Provider types to cycle through for unique constraint
|
||||
@provider_types ["printify", "gelato", "prodigi", "printful"]
|
||||
|
||||
@doc """
|
||||
Returns valid attributes for a provider connection.
|
||||
Uses a unique provider type to avoid constraint violations.
|
||||
"""
|
||||
def valid_provider_connection_attrs(attrs \\ %{}) do
|
||||
# Get a unique provider type if not specified
|
||||
provider_type = attrs[:provider_type] || unique_provider_type()
|
||||
|
||||
Enum.into(attrs, %{
|
||||
provider_type: provider_type,
|
||||
name: "Test #{String.capitalize(provider_type)} Connection",
|
||||
enabled: true,
|
||||
api_key: "test_api_key_#{System.unique_integer([:positive])}",
|
||||
config: %{"shop_id" => "12345"}
|
||||
})
|
||||
end
|
||||
|
||||
defp unique_provider_type do
|
||||
# Use modulo to cycle through provider types
|
||||
idx = rem(System.unique_integer([:positive]), length(@provider_types))
|
||||
Enum.at(@provider_types, idx)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a provider connection fixture.
|
||||
|
||||
Since provider_type has a unique constraint, this will reuse an existing
|
||||
connection of the same type if one exists.
|
||||
"""
|
||||
def provider_connection_fixture(attrs \\ %{}) do
|
||||
provider_type = attrs[:provider_type] || unique_provider_type()
|
||||
|
||||
# Try to find existing connection of this type first
|
||||
case Products.get_provider_connection_by_type(provider_type) do
|
||||
nil ->
|
||||
{:ok, conn} =
|
||||
attrs
|
||||
|> Map.put(:provider_type, provider_type)
|
||||
|> valid_provider_connection_attrs()
|
||||
|> Products.create_provider_connection()
|
||||
|
||||
conn
|
||||
|
||||
existing ->
|
||||
# Return existing connection (update if attrs differ)
|
||||
if map_size(Map.delete(attrs, :provider_type)) > 0 do
|
||||
{:ok, updated} = Products.update_provider_connection(existing, attrs)
|
||||
updated
|
||||
else
|
||||
existing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns valid attributes for a product.
|
||||
"""
|
||||
def valid_product_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
provider_product_id: unique_provider_product_id(),
|
||||
title: "Test Product",
|
||||
description: "A test product description",
|
||||
slug: unique_slug(),
|
||||
status: "active",
|
||||
visible: true,
|
||||
category: "Apparel",
|
||||
provider_data: %{"blueprint_id" => 145, "print_provider_id" => 29}
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a product fixture.
|
||||
Automatically creates a provider connection if not provided.
|
||||
"""
|
||||
def product_fixture(attrs \\ %{}) do
|
||||
conn = attrs[:provider_connection] || provider_connection_fixture()
|
||||
|
||||
{:ok, product} =
|
||||
attrs
|
||||
|> Map.delete(:provider_connection)
|
||||
|> valid_product_attrs()
|
||||
|> Map.put(:provider_connection_id, conn.id)
|
||||
|> Products.create_product()
|
||||
|
||||
product
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns valid attributes for a product image.
|
||||
"""
|
||||
def valid_product_image_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
src: "https://example.com/image-#{System.unique_integer([:positive])}.jpg",
|
||||
position: 0,
|
||||
alt: "Product image"
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a product image fixture.
|
||||
"""
|
||||
def product_image_fixture(attrs \\ %{}) do
|
||||
product = attrs[:product] || product_fixture()
|
||||
|
||||
{:ok, image} =
|
||||
attrs
|
||||
|> Map.delete(:product)
|
||||
|> valid_product_image_attrs()
|
||||
|> Map.put(:product_id, product.id)
|
||||
|> Products.create_product_image()
|
||||
|
||||
image
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns valid attributes for a product variant.
|
||||
"""
|
||||
def valid_product_variant_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
provider_variant_id: unique_variant_id(),
|
||||
title: "Medium / Black",
|
||||
sku: "TEST-SKU-#{System.unique_integer([:positive])}",
|
||||
price: 2500,
|
||||
compare_at_price: nil,
|
||||
cost: 1200,
|
||||
options: %{"Size" => "Medium", "Color" => "Black"},
|
||||
is_enabled: true,
|
||||
is_available: true
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a product variant fixture.
|
||||
"""
|
||||
def product_variant_fixture(attrs \\ %{}) do
|
||||
product = attrs[:product] || product_fixture()
|
||||
|
||||
{:ok, variant} =
|
||||
attrs
|
||||
|> Map.delete(:product)
|
||||
|> valid_product_variant_attrs()
|
||||
|> Map.put(:product_id, product.id)
|
||||
|> Products.create_product_variant()
|
||||
|
||||
variant
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a complete product fixture with images and variants.
|
||||
"""
|
||||
def complete_product_fixture(attrs \\ %{}) do
|
||||
conn = attrs[:provider_connection] || provider_connection_fixture()
|
||||
product = product_fixture(Map.put(attrs, :provider_connection, conn))
|
||||
|
||||
# Create images
|
||||
for i <- 0..1 do
|
||||
product_image_fixture(%{
|
||||
product: product,
|
||||
position: i,
|
||||
src: "https://example.com/#{product.slug}-#{i}.jpg"
|
||||
})
|
||||
end
|
||||
|
||||
# Create variants
|
||||
for size <- ["Small", "Medium", "Large"], color <- ["Black", "White"] do
|
||||
product_variant_fixture(%{
|
||||
product: product,
|
||||
title: "#{size} / #{color}",
|
||||
options: %{"Size" => size, "Color" => color},
|
||||
price: if(size == "Large", do: 2800, else: 2500)
|
||||
})
|
||||
end
|
||||
|
||||
# Return product with preloaded associations
|
||||
SimpleshopTheme.Repo.preload(product, [:images, :variants])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a sample Printify product API response for testing normalization.
|
||||
"""
|
||||
def printify_product_response do
|
||||
%{
|
||||
"id" => "12345",
|
||||
"title" => "Classic T-Shirt",
|
||||
"description" => "A comfortable cotton t-shirt",
|
||||
"blueprint_id" => 145,
|
||||
"print_provider_id" => 29,
|
||||
"tags" => ["apparel", "clothing"],
|
||||
"options" => [
|
||||
%{
|
||||
"name" => "Colors",
|
||||
"type" => "color",
|
||||
"values" => [
|
||||
%{"id" => 751, "title" => "Solid White"},
|
||||
%{"id" => 752, "title" => "Black"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"name" => "Sizes",
|
||||
"type" => "size",
|
||||
"values" => [
|
||||
%{"id" => 2, "title" => "S"},
|
||||
%{"id" => 3, "title" => "M"},
|
||||
%{"id" => 4, "title" => "L"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variants" => [
|
||||
%{
|
||||
"id" => 100,
|
||||
"title" => "Solid White / S",
|
||||
"sku" => "TSH-WH-S",
|
||||
"price" => 2500,
|
||||
"cost" => 1200,
|
||||
"options" => [751, 2],
|
||||
"is_enabled" => true,
|
||||
"is_available" => true
|
||||
},
|
||||
%{
|
||||
"id" => 101,
|
||||
"title" => "Black / M",
|
||||
"sku" => "TSH-BK-M",
|
||||
"price" => 2500,
|
||||
"cost" => 1200,
|
||||
"options" => [752, 3],
|
||||
"is_enabled" => true,
|
||||
"is_available" => true
|
||||
}
|
||||
],
|
||||
"images" => [
|
||||
%{"src" => "https://printify.com/img1.jpg", "position" => 0},
|
||||
%{"src" => "https://printify.com/img2.jpg", "position" => 1}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user