effective_learndash 0.6.1 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07c61be03e0ed75629e8ca5ba549ad5b6853baf84853d064ab182023e322aee7
4
- data.tar.gz: 0c49839157576ccc13f235dba925b278ec324a79c74a939655d455c83bf692f7
3
+ metadata.gz: 1f0eb60514e8eeffec5acb15be3ec18871d32deb63a220e4423798be3eb137d0
4
+ data.tar.gz: 99f7ddc61aeae6e835383237d64714f2de0865f6fe219f6437fa845ac43aa534
5
5
  SHA512:
6
- metadata.gz: afa87292d29e2ff756bc78dc4c848f576a9632d91ca70d9070fd87e914ddf2fef3cddfeaf34928a45e55067c56bac876eb003db8d20933a4f40524f7c1ce3954
7
- data.tar.gz: 665d1d771286353f7e0ab20dba5c9f6425c4cafeaddf00a36d41b2c4ed185edfb2b98527ff767d961a155a91b66c4d5e33891d420a10acee3679cc6bc674704c
6
+ metadata.gz: 0fe3018d26e29a3a2dbc1fd8c7863e120396d78d953562b7aa877dbbfc72aa59cd43dbb427b775a813789c89521849145d28c6ff97aec6cefbd8dfa3c997257b
7
+ data.tar.gz: 9c50f0c9b581b3e72ed59aad4d7d03d5dca525223369f1e33f6cbac867f775981b412b46a901aaa199d12907da660cf72159610d6875302a20f26c6e90322dd8
@@ -0,0 +1,23 @@
1
+ module Admin
2
+ class CourseFeeHistoriesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_learndash) }
5
+
6
+ include Effective::CrudController
7
+
8
+ # Inline datatable create/update/destroy do an in-place row swap and never re-render the
9
+ # price-timeline warnings shown on the course page. When triggered from an inline datatable,
10
+ # redirect back so the whole page reloads and the warnings recompute.
11
+ on(:save, redirect: -> { :back if params[:_datatable_id].present? })
12
+ on(:destroy, redirect: -> { :back if params[:_datatable_id].present? })
13
+
14
+ # Duplicating closes the current prices at year-end and opens next year's copy.
15
+ on(:duplicate, redirect: -> { :back },
16
+ success: -> { "Copied #{resource.learndash_course}'s prices forward — set next year's prices on the new history." })
17
+
18
+ def permitted_params
19
+ params.require(:effective_course_fee_history).permit!
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module Admin
2
+ class CourseFeeHistoriesDatatable < Effective::Datatable
3
+ datatable do
4
+ order :start_on, :desc
5
+
6
+ col :id, visible: false
7
+ col :created_at, visible: false
8
+ col :updated_at, visible: false
9
+
10
+ col :learndash_course, search: Effective::LearndashCourse.sorted
11
+
12
+ col :start_on
13
+ col :end_on, label: 'End on<br>(blank = current)'
14
+
15
+ col :regular_fee, as: :price
16
+ col :member_fee, as: :price
17
+
18
+ actions_col
19
+ end
20
+
21
+ collection do
22
+ Effective::CourseFeeHistory.deep.all
23
+ end
24
+
25
+ end
26
+ end
@@ -16,8 +16,7 @@ module Admin
16
16
  col :learndash_users, visible: false
17
17
 
18
18
  col :can_register
19
- col :regular_price, as: :price
20
- col :member_price, as: :price
19
+ # Prices now live on date-ranged CourseFeeHistory records — see Admin::CourseFeeHistoriesDatatable.
21
20
 
22
21
  actions_col
23
22
  end
@@ -49,9 +49,6 @@ module EffectiveLearndashCourseRegistration
49
49
  has_many :course_registrants, -> { order(:id) }, class_name: 'Effective::CourseRegistrant', inverse_of: :course_registration, dependent: :destroy
50
50
  accepts_nested_attributes_for :course_registrants, reject_if: :all_blank, allow_destroy: true
51
51
 
52
- has_many :orders, -> { order(:id) }, as: :parent, class_name: 'Effective::Order', dependent: :nullify
53
- accepts_nested_attributes_for :orders
54
-
55
52
  effective_resource do
56
53
  # Acts as Statused
57
54
  status :string, permitted: false
@@ -0,0 +1,54 @@
1
+ module Effective
2
+ # A dated set of prices for a learndash course.
3
+ #
4
+ # Prices used to live in flat columns (regular_price, member_price) directly on
5
+ # learndash_courses. They now live here so a price change can be scheduled ahead of
6
+ # time: each CourseFeeHistory covers a date range (start_on..end_on), and the price
7
+ # for a given date is read from whichever history covers it.
8
+ #
9
+ # There should always be exactly one open-ended (end_on: nil) history per course — the
10
+ # currently active prices. Read them via LearndashCourse#prices.
11
+ class CourseFeeHistory < ActiveRecord::Base
12
+ belongs_to :learndash_course, class_name: 'Effective::LearndashCourse'
13
+
14
+ log_changes(to: :learndash_course) if respond_to?(:log_changes)
15
+
16
+ effective_resource do
17
+ start_on :date
18
+ end_on :date
19
+
20
+ regular_fee :integer # Price to applicants or new users
21
+ member_fee :integer # Price to existing members
22
+
23
+ timestamps
24
+ end
25
+
26
+ scope :deep, -> { includes(:learndash_course) }
27
+ scope :sorted, -> { order(start_on: :desc) }
28
+
29
+ validates :start_on, presence: true
30
+ validates :end_on, comparison: { greater_than: :start_on }, allow_nil: true
31
+
32
+ # Price presence used to be validated on the course; it moved here with the prices.
33
+ with_options(if: -> { learndash_course&.can_register? }) do
34
+ validates :regular_fee, presence: true
35
+ validates :member_fee, presence: true
36
+ end
37
+
38
+ def to_s
39
+ "#{learndash_course} Fees #{start_on&.year}#{" - #{end_on.year}" if end_on.present?}"
40
+ end
41
+
42
+ # Schedule next year's prices: close the current (open-ended) prices at the end of this
43
+ # year and open a copy for next year, ready to have its new prices set.
44
+ def duplicate!
45
+ transaction do
46
+ copy = dup
47
+ copy.assign_attributes(start_on: (Time.zone.now + 1.year).beginning_of_year, end_on: nil)
48
+ copy.save!
49
+
50
+ update!(end_on: Time.zone.now.end_of_year)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,6 +3,10 @@ module Effective
3
3
  has_many :learndash_enrollments
4
4
  has_many :learndash_users, through: :learndash_enrollments
5
5
 
6
+ # Dated prices. The open-ended (end_on: nil) history holds the current prices. Read via #prices.
7
+ has_many :course_fee_histories, -> { order(start_on: :desc) },
8
+ class_name: 'Effective::CourseFeeHistory', dependent: :destroy
9
+
6
10
  log_changes if respond_to?(:log_changes)
7
11
 
8
12
  # rich_text_body - Used by the select step
@@ -31,9 +35,10 @@ module Effective
31
35
  # For course purchases
32
36
  can_register :boolean
33
37
 
34
- # Pricing
35
- regular_price :integer
36
- member_price :integer
38
+ # Pricing now lives on dated course_fee_histories, read via #prices / #regular_price /
39
+ # #member_price. These columns still exist in the database (a later migration drops them).
40
+ # regular_price :integer
41
+ # member_price :integer
37
42
 
38
43
  qb_item_name :string
39
44
  tax_exempt :boolean
@@ -80,10 +85,7 @@ module Effective
80
85
  validates :status, presence: true
81
86
  validates :title, presence: true
82
87
 
83
- with_options(if: -> { can_register? }) do
84
- validates :regular_price, presence: true
85
- validates :member_price, presence: true
86
- end
88
+ # Price presence now lives on CourseFeeHistory (validated when can_register?).
87
89
 
88
90
  # Syncs all courses
89
91
  def self.refresh!
@@ -110,5 +112,57 @@ module Effective
110
112
  false
111
113
  end
112
114
 
115
+ # The CourseFeeHistory (price list) in effect on the given date — today by default.
116
+ # Prices used to live in flat columns here; read them off this record now:
117
+ #
118
+ # course.prices.regular_fee # price right now
119
+ # course.prices(date: date).member_fee # price in effect on date
120
+ #
121
+ # Raises if no history covers the date so misconfiguration surfaces loudly.
122
+ def prices(date: nil)
123
+ date = (date || Time.zone.now).to_date
124
+
125
+ # course_fee_histories is ordered start_on desc — pick the newest window covering the
126
+ # date. end_on nil makes an endless range (start_on..), i.e. the current prices.
127
+ course_fee_histories.find { |history| (history.start_on..history.end_on).cover?(date) } ||
128
+ raise("No course_fee_history prices available for #{self} on #{date.strftime('%F')}, add a course fee history covering that date")
129
+ end
130
+
131
+ # Backwards-compatible readers so existing call sites keep working after prices moved to
132
+ # dated histories. These override the retired flat columns.
133
+ def regular_price(date: nil)
134
+ prices(date: date).regular_fee
135
+ end
136
+
137
+ def member_price(date: nil)
138
+ prices(date: date).member_fee
139
+ end
140
+
141
+ # Non-blocking warnings about the price timeline, shown on the admin edit screen. Prices
142
+ # should form one continuous timeline with a single open-ended (current) history.
143
+ def course_fee_histories_warnings
144
+ histories = course_fee_histories.sort_by(&:start_on)
145
+ return ['No prices have been set — add a course fee history.'] if histories.empty?
146
+
147
+ warnings = []
148
+
149
+ current = histories.select { |history| history.end_on.blank? }
150
+ warnings << 'There are no current prices — the most recent history should have no end date.' if current.empty?
151
+ warnings << 'There is more than one current history (more than one with no end date).' if current.size > 1
152
+
153
+ # Each history should pick up the day after the previous one ends — no gaps, no overlaps.
154
+ histories.each_cons(2) do |earlier, later|
155
+ next if earlier.end_on.blank? # an open-ended history in the middle is flagged above
156
+
157
+ if later.start_on > earlier.end_on + 1.day
158
+ warnings << "Gap in prices between #{earlier.end_on} and #{later.start_on}."
159
+ elsif later.start_on <= earlier.end_on
160
+ warnings << "Overlapping prices around #{later.start_on}."
161
+ end
162
+ end
163
+
164
+ warnings
165
+ end
166
+
113
167
  end
114
168
  end
@@ -0,0 +1,25 @@
1
+ = effective_form_with(model: [:admin, course_fee_history], engine: true) do |f|
2
+ - course = course_fee_history.learndash_course
3
+
4
+ - if course.present?
5
+ = f.hidden_field :learndash_course_id
6
+ = f.static_field :learndash_course, label: 'Course' do
7
+ = course.to_s
8
+ - else
9
+ = f.select :learndash_course_id, Effective::LearndashCourse.sorted, label: 'Course'
10
+
11
+ %p These prices take effect on the start date, and apply until the next history begins. Leave the end date blank for the current, ongoing prices.
12
+
13
+ .row
14
+ .col-lg-4= f.date_field :start_on
15
+ -# date_linked: false stops effective_bootstrap auto-filling end_on from start_on — blank means ongoing.
16
+ .col-lg-4= f.date_field :end_on, date_linked: false, hint: 'leave blank for the current, ongoing prices'
17
+
18
+ %h2 Pricing
19
+ %p Charge the following amounts during course registration. Enter $0 for free.
20
+
21
+ .row
22
+ .col-lg-4= f.price_field :regular_fee, label: 'Price to applicants or new users'
23
+ .col-lg-4= f.price_field :member_fee, label: 'Price to existing members'
24
+
25
+ = effective_submit(f)
@@ -3,6 +3,16 @@
3
3
  = render 'admin/learndash_courses/form_learndash_course', learndash_course: learndash_course
4
4
 
5
5
  - if learndash_course.persisted?
6
+ = tab 'Pricing' do
7
+ %h2 Pricing
8
+ %p Prices are kept in date-ranged histories so they can be scheduled to change over time. The history with no end date holds the current, ongoing prices.
9
+
10
+ - learndash_course.course_fee_histories_warnings.each do |warning|
11
+ .alert.alert-warning= warning
12
+
13
+ - datatable = Admin::CourseFeeHistoriesDatatable.new(learndash_course: learndash_course)
14
+ = render_inline_datatable(datatable)
15
+
6
16
  = tab 'Enrollments' do
7
17
  %h2 Enrollments
8
18
  %p Click the New button from the below table to enroll a user into this course.
@@ -15,14 +15,10 @@
15
15
 
16
16
  = f.show_if :can_register, true do
17
17
  = card('Pricing Information') do
18
- %p.text-muted
19
- Charge the following amounts during course registration. Enter $0 for free.
20
-
21
- .row
22
- .col-lg-3= f.price_field :regular_price, label: 'Price to applicants or new users'
23
-
24
- .row
25
- .col-lg-3= f.price_field :member_price, label: 'Price to existing members'
18
+ - if f.object.persisted?
19
+ %p.text-muted Prices are set on dated histories see the Pricing tab.
20
+ - else
21
+ %p.text-muted Save this course, then set its prices on the Pricing tab.
26
22
 
27
23
  = f.check_box :tax_exempt
28
24
 
data/config/routes.rb CHANGED
@@ -31,6 +31,10 @@ EffectiveLearndash::Engine.routes.draw do
31
31
  get :refresh, on: :collection
32
32
  end
33
33
 
34
+ resources :course_fee_histories, except: [:show] do
35
+ post :duplicate, on: :member
36
+ end
37
+
34
38
  resources :course_registrants, only: [:index]
35
39
  resources :course_registrations, only: [:index, :show]
36
40
  end
@@ -31,8 +31,7 @@ class CreateEffectiveLearndash < ActiveRecord::Migration[6.0]
31
31
  # Course Purchases
32
32
  t.boolean :can_register, default: false
33
33
 
34
- t.integer :regular_price
35
- t.integer :member_price
34
+ # Prices live on dated course_fee_histories (see below), not flat columns.
36
35
 
37
36
  t.string :qb_item_name
38
37
  t.boolean :tax_exempt, default: false
@@ -40,6 +39,23 @@ class CreateEffectiveLearndash < ActiveRecord::Migration[6.0]
40
39
  t.timestamps
41
40
  end
42
41
 
42
+ # Dated prices for a learndash course. The open-ended (end_on: nil) row holds the
43
+ # current, ongoing prices; earlier rows are scheduled/expired price lists.
44
+ create_table :course_fee_histories do |t|
45
+ t.integer :learndash_course_id
46
+
47
+ t.date :start_on
48
+ t.date :end_on
49
+
50
+ t.integer :regular_fee
51
+ t.integer :member_fee
52
+
53
+ t.datetime :updated_at
54
+ t.datetime :created_at
55
+ end
56
+
57
+ add_index :course_fee_histories, :learndash_course_id
58
+
43
59
  create_table :learndash_enrollments do |t|
44
60
  t.integer :owner_id
45
61
  t.string :owner_type
@@ -1,3 +1,3 @@
1
1
  module EffectiveLearndash
2
- VERSION = '0.6.1'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_learndash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-30 00:00:00.000000000 Z
11
+ date: 2026-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -179,12 +179,14 @@ files:
179
179
  - app/assets/javascripts/effective_learndash/base.js
180
180
  - app/assets/stylesheets/effective_learndash.scss
181
181
  - app/assets/stylesheets/effective_learndash/base.scss
182
+ - app/controllers/admin/course_fee_histories_controller.rb
182
183
  - app/controllers/admin/course_registrations_controller.rb
183
184
  - app/controllers/admin/learndash_courses_controller.rb
184
185
  - app/controllers/admin/learndash_enrollments_controller.rb
185
186
  - app/controllers/admin/learndash_users_controller.rb
186
187
  - app/controllers/effective/course_registrations_controller.rb
187
188
  - app/controllers/effective/learndash_courses_controller.rb
189
+ - app/datatables/admin/course_fee_histories_datatable.rb
188
190
  - app/datatables/admin/effective_course_registrations_datatable.rb
189
191
  - app/datatables/admin/effective_learndash_courses_datatable.rb
190
192
  - app/datatables/admin/effective_learndash_enrollments_datatable.rb
@@ -196,12 +198,14 @@ files:
196
198
  - app/helpers/effective_learndash_helper.rb
197
199
  - app/models/concerns/effective_learndash_course_registration.rb
198
200
  - app/models/concerns/effective_learndash_owner.rb
201
+ - app/models/effective/course_fee_history.rb
199
202
  - app/models/effective/course_registrant.rb
200
203
  - app/models/effective/course_registration.rb
201
204
  - app/models/effective/learndash_api.rb
202
205
  - app/models/effective/learndash_course.rb
203
206
  - app/models/effective/learndash_enrollment.rb
204
207
  - app/models/effective/learndash_user.rb
208
+ - app/views/admin/course_fee_histories/_form.html.haml
205
209
  - app/views/admin/learndash_courses/_form.html.haml
206
210
  - app/views/admin/learndash_courses/_form_course_registration_content.html.haml
207
211
  - app/views/admin/learndash_courses/_form_learndash_course.html.haml