refactor email workers and templates

This commit is contained in:
JasterV 2026-02-01 22:10:24 +01:00
parent 68422546ad
commit 038b8c462a
14 changed files with 281 additions and 186 deletions

View file

@ -125,6 +125,30 @@ defmodule SpazioSolazzo.BookingSystem do
end
end
def get_slot_booking_counts(space_id, date, start_time, end_time) do
with {:ok, all_bookings} <- list_booking_requests(space_id, nil, date) do
overlapping_bookings =
Enum.filter(all_bookings, fn booking ->
times_overlap?(
booking.start_time,
booking.end_time,
start_time,
end_time
)
end)
pending_count =
overlapping_bookings
|> Enum.count(&(&1.state == :requested))
approved_count =
overlapping_bookings
|> Enum.count(&(&1.state == :accepted))
{:ok, %{pending: pending_count, approved: approved_count}}
end
end
defp times_overlap?(start1, end1, start2, end2) do
Time.compare(start1, end2) == :lt and Time.compare(start2, end1) == :lt
end

View file

@ -14,9 +14,9 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
require Ash.Query
alias SpazioSolazzo.BookingSystem.Booking.{
NewRequestWorker,
DecisionWorker,
CancellationWorker
AdminActionEmailWorker,
RequestCreatedEmailWorker,
UserCancellationEmailWorker
}
postgres do
@ -172,7 +172,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
start_time: booking.start_time,
end_time: booking.end_time
}
|> NewRequestWorker.new()
|> RequestCreatedEmailWorker.new()
|> Oban.insert!()
{:ok, booking}
@ -196,10 +196,9 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time,
decision: "accepted",
rejection_reason: nil
action: "accepted"
}
|> DecisionWorker.new()
|> AdminActionEmailWorker.new()
|> Oban.insert!()
{:ok, booking}
@ -222,7 +221,6 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
booking = Ash.load!(booking, [:space])
%{
booking_id: booking.id,
customer_name: booking.customer_name,
customer_email: booking.customer_email,
customer_phone: booking.customer_phone,
@ -230,10 +228,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time,
decision: "rejected",
action: "rejected",
rejection_reason: booking.rejection_reason
}
|> DecisionWorker.new()
|> AdminActionEmailWorker.new()
|> Oban.insert!()
{:ok, booking}
@ -265,7 +263,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
end_time: booking.end_time,
cancellation_reason: booking.cancellation_reason
}
|> CancellationWorker.new()
|> UserCancellationEmailWorker.new()
|> Oban.insert!()
{:ok, booking}

View file

@ -0,0 +1,67 @@
defmodule SpazioSolazzo.BookingSystem.Booking.AdminActionEmailWorker do
@moduledoc """
Sends emails when an admin approves or rejects a booking request.
"""
use Oban.Worker, queue: :booking_email, max_attempts: 3
alias SpazioSolazzo.BookingSystem.Booking.Email
@impl Oban.Worker
def perform(%Oban.Job{
args: %{
"booking_id" => booking_id,
"customer_name" => customer_name,
"customer_email" => customer_email,
"customer_phone" => customer_phone,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"action" => "accepted"
}
}) do
%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time
}
|> Email.booking_request_approved()
|> SpazioSolazzo.Mailer.deliver!()
:ok
end
@impl Oban.Worker
def perform(%Oban.Job{
args: %{
"customer_name" => customer_name,
"customer_email" => customer_email,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"action" => "rejected",
"rejection_reason" => rejection_reason
}
}) do
%{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason
}
|> Email.booking_request_rejected()
|> SpazioSolazzo.Mailer.deliver!()
:ok
end
end

View file

@ -1,56 +0,0 @@
defmodule SpazioSolazzo.BookingSystem.Booking.DecisionWorker do
@moduledoc """
Sends emails when an admin approves or rejects a booking request.
"""
use Oban.Worker, queue: :booking_email, max_attempts: 3
alias SpazioSolazzo.BookingSystem.Booking.Email
@impl Oban.Worker
def perform(%Oban.Job{
args: %{
"booking_id" => booking_id,
"customer_name" => customer_name,
"customer_email" => customer_email,
"customer_phone" => customer_phone,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"decision" => decision,
"rejection_reason" => rejection_reason
}
}) do
case decision do
"accepted" ->
%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time
}
|> Email.accepted_user()
|> SpazioSolazzo.Mailer.deliver!()
"rejected" ->
%{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason
}
|> Email.rejected_user()
|> SpazioSolazzo.Mailer.deliver!()
end
:ok
end
end

View file

@ -11,7 +11,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
use SpazioSolazzoWeb, :verified_routes
alias SpazioSolazzo.BookingSystem.Booking.Token
def request_received_user(%{
def user_booking_request_confirmation(%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
@ -43,104 +43,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("request_received_user.html", assigns)
|> render_body("user_booking_request_confirmation.html", assigns)
end
def accepted_user(%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time
}) do
cancel_token = Token.generate_customer_cancel_token(booking_id)
cancel_url = url(~p"/bookings/cancel?token=#{cancel_token}")
assigns = %{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
cancel_url: cancel_url,
front_office_phone_number: front_office_phone_number(),
subject: "Booking Approved: #{date}"
}
new()
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("accepted_user.html", assigns)
end
def rejected_user(%{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason
}) do
assigns = %{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason,
front_office_phone_number: front_office_phone_number(),
subject: "Booking Request Update: #{date}"
}
new()
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("rejected_user.html", assigns)
end
def customer_confirmation(%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
customer_comment: customer_comment,
date: date,
start_time: start_time,
end_time: end_time
}) do
cancel_token = Token.generate_customer_cancel_token(booking_id)
cancel_url = url(~p"/bookings/cancel?token=#{cancel_token}")
assigns = %{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
customer_comment: customer_comment,
date: date,
start_time: start_time,
end_time: end_time,
cancel_url: cancel_url,
front_office_phone_number: front_office_phone_number(),
subject: "Booking Confirmed: #{date}"
}
new()
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("customer_confirmation.html", assigns)
end
def new_request_admin(%{
def admin_incoming_booking_request(%{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
@ -170,10 +76,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
|> to(admin_email)
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("new_request_admin.html", assigns)
|> render_body("admin_incoming_booking_request.html", assigns)
end
def cancelled_admin(%{
def booking_cancelled(%{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
@ -200,44 +106,68 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
|> to(admin_email)
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("cancelled_admin.html", assigns)
|> render_body("booking_cancelled.html", assigns)
end
# --- Admin Email ---
def admin_notification(%{
def booking_request_approved(%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
customer_comment: customer_comment,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
admin_email: admin_email
end_time: end_time
}) do
tokens = Token.generate_admin_tokens(booking_id)
confirm_url = url(~p"/bookings/confirm?token=#{tokens.confirm_token}")
cancel_url = url(~p"/bookings/cancel?token=#{tokens.cancel_token}")
cancel_token = Token.generate_customer_cancel_token(booking_id)
cancel_url = url(~p"/bookings/cancel?token=#{cancel_token}")
assigns = %{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
customer_comment: customer_comment,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
confirm_url: confirm_url,
cancel_url: cancel_url,
subject: "New Booking Action Required: #{customer_name}"
front_office_phone_number: front_office_phone_number(),
subject: "Booking Approved: #{date}"
}
new()
|> to(admin_email)
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("admin_notification.html", assigns)
|> render_body("booking_request_approved.html", assigns)
end
def booking_request_rejected(%{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason
}) do
assigns = %{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
rejection_reason: rejection_reason,
front_office_phone_number: front_office_phone_number(),
subject: "Booking Request Update: #{date}"
}
new()
|> to({customer_name, customer_email})
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|> subject(assigns.subject)
|> render_body("booking_request_rejected.html", assigns)
end
defp spazio_solazzo_email do

View file

@ -1,4 +1,4 @@
defmodule SpazioSolazzo.BookingSystem.Booking.NewRequestWorker do
defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorker do
@moduledoc """
Sends booking request confirmation emails to customers and notification emails to administrators.
Triggered when a new booking request is created.
@ -36,11 +36,11 @@ defmodule SpazioSolazzo.BookingSystem.Booking.NewRequestWorker do
}
email_data
|> Email.request_received_user()
|> Email.user_booking_request_confirmation()
|> SpazioSolazzo.Mailer.deliver!()
email_data
|> Email.new_request_admin()
|> Email.admin_incoming_booking_request()
|> SpazioSolazzo.Mailer.deliver!()
:ok

View file

@ -1,4 +1,4 @@
defmodule SpazioSolazzo.BookingSystem.Booking.CancellationWorker do
defmodule SpazioSolazzo.BookingSystem.Booking.UserCancellationEmailWorker do
@moduledoc """
Sends cancellation notification emails to administrators when a customer cancels a booking.
"""
@ -31,7 +31,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.CancellationWorker do
cancellation_reason: cancellation_reason,
admin_email: admin_email()
}
|> Email.cancelled_admin()
|> Email.booking_cancelled()
|> SpazioSolazzo.Mailer.deliver!()
:ok

View file

@ -0,0 +1,132 @@
defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
use SpazioSolazzo.DataCase, async: true
alias SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorker
alias Swoosh.Adapters.Local.Storage.Memory
describe "perform/1" do
test "sends confirmation email to customer" do
job_args = %{
"booking_id" => "test-booking-id",
"customer_name" => "John Doe",
"customer_email" => "john@example.com",
"customer_phone" => "+1234567890",
"customer_comment" => "Test comment",
"space_name" => "Coworking Space",
"date" => "Monday, February 02",
"start_time" => ~T[09:00:00],
"end_time" => ~T[13:00:00]
}
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
# Verify customer email was sent
emails = Memory.all()
assert Enum.any?(emails, fn email ->
email.to == [{"John Doe", "john@example.com"}]
end)
end
test "sends notification email to admin" do
job_args = %{
"booking_id" => "test-booking-id",
"customer_name" => "John Doe",
"customer_email" => "john@example.com",
"customer_phone" => "+1234567890",
"customer_comment" => "Test comment",
"space_name" => "Coworking Space",
"date" => "Monday, February 02",
"start_time" => ~T[09:00:00],
"end_time" => ~T[13:00:00]
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
# Verify admin email was sent
emails = Memory.all()
assert Enum.any?(emails, fn email ->
email.to == [{"", admin_email}]
end)
end
test "sends both customer and admin emails in single job execution" do
job_args = %{
"booking_id" => "test-booking-id",
"customer_name" => "Jane Smith",
"customer_email" => "jane@example.com",
"customer_phone" => "+1234567890",
"customer_comment" => "Another test",
"space_name" => "Meeting Room",
"date" => "Tuesday, February 03",
"start_time" => ~T[14:00:00],
"end_time" => ~T[18:00:00]
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
# Both emails should be sent
emails = Memory.all()
assert length(emails) == 2
email_recipients = Enum.map(emails, fn email -> email.to end)
assert [{"Jane Smith", "jane@example.com"}] in email_recipients
assert [{"", admin_email}] in email_recipients
end
test "customer email contains booking details" do
job_args = %{
"booking_id" => "test-booking-id",
"customer_name" => "Test User",
"customer_email" => "test@example.com",
"customer_phone" => "+1234567890",
"customer_comment" => "Test",
"space_name" => "Music Room",
"date" => "Wednesday, February 04",
"start_time" => ~T[10:00:00],
"end_time" => ~T[12:00:00]
}
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
emails = Memory.all()
customer_email = Enum.find(emails, fn email ->
email.to == [{"Test User", "test@example.com"}]
end)
assert customer_email != nil
assert String.contains?(customer_email.html_body, "Music Room")
assert String.contains?(customer_email.html_body, "Wednesday, February 04")
end
test "admin email contains customer information" do
job_args = %{
"booking_id" => "test-booking-id",
"customer_name" => "Admin Test",
"customer_email" => "admin.test@example.com",
"customer_phone" => "+1234567890",
"customer_comment" => "Admin comment",
"space_name" => "Coworking Space",
"date" => "Thursday, February 05",
"start_time" => ~T[09:00:00],
"end_time" => ~T[11:00:00]
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
emails = Memory.all()
admin_notification = Enum.find(emails, fn email ->
email.to == [{"", admin_email}]
end)
assert admin_notification != nil
assert String.contains?(admin_notification.html_body, "Admin Test")
assert String.contains?(admin_notification.html_body, "admin.test@example.com")
end
end
end