effective_memberships 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +114 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_memberships_manifest.js +3 -0
  6. data/app/assets/javascripts/effective_memberships/applicant_courses.js +29 -0
  7. data/app/assets/javascripts/effective_memberships/applicant_experiences.js +38 -0
  8. data/app/assets/javascripts/effective_memberships/base.js +0 -0
  9. data/app/assets/javascripts/effective_memberships.js +1 -0
  10. data/app/assets/stylesheets/effective_memberships/base.scss +5 -0
  11. data/app/assets/stylesheets/effective_memberships.scss +1 -0
  12. data/app/controllers/admin/applicant_course_areas_controller.rb +15 -0
  13. data/app/controllers/admin/applicant_course_names_controller.rb +15 -0
  14. data/app/controllers/admin/applicants_controller.rb +33 -0
  15. data/app/controllers/admin/categories_controller.rb +19 -0
  16. data/app/controllers/admin/fees_controller.rb +18 -0
  17. data/app/controllers/admin/registrar_actions_controller.rb +49 -0
  18. data/app/controllers/effective/applicant_references_controller.rb +37 -0
  19. data/app/controllers/effective/applicants_controller.rb +34 -0
  20. data/app/controllers/effective/fee_payments_controller.rb +29 -0
  21. data/app/datatables/admin/effective_applicant_course_areas_datatable.rb +17 -0
  22. data/app/datatables/admin/effective_applicant_course_names_datatable.rb +17 -0
  23. data/app/datatables/admin/effective_applicants_datatable.rb +55 -0
  24. data/app/datatables/admin/effective_categories_datatable.rb +23 -0
  25. data/app/datatables/admin/effective_fees_datatable.rb +69 -0
  26. data/app/datatables/admin/effective_membership_histories_datatable.rb +35 -0
  27. data/app/datatables/effective_applicant_courses_datatable.rb +30 -0
  28. data/app/datatables/effective_applicant_educations_datatable.rb +22 -0
  29. data/app/datatables/effective_applicant_experiences_datatable.rb +42 -0
  30. data/app/datatables/effective_applicant_references_datatable.rb +34 -0
  31. data/app/datatables/effective_applicants_datatable.rb +34 -0
  32. data/app/helpers/effective_memberships_helper.rb +15 -0
  33. data/app/mailers/effective/memberships_mailer.rb +84 -0
  34. data/app/models/concerns/effective_memberships_applicant.rb +612 -0
  35. data/app/models/concerns/effective_memberships_applicant_review.rb +149 -0
  36. data/app/models/concerns/effective_memberships_category.rb +196 -0
  37. data/app/models/concerns/effective_memberships_fee_payment.rb +229 -0
  38. data/app/models/concerns/effective_memberships_owner.rb +263 -0
  39. data/app/models/concerns/effective_memberships_registrar.rb +300 -0
  40. data/app/models/effective/applicant.rb +7 -0
  41. data/app/models/effective/applicant_course.rb +40 -0
  42. data/app/models/effective/applicant_course_area.rb +31 -0
  43. data/app/models/effective/applicant_course_name.rb +29 -0
  44. data/app/models/effective/applicant_education.rb +36 -0
  45. data/app/models/effective/applicant_experience.rb +79 -0
  46. data/app/models/effective/applicant_reference.rb +81 -0
  47. data/app/models/effective/applicant_review.rb +7 -0
  48. data/app/models/effective/category.rb +7 -0
  49. data/app/models/effective/fee.rb +142 -0
  50. data/app/models/effective/fee_payment.rb +7 -0
  51. data/app/models/effective/membership.rb +119 -0
  52. data/app/models/effective/membership_category.rb +11 -0
  53. data/app/models/effective/membership_history.rb +40 -0
  54. data/app/models/effective/registrar.rb +19 -0
  55. data/app/models/effective/registrar_action.rb +97 -0
  56. data/app/views/admin/applicant_course_areas/_form.html.haml +8 -0
  57. data/app/views/admin/applicant_course_areas/_form_applicant_course_area.html.haml +8 -0
  58. data/app/views/admin/applicant_course_areas/index.html.haml +8 -0
  59. data/app/views/admin/applicant_course_name/_form.html.haml +9 -0
  60. data/app/views/admin/applicants/_form.html.haml +38 -0
  61. data/app/views/admin/applicants/_form_approve.html.haml +34 -0
  62. data/app/views/admin/applicants/_form_decline.html.haml +11 -0
  63. data/app/views/admin/applicants/_form_process.html.haml +18 -0
  64. data/app/views/admin/applicants/_status.html.haml +133 -0
  65. data/app/views/admin/categories/_form.html.haml +14 -0
  66. data/app/views/admin/categories/_form_applicant.html.haml +12 -0
  67. data/app/views/admin/categories/_form_applicant_content.html.haml +19 -0
  68. data/app/views/admin/categories/_form_applicant_eligibility.html.haml +13 -0
  69. data/app/views/admin/categories/_form_applicant_fees.html.haml +29 -0
  70. data/app/views/admin/categories/_form_applicant_steps.html.haml +42 -0
  71. data/app/views/admin/categories/_form_category.html.haml +10 -0
  72. data/app/views/admin/categories/_form_fee_payment.html.haml +9 -0
  73. data/app/views/admin/categories/_form_fee_payment_content.html.haml +19 -0
  74. data/app/views/admin/categories/_form_fee_payment_steps.html.haml +9 -0
  75. data/app/views/admin/categories/_form_renewals.html.haml +32 -0
  76. data/app/views/admin/fees/_fee.html.haml +1 -0
  77. data/app/views/admin/fees/_form.html.haml +14 -0
  78. data/app/views/admin/memberships/_status.html.haml +6 -0
  79. data/app/views/admin/registrar_actions/_form.html.haml +10 -0
  80. data/app/views/admin/registrar_actions/_form_bad_standing.html.haml +43 -0
  81. data/app/views/admin/registrar_actions/_form_fees_paid.html.haml +30 -0
  82. data/app/views/admin/registrar_actions/_form_reclassify.html.haml +43 -0
  83. data/app/views/admin/registrar_actions/_form_register.html.haml +44 -0
  84. data/app/views/admin/registrar_actions/_form_remove.html.haml +17 -0
  85. data/app/views/effective/applicant_references/_applicant_reference.html.haml +51 -0
  86. data/app/views/effective/applicant_references/_datatable_actions.html.haml +4 -0
  87. data/app/views/effective/applicant_references/_form.html.haml +18 -0
  88. data/app/views/effective/applicant_references/_form_declaration.html.haml +37 -0
  89. data/app/views/effective/applicant_references/complete.html.haml +3 -0
  90. data/app/views/effective/applicant_references/edit.html.haml +8 -0
  91. data/app/views/effective/applicants/_applicant.html.haml +6 -0
  92. data/app/views/effective/applicants/_content.html.haml +10 -0
  93. data/app/views/effective/applicants/_course_amounts.html.haml +19 -0
  94. data/app/views/effective/applicants/_dashboard.html.haml +19 -0
  95. data/app/views/effective/applicants/_declarations.html.haml +16 -0
  96. data/app/views/effective/applicants/_demographics.html.haml +9 -0
  97. data/app/views/effective/applicants/_demographics_fields.html.haml +11 -0
  98. data/app/views/effective/applicants/_demographics_owner.html.haml +20 -0
  99. data/app/views/effective/applicants/_education.html.haml +14 -0
  100. data/app/views/effective/applicants/_experience.html.haml +28 -0
  101. data/app/views/effective/applicants/_files.html.haml +27 -0
  102. data/app/views/effective/applicants/_layout.html.haml +3 -0
  103. data/app/views/effective/applicants/_orders.html.haml +4 -0
  104. data/app/views/effective/applicants/_references.html.haml +15 -0
  105. data/app/views/effective/applicants/_summary.html.haml +40 -0
  106. data/app/views/effective/applicants/billing.html.haml +14 -0
  107. data/app/views/effective/applicants/checkout.html.haml +6 -0
  108. data/app/views/effective/applicants/course_amounts.html.haml +50 -0
  109. data/app/views/effective/applicants/declarations.html.haml +19 -0
  110. data/app/views/effective/applicants/demographics.html.haml +11 -0
  111. data/app/views/effective/applicants/education.html.haml +27 -0
  112. data/app/views/effective/applicants/experience.html.haml +51 -0
  113. data/app/views/effective/applicants/files.html.haml +14 -0
  114. data/app/views/effective/applicants/references.html.haml +24 -0
  115. data/app/views/effective/applicants/select.html.haml +27 -0
  116. data/app/views/effective/applicants/start.html.haml +18 -0
  117. data/app/views/effective/applicants/submitted.html.haml +44 -0
  118. data/app/views/effective/applicants/summary.html.haml +8 -0
  119. data/app/views/effective/fee_payments/_content.html.haml +8 -0
  120. data/app/views/effective/fee_payments/_declarations.html.haml +16 -0
  121. data/app/views/effective/fee_payments/_demographics.html.haml +9 -0
  122. data/app/views/effective/fee_payments/_demographics_fields.html.haml +11 -0
  123. data/app/views/effective/fee_payments/_demographics_owner.html.haml +20 -0
  124. data/app/views/effective/fee_payments/_fee_payment.html.haml +6 -0
  125. data/app/views/effective/fee_payments/_layout.html.haml +3 -0
  126. data/app/views/effective/fee_payments/_orders.html.haml +4 -0
  127. data/app/views/effective/fee_payments/billing.html.haml +14 -0
  128. data/app/views/effective/fee_payments/checkout.html.haml +6 -0
  129. data/app/views/effective/fee_payments/declarations.html.haml +20 -0
  130. data/app/views/effective/fee_payments/demographics.html.haml +12 -0
  131. data/app/views/effective/fee_payments/start.html.haml +20 -0
  132. data/app/views/effective/fee_payments/submitted.html.haml +14 -0
  133. data/app/views/effective/fee_payments/summary.html.haml +8 -0
  134. data/app/views/effective/fees/_dashboard.html.haml +22 -0
  135. data/app/views/effective/fees/_fee.html.haml +37 -0
  136. data/app/views/effective/memberships/_dashboard.html.haml +29 -0
  137. data/app/views/effective/memberships/_membership.html.haml +40 -0
  138. data/app/views/effective/memberships_mailer/applicant_approved.liquid +15 -0
  139. data/app/views/effective/memberships_mailer/applicant_declined.liquid +13 -0
  140. data/app/views/effective/memberships_mailer/applicant_reference_notification.liquid +15 -0
  141. data/app/views/layouts/effective_memberships_mailer_layout.html.haml +7 -0
  142. data/config/effective_memberships.rb +47 -0
  143. data/config/routes.rb +32 -0
  144. data/db/migrate/01_create_effective_memberships.rb.erb +380 -0
  145. data/db/seeds.rb +59 -0
  146. data/lib/effective_memberships/engine.rb +23 -0
  147. data/lib/effective_memberships/version.rb +3 -0
  148. data/lib/effective_memberships.rb +86 -0
  149. data/lib/generators/effective_memberships/install_generator.rb +40 -0
  150. data/lib/generators/templates/effective_memberships_mailer_preview.rb +4 -0
  151. data/lib/tasks/effective_memberships_tasks.rake +17 -0
  152. 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