feat: add Printify webhook endpoint for real-time product updates

- Add /webhooks/printify endpoint with HMAC-SHA256 signature verification
- Add Webhooks context to handle product:updated, product:deleted events
- Add ProductDeleteWorker for async product deletion
- Add webhook API methods to Printify client (create, list, delete)
- Add register_webhooks/2 to Printify provider
- Add mix register_webhooks task for one-time webhook registration
- Cache raw request body in endpoint for signature verification

Usage:
1. Generate webhook secret: openssl rand -hex 20
2. Add to provider connection config as "webhook_secret"
3. Register with Printify: mix register_webhooks https://yourshop.com/webhooks/printify

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-01-31 22:41:15 +00:00
parent a2157177b8
commit a9c15ea6ae
13 changed files with 596 additions and 4 deletions

View File

@@ -0,0 +1,48 @@
defmodule SimpleshopTheme.Webhooks.ProductDeleteWorkerTest do
use SimpleshopTheme.DataCase
use Oban.Testing, repo: SimpleshopTheme.Repo
alias SimpleshopTheme.Webhooks.ProductDeleteWorker
alias SimpleshopTheme.Products
import SimpleshopTheme.ProductsFixtures
describe "perform/1" do
test "deletes product when found" do
conn = provider_connection_fixture(%{provider_type: "printify"})
{:ok, product, :created} =
Products.upsert_product(conn, %{
provider_product_id: "test-product-123",
title: "Test Product",
provider_data: %{}
})
assert Products.get_product(product.id) != nil
assert :ok =
perform_job(ProductDeleteWorker, %{
provider_product_id: "test-product-123"
})
assert Products.get_product(product.id) == nil
end
test "returns ok when product not found" do
_conn = provider_connection_fixture(%{provider_type: "printify"})
assert :ok =
perform_job(ProductDeleteWorker, %{
provider_product_id: "nonexistent-product"
})
end
test "cancels when no provider connection" do
# No connection created
assert {:cancel, :no_connection} =
perform_job(ProductDeleteWorker, %{
provider_product_id: "test-product-123"
})
end
end
end

View File

@@ -0,0 +1,65 @@
defmodule SimpleshopTheme.WebhooksTest do
use SimpleshopTheme.DataCase
alias SimpleshopTheme.Webhooks
import SimpleshopTheme.ProductsFixtures
setup do
conn = provider_connection_fixture(%{provider_type: "printify"})
{:ok, provider_connection: conn}
end
describe "handle_printify_event/2" do
test "product:updated triggers sync", %{provider_connection: _conn} do
# With inline Oban, the job executes immediately (and fails due to no real API key)
# But the handler should still return {:ok, _} after inserting the job
result =
Webhooks.handle_printify_event(
"product:updated",
%{"id" => "123", "shop_id" => "456"}
)
assert {:ok, %Oban.Job{}} = result
end
test "product:publish:started triggers sync", %{provider_connection: _conn} do
result =
Webhooks.handle_printify_event(
"product:publish:started",
%{"id" => "123"}
)
assert {:ok, %Oban.Job{}} = result
end
test "product:deleted triggers delete", %{provider_connection: _conn} do
result =
Webhooks.handle_printify_event(
"product:deleted",
%{"id" => "123"}
)
assert {:ok, %Oban.Job{}} = result
end
test "shop:disconnected returns ok" do
assert :ok = Webhooks.handle_printify_event("shop:disconnected", %{})
end
test "unknown event returns ok" do
assert :ok = Webhooks.handle_printify_event("unknown:event", %{})
end
test "returns error when no provider connection" do
# Delete all connections first
SimpleshopTheme.Repo.delete_all(SimpleshopTheme.Products.ProviderConnection)
assert {:error, :no_connection} =
Webhooks.handle_printify_event(
"product:updated",
%{"id" => "123"}
)
end
end
end