effective_memberships 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +114 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_memberships_manifest.js +3 -0
- data/app/assets/javascripts/effective_memberships/applicant_courses.js +29 -0
- data/app/assets/javascripts/effective_memberships/applicant_experiences.js +38 -0
- data/app/assets/javascripts/effective_memberships/base.js +0 -0
- data/app/assets/javascripts/effective_memberships.js +1 -0
- data/app/assets/stylesheets/effective_memberships/base.scss +5 -0
- data/app/assets/stylesheets/effective_memberships.scss +1 -0
- data/app/controllers/admin/applicant_course_areas_controller.rb +15 -0
- data/app/controllers/admin/applicant_course_names_controller.rb +15 -0
- data/app/controllers/admin/applicants_controller.rb +33 -0
- data/app/controllers/admin/categories_controller.rb +19 -0
- data/app/controllers/admin/fees_controller.rb +18 -0
- data/app/controllers/admin/registrar_actions_controller.rb +49 -0
- data/app/controllers/effective/applicant_references_controller.rb +37 -0
- data/app/controllers/effective/applicants_controller.rb +34 -0
- data/app/controllers/effective/fee_payments_controller.rb +29 -0
- data/app/datatables/admin/effective_applicant_course_areas_datatable.rb +17 -0
- data/app/datatables/admin/effective_applicant_course_names_datatable.rb +17 -0
- data/app/datatables/admin/effective_applicants_datatable.rb +55 -0
- data/app/datatables/admin/effective_categories_datatable.rb +23 -0
- data/app/datatables/admin/effective_fees_datatable.rb +69 -0
- data/app/datatables/admin/effective_membership_histories_datatable.rb +35 -0
- data/app/datatables/effective_applicant_courses_datatable.rb +30 -0
- data/app/datatables/effective_applicant_educations_datatable.rb +22 -0
- data/app/datatables/effective_applicant_experiences_datatable.rb +42 -0
- data/app/datatables/effective_applicant_references_datatable.rb +34 -0
- data/app/datatables/effective_applicants_datatable.rb +34 -0
- data/app/helpers/effective_memberships_helper.rb +15 -0
- data/app/mailers/effective/memberships_mailer.rb +84 -0
- data/app/models/concerns/effective_memberships_applicant.rb +612 -0
- data/app/models/concerns/effective_memberships_applicant_review.rb +149 -0
- data/app/models/concerns/effective_memberships_category.rb +196 -0
- data/app/models/concerns/effective_memberships_fee_payment.rb +229 -0
- data/app/models/concerns/effective_memberships_owner.rb +263 -0
- data/app/models/concerns/effective_memberships_registrar.rb +300 -0
- data/app/models/effective/applicant.rb +7 -0
- data/app/models/effective/applicant_course.rb +40 -0
- data/app/models/effective/applicant_course_area.rb +31 -0
- data/app/models/effective/applicant_course_name.rb +29 -0
- data/app/models/effective/applicant_education.rb +36 -0
- data/app/models/effective/applicant_experience.rb +79 -0
- data/app/models/effective/applicant_reference.rb +81 -0
- data/app/models/effective/applicant_review.rb +7 -0
- data/app/models/effective/category.rb +7 -0
- data/app/models/effective/fee.rb +142 -0
- data/app/models/effective/fee_payment.rb +7 -0
- data/app/models/effective/membership.rb +119 -0
- data/app/models/effective/membership_category.rb +11 -0
- data/app/models/effective/membership_history.rb +40 -0
- data/app/models/effective/registrar.rb +19 -0
- data/app/models/effective/registrar_action.rb +97 -0
- data/app/views/admin/applicant_course_areas/_form.html.haml +8 -0
- data/app/views/admin/applicant_course_areas/_form_applicant_course_area.html.haml +8 -0
- data/app/views/admin/applicant_course_areas/index.html.haml +8 -0
- data/app/views/admin/applicant_course_name/_form.html.haml +9 -0
- data/app/views/admin/applicants/_form.html.haml +38 -0
- data/app/views/admin/applicants/_form_approve.html.haml +34 -0
- data/app/views/admin/applicants/_form_decline.html.haml +11 -0
- data/app/views/admin/applicants/_form_process.html.haml +18 -0
- data/app/views/admin/applicants/_status.html.haml +133 -0
- data/app/views/admin/categories/_form.html.haml +14 -0
- data/app/views/admin/categories/_form_applicant.html.haml +12 -0
- data/app/views/admin/categories/_form_applicant_content.html.haml +19 -0
- data/app/views/admin/categories/_form_applicant_eligibility.html.haml +13 -0
- data/app/views/admin/categories/_form_applicant_fees.html.haml +29 -0
- data/app/views/admin/categories/_form_applicant_steps.html.haml +42 -0
- data/app/views/admin/categories/_form_category.html.haml +10 -0
- data/app/views/admin/categories/_form_fee_payment.html.haml +9 -0
- data/app/views/admin/categories/_form_fee_payment_content.html.haml +19 -0
- data/app/views/admin/categories/_form_fee_payment_steps.html.haml +9 -0
- data/app/views/admin/categories/_form_renewals.html.haml +32 -0
- data/app/views/admin/fees/_fee.html.haml +1 -0
- data/app/views/admin/fees/_form.html.haml +14 -0
- data/app/views/admin/memberships/_status.html.haml +6 -0
- data/app/views/admin/registrar_actions/_form.html.haml +10 -0
- data/app/views/admin/registrar_actions/_form_bad_standing.html.haml +43 -0
- data/app/views/admin/registrar_actions/_form_fees_paid.html.haml +30 -0
- data/app/views/admin/registrar_actions/_form_reclassify.html.haml +43 -0
- data/app/views/admin/registrar_actions/_form_register.html.haml +44 -0
- data/app/views/admin/registrar_actions/_form_remove.html.haml +17 -0
- data/app/views/effective/applicant_references/_applicant_reference.html.haml +51 -0
- data/app/views/effective/applicant_references/_datatable_actions.html.haml +4 -0
- data/app/views/effective/applicant_references/_form.html.haml +18 -0
- data/app/views/effective/applicant_references/_form_declaration.html.haml +37 -0
- data/app/views/effective/applicant_references/complete.html.haml +3 -0
- data/app/views/effective/applicant_references/edit.html.haml +8 -0
- data/app/views/effective/applicants/_applicant.html.haml +6 -0
- data/app/views/effective/applicants/_content.html.haml +10 -0
- data/app/views/effective/applicants/_course_amounts.html.haml +19 -0
- data/app/views/effective/applicants/_dashboard.html.haml +19 -0
- data/app/views/effective/applicants/_declarations.html.haml +16 -0
- data/app/views/effective/applicants/_demographics.html.haml +9 -0
- data/app/views/effective/applicants/_demographics_fields.html.haml +11 -0
- data/app/views/effective/applicants/_demographics_owner.html.haml +20 -0
- data/app/views/effective/applicants/_education.html.haml +14 -0
- data/app/views/effective/applicants/_experience.html.haml +28 -0
- data/app/views/effective/applicants/_files.html.haml +27 -0
- data/app/views/effective/applicants/_layout.html.haml +3 -0
- data/app/views/effective/applicants/_orders.html.haml +4 -0
- data/app/views/effective/applicants/_references.html.haml +15 -0
- data/app/views/effective/applicants/_summary.html.haml +40 -0
- data/app/views/effective/applicants/billing.html.haml +14 -0
- data/app/views/effective/applicants/checkout.html.haml +6 -0
- data/app/views/effective/applicants/course_amounts.html.haml +50 -0
- data/app/views/effective/applicants/declarations.html.haml +19 -0
- data/app/views/effective/applicants/demographics.html.haml +11 -0
- data/app/views/effective/applicants/education.html.haml +27 -0
- data/app/views/effective/applicants/experience.html.haml +51 -0
- data/app/views/effective/applicants/files.html.haml +14 -0
- data/app/views/effective/applicants/references.html.haml +24 -0
- data/app/views/effective/applicants/select.html.haml +27 -0
- data/app/views/effective/applicants/start.html.haml +18 -0
- data/app/views/effective/applicants/submitted.html.haml +44 -0
- data/app/views/effective/applicants/summary.html.haml +8 -0
- data/app/views/effective/fee_payments/_content.html.haml +8 -0
- data/app/views/effective/fee_payments/_declarations.html.haml +16 -0
- data/app/views/effective/fee_payments/_demographics.html.haml +9 -0
- data/app/views/effective/fee_payments/_demographics_fields.html.haml +11 -0
- data/app/views/effective/fee_payments/_demographics_owner.html.haml +20 -0
- data/app/views/effective/fee_payments/_fee_payment.html.haml +6 -0
- data/app/views/effective/fee_payments/_layout.html.haml +3 -0
- data/app/views/effective/fee_payments/_orders.html.haml +4 -0
- data/app/views/effective/fee_payments/billing.html.haml +14 -0
- data/app/views/effective/fee_payments/checkout.html.haml +6 -0
- data/app/views/effective/fee_payments/declarations.html.haml +20 -0
- data/app/views/effective/fee_payments/demographics.html.haml +12 -0
- data/app/views/effective/fee_payments/start.html.haml +20 -0
- data/app/views/effective/fee_payments/submitted.html.haml +14 -0
- data/app/views/effective/fee_payments/summary.html.haml +8 -0
- data/app/views/effective/fees/_dashboard.html.haml +22 -0
- data/app/views/effective/fees/_fee.html.haml +37 -0
- data/app/views/effective/memberships/_dashboard.html.haml +29 -0
- data/app/views/effective/memberships/_membership.html.haml +40 -0
- data/app/views/effective/memberships_mailer/applicant_approved.liquid +15 -0
- data/app/views/effective/memberships_mailer/applicant_declined.liquid +13 -0
- data/app/views/effective/memberships_mailer/applicant_reference_notification.liquid +15 -0
- data/app/views/layouts/effective_memberships_mailer_layout.html.haml +7 -0
- data/config/effective_memberships.rb +47 -0
- data/config/routes.rb +32 -0
- data/db/migrate/01_create_effective_memberships.rb.erb +380 -0
- data/db/seeds.rb +59 -0
- data/lib/effective_memberships/engine.rb +23 -0
- data/lib/effective_memberships/version.rb +3 -0
- data/lib/effective_memberships.rb +86 -0
- data/lib/generators/effective_memberships/install_generator.rb +40 -0
- data/lib/generators/templates/effective_memberships_mailer_preview.rb +4 -0
- data/lib/tasks/effective_memberships_tasks.rake +17 -0
- metadata +377 -0
@@ -0,0 +1,612 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# EffectiveMembershipsApplicant
|
4
|
+
#
|
5
|
+
# Mark your owner model with effective_memberships_applicant to get all the includes
|
6
|
+
|
7
|
+
module EffectiveMembershipsApplicant
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module Base
|
11
|
+
def effective_memberships_applicant
|
12
|
+
include ::EffectiveMembershipsApplicant
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def effective_memberships_applicant?; true; end
|
18
|
+
|
19
|
+
def all_wizard_steps
|
20
|
+
const_get(:WIZARD_STEPS).keys
|
21
|
+
end
|
22
|
+
|
23
|
+
# For effective_category_applicant_wizard_steps_collection
|
24
|
+
def required_wizard_steps
|
25
|
+
[:start, :select, :summary, :billing, :checkout, :submitted]
|
26
|
+
end
|
27
|
+
|
28
|
+
def categories
|
29
|
+
['Apply to Join', 'Apply to Reclassify']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
included do
|
34
|
+
acts_as_email_form
|
35
|
+
acts_as_purchasable_parent
|
36
|
+
acts_as_tokened
|
37
|
+
|
38
|
+
acts_as_statused(
|
39
|
+
:draft, # Just Started
|
40
|
+
:submitted, # Completed wizard. Paid applicant fee.
|
41
|
+
:completed, # Admin has received all deliverables. The application is complete and ready for review.
|
42
|
+
:reviewed, # All applicant reviews completed
|
43
|
+
:declined, # Exit state. Application was declined.
|
44
|
+
:approved # Exit state. Application was approved.
|
45
|
+
)
|
46
|
+
|
47
|
+
acts_as_wizard(
|
48
|
+
start: 'Start',
|
49
|
+
select: 'Select Application Type',
|
50
|
+
demographics: 'Demographics',
|
51
|
+
education: 'Education',
|
52
|
+
course_amounts: 'Courses',
|
53
|
+
experience: 'Work Experience',
|
54
|
+
references: 'References',
|
55
|
+
files: 'Attach Files',
|
56
|
+
declarations: 'Declarations',
|
57
|
+
summary: 'Review',
|
58
|
+
billing: 'Billing Address',
|
59
|
+
checkout: 'Checkout',
|
60
|
+
submitted: 'Submitted'
|
61
|
+
)
|
62
|
+
|
63
|
+
log_changes(except: :wizard_steps) if respond_to?(:log_changes)
|
64
|
+
|
65
|
+
has_many_attached :applicant_files
|
66
|
+
|
67
|
+
# Declarations Step
|
68
|
+
attr_accessor :declare_code_of_ethics
|
69
|
+
attr_accessor :declare_truth
|
70
|
+
|
71
|
+
# Admin Approve Step
|
72
|
+
attr_accessor :approved_membership_number
|
73
|
+
attr_accessor :approved_membership_date
|
74
|
+
|
75
|
+
# Application Namespace
|
76
|
+
belongs_to :owner, polymorphic: true
|
77
|
+
accepts_nested_attributes_for :owner
|
78
|
+
|
79
|
+
belongs_to :category, polymorphic: true, optional: true
|
80
|
+
belongs_to :from_category, polymorphic: true, optional: true
|
81
|
+
|
82
|
+
has_many :applicant_reviews, -> { order(:id) }, inverse_of: :applicant, dependent: :destroy
|
83
|
+
accepts_nested_attributes_for :applicant_reviews, reject_if: :all_blank, allow_destroy: true
|
84
|
+
|
85
|
+
# Effective Namespace
|
86
|
+
has_many :applicant_courses, -> { order(:id) }, class_name: 'Effective::ApplicantCourse', inverse_of: :applicant, dependent: :destroy
|
87
|
+
accepts_nested_attributes_for :applicant_courses, reject_if: :all_blank, allow_destroy: true
|
88
|
+
|
89
|
+
has_many :applicant_educations, -> { order(:id) }, class_name: 'Effective::ApplicantEducation', inverse_of: :applicant, dependent: :destroy
|
90
|
+
accepts_nested_attributes_for :applicant_educations, reject_if: :all_blank, allow_destroy: true
|
91
|
+
|
92
|
+
has_many :applicant_experiences, -> { order(:id) }, class_name: 'Effective::ApplicantExperience', inverse_of: :applicant, dependent: :destroy
|
93
|
+
accepts_nested_attributes_for :applicant_experiences, reject_if: :all_blank, allow_destroy: true
|
94
|
+
|
95
|
+
has_many :applicant_references, -> { order(:id) }, class_name: 'Effective::ApplicantReference', inverse_of: :applicant, dependent: :destroy
|
96
|
+
accepts_nested_attributes_for :applicant_references, reject_if: :all_blank, allow_destroy: true
|
97
|
+
|
98
|
+
has_many :fees, -> { order(:id) }, as: :parent, class_name: 'Effective::Fee', dependent: :nullify
|
99
|
+
accepts_nested_attributes_for :fees, reject_if: :all_blank, allow_destroy: true
|
100
|
+
|
101
|
+
has_many :orders, -> { order(:id) }, as: :parent, class_name: 'Effective::Order', dependent: :nullify
|
102
|
+
accepts_nested_attributes_for :orders
|
103
|
+
|
104
|
+
effective_resource do
|
105
|
+
applicant_type :string
|
106
|
+
|
107
|
+
# Acts as Statused
|
108
|
+
status :string, permitted: false
|
109
|
+
status_steps :text, permitted: false
|
110
|
+
|
111
|
+
# Dates
|
112
|
+
submitted_at :datetime
|
113
|
+
completed_at :datetime
|
114
|
+
reviewed_at :datetime
|
115
|
+
approved_at :datetime
|
116
|
+
|
117
|
+
# Declined
|
118
|
+
declined_at :datetime
|
119
|
+
declined_reason :text
|
120
|
+
|
121
|
+
# Applicant Educations
|
122
|
+
applicant_educations_details :text
|
123
|
+
|
124
|
+
# Applicant Experiences
|
125
|
+
applicant_experiences_months :integer
|
126
|
+
applicant_experiences_details :text
|
127
|
+
|
128
|
+
# Additional Information
|
129
|
+
additional_information :text
|
130
|
+
|
131
|
+
# Acts as Wizard
|
132
|
+
wizard_steps :text, permitted: false
|
133
|
+
|
134
|
+
timestamps
|
135
|
+
end
|
136
|
+
|
137
|
+
scope :deep, -> { includes(:owner, :category, :from_category, :orders) }
|
138
|
+
scope :sorted, -> { order(:id) }
|
139
|
+
|
140
|
+
scope :in_progress, -> { where.not(status: [:approved, :declined]) }
|
141
|
+
scope :done, -> { where(status: [:approved, :declined]) }
|
142
|
+
|
143
|
+
# Set Apply to Join or Reclassification
|
144
|
+
before_validation(if: -> { new_record? && owner.present? }) do
|
145
|
+
self.applicant_type ||= (owner.membership.blank? ? 'Apply to Join' : 'Apply to Reclassify')
|
146
|
+
self.from_category ||= owner.membership&.category
|
147
|
+
end
|
148
|
+
|
149
|
+
before_validation(if: -> { current_step == :select && category_id.present? }) do
|
150
|
+
self.category_type ||= EffectiveMemberships.Category.name
|
151
|
+
end
|
152
|
+
|
153
|
+
before_validation(if: -> { current_step == :experience }) do
|
154
|
+
assign_applicant_experiences_months!
|
155
|
+
end
|
156
|
+
|
157
|
+
# All Steps validations
|
158
|
+
validates :owner, presence: true
|
159
|
+
validates :from_category, presence: true, if: -> { reclassification? }
|
160
|
+
|
161
|
+
validate(if: -> { reclassification? }) do
|
162
|
+
errors.add(:category_id, "can't reclassify to existing category") if category_id == from_category_id
|
163
|
+
end
|
164
|
+
|
165
|
+
# Start Step
|
166
|
+
with_options(if: -> { current_step == :start && owner.present? }) do
|
167
|
+
validate do
|
168
|
+
errors.add(:base, 'may not have outstanding fees') if owner.outstanding_fee_payment_fees.present?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Select Step
|
173
|
+
with_options(if: -> { current_step == :select || has_completed_step?(:select) }) do
|
174
|
+
validates :applicant_type, presence: true
|
175
|
+
validates :category, presence: true
|
176
|
+
end
|
177
|
+
|
178
|
+
# Applicant Educations Step
|
179
|
+
with_options(if: -> { current_step == :education }) do
|
180
|
+
validate do
|
181
|
+
required = min_applicant_educations()
|
182
|
+
existing = applicant_educations().reject(&:marked_for_destruction?).length
|
183
|
+
|
184
|
+
self.errors.add(:applicant_educations, "please include #{required} or more educations") if existing < required
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Applicant Experiences Step
|
189
|
+
with_options(if: -> { current_step == :experience }) do
|
190
|
+
validates :applicant_experiences_months, presence: true
|
191
|
+
|
192
|
+
validate do
|
193
|
+
if (min = min_applicant_experiences_months) > applicant_experiences_months.to_i
|
194
|
+
self.errors.add(:applicant_experiences_months, "must be at least #{min} months, or #{min / 12} years")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Make sure none of the full time applicant_experience dates overlap
|
199
|
+
validate do
|
200
|
+
experiences = applicant_experiences.reject(&:marked_for_destruction?).select(&:full_time?)
|
201
|
+
|
202
|
+
experiences.find do |x|
|
203
|
+
(experiences - [x]).find do |y|
|
204
|
+
next unless (x.start_on..x.end_on).overlaps?(y.start_on..y.end_on)
|
205
|
+
x.errors.add(:start_on, "can't overlap when full time")
|
206
|
+
x.errors.add(:end_on, "can't overlap when full time")
|
207
|
+
y.errors.add(:start_on, "can't overlap when full time")
|
208
|
+
y.errors.add(:end_on, "can't overlap when full time")
|
209
|
+
self.errors.add(:applicant_experiences, "can't have overlaping dates for full time experiences")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
with_options(if: -> { current_step == :course_amounts }) do
|
216
|
+
validate do
|
217
|
+
required = min_applicant_courses()
|
218
|
+
existing = applicant_courses().reject(&:marked_for_destruction?).length
|
219
|
+
|
220
|
+
self.errors.add(:applicant_courses, "please include #{required} or more courses") if existing < required
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Applicant References Step
|
225
|
+
with_options(if: -> { current_step == :references }) do
|
226
|
+
validate do
|
227
|
+
required = min_applicant_references()
|
228
|
+
existing = applicant_references().reject(&:marked_for_destruction?).length
|
229
|
+
|
230
|
+
self.errors.add(:applicant_references, "please include #{required} or more references") if existing < required
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Applicant Files Step
|
235
|
+
with_options(if: -> { current_step == :files }) do
|
236
|
+
validate do
|
237
|
+
required = min_applicant_files()
|
238
|
+
existing = applicant_files().length
|
239
|
+
|
240
|
+
self.errors.add(:applicant_files, "please include #{required} or more files") if existing < required
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Declarations Step
|
245
|
+
with_options(if: -> { current_step == :declarations }) do
|
246
|
+
validates :declare_code_of_ethics, acceptance: true
|
247
|
+
validates :declare_truth, acceptance: true
|
248
|
+
end
|
249
|
+
|
250
|
+
# Admin Approve
|
251
|
+
validate(if: -> { approved_membership_date.present? }) do
|
252
|
+
if approved_membership_date.to_date > Time.zone.now.to_date
|
253
|
+
errors.add(:approved_membership_date, "can't be in the future")
|
254
|
+
elsif approved_membership_date.to_date < (Time.zone.now - 1.year).to_date
|
255
|
+
errors.add(:approved_membership_date, "can't be more than 1 year in the past")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Admin Decline
|
260
|
+
validates :declined_reason, presence: true, if: -> { declined? }
|
261
|
+
|
262
|
+
# These two try completed and try reviewed
|
263
|
+
before_save(if: -> { submitted? }) { complete! }
|
264
|
+
before_save(if: -> { completed? }) { review! }
|
265
|
+
|
266
|
+
# Clear required steps memoization
|
267
|
+
after_save { @_required_steps = nil }
|
268
|
+
|
269
|
+
# This required_steps is defined inside the included do .. end block so it overrides the acts_as_wizard one.
|
270
|
+
def required_steps
|
271
|
+
return self.class.test_required_steps if Rails.env.test? && self.class.test_required_steps.present?
|
272
|
+
|
273
|
+
@_required_steps ||= begin
|
274
|
+
wizard_steps = self.class.all_wizard_steps
|
275
|
+
required_steps = self.class.required_wizard_steps
|
276
|
+
|
277
|
+
applicant_steps = Array(category&.applicant_wizard_steps)
|
278
|
+
|
279
|
+
wizard_steps.select do |step|
|
280
|
+
required_steps.include?(step) || category.blank? || applicant_steps.include?(step)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
after_purchase do |_|
|
286
|
+
raise('expected submit_order to be purchased') unless submit_order&.purchased?
|
287
|
+
submit_purchased!
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Instance Methods
|
292
|
+
def to_s
|
293
|
+
if category.present? && category.present?
|
294
|
+
[
|
295
|
+
owner.to_s,
|
296
|
+
'-',
|
297
|
+
category,
|
298
|
+
'for',
|
299
|
+
category,
|
300
|
+
("from #{from_category}" if reclassification?)
|
301
|
+
].compact.join(' ')
|
302
|
+
else
|
303
|
+
'New Applicant'
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def apply_to_join?
|
308
|
+
applicant_type == 'Apply to Join'
|
309
|
+
end
|
310
|
+
|
311
|
+
def reclassification?
|
312
|
+
applicant_type == 'Apply to Reclassify'
|
313
|
+
end
|
314
|
+
|
315
|
+
def owner_label
|
316
|
+
owner_type.to_s.split('::').last
|
317
|
+
end
|
318
|
+
|
319
|
+
def in_progress?
|
320
|
+
!approved? && !declined?
|
321
|
+
end
|
322
|
+
|
323
|
+
def done?
|
324
|
+
approved? || declined?
|
325
|
+
end
|
326
|
+
|
327
|
+
def can_visit_step?(step)
|
328
|
+
can_revisit_completed_steps(step)
|
329
|
+
end
|
330
|
+
|
331
|
+
def summary
|
332
|
+
case status_was
|
333
|
+
when 'draft'
|
334
|
+
"Applicant has not yet completed the #{category} wizard steps or paid to submit this application. This application will transition to 'submitted' after payment has been collected."
|
335
|
+
when 'submitted'
|
336
|
+
summary = "Application has been purchased and submitted. The following tasks must be done before this application will transition to 'completed':"
|
337
|
+
items = completed_requirements.map { |item, done| "<li>#{item}: #{done ? 'Complete' : 'Incomplete'}</li>" }.join
|
338
|
+
"<p>#{summary}</p><ul>#{items}</ul>"
|
339
|
+
when 'completed'
|
340
|
+
if applicant_reviews_required?
|
341
|
+
"All required materials have been provided. This application will transition to 'reviewed' after all reviewers have voted."
|
342
|
+
else
|
343
|
+
"This application has been completed and is now ready for an admin to approve or decline it. If approved, prorated fees will be generated."
|
344
|
+
end
|
345
|
+
when 'reviewed'
|
346
|
+
"This application has been reviewed and is now ready for an admin to approve or decline it. If approved, prorated fees will be generated."
|
347
|
+
when 'approved'
|
348
|
+
"The application has been approved! All done!"
|
349
|
+
when 'declined'
|
350
|
+
"This application has been declined."
|
351
|
+
else
|
352
|
+
raise("unexpected status #{status}")
|
353
|
+
end.html_safe
|
354
|
+
end
|
355
|
+
|
356
|
+
# Used by the select step
|
357
|
+
def can_apply_categories_collection
|
358
|
+
categories = EffectiveMemberships.Category.sorted.can_apply
|
359
|
+
|
360
|
+
if owner.blank? || owner.membership.blank?
|
361
|
+
return categories.where(can_apply_new: true)
|
362
|
+
end
|
363
|
+
|
364
|
+
category_ids = owner.membership.category_ids
|
365
|
+
|
366
|
+
categories.select do |cat|
|
367
|
+
cat.can_apply_existing? ||
|
368
|
+
(cat.can_apply_restricted? && (category_ids & cat.can_apply_restricted_ids).present?)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def select!
|
373
|
+
raise('cannot select a submitted applicant') if was_submitted?
|
374
|
+
raise('cannot select a purchased applicant') if orders.any? { |order| order.purchased? }
|
375
|
+
|
376
|
+
# Reset the progress so far. They have to click through screens again.
|
377
|
+
assign_attributes(wizard_steps: wizard_steps.slice(:start, :select))
|
378
|
+
|
379
|
+
# Delete any fees and orders. Keep all other data.
|
380
|
+
submit_fees.each { |fee| fee.mark_for_destruction }
|
381
|
+
submit_order.mark_for_destruction if submit_order
|
382
|
+
|
383
|
+
save!
|
384
|
+
end
|
385
|
+
|
386
|
+
# Educations Step
|
387
|
+
def min_applicant_educations
|
388
|
+
category&.min_applicant_educations.to_i
|
389
|
+
end
|
390
|
+
|
391
|
+
# Courses Amounts step
|
392
|
+
def min_applicant_courses
|
393
|
+
category&.min_applicant_courses.to_i
|
394
|
+
end
|
395
|
+
|
396
|
+
def applicant_course_areas_collection
|
397
|
+
Effective::ApplicantCourseArea.deep.sorted
|
398
|
+
end
|
399
|
+
|
400
|
+
def applicant_course_names_collection(applicant_course_area:)
|
401
|
+
applicant_course_area.applicant_course_names
|
402
|
+
end
|
403
|
+
|
404
|
+
def applicant_course(applicant_course_name: nil)
|
405
|
+
applicant_courses.find { |ac| ac.applicant_course_name_id == applicant_course_name.id } ||
|
406
|
+
applicant_courses.build(applicant_course_name: applicant_course_name, applicant_course_area: applicant_course_name.applicant_course_area)
|
407
|
+
end
|
408
|
+
|
409
|
+
def applicant_course_area_sum(applicant_course_area:)
|
410
|
+
applicant_courses.select { |ac| ac.applicant_course_area_id == applicant_course_area.id }.sum { |ac| ac.amount.to_i }
|
411
|
+
end
|
412
|
+
|
413
|
+
def applicant_courses_sum
|
414
|
+
applicant_courses.sum { |ac| ac.amount.to_i }
|
415
|
+
end
|
416
|
+
|
417
|
+
# Work Experiences Step
|
418
|
+
def min_applicant_experiences_months
|
419
|
+
category&.min_applicant_experiences_months.to_i
|
420
|
+
end
|
421
|
+
|
422
|
+
# References Step
|
423
|
+
def min_applicant_references
|
424
|
+
category&.min_applicant_references.to_i
|
425
|
+
end
|
426
|
+
|
427
|
+
# Files Step
|
428
|
+
def min_applicant_files
|
429
|
+
category&.min_applicant_files.to_i
|
430
|
+
end
|
431
|
+
|
432
|
+
# All Fees and Orders
|
433
|
+
def submit_fees
|
434
|
+
fees.select { |fee| fee.applicant_submit_fee? }
|
435
|
+
end
|
436
|
+
|
437
|
+
def submit_order
|
438
|
+
orders.find { |order| order.purchasables.any?(&:applicant_submit_fee?) }
|
439
|
+
end
|
440
|
+
|
441
|
+
def find_or_build_submit_fees
|
442
|
+
return submit_fees if submit_fees.present?
|
443
|
+
|
444
|
+
fees.build(
|
445
|
+
owner: owner,
|
446
|
+
fee_type: 'Applicant',
|
447
|
+
category: category,
|
448
|
+
price: category.applicant_fee
|
449
|
+
)
|
450
|
+
|
451
|
+
submit_fees
|
452
|
+
end
|
453
|
+
|
454
|
+
def find_or_build_submit_order
|
455
|
+
order = submit_order || orders.build(user: owner)
|
456
|
+
|
457
|
+
# Adds fees, but does not overwrite any existing price.
|
458
|
+
submit_fees.each do |fee|
|
459
|
+
order.add(fee) unless order.purchasables.include?(fee)
|
460
|
+
end
|
461
|
+
|
462
|
+
# From Billing Step
|
463
|
+
order.billing_address = owner.billing_address if owner.billing_address.present?
|
464
|
+
|
465
|
+
order
|
466
|
+
end
|
467
|
+
|
468
|
+
# Should be indempotent.
|
469
|
+
def build_submit_fees_and_order
|
470
|
+
return false if was_submitted?
|
471
|
+
|
472
|
+
fees = find_or_build_submit_fees()
|
473
|
+
raise('already has purchased submit fees') if fees.any? { |fee| fee.purchased? }
|
474
|
+
|
475
|
+
order = find_or_build_submit_order()
|
476
|
+
raise('already has purchased submit order') if order.purchased?
|
477
|
+
|
478
|
+
true
|
479
|
+
end
|
480
|
+
|
481
|
+
# Owner clicks on the Billing step. Next step is Checkout
|
482
|
+
def billing!
|
483
|
+
ready!
|
484
|
+
end
|
485
|
+
|
486
|
+
# Ready to check out
|
487
|
+
def ready!
|
488
|
+
build_submit_fees_and_order
|
489
|
+
save!
|
490
|
+
end
|
491
|
+
|
492
|
+
# Called automatically via after_purchase hook above
|
493
|
+
def submit_purchased!
|
494
|
+
return false if was_submitted?
|
495
|
+
|
496
|
+
wizard_steps[:checkout] = Time.zone.now
|
497
|
+
submit!
|
498
|
+
end
|
499
|
+
|
500
|
+
# Draft -> Submitted requirements
|
501
|
+
def submit!
|
502
|
+
raise('already submitted') if was_submitted?
|
503
|
+
raise('expected a purchased order') unless submit_order&.purchased?
|
504
|
+
|
505
|
+
after_commit do
|
506
|
+
applicant_references.each { |reference| reference.notify! if reference.submitted? }
|
507
|
+
end
|
508
|
+
|
509
|
+
wizard_steps[:checkout] ||= Time.zone.now
|
510
|
+
wizard_steps[:submitted] = Time.zone.now
|
511
|
+
submitted!
|
512
|
+
end
|
513
|
+
|
514
|
+
# Submitted -> Completed requirements
|
515
|
+
|
516
|
+
def applicant_references_required?
|
517
|
+
min_applicant_references > 0
|
518
|
+
end
|
519
|
+
|
520
|
+
# When an application is submitted, these must be done to go to completed
|
521
|
+
def completed_requirements
|
522
|
+
{
|
523
|
+
'Applicant References' => (!applicant_references_required? || applicant_references.count(&:completed?) >= min_applicant_references)
|
524
|
+
}
|
525
|
+
end
|
526
|
+
|
527
|
+
def complete!
|
528
|
+
return false unless submitted? && completed_requirements.values.all?
|
529
|
+
# Could send registrar an email here saying this applicant is ready to review
|
530
|
+
completed!
|
531
|
+
end
|
532
|
+
|
533
|
+
# Completed -> Reviewed requirements
|
534
|
+
def applicant_reviews_required?
|
535
|
+
(min_applicant_reviews > 0 || applicant_reviews.present?)
|
536
|
+
end
|
537
|
+
|
538
|
+
def min_applicant_reviews
|
539
|
+
category&.min_applicant_reviews.to_i
|
540
|
+
end
|
541
|
+
|
542
|
+
# When an application is completed, these must be done to go to reviewed
|
543
|
+
def reviewed_requirements
|
544
|
+
{
|
545
|
+
'Applicant Reviews' => (!applicant_reviews_required? || applicant_reviews.count(&:completed?) >= min_applicant_reviews)
|
546
|
+
}
|
547
|
+
end
|
548
|
+
|
549
|
+
def review!
|
550
|
+
return false unless completed? && reviewed_requirements.values.all?
|
551
|
+
# Could send registrar an email here saying this applicant is ready to approve
|
552
|
+
reviewed!
|
553
|
+
end
|
554
|
+
|
555
|
+
# Admin approves an applicant. Registers the owner. Sends an email.
|
556
|
+
def approve!
|
557
|
+
raise('already approved') if was_approved?
|
558
|
+
raise('applicant must have been submitted to approve!') unless was_submitted?
|
559
|
+
|
560
|
+
# Complete the wizard step. Just incase this is run out of order.
|
561
|
+
wizard_steps[:checkout] ||= Time.zone.now
|
562
|
+
wizard_steps[:submitted] ||= Time.zone.now
|
563
|
+
approved!
|
564
|
+
|
565
|
+
if apply_to_join?
|
566
|
+
EffectiveMemberships.Registrar.register!(
|
567
|
+
owner,
|
568
|
+
to: category,
|
569
|
+
date: approved_membership_date.presence, # Set by the Admin Process form, or nil
|
570
|
+
number: approved_membership_number.presence # Set by the Admin Process form, or nil
|
571
|
+
)
|
572
|
+
elsif reclassification?
|
573
|
+
EffectiveMemberships.Registrar.reclassify!(owner, to: category)
|
574
|
+
else
|
575
|
+
raise('unsupported approval applicant_type')
|
576
|
+
end
|
577
|
+
|
578
|
+
after_commit { send_email(:applicant_approved) }
|
579
|
+
|
580
|
+
save!
|
581
|
+
end
|
582
|
+
|
583
|
+
# Admin approves an applicant. Registers the owner. Sends an email.
|
584
|
+
def decline!
|
585
|
+
raise('already declined') if was_declined?
|
586
|
+
raise('previously approved') if was_approved?
|
587
|
+
raise('applicant must have been submitted to decline!') unless was_submitted?
|
588
|
+
|
589
|
+
# Complete the wizard step. Just incase this is run out of order.
|
590
|
+
wizard_steps[:checkout] ||= Time.zone.now
|
591
|
+
wizard_steps[:submitted] ||= Time.zone.now
|
592
|
+
declined!
|
593
|
+
|
594
|
+
after_commit { send_email(:applicant_declined) }
|
595
|
+
|
596
|
+
save!
|
597
|
+
end
|
598
|
+
|
599
|
+
private
|
600
|
+
|
601
|
+
def assign_applicant_experiences_months!
|
602
|
+
existing = applicant_experiences.reject(&:marked_for_destruction?)
|
603
|
+
existing.each { |ae| ae.assign_months! }
|
604
|
+
|
605
|
+
self.applicant_experiences_months = existing.sum { |ae| ae.months.to_i }
|
606
|
+
end
|
607
|
+
|
608
|
+
def send_email(email)
|
609
|
+
EffectiveMemberships.send_email(email, self, email_form_params) unless email_form_skip?
|
610
|
+
end
|
611
|
+
|
612
|
+
end
|