diff --git a/.credo.exs b/.credo.exs
index f806174..5526a45 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -89,7 +89,7 @@
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
- {Credo.Check.Design.TagTODO, [exit_status: 2]},
+ {Credo.Check.Design.TagTODO, [exit_status: 0]},
#
## Readability Checks
diff --git a/lib/spazio_solazzo/accounts/user.ex b/lib/spazio_solazzo/accounts/user.ex
index 23d7d3b..27af03c 100644
--- a/lib/spazio_solazzo/accounts/user.ex
+++ b/lib/spazio_solazzo/accounts/user.ex
@@ -83,7 +83,7 @@ defmodule SpazioSolazzo.Accounts.User do
change AshAuthentication.Strategy.MagicLink.SignInChange
# Conditionally validate name and phone_number for new users
- change SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields
+ change SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields
change {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange,
strategy_name: :remember_me}
@@ -149,7 +149,7 @@ defmodule SpazioSolazzo.Accounts.User do
end
attribute :phone_number, :string do
- allow_nil? false
+ allow_nil? true
public? true
end
end
diff --git a/lib/spazio_solazzo/accounts/user/changes/validate_registration_fields.ex b/lib/spazio_solazzo/accounts/user/changes/validate_registration_fields.ex
index 80466bc..88501ea 100644
--- a/lib/spazio_solazzo/accounts/user/changes/validate_registration_fields.ex
+++ b/lib/spazio_solazzo/accounts/user/changes/validate_registration_fields.ex
@@ -1,4 +1,4 @@
-defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
+defmodule SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields do
@moduledoc """
Conditionally validates that name and phone_number are present for new user registrations.
For existing users (upserts), these fields are not required.
@@ -11,28 +11,53 @@ defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
case SpazioSolazzo.Accounts.get_user_by_email(email, authorize?: false) do
{:ok, %{phone_number: phone, name: name}} ->
+ # User is already registered, we'll just set the same values it had
changeset
|> Ash.Changeset.force_change_attribute(:name, name)
|> Ash.Changeset.force_change_attribute(:phone_number, phone)
_ ->
+ # User is not yet registered, we'll parse & validate the new values
name = Ash.Changeset.get_argument(changeset, :name)
phone = Ash.Changeset.get_argument(changeset, :phone_number)
changeset
- |> validate_required_for_registration(:name, name)
- |> validate_required_for_registration(:phone_number, phone)
+ |> parse_name(name)
+ |> parse_phone_number(phone)
end
end
- defp validate_required_for_registration(changeset, field, value) do
- if is_nil(value) || value == "" do
- Ash.Changeset.add_error(
- changeset,
- Ash.Error.Changes.Required.exception(field: field, type: :argument)
- )
+ defp parse_name(changeset, nil) do
+ Ash.Changeset.add_error(
+ changeset,
+ Ash.Error.Changes.Required.exception(field: :name, type: :argument)
+ )
+ end
+
+ defp parse_name(changeset, value) do
+ value = String.trim(value)
+
+ if value == "" do
+ parse_name(changeset, nil)
else
- Ash.Changeset.change_attribute(changeset, field, value)
+ Ash.Changeset.change_attribute(changeset, :name, value)
+ end
+ end
+
+ defp parse_phone_number(changeset, nil) do
+ # The phone number is nullable, this is fine
+ Ash.Changeset.change_attribute(changeset, :phone_number, nil)
+ end
+
+ defp parse_phone_number(changeset, value) do
+ value = String.trim(value)
+
+ if value == "" do
+ # Instead of returning an error, we'll consider an empty phone number
+ # as if the user didn't want to set one, which is valid.
+ parse_name(changeset, nil)
+ else
+ Ash.Changeset.change_attribute(changeset, :phone_number, value)
end
end
end
diff --git a/lib/spazio_solazzo/booking_system/booking.ex b/lib/spazio_solazzo/booking_system/booking.ex
index 8e99b58..8545d94 100644
--- a/lib/spazio_solazzo/booking_system/booking.ex
+++ b/lib/spazio_solazzo/booking_system/booking.ex
@@ -52,7 +52,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
argument :date, :date, allow_nil?: false
argument :customer_name, :string, allow_nil?: false
argument :customer_email, :string, allow_nil?: false
- argument :customer_phone, :string, allow_nil?: false
+ argument :customer_phone, :string, allow_nil?: true
argument :customer_comment, :string, allow_nil?: true
change manage_relationship(:time_slot_template_id, :time_slot_template,
@@ -167,7 +167,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
attribute :customer_email, :string, allow_nil?: false
attribute :start_time, :time, allow_nil?: false
attribute :end_time, :time, allow_nil?: false
- attribute :customer_phone, :string, allow_nil?: false
+ attribute :customer_phone, :string, allow_nil?: true
attribute :customer_comment, :string, allow_nil?: true
attribute :state, :atom do
diff --git a/lib/spazio_solazzo_web/emails/email_templates/admin_notification.html.heex b/lib/spazio_solazzo_web/emails/email_templates/admin_notification.html.heex
index 0747713..5a52af4 100644
--- a/lib/spazio_solazzo_web/emails/email_templates/admin_notification.html.heex
+++ b/lib/spazio_solazzo_web/emails/email_templates/admin_notification.html.heex
@@ -3,7 +3,12 @@
<.details_list>
diff --git a/lib/spazio_solazzo_web/emails/email_templates/customer_confirmation.html.heex b/lib/spazio_solazzo_web/emails/email_templates/customer_confirmation.html.heex
index 4e6bab4..436db04 100644
--- a/lib/spazio_solazzo_web/emails/email_templates/customer_confirmation.html.heex
+++ b/lib/spazio_solazzo_web/emails/email_templates/customer_confirmation.html.heex
@@ -7,7 +7,7 @@
<.detail_item label="Date">{@date}
<.detail_item label="Time">{@start_time} - {@end_time}
<.detail_item label="Email">{@customer_email}
- <.detail_item label="Phone">{@customer_phone}
+ <.detail_item label="Phone">{@customer_phone || "N/A"}
<.detail_item label="Note">{@customer_comment || "N/A"}
diff --git a/lib/spazio_solazzo_web/live/auth/auth_callback_live.ex b/lib/spazio_solazzo_web/live/auth/auth_callback_live.ex
index 6e0190d..937d8dc 100644
--- a/lib/spazio_solazzo_web/live/auth/auth_callback_live.ex
+++ b/lib/spazio_solazzo_web/live/auth/auth_callback_live.ex
@@ -74,12 +74,17 @@ defmodule SpazioSolazzoWeb.AuthCallbackLive do
) do
%{token: token} = socket.assigns
remember_me = Map.get(args, "remember_me") == "on"
+ name = String.trim(name)
+ phone_number = String.trim(phone_number)
- {:noreply,
- redirect(socket,
- to:
- ~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
- )}
+ url =
+ if phone_number == "" do
+ ~p"/auth/magic/sign-in?token=#{token}&name=#{name}&remember_me=#{remember_me}"
+ else
+ ~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
+ end
+
+ {:noreply, redirect(socket, to: url)}
end
defp extract_email_from_token(token) do
diff --git a/lib/spazio_solazzo_web/live/auth/auth_callback_live.html.heex b/lib/spazio_solazzo_web/live/auth/auth_callback_live.html.heex
index 85f2aae..7644a33 100644
--- a/lib/spazio_solazzo_web/live/auth/auth_callback_live.html.heex
+++ b/lib/spazio_solazzo_web/live/auth/auth_callback_live.html.heex
@@ -98,21 +98,23 @@
for="phone_number"
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
>
- Phone Number *
+ Phone Number (Optional)
+
+ Your number will only be used to contact you personally about booking issues, never for marketing.
+
diff --git a/lib/spazio_solazzo_web/live/booking/asset_booking_live.ex b/lib/spazio_solazzo_web/live/booking/asset_booking_live.ex
index 41e1a23..0bfe020 100644
--- a/lib/spazio_solazzo_web/live/booking/asset_booking_live.ex
+++ b/lib/spazio_solazzo_web/live/booking/asset_booking_live.ex
@@ -55,7 +55,7 @@ defmodule SpazioSolazzoWeb.AssetBookingLive do
{:noreply, assign(socket, show_success_modal: false)}
end
- def handle_info({:create_booking, comment}, socket) do
+ def handle_info({:create_booking, booking_data}, socket) do
current_user = socket.assigns.current_user
result =
@@ -64,10 +64,10 @@ defmodule SpazioSolazzoWeb.AssetBookingLive do
socket.assigns.asset.id,
current_user.id,
socket.assigns.selected_date,
- current_user.name,
+ booking_data.customer_name,
current_user.email,
- current_user.phone_number,
- comment
+ booking_data.customer_phone,
+ booking_data.customer_comment
)
case result do
diff --git a/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex b/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex
index 5dcd15b..50a24bc 100644
--- a/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex
+++ b/lib/spazio_solazzo_web/live/booking/booking_form_live_component.ex
@@ -9,6 +9,8 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
def update(assigns, socket) do
initial_data = %{
+ "customer_name" => assigns.current_user.name,
+ "customer_phone" => assigns.current_user.phone_number || "",
"customer_comment" => ""
}
@@ -25,8 +27,13 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
end
def handle_event("submit_booking", params, socket) do
- comment = params["customer_comment"] || ""
- send(self(), {:create_booking, comment})
+ booking_data = %{
+ customer_name: params["customer_name"] || "",
+ customer_phone: params["customer_phone"] || "",
+ customer_comment: params["customer_comment"] || ""
+ }
+
+ send(self(), {:create_booking, booking_data})
{:noreply, socket}
end
@@ -53,16 +60,25 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
>
-
- Name
+
+ Name *
-
-
- <.icon name="hero-user" class="size-5 text-sky-600 dark:text-sky-400" />
+
+
+
+ <.icon name="hero-user" class="size-5 text-slate-400 dark:text-slate-500" />
-
- {@current_user.name}
-
@@ -81,26 +97,56 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
-
- Phone
+
+ Phone (Optional)
-
-
- <.icon name="hero-phone" class="size-5 text-sky-600 dark:text-sky-400" />
+
+
+
+ <.icon name="hero-phone" class="size-5 text-slate-400 dark:text-slate-500" />
-
- {@current_user.phone_number}
-
- <.input
- field={@form[:customer_comment]}
- type="textarea"
- label="Comments (Optional)"
- placeholder="Any special requests or notes..."
- rows="4"
- />
+
+
+ Comments (Optional)
+
+
+
+
+
+
+
+
+ <.icon name="hero-information-circle" class="size-5 text-sky-600 dark:text-sky-400" />
+
+
+
+ Cancel anytime with no commitment
+ Payment upon arrival only
+
+
+
diff --git a/lib/spazio_solazzo_web/live/user/profile_live.ex b/lib/spazio_solazzo_web/live/user/profile_live.ex
index c475052..de2fa31 100644
--- a/lib/spazio_solazzo_web/live/user/profile_live.ex
+++ b/lib/spazio_solazzo_web/live/user/profile_live.ex
@@ -26,10 +26,14 @@ defmodule SpazioSolazzoWeb.ProfileLive do
def handle_event("save_profile", %{"form" => form_params}, socket) do
case Form.submit(socket.assigns.profile_form, params: form_params) do
{:ok, updated_user} ->
+ form =
+ Accounts.form_to_update_profile(updated_user, actor: updated_user)
+ |> to_form()
+
{:noreply,
socket
|> assign(:current_user, updated_user)
- |> assign(:profile_form, to_form(form_params))
+ |> assign(:profile_form, form)
|> put_flash(:info, "Profile updated successfully")}
{:error, form} ->
diff --git a/lib/spazio_solazzo_web/live/user/profile_live.html.heex b/lib/spazio_solazzo_web/live/user/profile_live.html.heex
index 674b28d..eb69b8d 100644
--- a/lib/spazio_solazzo_web/live/user/profile_live.html.heex
+++ b/lib/spazio_solazzo_web/live/user/profile_live.html.heex
@@ -30,7 +30,8 @@
<.input
field={@profile_form[:name]}
type="text"
- label="Full Name"
+ label="Full Name *"
+ required
placeholder="Enter your full name"
class="w-full bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none"
/>
@@ -63,7 +64,7 @@
field={@profile_form[:phone_number]}
type="tel"
label="Phone Number"
- placeholder="+39"
+ placeholder="+39 123456789"
class="w-full bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none"
/>
diff --git a/priv/repo/migrations/20260115004442_set_phone_number_as_nullable.exs b/priv/repo/migrations/20260115004442_set_phone_number_as_nullable.exs
new file mode 100644
index 0000000..8ddc4d0
--- /dev/null
+++ b/priv/repo/migrations/20260115004442_set_phone_number_as_nullable.exs
@@ -0,0 +1,29 @@
+defmodule SpazioSolazzo.Repo.Migrations.SetPhoneNumberAsNullable do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:users) do
+ modify :phone_number, :text, null: true
+ end
+
+ alter table(:bookings) do
+ modify :customer_phone, :text, null: true
+ end
+ end
+
+ def down do
+ alter table(:bookings) do
+ modify :customer_phone, :text, null: false
+ end
+
+ alter table(:users) do
+ modify :phone_number, :text, null: false
+ end
+ end
+end
diff --git a/priv/resource_snapshots/repo/bookings/20260115004442.json b/priv/resource_snapshots/repo/bookings/20260115004442.json
new file mode 100644
index 0000000..df0e822
--- /dev/null
+++ b/priv/resource_snapshots/repo/bookings/20260115004442.json
@@ -0,0 +1,244 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "customer_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "customer_email",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "start_time",
+ "type": "time"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "end_time",
+ "type": "time"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "customer_phone",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "customer_comment",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "\"reserved\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "state",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "bookings_asset_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "assets"
+ },
+ "scale": null,
+ "size": null,
+ "source": "asset_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "bookings_time_slot_template_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "time_slot_templates"
+ },
+ "scale": null,
+ "size": null,
+ "source": "time_slot_template_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": true,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "bookings_user_id_fkey",
+ "on_delete": "nilify",
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "user_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "1480C13D76AD8CE079362CC851CF250063914A40A6CA48182E3D3B5D83CD174A",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.SpazioSolazzo.Repo",
+ "schema": null,
+ "table": "bookings"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/users/20260115004442.json b/priv/resource_snapshots/repo/users/20260115004442.json
new file mode 100644
index 0000000..e36391e
--- /dev/null
+++ b/priv/resource_snapshots/repo/users/20260115004442.json
@@ -0,0 +1,82 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "citext"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "phone_number",
+ "type": "text"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "6D20D84C076B09FEAFF7A68C69930AF48936A628BBEE432695F0ECC02B0F4EFA",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.SpazioSolazzo.Repo",
+ "schema": null,
+ "table": "users"
+}
\ No newline at end of file
diff --git a/test/spazio_solazzo/accounts/user_test.exs b/test/spazio_solazzo/accounts/user_test.exs
index 788da2f..7e423be 100644
--- a/test/spazio_solazzo/accounts/user_test.exs
+++ b/test/spazio_solazzo/accounts/user_test.exs
@@ -9,7 +9,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
describe "update_profile" do
test "allows user to update their own name and phone_number" do
- user = create_test_user("test@example.com")
+ user = register_user("test@example.com")
{:ok, updated_user} =
Accounts.update_profile(user, "Updated Name", "+9876543210", actor: user)
@@ -20,8 +20,8 @@ defmodule SpazioSolazzo.Accounts.UserTest do
end
test "prevents user from updating another user's profile" do
- user1 = create_test_user("user1@example.com")
- user2 = create_test_user("user2@example.com")
+ user1 = register_user("user1@example.com")
+ user2 = register_user("user2@example.com")
result =
Accounts.update_profile(user2, "Hacker", "1235837", actor: user1)
@@ -30,7 +30,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
end
test "validates that name is present" do
- user = create_test_user("test@example.com")
+ user = register_user("test@example.com")
result =
Accounts.update_profile(user, "", "+9876543210", actor: user)
@@ -42,7 +42,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
describe "terminate_account with delete_history: false (anonymization)" do
test "deletes user but preserves bookings with nullified user_id" do
- user = create_test_user("delete@example.com")
+ user = register_user("delete@example.com")
{_space, asset, time_slot} = create_booking_fixtures()
{:ok, booking1} =
@@ -87,7 +87,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
end
test "cancels future confirmed bookings before anonymizing" do
- user = create_test_user("cancel@example.com")
+ user = register_user("cancel@example.com")
{_space, asset, time_slot} = create_booking_fixtures()
future_date = Date.add(Date.utc_today(), 7)
@@ -133,7 +133,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
describe "terminate_account with delete_history: true (hard delete)" do
test "deletes user and all associated bookings permanently" do
- user = create_test_user("harddelete@example.com")
+ user = register_user("harddelete@example.com")
{_space, asset, time_slot} = create_booking_fixtures()
{:ok, booking1} =
@@ -173,8 +173,8 @@ defmodule SpazioSolazzo.Accounts.UserTest do
describe "terminate_account authorization" do
test "prevents user from deleting another user's account" do
- user1 = create_test_user("user1@example.com")
- user2 = create_test_user("user2@example.com")
+ user1 = register_user("user1@example.com")
+ user2 = register_user("user2@example.com")
result = Accounts.terminate_account(user2, false, actor: user1)
@@ -182,18 +182,6 @@ defmodule SpazioSolazzo.Accounts.UserTest do
end
end
- defp create_test_user(email) do
- {:ok, user} =
- SpazioSolazzo.Repo.insert(%User{
- id: Ash.UUID.generate(),
- email: email,
- name: "Test User",
- phone_number: "+1234567890"
- })
-
- user
- end
-
defp create_booking_fixtures do
unique_id = :erlang.unique_integer([:positive, :monotonic])
diff --git a/test/spazio_solazzo/booking_system/booking_test.exs b/test/spazio_solazzo/booking_system/booking_test.exs
index e81afd5..4816af2 100644
--- a/test/spazio_solazzo/booking_system/booking_test.exs
+++ b/test/spazio_solazzo/booking_system/booking_test.exs
@@ -4,8 +4,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
alias SpazioSolazzo.BookingSystem
alias SpazioSolazzo.BookingSystem.Booking
- alias SpazioSolazzo.Accounts.User
-
alias SpazioSolazzo.BookingSystem.Booking.EmailWorker
setup do
@@ -20,13 +18,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
space.id
)
- {:ok, user} =
- SpazioSolazzo.Repo.insert(%User{
- id: Ash.UUID.generate(),
- email: "test@example.com",
- name: "Test User",
- phone_number: "+1234567890"
- })
+ user = register_user("test@example.com")
%{space: space, asset: asset, time_slot: time_slot, user: user}
end
@@ -236,7 +228,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
# Load the booking with the user relationship
{:ok, booking_with_user} = Ash.load(booking, :user, authorize?: false)
assert booking_with_user.user.id == user.id
- assert to_string(booking_with_user.user.email) == user.email
+ assert booking_with_user.user.email == user.email
assert booking_with_user.user.name == user.name
end
end
diff --git a/test/spazio_solazzo_web/controllers/booking_controller_test.exs b/test/spazio_solazzo_web/controllers/booking_controller_test.exs
index 6053b65..45d5ef6 100644
--- a/test/spazio_solazzo_web/controllers/booking_controller_test.exs
+++ b/test/spazio_solazzo_web/controllers/booking_controller_test.exs
@@ -3,7 +3,6 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
alias SpazioSolazzo.BookingSystem
alias SpazioSolazzo.BookingSystem.Booking.Token
- alias SpazioSolazzo.Accounts.User
setup do
unique_id = :erlang.unique_integer([:positive, :monotonic])
@@ -21,13 +20,7 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
space.id
)
- {:ok, user} =
- SpazioSolazzo.Repo.insert(%User{
- id: Ash.UUID.generate(),
- email: "test@example.com",
- name: "Test User",
- phone_number: "+1234567890"
- })
+ user = register_user("test@example.com", "Test User", "+1234567890")
%{space: space, asset: asset, time_slot: time_slot, user: user}
end
diff --git a/test/spazio_solazzo_web/live/booking_live/asset_booking_test.exs b/test/spazio_solazzo_web/live/booking_live/asset_booking_test.exs
index d739066..2e5dd89 100644
--- a/test/spazio_solazzo_web/live/booking_live/asset_booking_test.exs
+++ b/test/spazio_solazzo_web/live/booking_live/asset_booking_test.exs
@@ -3,7 +3,6 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
import Phoenix.LiveViewTest
alias SpazioSolazzo.BookingSystem
- alias SpazioSolazzo.Accounts.User
setup %{conn: conn} do
{:ok, space} = BookingSystem.create_space("TestSpace", "test-space", "Test description")
@@ -20,28 +19,12 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
space.id
)
- conn = Plug.Test.init_test_session(conn, %{})
- conn = log_in_user(conn)
+ user = register_user("test@example.com", "Test User", "+1234567890")
+ conn = log_in_user(conn, user)
%{space: space, asset: asset, slot: slot, conn: conn}
end
- defp log_in_user(conn) do
- {:ok, user} =
- SpazioSolazzo.Repo.insert(%User{
- id: Ash.UUID.generate(),
- email: "test@example.com",
- name: "Test User",
- phone_number: "+1234567890"
- })
-
- {:ok, token, _claims} = AshAuthentication.Jwt.token_for_user(user)
-
- conn
- |> Plug.Test.init_test_session(%{})
- |> Plug.Conn.put_session("user_token", token)
- end
-
describe "AssetBooking mount" do
test "renders asset booking page with available time slots", %{
conn: conn,
@@ -105,7 +88,11 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
view
|> element("#booking-form")
- |> render_submit(%{"customer_comment" => "test comment"})
+ |> render_submit(%{
+ "customer_name" => "Test User",
+ "customer_phone" => "+1234567890",
+ "customer_comment" => "test comment"
+ })
assert has_element?(view, "#success-modal")
@@ -224,4 +211,113 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
assert html =~ ~s(
)
end
end
+
+ describe "AssetBooking without phone number" do
+ setup %{conn: conn} do
+ # Create a separate connection with a user without phone number
+ user = register_user("nophone@example.com", "User Without Phone", nil)
+ conn = log_in_user(conn, user)
+
+ %{conn: conn}
+ end
+
+ test "user without phone number can view booking form", %{conn: conn, asset: asset} do
+ {:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
+
+ view
+ |> element("button[phx-click='select_slot']")
+ |> render_click()
+
+ assert has_element?(view, "#booking-modal")
+ assert has_element?(view, "input[name='customer_name']")
+ assert has_element?(view, "input[name='customer_phone']")
+ assert has_element?(view, "textarea[name='customer_comment']")
+ end
+
+ test "user without phone number can create booking without providing phone", %{
+ conn: conn,
+ asset: asset
+ } do
+ {:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
+
+ view
+ |> element("button[phx-click='select_slot']")
+ |> render_click()
+
+ assert has_element?(view, "#booking-modal")
+
+ # Submit booking with name but no phone
+ view
+ |> element("#booking-form")
+ |> render_submit(%{
+ "customer_name" => "User Without Phone",
+ "customer_phone" => "",
+ "customer_comment" => "test comment"
+ })
+
+ assert has_element?(view, "#success-modal")
+
+ assert {:ok, [booking]} =
+ BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
+
+ assert booking.customer_email == "nophone@example.com"
+ assert booking.customer_name == "User Without Phone"
+ assert booking.customer_phone == nil or booking.customer_phone == ""
+ assert booking.customer_comment == "test comment"
+ end
+
+ test "user without phone number can edit name in booking form", %{
+ conn: conn,
+ asset: asset
+ } do
+ {:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
+
+ view
+ |> element("button[phx-click='select_slot']")
+ |> render_click()
+
+ # Change the name
+ view
+ |> element("#booking-form")
+ |> render_submit(%{
+ "customer_name" => "Different Name",
+ "customer_phone" => "",
+ "customer_comment" => ""
+ })
+
+ assert has_element?(view, "#success-modal")
+
+ assert {:ok, [booking]} =
+ BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
+
+ assert booking.customer_name == "Different Name"
+ end
+
+ test "user without phone number can optionally add phone during booking", %{
+ conn: conn,
+ asset: asset
+ } do
+ {:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
+
+ view
+ |> element("button[phx-click='select_slot']")
+ |> render_click()
+
+ # Add phone number during booking
+ view
+ |> element("#booking-form")
+ |> render_submit(%{
+ "customer_name" => "User Without Phone",
+ "customer_phone" => "+39 123 456 789",
+ "customer_comment" => ""
+ })
+
+ assert has_element?(view, "#success-modal")
+
+ assert {:ok, [booking]} =
+ BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
+
+ assert booking.customer_phone == "+39 123 456 789"
+ end
+ end
end
diff --git a/test/spazio_solazzo_web/live/user/profile_live_test.exs b/test/spazio_solazzo_web/live/user/profile_live_test.exs
index ec0360a..36ef751 100644
--- a/test/spazio_solazzo_web/live/user/profile_live_test.exs
+++ b/test/spazio_solazzo_web/live/user/profile_live_test.exs
@@ -8,8 +8,9 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
alias SpazioSolazzo.BookingSystem.Booking
setup %{conn: conn} do
- conn = Plug.Test.init_test_session(conn, %{})
- {conn, user} = log_in_user(conn)
+ user = register_user("test@example.com", "Test User", "+123456789")
+ conn = log_in_user(conn, user)
+
%{user: user, conn: conn}
end
@@ -191,25 +192,6 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
end
end
- defp log_in_user(conn) do
- user =
- SpazioSolazzo.Repo.insert!(%User{
- id: Ash.UUID.generate(),
- email: "test@example.com",
- name: "Test User",
- phone_number: "+1234567890"
- })
-
- {:ok, token, _claims} = AshAuthentication.Jwt.token_for_user(user)
-
- conn =
- conn
- |> Plug.Test.init_test_session(%{})
- |> Plug.Conn.put_session("user_token", token)
-
- {conn, user}
- end
-
defp create_booking_fixtures do
unique_id = :erlang.unique_integer([:positive, :monotonic])
diff --git a/test/support/auth_helpers.ex b/test/support/auth_helpers.ex
new file mode 100644
index 0000000..0a6e930
--- /dev/null
+++ b/test/support/auth_helpers.ex
@@ -0,0 +1,64 @@
+defmodule SpazioSolazzo.AuthHelpers do
+ @moduledoc """
+ Authentication helper functions for tests.
+
+ Provides utilities to create and authenticate users via the magic link flow,
+ simulating realistic authentication behavior in tests.
+ """
+
+ alias SpazioSolazzo.Accounts
+
+ @doc """
+ Creates a test session and logs the user into it.
+
+ ## Parameters
+
+ - `conn` - The test connection
+ - `user` - User to log in
+
+ ## Examples
+
+ conn = log_in_user(conn, user)
+ """
+ def log_in_user(conn, user) do
+ conn
+ |> Plug.Test.init_test_session(%{})
+ |> AshAuthentication.Phoenix.Plug.store_in_session(user)
+ end
+
+ @doc """
+ Creates a user via magic link authentication without attaching to a connection.
+
+ Useful for tests that need a user object but don't need an authenticated connection.
+
+ ## Parameters
+
+ - `email` - User's email address
+ - `name` - Optional user's full name (defaults to "Test User")
+ - `phone_number` - Optional phone number (defaults to nil)
+
+ ## Examples
+
+ user = register_user("test@example.com", "Test User", "+1234567890")
+ user = register_user("user@example.com", "User Name")
+ user = register_user("user@example.com")
+ """
+ def register_user(email, name \\ "Test user", phone_number \\ nil) do
+ strategy = AshAuthentication.Info.strategy!(SpazioSolazzo.Accounts.User, :magic_link)
+
+ {:ok, token} =
+ AshAuthentication.Strategy.MagicLink.request_token_for_identity(strategy, email)
+
+ # Sign in with magic link
+ {:ok, user} =
+ Accounts.sign_in_with_magic_link(
+ token,
+ false,
+ name,
+ phone_number,
+ authorize?: false
+ )
+
+ user
+ end
+end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index 30b1790..d74e790 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -31,6 +31,9 @@ defmodule SpazioSolazzoWeb.ConnCase do
# Import DataCase helpers for email testing
import SpazioSolazzo.DataCase, only: [pop_email: 0, pop_email: 2]
+
+ # Import authentication helpers
+ import SpazioSolazzo.AuthHelpers
end
end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index e4650da..a836592 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -26,6 +26,7 @@ defmodule SpazioSolazzo.DataCase do
import Ecto.Changeset
import Ecto.Query
import SpazioSolazzo.DataCase
+ import SpazioSolazzo.AuthHelpers
end
end