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,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