diff --git a/app/controllers/admin/memberships_controller.rb b/app/controllers/admin/memberships_controller.rb index 0ad4daf0..7d9eb8d0 100644 --- a/app/controllers/admin/memberships_controller.rb +++ b/app/controllers/admin/memberships_controller.rb @@ -60,9 +60,29 @@ def unmake_admin redirect_to admin_memberships_path end + # updating scholarship status + #if requested --> approve (approve=true), revoke (approve=false) + #if approved --> revoke (approve=true), continue (approve=true) + #if not-requested --> approve + def approve_or_continue_scholarship + user = User.find(params[:id]) + if user.scholarship_since? + user.scholarship_continued + else + user.scholarship_approved + end + redirect_to admin_memberships_path + end + + def remove_scholarship + user = User.find(params[:id]) + user.scholarship_rejected_or_revoked + redirect_to admin_memberships_path + end + private def user_params - params.require(:user).permit(:is_scholarship) + params.require(:user) end end diff --git a/app/controllers/members/dues_controller.rb b/app/controllers/members/dues_controller.rb index e649bf8d..2ab9baec 100644 --- a/app/controllers/members/dues_controller.rb +++ b/app/controllers/members/dues_controller.rb @@ -61,6 +61,7 @@ def update end def scholarship_request + current_user.request_scholarship DuesMailer.scholarship_requested(current_user, params[:reason]).deliver_now redirect_to members_user_dues_path, notice: "Your scholarship request has been submitted" diff --git a/app/models/concerns/admin_user.rb b/app/models/concerns/admin_user.rb index 1d7a7540..fbfd4907 100644 --- a/app/models/concerns/admin_user.rb +++ b/app/models/concerns/admin_user.rb @@ -14,4 +14,18 @@ def unmake_admin! self.is_admin = false save! end + + def scholarship_approved + update_attributes!({scholarship_since: DateTime.now, scholarship_last_checkin: DateTime.now}) + end + + def scholarship_rejected_or_revoked + update_attributes!({requested_scholarship: nil, scholarship_since: nil, scholarship_last_checkin: nil}) + end + + def scholarship_continued + return unless scholarship_since? + + touch :scholarship_last_checkin + end end diff --git a/app/models/user.rb b/app/models/user.rb index b87c695c..91c5173b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,7 +4,9 @@ class User < ApplicationRecord EMAIL_PATTERN = /\A.+@.+\Z/ attr_accessible :username, :name, :email, :profile_attributes, :pronounceable_name, - :application_attributes, :email_for_google, :dues_pledge, :is_scholarship, :voting_policy_agreement + :application_attributes, :email_for_google, :dues_pledge, + :requested_scholarship, :scholarship_since, :scholarship_last_checkin, + :voting_policy_agreement validates :state, presence: true @@ -164,6 +166,10 @@ def update_stripe_record ) end + def request_scholarship + touch :requested_scholarship + end + def display_state state.tr("_", " ") end @@ -219,12 +225,14 @@ def gravatar_email # email :string # email_for_google :string # is_admin :boolean default(FALSE) -# is_scholarship :boolean default(FALSE) # last_logged_in_at :datetime # last_stripe_charge_succeeded :datetime # membership_note :text # name :string # pronounceable_name :string +# requested_scholarship :datetime +# scholarship_last_checkin :datetime +# scholarship_since :datetime # setup_complete :boolean # state :string not null # username :string diff --git a/app/service_objects/account_setup_reminder.rb b/app/service_objects/account_setup_reminder.rb index cb6fb33e..ba892e2e 100644 --- a/app/service_objects/account_setup_reminder.rb +++ b/app/service_objects/account_setup_reminder.rb @@ -8,6 +8,9 @@ def send_emails processed_at = user.application.processed_at next unless processed_at + # if member requested a scholarship and it hasn't been approved yet, then don't send emails + next if user.requested_scholarship.present? and not user.scholarship_since.present? + if processed_at < 2.days.ago && processed_at > 4.days.ago NewMembersMailer.three_day_reminder(user).deliver_now elsif processed_at < 6.days.ago && processed_at > 8.days.ago diff --git a/app/views/admin/_new_member.html.haml b/app/views/admin/_new_member.html.haml index de93a727..9b0d3d90 100644 --- a/app/views/admin/_new_member.html.haml +++ b/app/views/admin/_new_member.html.haml @@ -12,7 +12,7 @@ .col-md-4 %ul.list-unstyled %li - %b Approved on #{ new_member.application.processed_at.strftime("%b %-d") } + %b Approved on #{ new_member.application.processed_at? ? new_member.application.processed_at.strftime("%b %-d") : '---' } %li - if new_member.email_for_google %b Google email: @@ -21,12 +21,11 @@ .admin-warning No Google email %li - - if new_member.last_stripe_charge_succeeded - %b Stripe Payment Made On: - = new_member.last_stripe_charge_succeeded + - if new_member.last_stripe_charge_succeeded or new_member.scholarship_since? + Stripe charge succeeded OR Scholarship approved - else .admin-warning - Dues Not Set Up + Dues Not Set Up or Waiting on Scholarship .col-md-4 = form_for :user, url: admin_save_membership_note_path do |f| = f.hidden_field :id, value: new_member.id diff --git a/app/views/admin/dues.html.haml b/app/views/admin/dues.html.haml index 9ab4982c..bc5675f0 100644 --- a/app/views/admin/dues.html.haml +++ b/app/views/admin/dues.html.haml @@ -26,6 +26,6 @@ - if user.name.present? = link_to user.name, members_user_path(user) %td= user.email - %td= user.is_scholarship + %td= user.scholarship_since.present? %td= user.stripe_customer_id.present? ? "Yes" : "No" %td= user.last_stripe_charge_succeeded.strftime('%b %d %Y') if user.last_stripe_charge_succeeded diff --git a/app/views/admin/memberships/_members.html.haml b/app/views/admin/memberships/_members.html.haml index a6645991..6dc14eef 100644 --- a/app/views/admin/memberships/_members.html.haml +++ b/app/views/admin/memberships/_members.html.haml @@ -28,14 +28,36 @@ Not set %td= user.display_state %td - = user.is_scholarship ? "Yes" : "No" - = form_for user, url: admin_membership_path(user) do |f| + -# 'No' if no request or scholarship status. Button for enabling + -# 'Requested/Approved/Checkin' dates if any status. Buttons for continuing or removing + - if not user.requested_scholarship.present? and not user.scholarship_since.present? + = "No" + - else + %table + %tr + %td + = "Requested:" + %td + = "#{user.requested_scholarship.present? ? user.requested_scholarship.try(:strftime, '%Y-%m-%d') : '---'}" + %tr + %td + = "Approved:" + %td + = "#{user.scholarship_since.present? ? user.scholarship_since.try(:strftime, '%Y-%m-%d') : '---'}" + %tr + %td + = "Last Checkin:" + %td + = "#{user.scholarship_last_checkin.try(:strftime, '%Y-%m-%d')}" + + = form_for user, url: admin_approve_or_continue_scholarship_path(user) do |f| = f.hidden_field(:id) - = f.hidden_field(:is_scholarship, value: !user.is_scholarship) - - if user.is_scholarship + = f.submit "#{user.scholarship_since.present? ? 'Continue scholarship' : 'Allow scholarship'}", class: "btn", data: { confirm: "Are you sure? Note: This does not change the member's dues in Stripe."} + + - if user.requested_scholarship.present? or user.scholarship_since.present? + = form_for user, url: admin_remove_scholarship_path(user) do |f| + = f.hidden_field(:id) = f.submit "Remove scholarship", class: "btn", data: { confirm: "Are you sure? Note: This does not change the member's dues in Stripe." } - - else - = f.submit "Mark as on scholarship", class: "btn", data: { confirm: "Are you sure? Note: This does not change the member's dues in Stripe." } %td= user.last_stripe_charge_succeeded.strftime('%b %d %Y') if user.last_stripe_charge_succeeded diff --git a/app/views/members/dues/show.html.haml b/app/views/members/dues/show.html.haml index 28a42817..c78eced7 100644 --- a/app/views/members/dues/show.html.haml +++ b/app/views/members/dues/show.html.haml @@ -22,7 +22,7 @@ %b #{number_to_currency (@subscription.plan.amount / 100)} per #{@subscription.plan.interval}, and your status is #{@subscription.status}. -- elsif current_user.is_scholarship? +- elsif current_user.scholarship_since.present? %p You are receiving a scholarship for your dues payments. - else %p You don't have a Stripe subscription yet, or it may have been canceled. Please add your credit or debit card, or request a scholarship below! <3 diff --git a/config/routes.rb b/config/routes.rb index a6b4fb99..70b1defd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,8 @@ patch "memberships/:id/change_membership_state" => "memberships#change_membership_state", :as => "change_membership_state" patch "memberships/:id/make_admin" => "memberships#make_admin", :as => "make_admin" patch "memberships/:id/unmake_admin" => "memberships#unmake_admin", :as => "unmake_admin" + patch "memberships/:id/approve_or_continue_scholarship" => "memberships#approve_or_continue_scholarship", :as => "approve_or_continue_scholarship" + patch "memberships/:id/remove_scholarship" => "memberships#remove_scholarship", :as => "remove_scholarship" end get "admin/new_members" => "admin#new_members" diff --git a/db/migrate/20221010052254_add_scholarship_since_to_user.rb b/db/migrate/20221010052254_add_scholarship_since_to_user.rb new file mode 100644 index 00000000..384b4715 --- /dev/null +++ b/db/migrate/20221010052254_add_scholarship_since_to_user.rb @@ -0,0 +1,34 @@ +class AddScholarshipSinceToUser < ActiveRecord::Migration[6.0] + def change + # Currently have a boolean column is_scholarship + # Add a column requested_scholarship + # that can be used to show who's requested a scholarship (approved or waiting) + # Add a column scholarship_since + # to show date the scholarship was approved + # Add a column scholarship_last_checkin + # to show the last check-in and member requested to continue scholarship + # For default date values, if is_scholarship is true, + # will set dates to 2022-07-10 (as approximate time of last checkin) + # To revert this migration, will have to go back to single boolean column + + reversible do |dir| + dir.up do + add_column :users, :requested_scholarship, :timestamp, default: nil + add_column :users, :scholarship_since, :timestamp, default: nil + add_column :users, :scholarship_last_checkin, :timestamp, default: nil + + execute "UPDATE users SET requested_scholarship = '2022-07-10', scholarship_since = '2022-07-10', scholarship_last_checkin = '2022-07-10' WHERE is_scholarship = true" + + remove_column :users, :is_scholarship + end + + dir.down do + add_column :users, :is_scholarship, :boolean, default: false + + execute "UPDATE users SET is_scholarship = true WHERE scholarship_since IS NOT NULL" + + remove_columns :users, :requested_scholarship, :scholarship_since, :scholarship_last_checkin + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e85f41e8..be86d9dd 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.define(version: 2022_01_29_183750) do +ActiveRecord::Schema.define(version: 2022_10_10_052254) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -112,9 +112,11 @@ t.text "membership_note" t.string "stripe_customer_id" t.datetime "last_stripe_charge_succeeded" - t.boolean "is_scholarship", default: false t.boolean "voting_policy_agreement", default: false t.string "pronounceable_name" + t.datetime "requested_scholarship" + t.datetime "scholarship_since" + t.datetime "scholarship_last_checkin" end create_table "votes", id: :serial, force: :cascade do |t| diff --git a/spec/controllers/admin/memberships_controller_spec.rb b/spec/controllers/admin/memberships_controller_spec.rb index a5985c30..e4728bf0 100644 --- a/spec/controllers/admin/memberships_controller_spec.rb +++ b/spec/controllers/admin/memberships_controller_spec.rb @@ -47,17 +47,51 @@ end end - describe "PUT update" do - subject { put :update, params: params } + describe "PATCH approve_scholarship" do + subject { patch :approve_or_continue_scholarship, params: params } + before { login_as(:member, is_admin: true) } - before { login_as(:voting_member, is_admin: true) } - - context "marking a member as on scholarship" do + context "mark any member not on scholarship as on scholarship" do let(:member) { create :member } - let(:params) { {id: member.id, user: {is_scholarship: true}} } + let(:params) { {id: member.id} } + + it "should set scholarship_since and last_checkin" do + expect { subject }.to change { member.reload.scholarship_since }.from(nil).to be_within(1.second).of Time.now + expect(member.scholarship_last_checkin).to be_within(1.second).of Time.now + end + end + + context "mark a scholarship member as continuing scholarship" do + let(:member) { create :member, scholarship_since: Time.now } + let(:params) { {id: member.id} } + + it "should set scholarship_since and last_checkin" do + expect { subject }.not_to change { member.reload.scholarship_since } + expect(member.scholarship_last_checkin).to be_within(1.second).of Time.now + end + end + end + + describe "PATCH remove_scholarship" do + subject { patch :remove_scholarship, params: params } + before { login_as(:member, is_admin: true) } + + context "mark member requesting scholarship as rejected" do + let(:member) { create :member, requested_scholarship: Time.now} + let(:params) { {id: member.id} } + + it "should remove scholarship request" do + expect { subject }.to change { member.reload.requested_scholarship }.to be_nil + end + end + + context "marking a member as not on scholarship" do + let(:member) { create :member, scholarship_since: Time.now} + let(:params) { {id: member.id} } - it "should mark scholarship as true" do - expect { subject }.to change { member.reload.is_scholarship }.from(false).to(true) + it "should mark scholarship as false" do + expect { subject }.to change { member.reload.scholarship_since }.to be_nil + expect(member.scholarship_last_checkin).to be_nil end end end diff --git a/spec/controllers/members/dues_controller_spec.rb b/spec/controllers/members/dues_controller_spec.rb index 0bfbe47d..ab72c9a9 100644 --- a/spec/controllers/members/dues_controller_spec.rb +++ b/spec/controllers/members/dues_controller_spec.rb @@ -216,10 +216,12 @@ context "logged in as a member" do before { login_as member } - it "sends an email" do + it "sends an email and marks as requested scholarship" do expect { subject }.to change { ActionMailer::Base.deliveries.count }.by(1) expect(ActionMailer::Base.deliveries.last.to).to eq(["scholarship@doubleunion.org", member.email]) expect(ActionMailer::Base.deliveries.last.body).to include "Lemurs are pretty great" + expect(member.reload.requested_scholarship).not_to be_nil + expect(member.reload.requested_scholarship).to be_within(1.second).of Time.now end end end diff --git a/spec/features/update_member_status_spec.rb b/spec/features/update_member_status_spec.rb index bea658f0..95292e55 100644 --- a/spec/features/update_member_status_spec.rb +++ b/spec/features/update_member_status_spec.rb @@ -52,16 +52,28 @@ it "allows members to be marked as on scholarship" do visit admin_memberships_path within(".user-#{member.id}") do - click_button "Mark as on scholarship" + click_button "Allow scholarship" end within(".user-#{member.id}") do - expect(page).to have_content "Yes" + expect(page).to have_content "Approved" + expect(page).to have_content "Last Checkin" end end context "with a scholarship member" do - before { member.update_attributes(is_scholarship: true) } + before { member.update_attributes(scholarship_since: Time.now) } + + it "allows members to be marked as continuing scholarship" do + visit admin_memberships_path + within(".user-#{member.id}") do + click_button "Continue scholarship" + end + + within(".user-#{member.id}") do + expect(page).to have_content "Last Checkin:" + end + end it "allows members to be marked as not on scholarship" do visit admin_memberships_path diff --git a/spec/service_objects/account_setup_reminder_spec.rb b/spec/service_objects/account_setup_reminder_spec.rb index 9b446983..7bf61b21 100644 --- a/spec/service_objects/account_setup_reminder_spec.rb +++ b/spec/service_objects/account_setup_reminder_spec.rb @@ -11,6 +11,8 @@ subject { AccountSetupReminder.new(users).send_emails } + #Steps for setup complete: google-email, dues, membership coordinator manual steps (access to google docs/drive, mailing list) + context "with no reminders needed" do it "sends no emails" do subject @@ -18,6 +20,33 @@ end end + context "scholarship applicant still waiting for approval after 21 days" do + before do + member.request_scholarship + member.application.update_column(:processed_at, 21.days.ago) + other_member.application.update_column(:processed_at, 21.days.ago) + end + + it "sends no emails" do + subject + expect(deliveries.count).to eq 0 + end + end + + context "scholarship applicant got approval within 3 days. But doesn't have setup complete" do + let(:member) { create :member, requested_scholarship: Time.now, scholarship_since: Time.now } + before do + member.application.update_column(:processed_at, 3.days.ago) + other_member.application.update_column(:processed_at, 3.days.ago) + end + + it "sends a reminder email (maybe they don't have google email setup?)" do + subject + expect(deliveries.count).to eq 1 + end + end + + context "with one 3 day reminder" do before do member.application.update_column(:processed_at, 3.days.ago)