diff --git a/lib/berrypod/contact_notifier.ex b/lib/berrypod/contact_notifier.ex new file mode 100644 index 0000000..9b3c4a5 --- /dev/null +++ b/lib/berrypod/contact_notifier.ex @@ -0,0 +1,61 @@ +defmodule Berrypod.ContactNotifier do + @moduledoc """ + Sends contact form submissions to the shop owner. + """ + + import Swoosh.Email + + alias Berrypod.{Mailer, Settings} + + require Logger + + @doc """ + Delivers a contact form message to the shop owner's email. + + Expects a map with "name", "email", and "message" keys (string keys, + as received from form params). "subject" is optional. + """ + def deliver_contact_message(%{"name" => name, "email" => email, "message" => message} = params) + when is_binary(name) and name != "" and is_binary(email) and email != "" and + is_binary(message) and message != "" do + subject = + if params["subject"] in [nil, ""], do: "Contact form message", else: params["subject"] + + shop_name = Settings.get_setting("shop_name", "Berrypod") + from_address = Settings.get_setting("email_from_address", "contact@example.com") + to_address = Settings.get_setting("contact_email") || from_address + + body = """ + ============================== + + New message from your #{shop_name} contact form + + Name: #{name} + Email: #{email} + Subject: #{subject} + + #{message} + + ============================== + """ + + email_msg = + new() + |> to(to_address) + |> from({shop_name, from_address}) + |> reply_to(email) + |> subject("[#{shop_name}] #{subject}") + |> text_body(body) + + case Mailer.deliver(email_msg) do + {:ok, _metadata} = result -> + result + + {:error, reason} = error -> + Logger.warning("Failed to send contact form email: #{inspect(reason)}") + error + end + end + + def deliver_contact_message(_), do: {:error, :invalid_params} +end diff --git a/lib/berrypod_web/components/layouts/shop_root.html.heex b/lib/berrypod_web/components/layouts/shop_root.html.heex index ac3e4e7..eafb8ee 100644 --- a/lib/berrypod_web/components/layouts/shop_root.html.heex +++ b/lib/berrypod_web/components/layouts/shop_root.html.heex @@ -74,6 +74,11 @@ +
<% end %> -
+ + +
- <.shop_input type="text" placeholder="Your name" /> + <.shop_input type="text" name="name" placeholder="Your name" required />
- <.shop_input type="email" placeholder="your@email.com" /> + <.shop_input type="email" name="email" placeholder="your@email.com" required />
- <.shop_input type="text" placeholder="How can I help?" /> + <.shop_input type="text" name="subject" placeholder="How can I help?" />
- <.shop_textarea rows="5" placeholder="Your message..." /> + <.shop_textarea name="message" rows="5" placeholder="Your message..." required />
<.shop_button type="submit" class="contact-form-submit"> - Send Message + Send message
diff --git a/lib/berrypod_web/controllers/contact_controller.ex b/lib/berrypod_web/controllers/contact_controller.ex new file mode 100644 index 0000000..07ebe89 --- /dev/null +++ b/lib/berrypod_web/controllers/contact_controller.ex @@ -0,0 +1,27 @@ +defmodule BerrypodWeb.ContactController do + use BerrypodWeb, :controller + + alias Berrypod.ContactNotifier + + @doc """ + Handles contact form submission (no-JS fallback). + """ + def create(conn, params) do + case ContactNotifier.deliver_contact_message(params) do + {:ok, _} -> + conn + |> put_flash(:info, "Message sent! We'll get back to you soon.") + |> redirect(to: ~p"/contact") + + {:error, :invalid_params} -> + conn + |> put_flash(:error, "Please fill in all required fields.") + |> redirect(to: ~p"/contact") + + {:error, _} -> + conn + |> put_flash(:error, "Sorry, something went wrong. Please try again.") + |> redirect(to: ~p"/contact") + end + end +end diff --git a/lib/berrypod_web/live/shop/contact.ex b/lib/berrypod_web/live/shop/contact.ex index 613774d..a3be4e2 100644 --- a/lib/berrypod_web/live/shop/contact.ex +++ b/lib/berrypod_web/live/shop/contact.ex @@ -1,7 +1,7 @@ defmodule BerrypodWeb.Shop.Contact do use BerrypodWeb, :live_view - alias Berrypod.Orders + alias Berrypod.{ContactNotifier, Orders} alias Berrypod.Orders.OrderNotifier alias Berrypod.Pages alias BerrypodWeb.OrderLookupController @@ -39,6 +39,23 @@ defmodule BerrypodWeb.Shop.Contact do {:noreply, assign(socket, :tracking_state, state)} end + @impl true + def handle_event("send_contact", params, socket) do + case ContactNotifier.deliver_contact_message(params) do + {:ok, _} -> + {:noreply, + socket + |> put_flash(:info, "Message sent! We'll get back to you soon.") + |> push_navigate(to: ~p"/contact")} + + {:error, :invalid_params} -> + {:noreply, put_flash(socket, :error, "Please fill in all required fields.")} + + {:error, _} -> + {:noreply, put_flash(socket, :error, "Sorry, something went wrong. Please try again.")} + end + end + @impl true def handle_event("reset_tracking", _params, socket) do {:noreply, assign(socket, :tracking_state, :idle)} diff --git a/lib/berrypod_web/router.ex b/lib/berrypod_web/router.ex index 6ec8cb6..6438052 100644 --- a/lib/berrypod_web/router.ex +++ b/lib/berrypod_web/router.ex @@ -270,7 +270,8 @@ defmodule BerrypodWeb.Router do # Checkout (POST — creates Stripe session and redirects) post "/checkout", CheckoutController, :create - # Order lookup (no-JS fallback for contact page form) + # Contact form + order lookup (no-JS fallbacks for contact page forms) + post "/contact/send", ContactController, :create post "/contact/lookup", OrderLookupController, :lookup # Cart form actions (no-JS fallbacks for LiveView cart events) diff --git a/test/berrypod/contact_notifier_test.exs b/test/berrypod/contact_notifier_test.exs new file mode 100644 index 0000000..6952c15 --- /dev/null +++ b/test/berrypod/contact_notifier_test.exs @@ -0,0 +1,87 @@ +defmodule Berrypod.ContactNotifierTest do + use Berrypod.DataCase, async: true + + import Swoosh.TestAssertions + + alias Berrypod.ContactNotifier + alias Berrypod.Settings + + describe "deliver_contact_message/1" do + test "sends email with valid params" do + assert {:ok, _} = + ContactNotifier.deliver_contact_message(%{ + "name" => "Jo Bloggs", + "email" => "jo@example.com", + "subject" => "Question about prints", + "message" => "Do you ship to Mars?" + }) + + assert_email_sent(fn email -> + assert email.subject =~ "Question about prints" + assert email.text_body =~ "Jo Bloggs" + assert email.text_body =~ "jo@example.com" + assert email.text_body =~ "Do you ship to Mars?" + assert {"", "jo@example.com"} = email.reply_to + end) + end + + test "sends to contact_email when set" do + Settings.put_setting("contact_email", "shop@example.com") + + assert {:ok, _} = + ContactNotifier.deliver_contact_message(%{ + "name" => "Test", + "email" => "test@example.com", + "message" => "Hello" + }) + + assert_email_sent(fn email -> + assert [{"", "shop@example.com"}] = email.to + end) + end + + test "uses default subject when not provided" do + assert {:ok, _} = + ContactNotifier.deliver_contact_message(%{ + "name" => "Test", + "email" => "test@example.com", + "message" => "Hello" + }) + + assert_email_sent(fn email -> + assert email.subject =~ "Contact form message" + end) + end + + test "returns error for missing name" do + assert {:error, :invalid_params} = + ContactNotifier.deliver_contact_message(%{ + "name" => "", + "email" => "test@example.com", + "message" => "Hello" + }) + end + + test "returns error for missing email" do + assert {:error, :invalid_params} = + ContactNotifier.deliver_contact_message(%{ + "name" => "Test", + "email" => "", + "message" => "Hello" + }) + end + + test "returns error for missing message" do + assert {:error, :invalid_params} = + ContactNotifier.deliver_contact_message(%{ + "name" => "Test", + "email" => "test@example.com", + "message" => "" + }) + end + + test "returns error for missing keys" do + assert {:error, :invalid_params} = ContactNotifier.deliver_contact_message(%{}) + end + end +end diff --git a/test/berrypod_web/controllers/contact_controller_test.exs b/test/berrypod_web/controllers/contact_controller_test.exs new file mode 100644 index 0000000..59cc263 --- /dev/null +++ b/test/berrypod_web/controllers/contact_controller_test.exs @@ -0,0 +1,48 @@ +defmodule BerrypodWeb.ContactControllerTest do + use BerrypodWeb.ConnCase, async: false + + import Berrypod.AccountsFixtures + import Swoosh.TestAssertions + + setup do + user_fixture() + {:ok, _} = Berrypod.Settings.set_site_live(true) + # Clear confirmation email from user_fixture + Swoosh.TestAssertions.assert_email_sent() + :ok + end + + describe "POST /contact/send" do + test "sends email and redirects with success flash", %{conn: conn} do + conn = + post(conn, ~p"/contact/send", %{ + "name" => "Jo Bloggs", + "email" => "jo@example.com", + "subject" => "Question", + "message" => "Do you ship internationally?" + }) + + assert redirected_to(conn) == "/contact" + assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Message sent" + + assert_email_sent(fn email -> + assert email.subject =~ "Question" + assert email.text_body =~ "Do you ship internationally?" + end) + end + + test "redirects with error flash when required fields missing", %{conn: conn} do + conn = post(conn, ~p"/contact/send", %{"name" => "", "email" => "", "message" => ""}) + + assert redirected_to(conn) == "/contact" + assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "required fields" + end + + test "redirects with error flash when params empty", %{conn: conn} do + conn = post(conn, ~p"/contact/send", %{}) + + assert redirected_to(conn) == "/contact" + assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "required fields" + end + end +end