diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 395f14b9a9..006a318a4b 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -97,7 +97,8 @@ def organization_params :repackage_essentials, :distribute_monthly, :ndbn_member_id, :enable_child_based_requests, :enable_individual_requests, :enable_quantity_based_requests, - :ytd_on_distribution_printout, partner_form_fields: [] + :ytd_on_distribution_printout, :one_step_partner_invite, + partner_form_fields: [] ) end diff --git a/app/controllers/partners_controller.rb b/app/controllers/partners_controller.rb index ceb8f297d0..fadbf5d213 100644 --- a/app/controllers/partners_controller.rb +++ b/app/controllers/partners_controller.rb @@ -49,6 +49,28 @@ def approve_application end end + def invite_and_approve + # Invite the partner + partner = current_organization.partners.find(params[:id]) + + partner_invite_service = PartnerInviteService.new(partner: partner, force: true) + partner_invite_service.call + + # If no errors inviting, then approve the partner + if partner_invite_service.errors.none? + partner_approval_service = PartnerApprovalService.new(partner: partner) + partner_approval_service.call + + if partner_approval_service.errors.none? + redirect_to partners_path, notice: "Partner invited and approved!" + else + redirect_to partners_path, error: "Failed to approve partner because: #{partner_approval_service.errors.full_messages}" + end + else + redirect_to partners_path, notice: "Failed to invite #{partner.name}! #{partner_invite_service.errors.full_messages}" + end + end + def show @partner = current_organization.partners.find(params[:id]) @impact_metrics = @partner.impact_metrics unless @partner.uninvited? diff --git a/app/models/organization.rb b/app/models/organization.rb index 4ce918a881..d5927c7c3d 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -2,33 +2,34 @@ # # Table name: organizations # -# id :integer not null, primary key -# city :string -# deadline_day :integer -# default_storage_location :integer -# distribute_monthly :boolean default(FALSE), not null -# email :string -# enable_child_based_requests :boolean default(TRUE), not null -# enable_individual_requests :boolean default(TRUE), not null -# enable_quantity_based_requests :boolean default(TRUE), not null -# intake_location :integer -# invitation_text :text -# latitude :float -# longitude :float -# name :string -# partner_form_fields :text default([]), is an Array -# reminder_day :integer -# repackage_essentials :boolean default(FALSE), not null -# short_name :string -# state :string -# street :string -# url :string -# ytd_on_distribution_printout :boolean default(TRUE), not null -# zipcode :string -# created_at :datetime not null -# updated_at :datetime not null -# account_request_id :integer -# ndbn_member_id :bigint +# id :integer not null, primary key +# city :string +# deadline_day :integer +# default_storage_location :integer +# distribute_monthly :boolean default(FALSE), not null +# email :string +# enable_child_based_requests :boolean default(TRUE), not null +# enable_individual_requests :boolean default(TRUE), not null +# enable_quantity_based_requests :boolean default(TRUE), not null +# intake_location :integer +# invitation_text :text +# latitude :float +# longitude :float +# name :string +# partner_form_fields :text default([]), is an Array +# reminder_day :integer +# repackage_essentials :boolean default(FALSE), not null +# short_name :string +# state :string +# street :string +# url :string +# one_step_partner_invite :boolean default(FALSE), not null +# ytd_on_distribution_printout :boolean default(TRUE), not null +# zipcode :string +# created_at :datetime not null +# updated_at :datetime not null +# account_request_id :integer +# ndbn_member_id :bigint # class Organization < ApplicationRecord diff --git a/app/views/organizations/_details.html.erb b/app/views/organizations/_details.html.erb index 091703e490..b30547f3e6 100644 --- a/app/views/organizations/_details.html.erb +++ b/app/views/organizations/_details.html.erb @@ -152,6 +152,12 @@ <%= humanize_boolean(@organization.ytd_on_distribution_printout) %>

+
+

Use One step Partner invite and approve process?

+

+ <%= humanize_boolean(@organization.one_step_partner_invite) %> +

+
<% if @organization.logo.attached? %>

Logo

diff --git a/app/views/organizations/edit.html.erb b/app/views/organizations/edit.html.erb index 0d36718c20..7c408f7ba0 100644 --- a/app/views/organizations/edit.html.erb +++ b/app/views/organizations/edit.html.erb @@ -85,6 +85,7 @@ <%= f.input :enable_individual_requests, label: 'Enable partners to make requests for individuals?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> <%= f.input :enable_quantity_based_requests, label: 'Enable partners to make quantity-based requests?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> <%= f.input :ytd_on_distribution_printout, label: 'Show Year-to-date values on distribution printout?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> + <%= f.input :one_step_partner_invite, label: 'Use One Step Invite and Approve partner process?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> <% default_email_text_hint = "You can use the variables %{partner_name}, %{delivery_method}, %{distribution_date}, and %{comment} to include the partner's name, delivery method, distribution date, and comments sent in the request." %> <%= f.input :default_email_text, label: "Distribution Email Content", hint: default_email_text_hint.html_safe do %> diff --git a/app/views/partners/_partner_row.html.erb b/app/views/partners/_partner_row.html.erb index 5aff732275..e295988684 100644 --- a/app/views/partners/_partner_row.html.erb +++ b/app/views/partners/_partner_row.html.erb @@ -1,4 +1,6 @@ <% status = partner_row.status %> +<% can_one_step_invite_and_approve = partner_row.organization.one_step_partner_invite %> + <%= link_to partner_row.name, partner_path(partner_row) %> <%= link_to partner_row.email, "mailto:#{partner_row.email}" %> @@ -25,7 +27,12 @@ <% case status %> <% when "uninvited" %> - <%= invite_button_to(invite_partner_path(partner_row), confirm: "Send an invitation to #{partner_row.name} to begin using the partner application?") %> + <% if can_one_step_invite_and_approve %> + <% button_options = { icon: "envelope", type: "warning", text: "Invite and Approve", size: "xs", confirm: "One step invite and approve #{partner_row.name} to begin using the partner application?" } %> + <%= invite_button_to(invite_and_approve_partner_path(partner_row), button_options) %> + <% else %> + <%= invite_button_to(invite_partner_path(partner_row), confirm: "Send an invitation to #{partner_row.name} to begin using the partner application?") %> + <% end %> <% when "invited" %> <%= view_button_to partner_path(partner_row) + "#partner-information", { text: "Review Application", icon: "check", type: "warning" } %> <%= invite_button_to(invite_partner_path(partner_row), confirm: "Re-send an invitation to #{partner_row.name}?", text: 'Re-send Invite') %> diff --git a/config/routes.rb b/config/routes.rb index 05e5b017f4..f357598263 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -177,6 +177,7 @@ def set_up_flipper patch :profile get :approve_application post :invite + post :invite_and_approve post :invite_partner_user post :recertify_partner put :deactivate diff --git a/db/migrate/20240131202431_add_one_step_partner_invite_to_organization.rb b/db/migrate/20240131202431_add_one_step_partner_invite_to_organization.rb new file mode 100644 index 0000000000..79fcd23775 --- /dev/null +++ b/db/migrate/20240131202431_add_one_step_partner_invite_to_organization.rb @@ -0,0 +1,6 @@ +class AddOneStepPartnerInviteToOrganization < ActiveRecord::Migration[7.0] + def change + add_column :organizations, :one_step_partner_invite, :boolean, null: false + change_column_default :organizations, :one_step_partner_invite, false + end +end diff --git a/db/schema.rb b/db/schema.rb index e6508b61b5..81e735ffdc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_12_29_200106) do +ActiveRecord::Schema[7.0].define(version: 2024_01_31_202431) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -478,6 +478,7 @@ t.boolean "enable_individual_requests", default: true, null: false t.boolean "enable_quantity_based_requests", default: true, null: false t.boolean "ytd_on_distribution_printout", default: true, null: false + t.boolean "one_step_partner_invite", default: false, null: false t.index ["latitude", "longitude"], name: "index_organizations_on_latitude_and_longitude" t.index ["short_name"], name: "index_organizations_on_short_name" end diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index cc03213cc8..2614552e12 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -2,33 +2,34 @@ # # Table name: organizations # -# id :integer not null, primary key -# city :string -# deadline_day :integer -# default_storage_location :integer -# distribute_monthly :boolean default(FALSE), not null -# email :string -# enable_child_based_requests :boolean default(TRUE), not null -# enable_individual_requests :boolean default(TRUE), not null -# enable_quantity_based_requests :boolean default(TRUE), not null -# intake_location :integer -# invitation_text :text -# latitude :float -# longitude :float -# name :string -# partner_form_fields :text default([]), is an Array -# reminder_day :integer -# repackage_essentials :boolean default(FALSE), not null -# short_name :string -# state :string -# street :string -# url :string -# ytd_on_distribution_printout :boolean default(TRUE), not null -# zipcode :string -# created_at :datetime not null -# updated_at :datetime not null -# account_request_id :integer -# ndbn_member_id :bigint +# id :integer not null, primary key +# city :string +# deadline_day :integer +# default_storage_location :integer +# distribute_monthly :boolean default(FALSE), not null +# email :string +# enable_child_based_requests :boolean default(TRUE), not null +# enable_individual_requests :boolean default(TRUE), not null +# enable_quantity_based_requests :boolean default(TRUE), not null +# intake_location :integer +# invitation_text :text +# latitude :float +# longitude :float +# name :string +# partner_form_fields :text default([]), is an Array +# reminder_day :integer +# repackage_essentials :boolean default(FALSE), not null +# short_name :string +# state :string +# street :string +# url :string +# one_step_partner_invite :boolean default(FALSE), not null +# ytd_on_distribution_printout :boolean default(TRUE), not null +# zipcode :string +# created_at :datetime not null +# updated_at :datetime not null +# account_request_id :integer +# ndbn_member_id :bigint # FactoryBot.define do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index bf9a70c2fd..4e16366d73 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: events +# +# id :bigint not null, primary key +# data :jsonb +# event_time :datetime not null +# eventable_type :string +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# eventable_id :bigint +# organization_id :bigint +# user_id :bigint +# RSpec.describe Event, type: :model do let(:organization) { FactoryBot.create(:organization) } describe "#most_recent_snapshot" do diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 8ef4d53e5d..6a8bc16688 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -2,33 +2,34 @@ # # Table name: organizations # -# id :integer not null, primary key -# city :string -# deadline_day :integer -# default_storage_location :integer -# distribute_monthly :boolean default(FALSE), not null -# email :string -# enable_child_based_requests :boolean default(TRUE), not null -# enable_individual_requests :boolean default(TRUE), not null -# enable_quantity_based_requests :boolean default(TRUE), not null -# intake_location :integer -# invitation_text :text -# latitude :float -# longitude :float -# name :string -# partner_form_fields :text default([]), is an Array -# reminder_day :integer -# repackage_essentials :boolean default(FALSE), not null -# short_name :string -# state :string -# street :string -# url :string -# ytd_on_distribution_printout :boolean default(TRUE), not null -# zipcode :string -# created_at :datetime not null -# updated_at :datetime not null -# account_request_id :integer -# ndbn_member_id :bigint +# id :integer not null, primary key +# city :string +# deadline_day :integer +# default_storage_location :integer +# distribute_monthly :boolean default(FALSE), not null +# email :string +# enable_child_based_requests :boolean default(TRUE), not null +# enable_individual_requests :boolean default(TRUE), not null +# enable_quantity_based_requests :boolean default(TRUE), not null +# intake_location :integer +# invitation_text :text +# latitude :float +# longitude :float +# name :string +# partner_form_fields :text default([]), is an Array +# reminder_day :integer +# repackage_essentials :boolean default(FALSE), not null +# short_name :string +# state :string +# street :string +# url :string +# one_step_partner_invite :boolean default(FALSE), not null +# ytd_on_distribution_printout :boolean default(TRUE), not null +# zipcode :string +# created_at :datetime not null +# updated_at :datetime not null +# account_request_id :integer +# ndbn_member_id :bigint # RSpec.describe Organization, type: :model do diff --git a/spec/requests/partners_requests_spec.rb b/spec/requests/partners_requests_spec.rb index c7472bb914..38240d9124 100644 --- a/spec/requests/partners_requests_spec.rb +++ b/spec/requests/partners_requests_spec.rb @@ -423,4 +423,65 @@ end end end + + describe "POST #invite_and_approve" do + let(:partner) { create(:partner, organization: @organization) } + + context "when invitation succeeded and approval succeed" do + before do + fake_partner_invite_service = instance_double(PartnerInviteService, call: nil, errors: []) + allow(PartnerInviteService).to receive(:new).and_return(fake_partner_invite_service) + + fake_partner_approval_service = instance_double(PartnerApprovalService, call: nil, errors: []) + allow(PartnerApprovalService).to receive(:new).with(partner: partner).and_return(fake_partner_approval_service) + end + + it "sends invitation email and approve partner in single step" do + post invite_and_approve_partner_path(default_params.merge(id: partner.id)) + + expect(PartnerInviteService).to have_received(:new).with(partner: partner, force: true) + expect(response).to have_http_status(:found) + + expect(PartnerApprovalService).to have_received(:new).with(partner: partner) + expect(response).to redirect_to(partners_path(organization_id: @organization.to_param)) + expect(flash[:notice]).to eq("Partner invited and approved!") + end + end + + context "when invitation failed" do + let(:fake_error_msg) { Faker::Games::ElderScrolls.dragon } + + before do + fake_partner_invite_service = instance_double(PartnerInviteService, call: nil) + allow(PartnerInviteService).to receive(:new).with(partner: partner, force: true).and_return(fake_partner_invite_service) + allow(fake_partner_invite_service).to receive_message_chain(:errors, :none?).and_return(false) + allow(fake_partner_invite_service).to receive_message_chain(:errors, :full_messages).and_return(fake_error_msg) + end + + it "should redirect to the partners index page with a notice flash message" do + post invite_and_approve_partner_path(default_params.merge(id: partner.id)) + + expect(response).to redirect_to(partners_path(organization_id: @organization.to_param)) + expect(flash[:notice]).to eq("Failed to invite #{partner.name}! #{fake_error_msg}") + end + end + + context "when approval fails" do + let(:fake_error_msg) { Faker::Games::ElderScrolls.dragon } + + before do + fake_partner_approval_service = instance_double(PartnerApprovalService, call: nil) + allow(PartnerApprovalService).to receive(:new).with(partner: partner).and_return(fake_partner_approval_service) + allow(fake_partner_approval_service).to receive_message_chain(:errors, :none?).and_return(false) + allow(fake_partner_approval_service).to receive_message_chain(:errors, :full_messages).and_return(fake_error_msg) + end + + it "should redirect to the partners index page with a notice flash message" do + post invite_and_approve_partner_path(default_params.merge(id: partner.id)) + + expect(response).to redirect_to(partners_path(organization_id: @organization.to_param)) + expect(flash[:error]).to eq("Failed to approve partner because: #{fake_error_msg}") + end + end + end end diff --git a/spec/system/organization_system_spec.rb b/spec/system/organization_system_spec.rb index b4f83faf18..d0b96bd6de 100644 --- a/spec/system/organization_system_spec.rb +++ b/spec/system/organization_system_spec.rb @@ -26,6 +26,8 @@ describe "Viewing the organization" do it "can view organization details", :aggregate_failures do + @organization.update!(one_step_partner_invite: true) + visit organization_path(@organization) expect(page.find("h1")).to have_text(@organization.name) @@ -44,6 +46,7 @@ expect(page).to have_content("Quantity Based Requests?") expect(page).to have_content("Show Year-to-date values on distribution printout?") expect(page).to have_content("Logo") + expect(page).to have_content("Use One step Partner invite and approve process?") end end @@ -116,6 +119,20 @@ expect(page).to_not have_content('Media Information') expect(@organization.reload.partner_form_fields).to eq([]) end + + it "can disable if the org does NOT use single step invite and approve partner process" do + choose("organization[one_step_partner_invite]", option: false) + + click_on "Save" + expect(page).to have_content("No") + end + + it "can enable if the org uses single step invite and approve partner process" do + choose("organization[one_step_partner_invite]", option: true) + + click_on "Save" + expect(page).to have_content("Yes") + end end it "can add a new user to an organization" do diff --git a/spec/system/partner_system_spec.rb b/spec/system/partner_system_spec.rb index 58d4bb2f1a..9bf1eff6e8 100644 --- a/spec/system/partner_system_spec.rb +++ b/spec/system/partner_system_spec.rb @@ -112,6 +112,36 @@ end end + describe "one step inviting a partner" do + before do + Partner.delete_all # ensure no pre created partner + end + + let!(:uninvited_partner) { create(:partner, :uninvited) } + + context "when partner is uninvited and one step partner invite setting is on" do + it "shows Invite and Approve button and approves the partner when clicked" do + @organization.update!(one_step_partner_invite: true) + visit url_prefix + "/partners" + + assert page.has_content? "Invite and Approve" + expect do + click_on "Invite and Approve" + end.to change { uninvited_partner.reload.status }.from("uninvited").to("approved") + end + end + + context "when one step partner invite setting is off" do + it "does not show invite and approve button" do + @organization.update!(one_step_partner_invite: false) + + visit url_prefix + "/partners" + + assert page.should have_no_content "Invite and Approve" + end + end + end + describe 'requesting recertification of a partner' do context 'GIVEN a user goes through the process of requesting recertification of partner' do let!(:partner_to_request_recertification) { create(:partner, status: 'approved') }