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