Please consider leaving a donation if you appreciate this information. Lightning network now available
WooCommerce Bookings provides a way for people to sell time based products and it can be made to work with a wide variety of use-cases. Unfortunately, in order to be able to work with this wide range of use-cases the configuration can be quite challenging and the interface can get difficult to use to set up complex pricing rules based on duration + person count or other variables that might determine the final cost of a booking. Consider this example for a business offering Jet Ski rentals and the pricing structure that they wish to achieve:
At first glance, this looks fairly straightforward to set up using block count price adjustments and person count pricing adjustments. However, as the block count and person count increases, the mathematical relationships begin to become less clear and would probably require a mathematician to figure out how to represent the various price points within the bookable product configuration screen. I’m no mathematician. Fortunately, Bookings does offer a filter which can be used to dynamically set the booking cost based on the booking duration and person count. Just to be clear, in this example the jetski count is represented by the person count in the bookable product.
The filter that I would used to do this is named booking_form_calculated_booking_cost and is located in wp-content/plugins/woocommerce-bookings/includes/booking-form/class-wc-booking-form.php in case you want to browse the code to see how it works. The call to apply_filters passes three parameters. The first is the booking cost – this is the value that will be conditionally filtered and eventually returned. The second parameter is an object which represents the booking form itself and contains some information about the bookable product that is currently being viewed. This object will expose the product id which we will need to ensure that only this particular product will have its cost adjusted by this filter implementation. The third parameter that is passed is the data that was posted. This data is important as it contains the block count (duration) and the person count.
Below is an example of how to achieve the required pricing table using the filter that I mentioned. The idea is this:
- On line 8 if the product id matches YOUR_PRODUCT_ID_HERE (replace with your product id – unsure how to find your product id, see this post ) then filter the price.
- Beginning on line 12 the pricing table in represented in an array. There are certainly more elegant ways to do this but here’s what I’ve done, I’ve taken the number of days (duration) and the number of jetskis (person count) – i.e. 3 jetskis for 5 days the array key will be ’35
- On line 46 there is a ternary operator that will set a default cost if the array key does not exist. You will notice that there are no array keys for 1 day 1 person, 2 days 1 person…etc. The cost in these cases would simply be 275 * (number of days) * (number of persons). This will also serve as a default price calculation in case the product configuration changes without updating the filter’s callback function – without this error handling, there is a chance that the product could be booked for free.
- Return the booking cost.
And here is the example code of how this could be achieved:
Keep in mind that since the prices for this particular product are completely overridden then you may wish to configure the “Display cost” as appropriate in your bookable product configuration. The lowest possible cost in this case would be for 5 jetskis for 7 days. This works out to $177.14 per person, per day. This might be the most appropriate value to enter in the “Display cost” field.
Not sure what to do with code snippets? See What Do I Do With These Code Snippets?
Please consider leaving a donation if you appreciate this information. Lightning network now available
dinkadonk says
This was very helpful in getting me in the right direction. I wonder if I could ask a question. We have a multi-vendor booking site. On the front-end form for creating a bookable product, the “Multiply all costs by number of persons” is enabled by default and hidden. This does what we want when costs are entered in the Costs tab, but when there are Person Types, the number of persons gets multiplied twice. To reverse that I need to divide by number of persons. Here’s what I’m trying to do, using the same filter:
Your post showed me how to access the number of persons entered by the customer on the booking form (I think), but I don’t know how to access whether the product has person types enabled. I think I need something else between the question marks in the “if” statement? I don’t know how to see the structure of these objects to find the right element. Thank you!
will says
Bookings provides a method named
has_person_types
so something like$booking ->product ->has_person_types()
– that would return true if so.dinkadonk says
Thank you. I found that public function, but it’s apparently returning neither TRUE nor FALSE. When I do this and test a product with person types enabled, no booking cost at all appears, even if I set it outside the if-else. Seems like the whole thing must be throwing an error. I tried calling it as WC_Product_Booking::has_person_types() instead, but still no go.
add_filter(‘booking_form_calculated_booking_cost’, ‘cc_fix_costs’, 3 );
function cc_fix_costs( $booking_cost, $booking, $posted ) {
if ( $booking->product->has_person_types() ) {
// $num_persons = $posted[‘wc_bookings_field_persons’];
// $booking_cost = $booking_cost / $num_persons;
$booking_cost = 5;
}
else {
$booking_cost = 7;
}
return $booking_cost;
}
I would guess that boolean value is somewhere inside $booking, but I don’t know how to find it.
will says
see below – not sure you would have gotten a notification since I replied in the wrong spot…
will says
Depending on the PHP version in use, it probably is. You will want to check your call to
add_filter
The 3rd parameter is a priority and the 4th parameter is the number of arguments that are passed. The way that it is written it would have a priority of 3 and the callback function will expect only 1 argument to be passed to it yet the callback function declaration specifies 3 arguments. PHP versions >= 7.0 will throw a fatal.Also, it appears that there are curly quotes rather than straight quotes so that is going to cause problems – check those.
Get all of that corrected and you’re on the right track – see screencast here: http://cld.wthms.co/KGj3o9
dinkadonk says
Doh! That was it, I got those parameters of add_filter backwards. The quotes are straight in the functions.php file. So now, as you showed, the function is executing. Problem is, when I try to do the actual calculation (with person types enabled), it’s apparently returning $inf (that’s what displays on front end instead of cost). Does that mean I’m dividing by 0?
If so, is there something wrong with the way I’m getting $num_persons? Here’s what I’m testing now:
add_filter('booking_form_calculated_booking_cost', 'cc_fix_costs', 10, 3 );
function cc_fix_costs( $booking_cost, $booking, $posted ) {
if ( $booking->product->has_person_types() ) {
$num_persons = $posted['wc_bookings_field_persons'];
$booking_cost = $booking_cost / $num_persons;
}
return $booking_cost;
}
Thanks very much for your help!
will says
http://cld.wthms.co/umPXxJ
dinkadonk says
Oh crap! So every person type input field on every product form is going to have a different name. Clearly I’m getting in over my head.
Maybe if I knew how $posted was structured, I could specify them by position instead of name. But I would also have to access the number of person types in the $booking object – maybe some products would have more than two.
Or maybe there is another variable in $posted that gives the total number of persons?
will says
Correct.
Probably.
It’s an array of the data containted in the POST request that has had a bit of sanitization done to it. You can always error log things out to see what is in the array using something like
error_log( print_r( $variable, true ) );
Nope, but there is a PHP function that you might find useful. Here’s a link to strpos and if it were me, I would just loop through the posted data and match for
wc_bookings_field_persons_
– if strpos returns something other than false, then this must be a persons type field and its value could be used to increment the$num_persons
variable. Something like this would do the trick:dinkadonk says
Yay! It works! That’s some powerful juju code.
Also thanks for the tip on using error_log – very handy. It looks like that $posted array is short and sweet, but the $booking object is huge and complex. Someday I hope to learn how to drill down and get individual elements from something like that.
Thank you so much for your generosity Will. I’ve learned a lot.
dinkadonk says
Actually, I was thinking, the way you set it up, even without the if statement, if person types is not enabled, and thus there are no wc_bookings_field_persons_*** variables, **$num_persons would remain at 0, and the original $booking_cost would be returned.
So I think the if statement isn’t even strictly needed. But maybe it processes faster as is when there are no person types?
will says
I’m not going to benchmark it to see what is more efficient. You’re more than welcome to though, but you’re right, the end result would be the same either way.