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:
2026-01-29 08:32:24 +00:00
parent 62faf86abe
commit c5c06d9979
29 changed files with 4162 additions and 8 deletions

View File

@@ -0,0 +1,69 @@
defmodule SimpleshopTheme.Sync.ProductSyncWorkerTest do
use SimpleshopTheme.DataCase, async: false
use Oban.Testing, repo: SimpleshopTheme.Repo
alias SimpleshopTheme.Sync.ProductSyncWorker
import SimpleshopTheme.ProductsFixtures
describe "perform/1" do
test "cancels for missing connection" do
fake_id = Ecto.UUID.generate()
assert {:cancel, :connection_not_found} =
perform_job(ProductSyncWorker, %{provider_connection_id: fake_id})
end
test "cancels for disabled connection" do
conn = provider_connection_fixture(%{enabled: false})
assert {:cancel, :connection_disabled} =
perform_job(ProductSyncWorker, %{provider_connection_id: conn.id})
end
end
describe "enqueue/1" do
test "creates a job with correct args" do
# Temporarily switch to manual mode to avoid inline execution
Oban.Testing.with_testing_mode(:manual, fn ->
conn = provider_connection_fixture()
assert {:ok, %Oban.Job{} = job} = ProductSyncWorker.enqueue(conn.id)
# In manual mode, args use atom keys
assert job.args == %{provider_connection_id: conn.id}
assert job.queue == "sync"
end)
end
end
describe "enqueue/2 with delay" do
test "schedules a job for later" do
Oban.Testing.with_testing_mode(:manual, fn ->
conn = provider_connection_fixture()
assert {:ok, %Oban.Job{} = job} = ProductSyncWorker.enqueue(conn.id, 60)
# In manual mode, args use atom keys
assert job.args == %{provider_connection_id: conn.id}
# Job should be scheduled in the future
assert DateTime.compare(job.scheduled_at, DateTime.utc_now()) == :gt
end)
end
end
describe "job creation" do
test "new/1 creates job changeset with provider_connection_id" do
changeset = ProductSyncWorker.new(%{provider_connection_id: "test-id"})
assert changeset.changes.args == %{provider_connection_id: "test-id"}
assert changeset.changes.queue == "sync"
end
test "new/2 with scheduled_at creates scheduled job" do
future = DateTime.add(DateTime.utc_now(), 60, :second)
changeset = ProductSyncWorker.new(%{provider_connection_id: "test-id"}, scheduled_at: future)
assert changeset.changes.scheduled_at == future
end
end
end