effective_memberships 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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