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') }
|