From 100f6e2f56052844173a6706b4fdf14504f09b07 Mon Sep 17 00:00:00 2001
From: Zach Latta <zach@zachlatta.com>
Date: Tue, 30 Jan 2018 17:43:13 -0800
Subject: [PATCH] Implement rejection of new club applications

---
 .../v1/new_club_applications_controller.rb    |  5 +-
 api/app/models/new_club_application.rb        | 13 +++
 .../new_club_application_serializer.rb        |  5 +-
 ..._add_rejection_to_new_club_applications.rb |  9 +++
 api/db/schema.rb                              |  5 +-
 api/spec/models/new_club_application_spec.rb  | 55 +++++++++++++
 .../requests/v1/new_club_applications_spec.rb | 79 ++++++++++++++++++-
 7 files changed, 166 insertions(+), 5 deletions(-)
 create mode 100644 api/db/migrate/20180131005432_add_rejection_to_new_club_applications.rb

diff --git a/api/app/controllers/v1/new_club_applications_controller.rb b/api/app/controllers/v1/new_club_applications_controller.rb
index 1489b2e08..d16111fb3 100644
--- a/api/app/controllers/v1/new_club_applications_controller.rb
+++ b/api/app/controllers/v1/new_club_applications_controller.rb
@@ -130,7 +130,10 @@ def club_application_params
         :point_of_contact_id,
         :interviewed_at,
         :interview_duration,
-        :interview_notes
+        :interview_notes,
+        :rejected_at,
+        :rejected_reason,
+        :rejected_notes
       )
     end
   end
diff --git a/api/app/models/new_club_application.rb b/api/app/models/new_club_application.rb
index 1c0cf3327..a14450f7c 100644
--- a/api/app/models/new_club_application.rb
+++ b/api/app/models/new_club_application.rb
@@ -26,6 +26,10 @@ class NewClubApplication < ApplicationRecord
     private_school charter_school
   ]
 
+  enum rejected_reason: %i[
+    other
+  ]
+
   with_options if: -> { submitted_at.present? } do |application|
     application.validates :high_school_name,
                           :high_school_type,
@@ -63,6 +67,15 @@ class NewClubApplication < ApplicationRecord
                 interview_notes.present?
             }
 
+  # submitted_at must be set for rejected_at to be set
+  validates :submitted_at, presence: true, if: -> { rejected_at.present? }
+  validates :rejected_at, :rejected_reason, :rejected_notes,
+            presence: true, if: lambda {
+              rejected_at.present? ||
+                rejected_reason.present? ||
+                rejected_notes.present?
+            }
+
   def submit!
     self.submitted_at = Time.current
 
diff --git a/api/app/serializers/new_club_application_serializer.rb b/api/app/serializers/new_club_application_serializer.rb
index b085c9258..0228e81be 100644
--- a/api/app/serializers/new_club_application_serializer.rb
+++ b/api/app/serializers/new_club_application_serializer.rb
@@ -30,9 +30,12 @@ class NewClubApplicationSerializer < ActiveModel::Serializer
              :point_of_contact_id,
              :submitted_at,
              :interviewed_at,
-             :interview_duration
+             :interview_duration,
+             :rejected_at
 
   attribute :interview_notes, if: :admin?
+  attribute :rejected_reason, if: :admin?
+  attribute :rejected_notes, if: :admin?
 
   has_many :leader_profiles
 
diff --git a/api/db/migrate/20180131005432_add_rejection_to_new_club_applications.rb b/api/db/migrate/20180131005432_add_rejection_to_new_club_applications.rb
new file mode 100644
index 000000000..a52ab00fa
--- /dev/null
+++ b/api/db/migrate/20180131005432_add_rejection_to_new_club_applications.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddRejectionToNewClubApplications < ActiveRecord::Migration[5.1]
+  def change
+    add_column :new_club_applications, :rejected_at, :datetime
+    add_column :new_club_applications, :rejected_reason, :integer
+    add_column :new_club_applications, :rejected_notes, :text
+  end
+end
diff --git a/api/db/schema.rb b/api/db/schema.rb
index fe56b42d0..e91b25ccd 100644
--- a/api/db/schema.rb
+++ b/api/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: 20180130224937) do
+ActiveRecord::Schema.define(version: 20180131005432) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -278,6 +278,9 @@
     t.datetime "interviewed_at"
     t.text "interview_notes"
     t.integer "interview_duration"
+    t.datetime "rejected_at"
+    t.integer "rejected_reason"
+    t.text "rejected_notes"
     t.index ["point_of_contact_id"], name: "index_new_club_applications_on_point_of_contact_id"
   end
 
diff --git a/api/spec/models/new_club_application_spec.rb b/api/spec/models/new_club_application_spec.rb
index 50c4ca63f..621598873 100644
--- a/api/spec/models/new_club_application_spec.rb
+++ b/api/spec/models/new_club_application_spec.rb
@@ -55,9 +55,14 @@
   it { should have_db_column :interview_duration }
   it { should have_db_column :interview_notes }
 
+  it { should have_db_column :rejected_at }
+  it { should have_db_column :rejected_reason }
+  it { should have_db_column :rejected_notes }
+
   ## enums ##
 
   it { should define_enum_for :high_school_type }
+  it { should define_enum_for :rejected_reason }
 
   ## validations ##
 
@@ -129,6 +134,56 @@
     end
   end
 
+  describe 'rejected fields' do
+    subject { create(:completed_new_club_application) }
+    before { subject.submit! }
+
+    it 'does not allow rejected_at to be set unless submitted_at is' do
+      subject.submitted_at = nil
+      subject.rejected_at = Time.current
+
+      subject.validate
+      expect(subject.errors).to include('submitted_at')
+    end
+
+    it 'should require other fields to be set if rejected_at is' do
+      expect(subject.valid?).to be(true)
+
+      subject.rejected_at = Time.current
+
+      subject.validate
+      expect(subject.errors).to include('rejected_reason')
+      expect(subject.errors).to include('rejected_notes')
+    end
+
+    it 'should require other fields to be set if rejected_reason is' do
+      expect(subject.valid?).to be(true)
+
+      subject.rejected_reason = :other
+
+      subject.validate
+      expect(subject.errors).to include('rejected_at')
+      expect(subject.errors).to include('rejected_notes')
+    end
+
+    it 'should require other fields to be set if rejected_notes is' do
+      expect(subject.valid?).to be(true)
+
+      subject.rejected_notes = "Didn't have faith in leadship skills."
+
+      subject.validate
+      expect(subject.errors).to include('rejected_at')
+      expect(subject.errors).to include('rejected_reason')
+    end
+
+    it 'should be valid if all rejected fields are set' do
+      subject.rejected_at = Time.current
+      subject.rejected_reason = :other
+      subject.rejected_notes = 'Example rejection reason.'
+      expect(subject.valid?).to eq(true)
+    end
+  end
+
   describe ':submit!' do
     subject { create(:completed_new_club_application, profile_count: 3) }
     let(:user) { subject.point_of_contact }
diff --git a/api/spec/requests/v1/new_club_applications_spec.rb b/api/spec/requests/v1/new_club_applications_spec.rb
index 61cb59802..9ec78769d 100644
--- a/api/spec/requests/v1/new_club_applications_spec.rb
+++ b/api/spec/requests/v1/new_club_applications_spec.rb
@@ -136,9 +136,14 @@
       expect(json).to include('interviewed_at')
       expect(json).to include('interview_duration')
       expect(json).to_not include('interview_notes')
+
+      # includes rejected_at, but not rejected_reason or rejected_notes
+      expect(json).to include('rejected_at')
+      expect(json).to_not include('rejected_reason')
+      expect(json).to_not include('rejected_notes')
     end
 
-    it 'includes interview_notes when authed as an admin' do
+    it 'includes private fields when authed as an admin' do
       user.make_admin!
       user.save
 
@@ -146,7 +151,14 @@
           headers: auth_headers
 
       expect(response.status).to eq(200)
+
+      expect(json).to include('interviewed_at')
+      expect(json).to include('interview_duration')
       expect(json).to include('interview_notes')
+
+      expect(json).to include('rejected_at')
+      expect(json).to include('rejected_reason')
+      expect(json).to include('rejected_notes')
     end
 
     it '404s when application does not exist' do
@@ -309,6 +321,28 @@
       expect(response.status).to eq(422)
     end
 
+    it 'fails to update rejection fields' do
+      application = create(:completed_new_club_application)
+      create(:completed_leader_profile, new_club_application: application,
+                                        user: user)
+      application.update_attributes(point_of_contact: user)
+
+      # application must be submitted for any modification (even by admins) to
+      # be allowed
+      post "/v1/new_club_applications/#{application.id}/submit",
+           headers: auth_headers
+
+      patch "/v1/new_club_applications/#{application.id}",
+            headers: auth_headers,
+            params: {
+              rejected_at: Time.current,
+              rejected_reason: :other,
+              rejected_notes: 'Example reason'
+            }
+
+      expect(response.status).to eq(422)
+    end
+
     context 'when admin' do
       let(:club_application) do
         app = create(:completed_new_club_application)
@@ -352,7 +386,7 @@
         expect(json['errors']).to include('interview_notes')
       end
 
-      it 'fails if application is not submitted' do
+      it 'fails to update interview fields if application is not submitted' do
         club_application = create(:new_club_application)
 
         patch "/v1/new_club_applications/#{club_application.id}",
@@ -364,6 +398,47 @@
         expect(response.status).to eq(422)
         expect(json['errors']['submitted_at']).to include("can't be blank")
       end
+
+      it 'allows updating rejected fields' do
+        patch "/v1/new_club_applications/#{club_application.id}",
+              headers: auth_headers,
+              params: {
+                rejected_at: Time.current,
+                rejected_reason: :other,
+                rejected_notes: 'Example reason'
+              }
+
+        expect(response.status).to eq(200)
+        expect(
+          Time.zone.parse(json['rejected_at'])
+        ).to be_within(3.seconds).of(Time.current)
+        expect(json).to include('rejected_reason' => 'other')
+        expect(json).to include('rejected_notes' => 'Example reason')
+      end
+
+      it 'fails if not all rejected fields are set' do
+        patch "/v1/new_club_applications/#{club_application.id}",
+              headers: auth_headers,
+              params: {
+                rejected_at: Time.current
+              }
+
+        expect(response.status).to eq(422)
+        expect(json['errors']).to include('rejected_reason', 'rejected_notes')
+      end
+
+      it 'fails to update rejected fields if application is not submitted' do
+        club_application = create(:new_club_application)
+
+        patch "/v1/new_club_applications/#{club_application.id}",
+              headers: auth_headers,
+              params: {
+                rejected_at: Time.current
+              }
+
+        expect(response.status).to eq(422)
+        expect(json['errors']['submitted_at']).to include("can't be blank")
+      end
     end
   end