diff --git a/README.md b/README.md index 60e5097..8b4c01b 100644 --- a/README.md +++ b/README.md @@ -207,3 +207,73 @@ add_filter( 'limit_orders_message_placeholders', function ( $placeholders ) { Now, we can create customer-facing notices like: > {store_name} is a little overwhelmed right now, but we'll be able to take more orders on {next_interval:date}. Please check back then! + +### Dynamically changing limiter behavior + +In certain cases, you may want to further customize the logic around _which_ orders count toward the limit or, for example, change the behavior based on time of day. Limit Orders for WooCommerce has you covered: + +#### Customize the counting of qualified orders + +Sometimes, you only want to limit certain types of orders. Maybe some orders are fulfilled via third parties (e.g. [dropshipping](https://www.liquidweb.com/woocommerce-resource/dropshipping-glossary/)), or perhaps you're willing to bend the limits a bit for orders that contain certain products. + +You can customize the logic used to calculate the count via the `limit_orders_pre_count_qualifying_orders` filter: + +```php +/** + * Determine how many orders to count against the current interval. + * + * @param bool $preempt Whether the counting logic should be preempted. Returning + * anything but FALSE will bypass the default logic. + * @param OrderLimiter $limiter The current OrderLimiter instance. + * + * @return int The number of orders that should be counted against the limit. + */ +add_filter( 'limit_orders_pre_count_qualifying_orders', function ( $preempt, $limiter ) { + /* + * Do whatever you need to do here to count how many orders count. + * + * Pay close attention to date ranges here, and check out the public methods + * on the Nexcess\LimitOrders\OrderLimiter class. + */ +}, 10, 2 ); +``` + +Please note that the `LimitOrders::count_qualifying_orders()` method (where this filter is defined) is only called in two situations: + +1. When a new order is created. +2. If the `limit_orders_order_count` transient disappears. + +#### Dynamically change the order limit + +If, for example, you want to automatically turn off the store overnight, you might do so by setting the limit to `0` only during certain hours. + +You can accomplish this using the `limit_orders_pre_get_remaining_orders` filter: + +```php +/** + * Disable the store between 10pm and 8am. + * + * This works by setting the limit on Limit Orders for WooCommerce to zero if + * the current time is between those hours. + * + * @param bool $preempt Whether or not the default logic should be preempted. + * Returning anything besides FALSE will be treated as the + * number of remaining orders that can be accepted. + * + * @return int|bool Either 0 if the store is closed (meaning zero orders remaining) + * or the value of $preempt if Limit Orders should proceed normally. + */ +add_filter( 'limit_orders_pre_get_remaining_orders', function ( $preempt ) { + $open = new \DateTime('08:00', wp_timezone()); + $close = new \DateTime('22:00', wp_timezone()); + $now = current_datetime(); + + // We're currently inside normal business hours. + if ( $now >= $open && $now < $close ) { + return $preempt; + } + + // If we've gotten this far, turn off ordering. + return 0; +} ); +``` diff --git a/src/OrderLimiter.php b/src/OrderLimiter.php index 745c3ec..577f699 100644 --- a/src/OrderLimiter.php +++ b/src/OrderLimiter.php @@ -158,6 +158,21 @@ public function get_placeholders( $setting = '', $message = '' ) { public function get_remaining_orders() { $limit = $this->get_limit(); + /** + * Filter the number of orders remaining for the current interval. + * + * @param bool $preempt Whether or not the default logic should be preempted. + * Returning anything besides FALSE will be treated as the + * number of remaining orders that can be accepted. + * @param OrderLimiter $limiter The current OrderLimiter object. + */ + $remaining = apply_filters( 'limit_orders_pre_get_remaining_orders', false, $this ); + + // Return early if a non-false value was returned from the filter. + if ( false !== $remaining ) { + return (int) $remaining; + } + // If there are no limits set, return -1. if ( ! $this->is_enabled() || -1 === $limit ) { return -1; @@ -384,6 +399,19 @@ public function reset_limiter_on_update( $previous, $new ) { * @return int The number of orders that have taken place within the defined interval. */ protected function count_qualifying_orders() { + /** + * Replace the logic used to count qualified orders. + * + * @param bool $preempt Whether the counting logic should be preempted. Returning + * anything but FALSE will bypass the default logic. + * @param OrderLimiter $limiter The current OrderLimiter instance. + */ + $count = apply_filters( 'limit_orders_pre_count_qualifying_orders', false, $this ); + + if ( false !== $count ) { + return (int) $count; + } + $orders = wc_get_orders( [ 'type' => wc_get_order_types( 'order-count' ), 'date_created' => '>=' . $this->get_interval_start()->getTimestamp(), diff --git a/tests/OrderLimiterTest.php b/tests/OrderLimiterTest.php index d1d1414..03a4f42 100644 --- a/tests/OrderLimiterTest.php +++ b/tests/OrderLimiterTest.php @@ -323,6 +323,46 @@ public function get_remaining_orders_should_return_zero_if_limits_are_met_or_exc $this->assertSame( 0, ( new OrderLimiter() )->get_remaining_orders() ); } + /** + * @test + * @testdox get_remaining_orders() should be filterable + */ + public function get_remaining_orders_should_be_filterable() { + $instance = new OrderLimiter(); + $called = false; + + update_option( OrderLimiter::OPTION_KEY, [ + 'enabled' => true, + 'limit' => 5, + ] ); + + add_filter( 'limit_orders_pre_get_remaining_orders', function ( $preempt, $limiter ) use ( $instance, &$called ) { + $this->assertFalse( $preempt, 'The $preempt argument should start as false.' ); + $this->assertSame( $instance, $limiter ); + $called = true; + + return -1; + }, 10, 2 ); + + $this->assertSame( -1, $instance->get_remaining_orders() ); + $this->assertTrue( $called ); + } + + /** + * @test + * @testdox get_remaining_orders() should be filterable + */ + public function get_remaining_orders_should_cast_the_return_values_as_integers() { + update_option( OrderLimiter::OPTION_KEY, [ + 'enabled' => true, + 'limit' => 5, + ] ); + + add_filter( 'limit_orders_pre_get_remaining_orders', '__return_true' ); + + $this->assertSame( 1, ( new OrderLimiter() )->get_remaining_orders(), 'TRUE should be cast as 1.' ); + } + /** * @test * @group Intervals @@ -946,4 +986,50 @@ public function count_qualifying_orders_should_not_limit_results() { $this->assertSame( 5, $method->invoke( $instance ) ); } + + /** + * @test + * @testdox count_qualifying_orders() should be filterable + */ + public function count_qualifying_orders_should_be_filterable() { + $instance = new OrderLimiter(); + $called = false; + $method = new \ReflectionMethod( $instance, 'count_qualifying_orders' ); + $method->setAccessible( true ); + + update_option( OrderLimiter::OPTION_KEY, [ + 'enabled' => true, + 'limit' => 1, + ] ); + + add_filter( 'limit_orders_pre_count_qualifying_orders', function ( $preempt, $limiter ) use ( $instance, &$called ) { + $this->assertFalse( $preempt ); + $this->assertSame( $instance, $limiter ); + $called = true; + + return 5; + }, 10, 2 ); + + $this->assertSame( 5, $method->invoke( $instance ) ); + $this->assertTrue( $called ); + } + + /** + * @test + * @testdox Return values from the limit_orders_pre_count_qualifying_orders filter should be cast as integers + */ + public function return_values_from_pre_count_qualifying_orders_should_be_cast_as_int() { + $instance = new OrderLimiter(); + $method = new \ReflectionMethod( $instance, 'count_qualifying_orders' ); + $method->setAccessible( true ); + + update_option( OrderLimiter::OPTION_KEY, [ + 'enabled' => true, + 'limit' => 1, + ] ); + + add_filter( 'limit_orders_pre_count_qualifying_orders', '__return_true' ); + + $this->assertSame( 1, $method->invoke( $instance ) ); + } }