From 05db34978b822f3d8eff2087f61e11a6f7a558d6 Mon Sep 17 00:00:00 2001 From: Joaquin Correa Date: Mon, 29 Jun 2026 12:28:54 -0300 Subject: [PATCH 1/3] Labels update --- config/locales/en.models.yml | 2 +- config/locales/views/en.facility_product_notifications.yml | 4 ++-- config/locales/views/en.product_notification_mailer.yml | 6 +++++- spec/requests/facility_product_notifications_spec.rb | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config/locales/en.models.yml b/config/locales/en.models.yml index 07344ede43..c7d11ebb41 100644 --- a/config/locales/en.models.yml +++ b/config/locales/en.models.yml @@ -224,7 +224,7 @@ en: product_display_group: one: Product Group other: Product Groups - product_notification: Product Notification + product_notification: User Notification research_safety_certificate: one: Certificate other: Certificates diff --git a/config/locales/views/en.facility_product_notifications.yml b/config/locales/views/en.facility_product_notifications.yml index 147f194077..b1d91bf878 100644 --- a/config/locales/views/en.facility_product_notifications.yml +++ b/config/locales/views/en.facility_product_notifications.yml @@ -24,10 +24,10 @@ en: no_results: No users found selected_users: Selected Users select_products_hint: Select instruments - select_users_hint: Search and select users one by one + select_users_hint: Search and select users to receive notifications hints: email_subject: Override default email subject - reservation_days: Reservations canceled within this amount of days will trigger the notification + reservation_days: Reservations canceled within this number of days from today will trigger the notification. slot_available: Notify users when a Reservation or Admin Hold is canceled for any of the selected instruments user_search_results: add_user: Add diff --git a/config/locales/views/en.product_notification_mailer.yml b/config/locales/views/en.product_notification_mailer.yml index 935428dbaf..274f4a1351 100644 --- a/config/locales/views/en.product_notification_mailer.yml +++ b/config/locales/views/en.product_notification_mailer.yml @@ -6,6 +6,10 @@ en: body: | Hello %{user_name}, - Time slot %{time_range} became available for %{product} at %{facility}. + There has been a cancellation on %{product}. + + The following time slot is now available: + + %{time_range} [Make a reservation](%{new_reservation_url}) diff --git a/spec/requests/facility_product_notifications_spec.rb b/spec/requests/facility_product_notifications_spec.rb index 7c03b62c7d..f4b8f7afcf 100644 --- a/spec/requests/facility_product_notifications_spec.rb +++ b/spec/requests/facility_product_notifications_spec.rb @@ -43,7 +43,7 @@ expect(page).to have_content("No Product Notifications have been created") expect(page).to have_link( - "Add Product Notification", + "Add User Notification", href: new_facility_product_notification_path(facility), ) end From 0f0287d4d780ca98f4113ddd5f1afb138b954121 Mon Sep 17 00:00:00 2001 From: Joaquin Correa Date: Mon, 29 Jun 2026 12:51:15 -0300 Subject: [PATCH 2/3] Handle start parameter in reservation new --- app/controllers/reservations_controller.rb | 1 + .../single_reservations_controller.rb | 9 +++- .../next_available_reservation_finder.rb | 11 +++-- .../slot_available.html.haml | 2 +- .../slot_available.txt.erb | 2 +- spec/requests/single_reservations_spec.rb | 47 +++++++++++++++++++ .../next_available_reservation_finder_spec.rb | 29 +++++++++++- 7 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 spec/requests/single_reservations_spec.rb diff --git a/app/controllers/reservations_controller.rb b/app/controllers/reservations_controller.rb index 5887052cce..4e6723fea6 100644 --- a/app/controllers/reservations_controller.rb +++ b/app/controllers/reservations_controller.rb @@ -150,6 +150,7 @@ def new raise ActiveRecord::RecordNotFound unless @reservation.nil? @reservation = NextAvailableReservationFinder.new(@instrument).next_available_for(current_user, acting_user) + @reservation.order_detail = @order_detail authorize! :new, @reservation diff --git a/app/controllers/single_reservations_controller.rb b/app/controllers/single_reservations_controller.rb index 1c2e05496f..f5e2ee01e6 100644 --- a/app/controllers/single_reservations_controller.rb +++ b/app/controllers/single_reservations_controller.rb @@ -10,7 +10,7 @@ class SingleReservationsController < ApplicationController def new @reservation = NextAvailableReservationFinder - .new(@instrument) + .new(@instrument, start_at: start_at_param) .next_available_for(current_user, acting_user) @reservation.order_detail = @order_detail @@ -64,4 +64,11 @@ def set_windows @reservation_window = ReservationWindow.new(@reservation, current_user) end + def start_at_param + return if (start_at = params[:start_at]).blank? + + Time.zone.parse(start_at) + rescue ArgumentError + nil + end end diff --git a/app/services/next_available_reservation_finder.rb b/app/services/next_available_reservation_finder.rb index e0199c3763..aa5a6ae164 100644 --- a/app/services/next_available_reservation_finder.rb +++ b/app/services/next_available_reservation_finder.rb @@ -3,14 +3,17 @@ # Used by the reservations controllers to find the default # reservation times to display class NextAvailableReservationFinder - def initialize(product) + attr_reader :product, :start_at + + def initialize(product, start_at: nil) @product = product + @start_at = start_at || 1.minute.from_now end def next_available_for(current_user, acting_user) options = current_user.can_override_restrictions?(@product) ? {} : { user: acting_user } next_available = @product.next_available_reservation( - after: 1.minute.from_now, + after: start_at, duration: default_duration, options: ) @@ -22,8 +25,8 @@ def next_available_for(current_user, acting_user) def default_reservation Reservation.new(product: @product, - reserve_start_at: Time.current, - reserve_end_at: default_duration.from_now) + reserve_start_at: start_at, + reserve_end_at: start_at + default_duration) end def default_duration diff --git a/app/views/product_notification_mailer/slot_available.html.haml b/app/views/product_notification_mailer/slot_available.html.haml index eaca8e3236..393ab4da19 100644 --- a/app/views/product_notification_mailer/slot_available.html.haml +++ b/app/views/product_notification_mailer/slot_available.html.haml @@ -1,6 +1,6 @@ = html("body", user_name: @user.full_name, - new_reservation_url: new_facility_instrument_single_reservation_url(@product.facility, @product), + new_reservation_url: new_facility_instrument_single_reservation_url(@product.facility, @product, start_at: @time_range.start_at&.iso8601), time_range: @time_range, product: @product, facility: @product.facility, diff --git a/app/views/product_notification_mailer/slot_available.txt.erb b/app/views/product_notification_mailer/slot_available.txt.erb index 1f257b1320..5bfc71c057 100644 --- a/app/views/product_notification_mailer/slot_available.txt.erb +++ b/app/views/product_notification_mailer/slot_available.txt.erb @@ -1,6 +1,6 @@ <%= text("body", user_name: @user.full_name, - new_reservation_url: new_facility_instrument_single_reservation_url(@product.facility, @product), + new_reservation_url: new_facility_instrument_single_reservation_url(@product.facility, @product, start_at: @time_range.start_at&.iso8601), time_range: @time_range, product: @product, facility: @product.facility) diff --git a/spec/requests/single_reservations_spec.rb b/spec/requests/single_reservations_spec.rb new file mode 100644 index 0000000000..a9cc5ab456 --- /dev/null +++ b/spec/requests/single_reservations_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "SingleReservationsController" do + describe "new" do + let(:user) { create(:user) } + let(:instrument) { create(:setup_instrument) } + let(:facility) { instrument.facility } + + let(:action) do + lambda do |params = {}| + get new_facility_instrument_single_reservation_path(facility, instrument, **params) + end + end + + before do + login_as user + end + + context "when start_at param is provided" do + let(:start_at) { 1.day.from_now } + + it "finds next available with that value" do + expect(NextAvailableReservationFinder).to( + receive(:new) + .with(instrument, start_at:) + ).and_call_original + + action.call(start_at:) + end + + context "when start at is invalid" do + let(:start_at) { "Some invalid date" } + + it "finds next available with start_at nil" do + expect(NextAvailableReservationFinder).to( + receive(:new) + .with(instrument, start_at: nil) + ).and_call_original + + action.call(start_at:) + end + end + end + end +end diff --git a/spec/services/next_available_reservation_finder_spec.rb b/spec/services/next_available_reservation_finder_spec.rb index 39eb588c8f..08a6800305 100644 --- a/spec/services/next_available_reservation_finder_spec.rb +++ b/spec/services/next_available_reservation_finder_spec.rb @@ -4,7 +4,9 @@ RSpec.describe NextAvailableReservationFinder do let(:user) { build(:user) } - subject(:reservation) { described_class.new(instrument).next_available_for(user, user) } + subject(:reservation) do + described_class.new(instrument).next_available_for(user, user) + end describe "time scheduled instrument" do let(:instrument) { build(:instrument, min_reserve_mins: 0) } @@ -66,4 +68,29 @@ end end end + + describe "start time specified" do + let(:start_at) { 10.days.from_now } + let(:instrument) { build(:instrument, min_reserve_mins: 0) } + let(:finder) do + described_class + .new(instrument, start_at:) + end + let(:reservation) do + finder.next_available_for(user, user) + end + + it "returns a reservation starting on start_at" do + expect(reservation.reserve_start_at).to eq(start_at) + end + + it "calls next available reservation after start_at" do + expect(instrument).to( + receive(:next_available_reservation) + .with(a_hash_including(after: start_at)) + ) + + finder.next_available_for(user, user) + end + end end From 8975956ca9dcb4faa0495ed5ba8b19db9d6f2c41 Mon Sep 17 00:00:00 2001 From: Joaquin Correa Date: Tue, 30 Jun 2026 13:03:20 -0300 Subject: [PATCH 3/3] Use attr reader --- app/services/next_available_reservation_finder.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/next_available_reservation_finder.rb b/app/services/next_available_reservation_finder.rb index aa5a6ae164..3a47e9625f 100644 --- a/app/services/next_available_reservation_finder.rb +++ b/app/services/next_available_reservation_finder.rb @@ -11,8 +11,8 @@ def initialize(product, start_at: nil) end def next_available_for(current_user, acting_user) - options = current_user.can_override_restrictions?(@product) ? {} : { user: acting_user } - next_available = @product.next_available_reservation( + options = current_user.can_override_restrictions?(product) ? {} : { user: acting_user } + next_available = product.next_available_reservation( after: start_at, duration: default_duration, options: @@ -24,16 +24,16 @@ def next_available_for(current_user, acting_user) private def default_reservation - Reservation.new(product: @product, + Reservation.new(product:, reserve_start_at: start_at, reserve_end_at: start_at + default_duration) end def default_duration - if @product.daily_booking? - (@product.min_reserve_days || 1).days + if product.daily_booking? + (product.min_reserve_days || 1).days else - (@product.min_reserve_mins.to_i > 0 ? @product.min_reserve_mins : 30).minutes + (product.min_reserve_mins.to_i > 0 ? product.min_reserve_mins : 30).minutes end end end