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,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveMembershipsOwner
4
+ #
5
+ # Mark your owner model with effective_memberships_owner to get all the includes
6
+
7
+ module EffectiveMembershipsOwner
8
+ extend ActiveSupport::Concern
9
+
10
+ mattr_accessor :descendants
11
+
12
+ module Base
13
+ def effective_memberships_owner
14
+ include ::EffectiveMembershipsOwner
15
+ (EffectiveMembershipsOwner.descendants ||= []) << self
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def effective_memberships_owner?; true; end
21
+ end
22
+
23
+ included do
24
+ # App scoped
25
+ has_many :applicants, as: :owner
26
+ has_many :fee_payments, as: :owner
27
+
28
+ # Effective scoped
29
+ has_many :fees, -> { order(:id) }, inverse_of: :owner, as: :owner, class_name: 'Effective::Fee', dependent: :nullify
30
+ accepts_nested_attributes_for :fees, reject_if: :all_blank, allow_destroy: true
31
+
32
+ has_many :orders, -> { order(:id) }, inverse_of: :user, as: :user, class_name: 'Effective::Order', dependent: :nullify
33
+ accepts_nested_attributes_for :orders, reject_if: :all_blank, allow_destroy: true
34
+
35
+ has_one :membership, inverse_of: :owner, as: :owner, class_name: 'Effective::Membership'
36
+ accepts_nested_attributes_for :membership
37
+
38
+ has_many :membership_histories, -> { Effective::MembershipHistory.sorted }, inverse_of: :owner, as: :owner, class_name: 'Effective::MembershipHistory'
39
+ accepts_nested_attributes_for :membership_histories
40
+
41
+ effective_resource do
42
+ timestamps
43
+ end
44
+
45
+ scope :members, -> { joins(:membership) }
46
+ end
47
+
48
+ def effective_memberships_owner
49
+ self
50
+ end
51
+
52
+ def owner_label
53
+ self.class.name.split('::').last
54
+ end
55
+
56
+ def membership_fees_paid?
57
+ outstanding_fee_payment_fees.blank? && membership && membership.fees_paid?
58
+ end
59
+
60
+ def outstanding_fee_payment_fees
61
+ fees.select { |fee| fee.fee_payment_fee? && !fee.purchased? }
62
+ end
63
+
64
+ def outstanding_fee_payment_orders
65
+ orders.select { |order| order.parent_type.to_s.include?('FeePayment') && !order.purchased? }
66
+ end
67
+
68
+ def bad_standing_fees
69
+ fees.select { |fee| fee.bad_standing? }
70
+ end
71
+
72
+ def max_fees_paid_period
73
+ fees.select { |fee| fee.membership_period_fee? && fee.purchased? }.map(&:period).max
74
+ end
75
+
76
+ def max_fees_paid_through_period
77
+ return nil if max_fees_paid_period.blank?
78
+ EffectiveMemberships.Registrar.period_end_on(date: max_fees_paid_period)
79
+ end
80
+
81
+ def membership_removed?
82
+ membership.blank? && membership_histories.any? { |history| history.removed? }
83
+ end
84
+
85
+ def membership_removed_on
86
+ return nil unless membership_removed?
87
+ membership_histories.find { |history| history.removed? }.start_on
88
+ end
89
+
90
+ # Instance Methods
91
+ def additional_fee_attributes(fee)
92
+ raise('expected an Effective::Fee') unless fee.kind_of?(Effective::Fee)
93
+ {}
94
+ end
95
+
96
+ def build_prorated_fee(date: nil)
97
+ raise('must have an existing membership') unless membership.present?
98
+
99
+ date ||= Time.zone.now
100
+ price = membership.category.prorated_fee(date: date)
101
+ period = EffectiveMemberships.Registrar.period(date: date)
102
+ category = membership.category
103
+
104
+ fee = fees.find { |fee| fee.fee_type == 'Prorated' && fee.period == period && fee.category == category } || fees.build()
105
+ return fee if fee.purchased?
106
+
107
+ fee.assign_attributes(
108
+ fee_type: 'Prorated',
109
+ category: category,
110
+ price: price,
111
+ period: period
112
+ )
113
+
114
+ fee
115
+ end
116
+
117
+ def build_discount_fee(from:, date: nil)
118
+ raise('must have an existing membership') unless membership.present?
119
+ raise('existing membership category may not be same as from') if membership.category == from
120
+
121
+ date ||= Time.zone.now
122
+ price = from.discount_fee(date: date)
123
+ period = EffectiveMemberships.Registrar.period(date: date)
124
+ category = membership.category
125
+
126
+ fee = fees.find { |fee| fee.fee_type == 'Discount' && fee.period == period && fee.category == category } || fees.build()
127
+ return fee if fee.purchased?
128
+
129
+ fee.assign_attributes(
130
+ fee_type: 'Discount',
131
+ category: category,
132
+ price: price,
133
+ period: period
134
+ )
135
+
136
+ fee
137
+ end
138
+
139
+ def build_title_fee(period:, title:, fee_type: nil, category: nil, price: nil, qb_item_name: nil, tax_exempt: nil)
140
+ fee_type ||= 'Renewal'
141
+
142
+ fee = fees.find do |fee|
143
+ fee.fee_type == fee_type && fee.period == period && fee.title == title &&
144
+ (category.blank? || fee.category_id == category.id && fee.category_type == category.class.name)
145
+ end
146
+
147
+ return fee if fee&.purchased?
148
+
149
+ # Build the title fee
150
+ fee ||= fees.build()
151
+ price ||= (category.renewal_fee.to_i if category.present? && fee_type == 'Renewal')
152
+
153
+ fee.assign_attributes(
154
+ fee_type: fee_type,
155
+ title: title,
156
+ category: category,
157
+ price: price,
158
+ period: period,
159
+ qb_item_name: qb_item_name,
160
+ tax_exempt: tax_exempt,
161
+ late_on: nil,
162
+ bad_standing_on: nil
163
+ )
164
+ end
165
+
166
+ def build_renewal_fee(category:, period:, late_on:, bad_standing_on:)
167
+ raise('must have an existing membership') unless membership.present?
168
+
169
+ fee = fees.find { |fee| fee.fee_type == 'Renewal' && fee.period == period && fee.category_id == category.id && fee.category_type == category.class.name }
170
+ return fee if fee&.purchased?
171
+
172
+ # Build the renewal fee
173
+ fee ||= fees.build()
174
+
175
+ fee.assign_attributes(
176
+ fee_type: 'Renewal',
177
+ category: category,
178
+ price: category.renewal_fee.to_i,
179
+ period: period,
180
+ late_on: late_on,
181
+ bad_standing_on: bad_standing_on
182
+ )
183
+
184
+ fee
185
+ end
186
+
187
+ def build_late_fee(category:, period:)
188
+ raise('must have an existing membership') unless membership.present?
189
+
190
+ # Return existing but do not build yet
191
+ fee = fees.find { |fee| fee.fee_type == 'Late' && fee.period == period && fee.category_id == category.id && fee.category_type == category.class.name }
192
+ return fee if fee&.purchased?
193
+
194
+ # Only continue if there is a late renewal fee for the same period
195
+ renewal_fee = fees.find { |fee| fee.fee_type == 'Renewal' && fee.period == period && fee.category_id == category.id && fee.category_type == category.class.name }
196
+ return unless fee.present? || renewal_fee&.late?
197
+
198
+ # Build the late fee
199
+ fee ||= fees.build()
200
+
201
+ fee.assign_attributes(
202
+ fee_type: 'Late',
203
+ category: category,
204
+ price: category.late_fee.to_i,
205
+ period: period,
206
+ )
207
+
208
+ fee
209
+ end
210
+
211
+ def update_membership_status!
212
+ raise('expected membership to be present') unless membership.present?
213
+
214
+ # Assign fees paid through period
215
+ membership.fees_paid_period = max_fees_paid_period()
216
+ membership.fees_paid_through_period = max_fees_paid_through_period()
217
+
218
+ # Assign in bad standing
219
+ if membership.bad_standing_admin?
220
+ # Nothing to do
221
+ elsif bad_standing_fees.present?
222
+ membership.bad_standing = true
223
+ membership.bad_standing_reason = 'Unpaid Fees'
224
+ else
225
+ membership.bad_standing = false
226
+ membership.bad_standing_reason = nil
227
+ end
228
+
229
+ if membership.bad_standing_changed? || membership_histories.blank?
230
+ build_membership_history()
231
+ end
232
+
233
+ save!
234
+ end
235
+
236
+ def build_membership_history(start_on: nil)
237
+ raise('expected membership to be present') unless membership.present?
238
+
239
+ # The date of change
240
+ start_on ||= Time.zone.now
241
+ removed = membership.marked_for_destruction?
242
+
243
+ # End the other membership histories
244
+ membership_histories.each { |history| history.end_on ||= start_on }
245
+
246
+ # Snapshot of the current membership at this time
247
+ membership_histories.build(
248
+ start_on: start_on,
249
+ end_on: nil,
250
+ removed: removed,
251
+ bad_standing: membership.bad_standing?,
252
+ categories: (membership.categories.map(&:to_s) unless removed),
253
+ category_ids: (membership.categories.map(&:id) unless removed),
254
+ number: (membership.number unless removed)
255
+ )
256
+ end
257
+
258
+ def membership_history_on(date)
259
+ raise('expected a date') unless date.respond_to?(:strftime)
260
+ membership_histories.find { |history| (history.start_on..history.end_on).cover?(date) } # Ruby 2.6 supports endless ranges
261
+ end
262
+
263
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveMembershipsRegistrar
4
+ #
5
+ # This is different cause its not an ActiveRecord one
6
+ #
7
+ # Mark your registrar with include EffectiveMembershipsRegistrar
8
+ #
9
+ # Mark your category model with effective_memberships_category to get all the includes
10
+
11
+ module EffectiveMembershipsRegistrar
12
+ extend ActiveSupport::Concern
13
+
14
+ module ClassMethods
15
+ def effective_memberships_registrar?; true; end
16
+ end
17
+
18
+ included do
19
+ end
20
+
21
+ def renewal_fee_date(date:)
22
+ Date.new(date.year, 12, 1) # Fees roll over every December 1st
23
+ raise('to be implemented by app registrar')
24
+ end
25
+
26
+ def late_fee_date(period:)
27
+ Date.new(period.year, 2, 1) # Fees are late after February 1st
28
+ raise('to be implemented by app registrar')
29
+ end
30
+
31
+ def bad_standing_date(period:)
32
+ Date.new(period.year, 3, 1) # Membership in bad standing after March 1st
33
+ raise('to be implemented by app registrar')
34
+ end
35
+
36
+ def assign!(owner, categories:, date: nil, number: nil)
37
+ categories = Array(categories)
38
+
39
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
40
+ raise('expecting a membership category') unless categories.all? { |cat| cat.class.respond_to?(:effective_memberships_category?) }
41
+
42
+ # Default Date and next number
43
+ date ||= Time.zone.now
44
+ number = next_membership_number(owner, to: categories.first) if number.blank?
45
+ period = period(date: date)
46
+ period_end_on = period_end_on(date: date)
47
+
48
+ # Find or build a membership
49
+ membership = owner.membership || owner.build_membership
50
+
51
+ # Assign Dates
52
+ membership.joined_on ||= date # Only if not already present
53
+
54
+ # Assign Number
55
+ membership.number ||= number
56
+ membership.number_as_integer ||= (Integer(number) rescue nil)
57
+
58
+ # Delete any removed categories
59
+ membership.membership_categories.each do |membership_category|
60
+ next if categories.include?(membership_category.category)
61
+ membership_category.mark_for_destruction
62
+ end
63
+
64
+ # Build any additional categories
65
+ categories.each do |category|
66
+ membership.build_membership_category(category: category)
67
+ end
68
+
69
+ changed = membership.membership_categories.any? { |mc| mc.new_record? || mc.marked_for_destruction? }
70
+
71
+ if changed
72
+ membership.registration_on = date # Always new registration_on
73
+ save!(owner, date: date)
74
+ end
75
+
76
+ owner.update_membership_status!
77
+ end
78
+
79
+ def register!(owner, to:, date: nil, number: nil, skip_fees: false)
80
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
81
+ raise('expecting a memberships category') unless to.class.respond_to?(:effective_memberships_category?)
82
+ raise('owner has existing membership. use reclassify! instead.') if owner.membership.present?
83
+
84
+ # Default Date and next number
85
+ date ||= Time.zone.now
86
+ number = next_membership_number(owner, to: to) if number.blank?
87
+ period = period(date: date)
88
+ period_end_on = period_end_on(date: date)
89
+
90
+ # Build a membership
91
+ membership = owner.build_membership
92
+
93
+ # Assign Dates
94
+ membership.joined_on ||= date # Only if not already present
95
+ membership.registration_on = date # Always new registration_on
96
+
97
+ # Assign Number
98
+ membership.number = number
99
+ membership.number_as_integer = (Integer(number) rescue nil)
100
+
101
+ # Assign Category
102
+ membership.build_membership_category(category: to)
103
+
104
+ # Assign fees paid through period
105
+ if skip_fees
106
+ membership.fees_paid_period = period
107
+ membership.fees_paid_through_period = period_end_on
108
+ end
109
+
110
+ # Or, Build Fees
111
+ unless skip_fees
112
+ fee = owner.build_prorated_fee(date: date)
113
+ raise('already has purchased prorated fee') if fee.purchased?
114
+ end
115
+
116
+ # Save owner
117
+ save!(owner, date: date)
118
+ end
119
+
120
+ def reclassify!(owner, to:, date: nil, skip_fees: false)
121
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
122
+ raise('owner must have an existing membership. use register! instead') if owner.membership.blank?
123
+
124
+ # Todo. I dunno this was owner.membership.category
125
+ from = owner.membership.category
126
+
127
+ raise('expecting a to memberships category') unless to.class.respond_to?(:effective_memberships_category?)
128
+ raise('expecting a from memberships category') unless from.class.respond_to?(:effective_memberships_category?)
129
+ raise('expected to and from to be different') if from == to
130
+
131
+ date ||= Time.zone.now
132
+
133
+ membership = owner.membership
134
+
135
+ # Assign Category
136
+ membership.registration_on = date
137
+
138
+ membership.build_membership_category(category: to)
139
+ membership.membership_category(category: from).mark_for_destruction
140
+
141
+ unless skip_fees
142
+ fee = owner.build_prorated_fee(date: date)
143
+ raise('already has purchased prorated fee') if fee.purchased?
144
+
145
+ fee = owner.build_discount_fee(date: date, from: from)
146
+ raise('already has purchased discount fee') if fee.purchased?
147
+ end
148
+
149
+ save!(owner, date: date)
150
+ end
151
+
152
+ def remove!(owner, date: nil)
153
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
154
+ raise('expected a member') unless owner.membership.present?
155
+
156
+ # Date
157
+ date ||= Time.zone.now
158
+
159
+ # Remove Membership
160
+ owner.membership.mark_for_destruction
161
+
162
+ # Delete unpurchased fees and orders
163
+ owner.outstanding_fee_payment_fees.each { |fee| fee.mark_for_destruction }
164
+ owner.outstanding_fee_payment_orders.each { |order| order.mark_for_destruction }
165
+
166
+ save!(owner, date: date)
167
+ end
168
+
169
+ def bad_standing!(owner, reason:, date: nil)
170
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
171
+ raise('expected a member') unless owner.membership.present?
172
+ raise('expected owner to be in good standing') if owner.membership.bad_standing?
173
+
174
+ # Date
175
+ date ||= Time.zone.now
176
+ membership = owner.membership
177
+
178
+ membership.bad_standing = true
179
+ membership.bad_standing_admin = true
180
+ membership.bad_standing_reason = reason
181
+
182
+ save!(owner, date: date)
183
+ end
184
+
185
+ def good_standing!(owner, date: nil)
186
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
187
+ raise('expected a member') unless owner.membership.present?
188
+ raise('expected owner to be in bad standing') unless owner.membership.bad_standing?
189
+
190
+ # Date
191
+ date ||= Time.zone.now
192
+ membership = owner.membership
193
+
194
+ membership.bad_standing = false
195
+ membership.bad_standing_admin = false
196
+ membership.bad_standing_reason = nil
197
+
198
+ save!(owner, date: date)
199
+ end
200
+
201
+ def fees_paid!(owner, date: nil)
202
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
203
+ raise('expected a member') unless owner.membership.present?
204
+
205
+ # Date
206
+ date ||= Time.zone.now
207
+
208
+ period = period(date: date)
209
+ period_end_on = period_end_on(date: date)
210
+
211
+ if owner.outstanding_fee_payment_fees.present?
212
+ fp = EffectiveMemberships.FeePayment.new(owner: owner)
213
+ fp.ready!
214
+ fp.submit_order.purchase!(skip_buyer_validations: true, email: false)
215
+ end
216
+
217
+ owner.membership.update!(fees_paid_period: period, fees_paid_through_period: period_end_on)
218
+ end
219
+
220
+ def next_membership_number(owner, to:)
221
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
222
+ raise('expecting a membership category') unless Array(to).all? { |to| to.class.respond_to?(:effective_memberships_category?) }
223
+
224
+ # Just a simple number right now
225
+ number = (Effective::Membership.all.max_number || 0) + 1
226
+
227
+ # Returns a string
228
+ number.to_s
229
+ end
230
+
231
+ def current_period
232
+ period(date: Time.zone.now)
233
+ end
234
+
235
+ # Returns a date of Jan 1, Year
236
+ def period(date:)
237
+ cutoff = renewal_fee_date(date: date) # period_end_on
238
+ period = (date < cutoff) ? date.beginning_of_year : date.advance(years: 1).beginning_of_year
239
+ period.to_date
240
+ end
241
+
242
+ def period_end_on(date:)
243
+ period(date: date).end_of_year
244
+ end
245
+
246
+ # This is intended to be run once per day in a rake task
247
+ # Create Renewal and Late fees
248
+ def create_fees!(period: nil, late_on: nil, bad_standing_on: nil)
249
+ # The current period, based on Time.zone.now
250
+ period ||= current_period
251
+ late_on ||= late_fee_date(period: period)
252
+ bad_standing_on ||= bad_standing_date(period: period)
253
+
254
+ # Create Renewal Fees
255
+ Effective::Membership.create_renewal_fees(period).find_each do |membership|
256
+ membership.categories.select(&:create_renewal_fees?).map do |category|
257
+ fee = membership.owner.build_renewal_fee(category: category, period: period, late_on: late_on, bad_standing_on: bad_standing_on)
258
+ raise("expected build_renewal_fee to return a fee for period #{period}") unless fee.kind_of?(Effective::Fee)
259
+ next if fee.purchased?
260
+
261
+ fee.save!
262
+ end
263
+ end
264
+
265
+ GC.start
266
+
267
+ # Create Late Fees
268
+ Effective::Membership.create_late_fees(period).find_each do |membership|
269
+ membership.categories.select(&:create_late_fees?).map do |category|
270
+ fee = membership.owner.build_late_fee(category: category, period: period)
271
+ next if fee.blank? || fee.purchased?
272
+
273
+ fee.save!
274
+ end
275
+ end
276
+
277
+ GC.start
278
+
279
+ # Update Membership Status - Assign In Bad Standing
280
+ Effective::Membership.deep.with_unpaid_fees_through(period).find_each do |membership|
281
+ membership.owner.update_membership_status!
282
+ end
283
+
284
+ true
285
+ end
286
+
287
+ # Called in the after_purchase of fee payment
288
+ def fee_payment_purchased!(owner)
289
+ raise('expecting a memberships owner') unless owner.class.respond_to?(:effective_memberships_owner?)
290
+ owner.update_membership_status!
291
+ end
292
+
293
+ protected
294
+
295
+ def save!(owner, date: Time.zone.now)
296
+ owner.build_membership_history(start_on: date)
297
+ owner.save!
298
+ end
299
+
300
+ end
@@ -0,0 +1,7 @@
1
+ module Effective
2
+ class Applicant < ActiveRecord::Base
3
+ self.table_name = EffectiveMemberships.applicants_table_name.to_s
4
+
5
+ effective_memberships_applicant
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ module Effective
2
+ class ApplicantCourse < ActiveRecord::Base
3
+ log_changes(to: :applicant) if respond_to?(:log_changes)
4
+
5
+ belongs_to :applicant_course_area
6
+
7
+ belongs_to :applicant_course_name, optional: true
8
+ belongs_to :applicant, optional: true
9
+
10
+ effective_resource do
11
+ title :string
12
+ amount :integer
13
+
14
+ code :string
15
+ description :text
16
+
17
+ timestamps
18
+ end
19
+
20
+ scope :deep, -> { includes(:applicant_course_area, :applicant_course_name, :applicant) }
21
+ scope :sorted, -> { order(:title) }
22
+
23
+ before_validation(if: -> { applicant_course_name.present? }) do
24
+ self.title = applicant_course_name.title
25
+ self.applicant_course_area = applicant_course_name.applicant_course_area
26
+ end
27
+
28
+ validates :title, presence: true
29
+
30
+ with_options(if: -> { applicant_course_name.blank? }) do
31
+ validates :code, presence: true
32
+ validates :description, presence: true
33
+ end
34
+
35
+ def to_s
36
+ title || 'course'
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ module Effective
2
+ class ApplicantCourseArea < ActiveRecord::Base
3
+ log_changes if respond_to?(:log_changes)
4
+
5
+ has_rich_text :body
6
+
7
+ has_many :applicant_course_names, dependent: :delete_all
8
+ accepts_nested_attributes_for :applicant_course_names
9
+
10
+ effective_resource do
11
+ title :string
12
+ position :integer
13
+
14
+ timestamps
15
+ end
16
+
17
+ scope :deep, -> { with_rich_text_body }
18
+ scope :sorted, -> { order(:position) }
19
+
20
+ before_validation do
21
+ self.position ||= (self.class.pluck(:position).compact.max || -1) + 1
22
+ end
23
+
24
+ validates :title, presence: true, uniqueness: true
25
+
26
+ def to_s
27
+ title || 'course area'
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ module Effective
2
+ class ApplicantCourseName < ActiveRecord::Base
3
+ log_changes(to: :applicant_course_area) if respond_to?(:log_changes)
4
+
5
+ belongs_to :applicant_course_area
6
+
7
+ effective_resource do
8
+ title :string
9
+ position :integer
10
+
11
+ timestamps
12
+ end
13
+
14
+ scope :deep, -> { all }
15
+ scope :sorted, -> { order(:position) }
16
+
17
+ before_validation(if: -> { applicant_course_area.present? }) do
18
+ self.position ||= (applicant_course_area.applicant_course_names.map(&:position).compact.max || -1) + 1
19
+ end
20
+
21
+ validates :title, presence: true, uniqueness: true
22
+ validates :position, presence: true
23
+
24
+ def to_s
25
+ title || 'course name'
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ module Effective
2
+ class ApplicantEducation < ActiveRecord::Base
3
+ belongs_to :applicant
4
+
5
+ log_changes(to: :applicant) if respond_to?(:log_changes)
6
+
7
+ effective_resource do
8
+ start_on :date
9
+ end_on :date
10
+
11
+ institution :string
12
+ location :string
13
+
14
+ degree_obtained :string
15
+
16
+ timestamps
17
+ end
18
+
19
+ scope :deep, -> { includes(:applicant) }
20
+
21
+ validates :start_on, presence: true
22
+ validates :end_on, presence: true
23
+ validates :institution, presence: true
24
+ validates :location, presence: true
25
+ validates :degree_obtained, presence: true
26
+
27
+ validate(if: -> { start_on.present? && end_on.present? }) do
28
+ errors.add(:end_on, 'must be after start date') unless start_on < end_on
29
+ end
30
+
31
+ def to_s
32
+ degree_obtained || 'education'
33
+ end
34
+
35
+ end
36
+ end