WooCommerce Course Pricing
Pro-rata pricing for WooCommerce course products: the price is scaled by the share of sessions still to come, with holidays excluded
by Alex Mochulskyi · github.com/alexskybrain/woocommerce-course-pricing
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/alexskybrain/woocommerce-course-pricing/archive/refs/heads/main.zipA term course sold at a fixed price is not worth the same in week six as it was in week one. A school that keeps charging the full amount either turns away late enrolments or issues refunds by hand, every time.
This plugin prices a course by the share of sessions the student will actually attend. It was built for a language school running term courses as WooCommerce products, where late enrolments were being adjusted manually on every order.
How the price is worked out
Given a course that runs weekly on a fixed weekday between two dates:
- Build the list of dates the class meets, starting at the first matching weekday on or after the start date.
- Remove any date inside a holiday period.
- Count how many of the remaining dates are in the past. A session happening today counts as upcoming, because it has not happened yet.
- Scale the price:
full price × remaining ÷ total, rounded to the store's price precision.
A course whose sessions have all passed prices at zero. A schedule with no sessions at all is treated as a misconfiguration and leaves the price untouched, rather than silently making the course free.
Worked example
A ten-week Monday course at £300, with one week of half term removed, is nine sessions at £33.33 each. Enrol after the sixth session and you pay for the three that are left: £100.
Requirements
- WordPress 6.4+
- WooCommerce
- Advanced Custom Fields, for the schedule fields
- PHP 8.2+
Both dependencies are checked on load, and the plugin tells you in the admin and leaves prices alone rather than failing.
Setup
Add an ACF group named course_dates to your course products:
| Field | Type | Purpose |
|---|---|---|
start_date |
Date picker | First day of the term |
end_date |
Date picker | Last day of the term |
apply_price_calculator |
True / False | Switches pro-rata pricing on for this product |
holidays |
Repeater of start_date + end_date |
Periods with no session |
The meeting day is read from a product attribute such as Day and Time, with a value like Monday 19:00-20:00. The variation is checked first and the parent product second.
By default the plugin only touches products in the courses category, and only the variation whose attribute value is full — a trial lesson costs the same whenever you buy it.
Filters
| Filter | Default | Purpose |
|---|---|---|
course_pricing_categories |
['courses'] |
Product categories in scope. Empty array means all. |
course_pricing_full_terms |
['full'] |
Attribute values that get pro-rata pricing |
course_pricing_excluded_terms |
['trial'] |
Attribute values that never do |
course_pricing_day_attributes |
['day-and-time', 'pa_day-and-time', 'Day and Time'] |
Attribute names checked for the meeting day |
course_pricing_date_format |
d/m/Y |
Format the ACF date picker returns |
Architecture
src/
Schedule/DateRange.php an inclusive, self-normalising range of days
Schedule/CourseSchedule.php dates, weekday, holidays
Schedule/SessionPlanner.php schedule to list of session dates
Pricing/ProRataCalculator.php the rule, with no WordPress in it
Pricing/Quote.php price plus the counts behind it
WooCommerce/ScheduleSource.php interface: where dates come from
WooCommerce/AcfScheduleSource.php the ACF implementation
WooCommerce/VariationMatcher.php which variation qualifies
WooCommerce/PriceApplier.php hooks into cart and display
The calculation is free of WordPress calls, and the schedule arrives through an interface, so the pricing rules can be tested with nothing but a PHP binary and the storage can be swapped without touching them.
Quote carries the session counts alongside the amount, not just the number. "Why am I paying £100 and not £300" is the first question a parent asks, and the answer is shown as a line under the cart item rather than recomputed somewhere else.
Tests
php tests/pro-rata-test.php
Sixteen checks covering session planning, holiday handling, the boundaries at both ends of a term, rounding, and the guards against an impossible schedule.
Notes on the rewrite
This is a rewrite of code that ran in production, and two behaviours were deliberately changed:
The price is now applied in the cart, not only at checkout. The original hooked woocommerce_before_calculate_totals but returned early unless is_checkout(), so the cart displayed the full term price and the total dropped on the next screen. The variation picker and the displayed price now come from the same calculation as the amount charged.
A session happening today is no longer counted as passed. The original comment said today should not count, and the comparison used <=, which counted it. Every parent enrolling on a class morning was charged for a lesson that had not started.
License
GPL-2.0-or-later — see LICENSE.