berrypod/lib/simpleshop_theme/orders/order.ex
jamey 3e19887499 feat: add Printify order submission and fulfilment tracking
Submit paid orders to Printify via provider API with idempotent
guards, Stripe address mapping, and error handling. Track fulfilment
status through submitted → processing → shipped → delivered via
webhook-driven updates (primary) and Oban Cron polling fallback.

- 9 fulfilment fields on orders (status, provider IDs, tracking, timestamps)
- OrderSubmissionWorker with retry logic, auto-enqueued after Stripe payment
- FulfilmentStatusWorker polls every 30 mins for missed webhook events
- Printify order webhook handlers (sent-to-production, shipment, delivered)
- Admin UI: fulfilment column in table, fulfilment card with tracking info,
  submit/retry and refresh buttons on order detail
- Mox provider mocking for test isolation (Provider.for_type configurable)
- 33 new tests (555 total), verified against real Printify API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 09:51:51 +00:00

79 lines
2.2 KiB
Elixir

defmodule SimpleshopTheme.Orders.Order do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@payment_statuses ~w(pending paid failed refunded)
@fulfilment_statuses ~w(unfulfilled submitted processing shipped delivered failed cancelled)
def fulfilment_statuses, do: @fulfilment_statuses
schema "orders" do
field :order_number, :string
field :stripe_session_id, :string
field :stripe_payment_intent_id, :string
field :payment_status, :string, default: "pending"
field :customer_email, :string
field :shipping_address, :map, default: %{}
field :subtotal, :integer
field :total, :integer
field :currency, :string, default: "gbp"
field :metadata, :map, default: %{}
# Fulfilment
field :fulfilment_status, :string, default: "unfulfilled"
field :provider_order_id, :string
field :provider_status, :string
field :fulfilment_error, :string
field :tracking_number, :string
field :tracking_url, :string
field :submitted_at, :utc_datetime
field :shipped_at, :utc_datetime
field :delivered_at, :utc_datetime
has_many :items, SimpleshopTheme.Orders.OrderItem
timestamps(type: :utc_datetime)
end
def changeset(order, attrs) do
order
|> cast(attrs, [
:order_number,
:stripe_session_id,
:stripe_payment_intent_id,
:payment_status,
:customer_email,
:shipping_address,
:subtotal,
:total,
:currency,
:metadata
])
|> validate_required([:order_number, :subtotal, :total, :currency])
|> validate_inclusion(:payment_status, @payment_statuses)
|> validate_number(:subtotal, greater_than_or_equal_to: 0)
|> validate_number(:total, greater_than_or_equal_to: 0)
|> unique_constraint(:order_number)
|> unique_constraint(:stripe_session_id)
end
def fulfilment_changeset(order, attrs) do
order
|> cast(attrs, [
:fulfilment_status,
:provider_order_id,
:provider_status,
:fulfilment_error,
:tracking_number,
:tracking_url,
:submitted_at,
:shipped_at,
:delivered_at
])
|> validate_inclusion(:fulfilment_status, @fulfilment_statuses)
end
end