feat: add Stripe checkout, order persistence, and webhook handling

Stripe-hosted Checkout integration with full order lifecycle:
- stripity_stripe ~> 3.2 with sandbox/prod config via env vars
- Order and OrderItem schemas with price snapshots at purchase time
- CheckoutController creates pending order then redirects to Stripe
- StripeWebhookController verifies signatures and confirms payment
- Success page with real-time PubSub updates from webhook
- Shop flash messages for checkout error feedback
- Cart cleared after successful payment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-07 08:30:17 +00:00
parent cff21703f1
commit ff1bc483b9
19 changed files with 931 additions and 69 deletions

View File

@@ -0,0 +1,48 @@
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)
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: %{}
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
end

View File

@@ -0,0 +1,27 @@
defmodule SimpleshopTheme.Orders.OrderItem do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "order_items" do
field :variant_id, :string
field :product_name, :string
field :variant_title, :string
field :quantity, :integer
field :unit_price, :integer
belongs_to :order, SimpleshopTheme.Orders.Order
timestamps(type: :utc_datetime)
end
def changeset(item, attrs) do
item
|> cast(attrs, [:variant_id, :product_name, :variant_title, :quantity, :unit_price, :order_id])
|> validate_required([:variant_id, :product_name, :quantity, :unit_price])
|> validate_number(:quantity, greater_than: 0)
|> validate_number(:unit_price, greater_than_or_equal_to: 0)
end
end