effective_memberships 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +114 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_memberships_manifest.js +3 -0
- data/app/assets/javascripts/effective_memberships/applicant_courses.js +29 -0
- data/app/assets/javascripts/effective_memberships/applicant_experiences.js +38 -0
- data/app/assets/javascripts/effective_memberships/base.js +0 -0
- data/app/assets/javascripts/effective_memberships.js +1 -0
- data/app/assets/stylesheets/effective_memberships/base.scss +5 -0
- data/app/assets/stylesheets/effective_memberships.scss +1 -0
- data/app/controllers/admin/applicant_course_areas_controller.rb +15 -0
- data/app/controllers/admin/applicant_course_names_controller.rb +15 -0
- data/app/controllers/admin/applicants_controller.rb +33 -0
- data/app/controllers/admin/categories_controller.rb +19 -0
- data/app/controllers/admin/fees_controller.rb +18 -0
- data/app/controllers/admin/registrar_actions_controller.rb +49 -0
- data/app/controllers/effective/applicant_references_controller.rb +37 -0
- data/app/controllers/effective/applicants_controller.rb +34 -0
- data/app/controllers/effective/fee_payments_controller.rb +29 -0
- data/app/datatables/admin/effective_applicant_course_areas_datatable.rb +17 -0
- data/app/datatables/admin/effective_applicant_course_names_datatable.rb +17 -0
- data/app/datatables/admin/effective_applicants_datatable.rb +55 -0
- data/app/datatables/admin/effective_categories_datatable.rb +23 -0
- data/app/datatables/admin/effective_fees_datatable.rb +69 -0
- data/app/datatables/admin/effective_membership_histories_datatable.rb +35 -0
- data/app/datatables/effective_applicant_courses_datatable.rb +30 -0
- data/app/datatables/effective_applicant_educations_datatable.rb +22 -0
- data/app/datatables/effective_applicant_experiences_datatable.rb +42 -0
- data/app/datatables/effective_applicant_references_datatable.rb +34 -0
- data/app/datatables/effective_applicants_datatable.rb +34 -0
- data/app/helpers/effective_memberships_helper.rb +15 -0
- data/app/mailers/effective/memberships_mailer.rb +84 -0
- data/app/models/concerns/effective_memberships_applicant.rb +612 -0
- data/app/models/concerns/effective_memberships_applicant_review.rb +149 -0
- data/app/models/concerns/effective_memberships_category.rb +196 -0
- data/app/models/concerns/effective_memberships_fee_payment.rb +229 -0
- data/app/models/concerns/effective_memberships_owner.rb +263 -0
- data/app/models/concerns/effective_memberships_registrar.rb +300 -0
- data/app/models/effective/applicant.rb +7 -0
- data/app/models/effective/applicant_course.rb +40 -0
- data/app/models/effective/applicant_course_area.rb +31 -0
- data/app/models/effective/applicant_course_name.rb +29 -0
- data/app/models/effective/applicant_education.rb +36 -0
- data/app/models/effective/applicant_experience.rb +79 -0
- data/app/models/effective/applicant_reference.rb +81 -0
- data/app/models/effective/applicant_review.rb +7 -0
- data/app/models/effective/category.rb +7 -0
- data/app/models/effective/fee.rb +142 -0
- data/app/models/effective/fee_payment.rb +7 -0
- data/app/models/effective/membership.rb +119 -0
- data/app/models/effective/membership_category.rb +11 -0
- data/app/models/effective/membership_history.rb +40 -0
- data/app/models/effective/registrar.rb +19 -0
- data/app/models/effective/registrar_action.rb +97 -0
- data/app/views/admin/applicant_course_areas/_form.html.haml +8 -0
- data/app/views/admin/applicant_course_areas/_form_applicant_course_area.html.haml +8 -0
- data/app/views/admin/applicant_course_areas/index.html.haml +8 -0
- data/app/views/admin/applicant_course_name/_form.html.haml +9 -0
- data/app/views/admin/applicants/_form.html.haml +38 -0
- data/app/views/admin/applicants/_form_approve.html.haml +34 -0
- data/app/views/admin/applicants/_form_decline.html.haml +11 -0
- data/app/views/admin/applicants/_form_process.html.haml +18 -0
- data/app/views/admin/applicants/_status.html.haml +133 -0
- data/app/views/admin/categories/_form.html.haml +14 -0
- data/app/views/admin/categories/_form_applicant.html.haml +12 -0
- data/app/views/admin/categories/_form_applicant_content.html.haml +19 -0
- data/app/views/admin/categories/_form_applicant_eligibility.html.haml +13 -0
- data/app/views/admin/categories/_form_applicant_fees.html.haml +29 -0
- data/app/views/admin/categories/_form_applicant_steps.html.haml +42 -0
- data/app/views/admin/categories/_form_category.html.haml +10 -0
- data/app/views/admin/categories/_form_fee_payment.html.haml +9 -0
- data/app/views/admin/categories/_form_fee_payment_content.html.haml +19 -0
- data/app/views/admin/categories/_form_fee_payment_steps.html.haml +9 -0
- data/app/views/admin/categories/_form_renewals.html.haml +32 -0
- data/app/views/admin/fees/_fee.html.haml +1 -0
- data/app/views/admin/fees/_form.html.haml +14 -0
- data/app/views/admin/memberships/_status.html.haml +6 -0
- data/app/views/admin/registrar_actions/_form.html.haml +10 -0
- data/app/views/admin/registrar_actions/_form_bad_standing.html.haml +43 -0
- data/app/views/admin/registrar_actions/_form_fees_paid.html.haml +30 -0
- data/app/views/admin/registrar_actions/_form_reclassify.html.haml +43 -0
- data/app/views/admin/registrar_actions/_form_register.html.haml +44 -0
- data/app/views/admin/registrar_actions/_form_remove.html.haml +17 -0
- data/app/views/effective/applicant_references/_applicant_reference.html.haml +51 -0
- data/app/views/effective/applicant_references/_datatable_actions.html.haml +4 -0
- data/app/views/effective/applicant_references/_form.html.haml +18 -0
- data/app/views/effective/applicant_references/_form_declaration.html.haml +37 -0
- data/app/views/effective/applicant_references/complete.html.haml +3 -0
- data/app/views/effective/applicant_references/edit.html.haml +8 -0
- data/app/views/effective/applicants/_applicant.html.haml +6 -0
- data/app/views/effective/applicants/_content.html.haml +10 -0
- data/app/views/effective/applicants/_course_amounts.html.haml +19 -0
- data/app/views/effective/applicants/_dashboard.html.haml +19 -0
- data/app/views/effective/applicants/_declarations.html.haml +16 -0
- data/app/views/effective/applicants/_demographics.html.haml +9 -0
- data/app/views/effective/applicants/_demographics_fields.html.haml +11 -0
- data/app/views/effective/applicants/_demographics_owner.html.haml +20 -0
- data/app/views/effective/applicants/_education.html.haml +14 -0
- data/app/views/effective/applicants/_experience.html.haml +28 -0
- data/app/views/effective/applicants/_files.html.haml +27 -0
- data/app/views/effective/applicants/_layout.html.haml +3 -0
- data/app/views/effective/applicants/_orders.html.haml +4 -0
- data/app/views/effective/applicants/_references.html.haml +15 -0
- data/app/views/effective/applicants/_summary.html.haml +40 -0
- data/app/views/effective/applicants/billing.html.haml +14 -0
- data/app/views/effective/applicants/checkout.html.haml +6 -0
- data/app/views/effective/applicants/course_amounts.html.haml +50 -0
- data/app/views/effective/applicants/declarations.html.haml +19 -0
- data/app/views/effective/applicants/demographics.html.haml +11 -0
- data/app/views/effective/applicants/education.html.haml +27 -0
- data/app/views/effective/applicants/experience.html.haml +51 -0
- data/app/views/effective/applicants/files.html.haml +14 -0
- data/app/views/effective/applicants/references.html.haml +24 -0
- data/app/views/effective/applicants/select.html.haml +27 -0
- data/app/views/effective/applicants/start.html.haml +18 -0
- data/app/views/effective/applicants/submitted.html.haml +44 -0
- data/app/views/effective/applicants/summary.html.haml +8 -0
- data/app/views/effective/fee_payments/_content.html.haml +8 -0
- data/app/views/effective/fee_payments/_declarations.html.haml +16 -0
- data/app/views/effective/fee_payments/_demographics.html.haml +9 -0
- data/app/views/effective/fee_payments/_demographics_fields.html.haml +11 -0
- data/app/views/effective/fee_payments/_demographics_owner.html.haml +20 -0
- data/app/views/effective/fee_payments/_fee_payment.html.haml +6 -0
- data/app/views/effective/fee_payments/_layout.html.haml +3 -0
- data/app/views/effective/fee_payments/_orders.html.haml +4 -0
- data/app/views/effective/fee_payments/billing.html.haml +14 -0
- data/app/views/effective/fee_payments/checkout.html.haml +6 -0
- data/app/views/effective/fee_payments/declarations.html.haml +20 -0
- data/app/views/effective/fee_payments/demographics.html.haml +12 -0
- data/app/views/effective/fee_payments/start.html.haml +20 -0
- data/app/views/effective/fee_payments/submitted.html.haml +14 -0
- data/app/views/effective/fee_payments/summary.html.haml +8 -0
- data/app/views/effective/fees/_dashboard.html.haml +22 -0
- data/app/views/effective/fees/_fee.html.haml +37 -0
- data/app/views/effective/memberships/_dashboard.html.haml +29 -0
- data/app/views/effective/memberships/_membership.html.haml +40 -0
- data/app/views/effective/memberships_mailer/applicant_approved.liquid +15 -0
- data/app/views/effective/memberships_mailer/applicant_declined.liquid +13 -0
- data/app/views/effective/memberships_mailer/applicant_reference_notification.liquid +15 -0
- data/app/views/layouts/effective_memberships_mailer_layout.html.haml +7 -0
- data/config/effective_memberships.rb +47 -0
- data/config/routes.rb +32 -0
- data/db/migrate/01_create_effective_memberships.rb.erb +380 -0
- data/db/seeds.rb +59 -0
- data/lib/effective_memberships/engine.rb +23 -0
- data/lib/effective_memberships/version.rb +3 -0
- data/lib/effective_memberships.rb +86 -0
- data/lib/generators/effective_memberships/install_generator.rb +40 -0
- data/lib/generators/templates/effective_memberships_mailer_preview.rb +4 -0
- data/lib/tasks/effective_memberships_tasks.rake +17 -0
- metadata +377 -0
@@ -0,0 +1,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,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
|