Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
# Pre-existing false positive unrelated to UUID migration
{"lib/modules/entities/entities.ex", :pattern_match},

# UUID FK columns migration - prefix parameter is typed as binary() by Dialyzer
# but nil is a valid runtime value (no prefix configured)
{"lib/phoenix_kit/migrations/uuid_fk_columns.ex", :pattern_match},

# ExUnit.CaseTemplate macro generates calls to internal ExUnit functions
# that Dialyzer cannot resolve (Elixir 1.18+ internal API changes)
{"test/support/conn_case.ex", :unknown_function},
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/import/product_transformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ defmodule PhoenixKit.Modules.Shop.Import.ProductTransformer do
{:error, _changeset} ->
# Unique constraint hit - category was created by concurrent process, fetch it
case Shop.get_category_by_slug_localized(slug, language) do
{:ok, %{id: id}} ->
Logger.info("Category #{slug} already exists (id: #{id}), using existing")
id
{:ok, %{uuid: uuid}} ->
Logger.info("Category #{slug} already exists (uuid: #{uuid}), using existing")
uuid

{:error, :not_found} ->
Logger.warning("Failed to create or find category: #{slug}")
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/import/prom_ua_format.ex
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ defmodule PhoenixKit.Modules.Shop.Import.PromUaFormat do
lang = Translations.default_language()

case Shop.get_category_by_slug_localized(slug, lang) do
{:ok, %{id: id}} ->
id
{:ok, %{uuid: uuid}} ->
uuid

{:error, :not_found} ->
attrs = %{
Expand All @@ -237,7 +237,7 @@ defmodule PhoenixKit.Modules.Shop.Import.PromUaFormat do
case Shop.create_category(attrs) do
{:ok, category} ->
Logger.info("Auto-created Prom.ua category: #{slug} (#{group_name})")
category.id
category.uuid

{:error, changeset} ->
Logger.warning("Failed to create category #{slug}: #{inspect(changeset.errors)}")
Expand Down
3 changes: 2 additions & 1 deletion lib/modules/shop/schemas/cart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ defmodule PhoenixKit.Modules.Shop.Cart do

defp validate_status_transition(changeset, from, to) do
valid_transitions = %{
"active" => ~w(merged converted abandoned expired),
"active" => ~w(converting merged converted abandoned expired),
"converting" => ~w(converted active),
"merged" => [],
"converted" => [],
"abandoned" => ~w(active),
Expand Down
10 changes: 7 additions & 3 deletions lib/modules/shop/shop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2134,9 +2134,13 @@ defmodule PhoenixKit.Modules.Shop do
:ok <- maybe_send_guest_confirmation(user_id) do
{:ok, order}
else
error ->
# Rollback transaction on any error
repo().rollback(error)
{:error, reason} ->
# Rollback transaction on any error, unwrapping the {:error, _} tuple
# so the transaction returns {:error, reason} (not {:error, {:error, reason}})
repo().rollback(reason)

other ->
repo().rollback(other)
end
end)
# unwrap the transaction result
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/web/catalog_category.ex
Original file line number Diff line number Diff line change
Expand Up @@ -576,10 +576,10 @@ defmodule PhoenixKit.Modules.Shop.Web.CatalogCategory do
# Get signed URL for Storage image
defp get_storage_image_url(file_id, variant) do
case Storage.get_file(file_id) do
%{id: id} ->
case Storage.get_file_instance_by_name(id, variant) do
%{uuid: uuid} ->
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
case Storage.get_file_instance_by_name(id, "original") do
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> nil
_instance -> URLSigner.signed_url(file_id, "original")
end
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/web/catalog_product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1143,12 +1143,12 @@ defmodule PhoenixKit.Modules.Shop.Web.CatalogProduct do
defp get_storage_image_url(file_id, variant) do
# Storage.get_file/1 returns %File{} struct or nil (not {:ok, file} tuple)
case Storage.get_file(file_id) do
%{id: id} = _file ->
%{uuid: uuid} = _file ->
# Check if requested variant exists, fall back to original if not
case Storage.get_file_instance_by_name(id, variant) do
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
# Variant doesn't exist - try original
case Storage.get_file_instance_by_name(id, "original") do
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> placeholder_image_url()
_instance -> URLSigner.signed_url(file_id, "original")
end
Expand Down
24 changes: 15 additions & 9 deletions lib/modules/shop/web/checkout_complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,22 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutComplete do

<%!-- Guest Order Email Confirmation Reminder --%>
<%= if @is_guest_order do %>
<div class="card bg-warning/10 border border-warning mb-6">
<div class="card bg-info/10 border border-info mb-6">
<div class="card-body">
<div class="flex items-start gap-4">
<.icon name="hero-envelope" class="w-8 h-8 text-warning flex-shrink-0" />
<.icon name="hero-envelope" class="w-8 h-8 text-info flex-shrink-0" />
<div>
<h3 class="font-semibold text-lg">Please confirm your email</h3>
<h3 class="font-semibold text-lg">Check your inbox</h3>
<p class="text-sm mt-1">
We've sent a confirmation email to <strong>{@order_email}</strong>.
Please click the link in the email to verify your address.
</p>
<p class="text-sm text-base-content/60 mt-2">
Your order will remain in "pending" status until your email is confirmed.
<ol class="text-sm mt-3 space-y-1.5 list-decimal list-inside text-base-content/80">
<li>Open the email titled <strong>"Confirm your account"</strong></li>
<li>Click the confirmation link inside</li>
<li>Your account will be activated and you can track your order</li>
</ol>
<p class="text-xs text-base-content/50 mt-3">
Don't see it? Check your spam or junk folder. The email may take a minute to arrive.
</p>
</div>
</div>
Expand All @@ -142,9 +146,11 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutComplete do
<div class="card-body text-center">
<div class="text-sm text-base-content/60">Order Number</div>
<div class="text-2xl font-mono font-bold">{@order.order_number}</div>
<div class="text-sm text-base-content/60 mt-2">
A confirmation email will be sent to your email address.
</div>
<%= unless @is_guest_order do %>
<div class="text-sm text-base-content/60 mt-2">
A confirmation email will be sent to your email address.
</div>
<% end %>
</div>
</div>

Expand Down
48 changes: 37 additions & 11 deletions lib/modules/shop/web/checkout_page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutPage do
|> assign(:step, assigns.initial_step)
|> assign(:processing, false)
|> assign(:error_message, nil)
|> assign(:email_exists_error, false)
|> assign(:form_errors, %{})
end

Expand Down Expand Up @@ -410,11 +411,8 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutPage do
{:noreply,
socket
|> assign(:processing, false)
|> assign(
:error_message,
"An account with this email already exists. Please log in to continue."
)
|> put_flash(:error, "Email already registered. Please log in.")}
|> assign(:email_exists_error, true)
|> assign(:error_message, nil)}

{:error, _reason} ->
{:noreply,
Expand Down Expand Up @@ -540,15 +538,15 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutPage do
<div class={["step", @step == :review && "step-primary"]}>Review & Confirm</div>
</div>

<%!-- Guest Checkout Warning --%>
<%!-- Guest Checkout Info --%>
<%= if @is_guest do %>
<div class="alert alert-warning mb-6">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div class="alert alert-info mb-6">
<.icon name="hero-envelope" class="w-5 h-5" />
<div>
<div class="font-semibold">Email confirmation required</div>
<div class="font-semibold">Checking out as a guest</div>
<div class="text-sm">
Your order will require email verification. After checkout, you will receive
a confirmation email. Please click the link to verify your email address.
After placing your order, we'll send a confirmation email to verify your address.
Check your inbox and click the link to activate your account and track your order.
</div>
</div>
</div>
Expand Down Expand Up @@ -587,6 +585,7 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutPage do
currency={@currency}
processing={@processing}
error_message={@error_message}
email_exists_error={@email_exists_error}
selected_payment_option={@selected_payment_option}
needs_billing={@needs_billing}
payment_options={@payment_options}
Expand Down Expand Up @@ -1101,6 +1100,33 @@ defmodule PhoenixKit.Modules.Shop.Web.CheckoutPage do
</div>
</div>

<%!-- Email Already Registered --%>
<%= if @email_exists_error do %>
<div class="card bg-warning/10 border border-warning">
<div class="card-body">
<div class="flex items-start gap-4">
<.icon name="hero-user-circle" class="w-8 h-8 text-warning flex-shrink-0" />
<div>
<h3 class="font-semibold text-lg">Account already exists</h3>
<p class="text-sm mt-1">
An account with this email is already registered.
Please log in to complete your order.
</p>
<div class="mt-3">
<.link
navigate={Routes.path("/users/log-in") <> "?return_to=" <> Routes.path("/checkout")}
class="btn btn-primary btn-sm"
>
<.icon name="hero-arrow-right-on-rectangle" class="w-4 h-4 mr-1" />
Log in to continue
</.link>
</div>
</div>
</div>
</div>
</div>
<% end %>

<%!-- Error Message --%>
<%= if @error_message do %>
<div class="alert alert-error">
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/web/product_detail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -664,10 +664,10 @@ defmodule PhoenixKit.Modules.Shop.Web.ProductDetail do

defp get_storage_image_url(file_id, variant) do
case Storage.get_file(file_id) do
%{id: id} ->
case Storage.get_file_instance_by_name(id, variant) do
%{uuid: uuid} ->
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
case Storage.get_file_instance_by_name(id, "original") do
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> nil
_instance -> URLSigner.signed_url(file_id, "original")
end
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/web/products.ex
Original file line number Diff line number Diff line change
Expand Up @@ -756,10 +756,10 @@ defmodule PhoenixKit.Modules.Shop.Web.Products do

defp get_storage_image_url(file_id, variant) do
case Storage.get_file(file_id) do
%{id: id} ->
case Storage.get_file_instance_by_name(id, variant) do
%{uuid: uuid} ->
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
case Storage.get_file_instance_by_name(id, "original") do
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> nil
_instance -> URLSigner.signed_url(file_id, "original")
end
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/shop/web/shop_catalog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -501,10 +501,10 @@ defmodule PhoenixKit.Modules.Shop.Web.ShopCatalog do
# Get signed URL for Storage image
defp get_storage_image_url(file_id, variant) do
case Storage.get_file(file_id) do
%{id: id} ->
case Storage.get_file_instance_by_name(id, variant) do
%{uuid: uuid} ->
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
case Storage.get_file_instance_by_name(id, "original") do
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> nil
_instance -> URLSigner.signed_url(file_id, "original")
end
Expand Down
52 changes: 51 additions & 1 deletion lib/phoenix_kit/migrations/uuid_fk_columns.ex
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,18 @@ defmodule PhoenixKit.Migrations.UUIDFKColumns do
{:phoenix_kit_referral_code_usage, "code_uuid"}
]

# ── Legacy Integer FK Columns to Relax (DROP NOT NULL) ──────────────────
# Integer FK columns where the Ecto schema now exclusively uses UUID FKs.
# Keeping NOT NULL on these causes insert failures when code only writes UUIDs.
# Applied AFTER UUID NOT NULL constraints are set (UUID is the new primary path).

@relax_integer_fks [
{:phoenix_kit_user_role_assignments, "user_id"},
{:phoenix_kit_user_role_assignments, "role_id"},
{:phoenix_kit_user_role_assignments, "assigned_by"},
{:phoenix_kit_role_permissions, "role_id"}
]

# ── FK Constraints for UUID FK columns ──────────────────────────────────
# Only where the integer FK has an explicit DB-level FK constraint.
# ON DELETE behavior matches the integer FK's behavior.
Expand Down Expand Up @@ -414,6 +426,13 @@ defmodule PhoenixKit.Migrations.UUIDFKColumns do
for {table, uuid_fk, ref_table, ref_col, on_delete} <- @fk_constraints do
add_fk_constraint(table, uuid_fk, ref_table, ref_col, on_delete, prefix, escaped_prefix)
end

# Relax NOT NULL on legacy integer FK columns where Ecto schemas
# now exclusively write UUID FKs. Without this, inserts that only
# populate UUID columns fail with NOT NULL violations.
for {table, int_fk} <- @relax_integer_fks do
relax_integer_not_null(table, int_fk, prefix, escaped_prefix)
end
end

@doc """
Expand Down Expand Up @@ -526,7 +545,7 @@ defmodule PhoenixKit.Migrations.UUIDFKColumns do

execute("""
ALTER TABLE #{table_name}
ADD COLUMN #{uuid_fk} UUID
ADD COLUMN IF NOT EXISTS #{uuid_fk} UUID
""")
end
end
Expand Down Expand Up @@ -637,6 +656,37 @@ defmodule PhoenixKit.Migrations.UUIDFKColumns do
end
end

# ── Legacy Integer FK Relaxation ─────────────────────────────────────

defp relax_integer_not_null(table, int_fk, prefix, escaped_prefix) do
table_str = Atom.to_string(table)

if table_exists?(table_str, escaped_prefix) and
column_exists?(table_str, int_fk, escaped_prefix) and
column_is_not_null?(table_str, int_fk, escaped_prefix) do
table_name = prefix_table_name(table_str, prefix)

execute("""
ALTER TABLE #{table_name}
ALTER COLUMN #{int_fk} DROP NOT NULL
""")
end
end

defp column_is_not_null?(table_str, column_str, escaped_prefix) do
query = """
SELECT is_nullable FROM information_schema.columns
WHERE table_name = '#{table_str}'
AND column_name = '#{column_str}'
AND table_schema = '#{escaped_prefix}'
"""

case repo().query(query, [], log: false) do
{:ok, %{rows: [["NO"]]}} -> true
_ -> false
end
end

# ── FK Constraint Operations ──────────────────────────────────────────

defp add_fk_constraint(table, uuid_fk, ref_table, ref_col, on_delete, prefix, escaped_prefix) do
Expand Down
2 changes: 1 addition & 1 deletion lib/phoenix_kit_web/components/layout_wrapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ defmodule PhoenixKitWeb.Components.LayoutWrapper do
attr :page_title, :string, default: nil
attr :current_path, :string, default: nil
attr :inner_content, :string, default: nil
attr :project_title, :string, default: "PhoenixKit"
attr :project_title, :string, default: nil
attr :current_locale, :string, default: nil

slot :inner_block, required: false
Expand Down
4 changes: 2 additions & 2 deletions lib/phoenix_kit_web/live/modules.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@
<%= if @languages_enabled do %>
<span class="badge badge-outline ml-2 whitespace-nowrap">
{if @languages_default,
do: @languages_default["name"],
do: @languages_default.name,
else: "No Default"}
</span>
<% end %>
Expand Down Expand Up @@ -358,7 +358,7 @@
# Determine default language base code for URL display
default_lang_code =
if @languages_enabled && @languages_default do
@languages_default["code"]
@languages_default.code
|> PhoenixKit.Modules.Languages.DialectMapper.extract_base()
else
nil
Expand Down
Loading
Loading