Commit 1477f0b68fa for woocommerce
commit 1477f0b68fa719778a93b84845717c901f92fd58
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date: Wed Aug 26 11:04:45 2026 +0100
Refactor the Orders Stats report tests into focused classes (#68026)
* Refactor the Orders Stats report tests into focused classes
The legacy WC_Admin_Tests_Reports_Orders_Stats class had grown to
6,755 lines: one 3,064-line test method covering 36 filter scenarios
through near-identical 80-line expected-array blocks, two zero-fill
tests that were ~86% copies of each other, and a dozen unrelated
concerns sharing one file. Failures pointed at the class rather than
the scenario, and the file was excluded from coverage runs pending
this refactor.
Split the class by concern into focused classes under
tests/php/src/Admin/API/Reports/Orders/Stats, following the modern
test conventions:
- DataStoreFilterQueriesTest: the 36 filter scenarios as a data
provider over per-product-mix order counts, with the 36-order
fixture created once per class.
- DataStoreZeroFillTest: the two zero-fill tests deduplicated into
one class parameterized over both orderings.
- DataStoreBasicsTest, DataStoreRefundFiltersTest,
DataStoreSegmentingTest, DataStoreReturningCustomersTest and
DataStoreFulfillmentsTest: ports of the remaining tests.
Assertion-for-assertion parity with the legacy class: 57 tests carry
the same 105 assertions, green with HPOS on and off, and a line
coverage diff against a run of the legacy class shows no lost lines
in any report or fulfillment source file. Removing the legacy class
also retires its run-in-separate-process group, so these tests are
included in coverage runs again.
* Add changelog entry for the Orders Stats test refactor
* Extract a base test case for the Orders Stats report tests
The migrated Orders Stats test classes each declared full expected
get_data() result arrays: fourteen totals keys, a duplicated subtotals
block and the single-interval wrapper, repeated per scenario. They also
each carried the same report-cache-disabling setUp().
Add OrdersStatsTestCase with the shared setUp(), an expected_totals()
builder that zero-fills every totals key so tests only spell out the
values that matter, an expected_stats_single_interval() wrapper that
derives the interval subtotals from the totals, and the shared
assert_report_data() helper. Rework the test classes on top of it.
No change in what is asserted: still 57 tests with 105 assertions,
green with HPOS on and off.
* Improve naming and remove magic numbers in the Orders Stats tests
The migrated classes kept the legacy naming: numbered orders
(order_1, order_2), cryptic aggregate prefixes (o1_net_revenue,
p2_shipping, i3_tot_orders_count) and the helper's hardcoded item
quantity and shipping amount repeated as bare 4s and 10s.
Name orders and aggregates by role (mixed_order, fulfilled_order,
latest_interval_revenue), move the QTY_PER_PRODUCT and
SHIPPING_AMOUNT constants for WC_Helper_Order::create_order into
OrdersStatsTestCase, and derive previously hardcoded item and
revenue figures from them.
No change in what is asserted: still 57 tests with 105 assertions.
* Replace numbered fixture indirection in the filter queries test
ORDER_STATUS_1/ORDER_STATUS_2 only aliased the OrderStatus constants,
and the product prices lived in an array keyed by fixture number,
hiding that the fixture is really three primary products plus one
add-on product shared by every two-product order.
Use the OrderStatus constants directly (scenarios included, so only
product and coupon IDs go through placeholder resolution), model the
products as a primary price list plus an ADD_ON_PRODUCT_PRICE
constant, and give the two coupons named ID properties.
No change in what is asserted: still 36 scenarios, 36 assertions.
* Name the filter test coupons by their discount size
COUPON_1_AMOUNT and COUPON_2_AMOUNT numbered the coupons even though
their only difference is the discount amount, and coupon 1 relied on
the WC_Helper_Coupon::create_coupon default instead of setting its
amount, which needed a comment to explain.
Name them the small and large coupon, set both amounts explicitly in
the fixture, and rename the scenario placeholders to match.
* Describe the filter scenarios by their expected match
The scenario keys named the filter args ('status_is both statuses',
'match all: five filters'), which said nothing about what the
scenario expects. They surface in test output as the data set name,
so a failure gave no hint of what broke.
Rename each key to state the filters and the orders they should
match, e.g. 'status_is_not with both statuses matches nothing' and
'match any: excluding the large coupon widens the union again'.
* Build the zero-fill fixture once per class
Review feedback on the migration: the zero-fill fixture was rebuilt
for all eight provider cases, while the legacy class built it twice,
and #32403 calls out fixture runtime. The two orderby variants only
needed different order distributions so counting and date ordering
disagree, which one shared distribution can provide.
Create the fixture once in wpSetUpBeforeClass with two orders in the
current hour and three in the previous hour. Two orders tied at the
newest timestamp are kept deliberately: a line coverage diff against
the legacy run showed the not-the-latest-order early return in the
Customers DataStore sync is only exercised through that tie.
Also restore the legacy explanation of why the report cache filter
must be added after parent::setUp().
Still 57 tests with 105 assertions, and the coverage diff against
the legacy baseline is back to zero lost lines.
* Address review feedback on the Orders Stats tests
Fixes from the CodeRabbit review of the migration:
- Restore the tax options with their matching saved values in the
order deletion test; the legacy class swapped them, which only the
per-test transaction rollback made harmless.
- Repair two arrays that an earlier phpcbf run mangled while
relocating trailing comments, in the returning-customer scenario
and the returning-customer classification test, and drop the stale
'To skip cache.' comment now that the base class disables caching.
- Expect total_customers as a literal 1 instead of reusing the
returning-orders count that happens to equal it.
- Correct the coupon-application comment: the orders receive 3, 2
and 1 coupons, not 1, 2 and 3.
- Delete only the tracked fixture orders in the class teardowns
instead of every order in the database.
- Clear the full-refund format option in tearDown so it is removed
after failed assertions too, and drop the inline deletes.
- Replace a one-iteration foreach with direct first-item access when
refunding.
- Reset the stats tables at the start of the segmenting test and
document that the zero-fill query start keeps its seconds part.
Still 57 tests with 105 assertions.
* Delete fixture products through CRUD in the class teardowns
The class fixtures deleted their orders but left their products to
the WordPress test framework, which truncates the posts tables
directly. That skips the WooCommerce CRUD hooks, so each fixture
product left an orphaned wc_product_meta_lookup row behind: five
rows across the two classes, verified with a probe after running
them, and zero rows once the teardowns delete the products through
wc_get_product()->delete().
* Pin the segmenting fixture orders to the captured order time
The mixed and variations orders kept their creation-time date while
the expected intervals derive from a time() value captured earlier,
so an hour rollover between the two put the orders in a different
bucket than the expectations assume. The legacy test had the same
race. Set both orders' created and paid dates to the captured time,
as the earlier order already does.
* Remove the migration provenance lines from the test docblocks
The 'Migrated from the legacy class' lines point at a class this
branch deletes, and the migration story lives in the commit
messages. The docblocks keep only what describes the tests
themselves.
* Anchor the segmenting query window on the captured order time
The query end and the newest interval boundary came from a DateTime
captured after the analytics queue ran. An hour rollover between the
order-time capture and that point adds a fourth hourly interval to
the response while the expectations hold three. Derive the window
end from the captured order time instead; the orders are pinned to
that same second, so the inclusive before bound still covers them.
diff --git a/plugins/woocommerce/changelog/refactor-wooplug-656-split-orders-stats-tests b/plugins/woocommerce/changelog/refactor-wooplug-656-split-orders-stats-tests
new file mode 100644
index 00000000000..c5b0778ce8c
--- /dev/null
+++ b/plugins/woocommerce/changelog/refactor-wooplug-656-split-orders-stats-tests
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Split the legacy Orders Stats report test class into focused test classes under tests/php/src, with no change in coverage
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
deleted file mode 100644
index 84ff630f059..00000000000
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
+++ /dev/null
@@ -1,6755 +0,0 @@
-<?php
-/**
- * Reports order stats tests.
- *
- * @package WooCommerce\Admin\Tests\Orders
- */
-
-use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
-use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query as OrdersStatsQuery;
-use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
-use Automattic\WooCommerce\Enums\OrderStatus;
-use Automattic\WooCommerce\Enums\ProductStockStatus;
-use Automattic\WooCommerce\Internal\Admin\Analytics;
-use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
-
-/**
- * Class WC_Admin_Tests_Reports_Orders_Stats
- * Excluding it from code coverage until we refactor this test.
- * @group run-in-separate-process
- */
-class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
- /**
- * Set the database version and clear the fulfillment-status column flag for this class.
- */
- public static function setUpBeforeClass(): void {
- // Must come first: the parent reconnects `$wpdb`, which discards anything this
- // method has written but not committed.
- parent::setUpBeforeClass();
-
- $db_version = strstr( WC()->version, '-', true );
- $db_version = $db_version ? $db_version : WC()->version;
- update_option( 'woocommerce_db_version', $db_version );
-
- delete_option( OrdersStatsDataStore::OPTION_ORDER_STATS_TABLE_HAS_COLUMN_ORDER_FULFILLMENT_STATUS );
- }
-
- /**
- * Don't cache report data during these tests.
- *
- * This has to be registered per test, and after `parent::setUp()`. WordPress snapshots the
- * hook globals once per process and restores that snapshot after every test, so a filter
- * added from `setUpBeforeClass` is dropped once the first test finishes — leaving the rest
- * of the class running with report caching enabled, which is what these tests set out to avoid.
- * Registering it after the snapshot also means the restore removes it, so no class-level
- * teardown is needed to keep it away from other tests.
- */
- public function setUp(): void {
- parent::setUp();
-
- add_filter( 'woocommerce_analytics_report_should_use_cache', '__return_false' );
- }
-
- /**
- * Test the calculations and querying works correctly for the base case of 1 order.
- *
- * @since 3.5.0
- */
- public function test_populate_and_query() {
- WC_Helper_Reports::reset_stats_dbs();
-
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $coupon = WC_Helper_Coupon::create_coupon( 'test-coupon' );
- $coupon->set_amount( 20 );
- $coupon->save();
-
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->set_status( OrderStatus::COMPLETED );
- $order->set_shipping_total( 10 );
- $order->apply_coupon( $coupon );
- $order->set_cart_tax( 5 );
- $order->set_shipping_tax( 2 );
- $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
- $order->save();
-
- $refund = wc_create_refund(
- array(
- 'amount' => 12,
- 'order_id' => $order->get_id(),
- )
- );
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
-
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 1,
- 'num_items_sold' => 4,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 80,
- 'total_sales' => 85,
- 'gross_sales' => 100,
- 'coupons' => 20,
- 'coupons_count' => 1,
- 'refunds' => 12,
- 'taxes' => 7,
- 'shipping' => 10,
- 'net_revenue' => 68,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => 85,
- 'gross_sales' => 100,
- 'net_revenue' => 68,
- 'coupons' => 20,
- 'coupons_count' => 1,
- 'shipping' => 10,
- 'taxes' => 7,
- 'refunds' => 12,
- 'orders_count' => 1,
- 'num_items_sold' => 4,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 80,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- // Test retrieving the stats from the data store.
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
-
- // Test retrieving the stats through the query class.
- $query = new OrdersStatsQuery( $args );
- $expected_stats = array(
- 'totals' => array(
- 'net_revenue' => 68,
- 'avg_order_value' => 80,
- 'orders_count' => 1,
- 'avg_items_per_order' => 4,
- 'num_items_sold' => 4,
- 'coupons' => 20,
- 'coupons_count' => 1,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'net_revenue' => 68,
- 'avg_order_value' => 80,
- 'orders_count' => 1,
- 'avg_items_per_order' => 4,
- 'num_items_sold' => 4,
- 'coupons' => 20,
- 'coupons_count' => 1,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $query->get_data() ), true ) );
- }
-
- /**
- * Test that querying statuses includes the default or query-specific statuses.
- */
- public function test_populate_and_query_statuses() {
- WC_Helper_Reports::reset_stats_dbs();
-
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $order_types = array(
- array(
- 'status' => OrderStatus::REFUNDED,
- 'total' => 50,
- ),
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 100,
- ),
- array(
- 'status' => OrderStatus::FAILED,
- 'total' => 75,
- ),
- );
-
- $time = time();
-
- foreach ( $order_types as $order_type ) {
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->set_status( $order_type['status'] );
- $order->set_total( $order_type['total'] );
- $order->set_date_created( $time );
- $order->set_date_paid( $time );
- $order->set_shipping_total( 0 );
- $order->set_cart_tax( 0 );
- $order->save();
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
-
- // Query default statuses that should not include excluded or refunded order statuses.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 2,
- 'num_items_sold' => 4, // 4 items sold in completed order, non in failed and refunded.
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_sales' => 100,
- 'gross_sales' => 150,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 50,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 100,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => 100,
- 'gross_sales' => 150,
- 'net_revenue' => 100,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 50,
- 'orders_count' => 2,
- 'num_items_sold' => 4,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
-
- // Query an excluded status which should still return orders with the queried status.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'status_is' => array( OrderStatus::FAILED ),
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 1,
- 'num_items_sold' => 4,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_sales' => 75,
- 'gross_sales' => 75,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 75,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => 75,
- 'gross_sales' => 75,
- 'net_revenue' => 75,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 0,
- 'orders_count' => 1,
- 'num_items_sold' => 4,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
- }
-
- /**
- * Test refund type filtering.
- */
- public function test_populate_and_query_refunds() {
- WC_Helper_Reports::reset_stats_dbs();
-
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $order_types = array(
- array(
- 'status' => OrderStatus::REFUNDED,
- 'total' => 50,
- ),
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 100,
- ),
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 75,
- ),
- );
-
- $time = time();
-
- foreach ( $order_types as $order_type ) {
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->set_status( $order_type['status'] );
- $order->set_total( $order_type['total'] );
- $order->set_date_created( $time );
- $order->set_date_paid( $time );
- $order->set_shipping_total( 0 );
- $order->set_cart_tax( 0 );
- $order->save();
- }
-
- // Add a partial refund on the last order.
- foreach ( $order->get_items() as $item_key => $item_values ) {
- $item_data = $item_values->get_data();
- $refund = wc_create_refund(
- array(
- 'amount' => 10,
- 'order_id' => $order->get_id(),
- 'line_items' => array(
- $item_data['id'] => array(
- 'qty' => 0,
- 'refund_total' => 10,
- ),
- ),
- )
- );
- break;
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
-
- // Query all refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'all',
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => -4,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_sales' => -60,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 60,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => -60,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => -60,
- 'gross_sales' => 0,
- 'net_revenue' => -60,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 60,
- 'orders_count' => 0,
- 'num_items_sold' => -4,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
-
- // Query all non-refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'none',
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 3,
- 'num_items_sold' => 12,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_sales' => 225,
- 'gross_sales' => 225,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 225,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => 225,
- 'gross_sales' => 225,
- 'net_revenue' => 225,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 0,
- 'orders_count' => 3,
- 'num_items_sold' => 12,
- 'avg_items_per_order' => 4,
- 'avg_order_value' => 75,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
-
- // Query partial refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'partial',
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_sales' => -10,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 10,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => -10,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => -10,
- 'gross_sales' => 0,
- 'net_revenue' => -10,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 10,
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
-
- // Query full refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'full',
- );
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => -4,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_sales' => -50,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 50,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => -50, // @todo - does this value make sense?
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => gmdate( 'Y-m-d H', $order->get_date_created()->getOffsetTimestamp() ),
- 'date_start' => $start_time,
- 'date_start_gmt' => $start_time,
- 'date_end' => $end_time,
- 'date_end_gmt' => $end_time,
- 'subtotals' => array(
- 'total_sales' => -50,
- 'gross_sales' => 0,
- 'net_revenue' => -50,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'shipping' => 0,
- 'taxes' => 0,
- 'refunds' => 50,
- 'orders_count' => 0,
- 'num_items_sold' => -4,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ) );
- }
-
- /**
- * Test refund type filtering.
- */
- public function test_populate_and_query_refunds_with_old_full_refund_data() {
- WC_Helper_Reports::reset_stats_dbs();
- update_option( 'woocommerce_analytics_uses_old_full_refund_data', 'yes' );
-
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $order_types = array(
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 50,
- ),
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 100,
- ),
- );
-
- $time = time();
-
- foreach ( $order_types as $order_type ) {
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->set_status( $order_type['status'] );
- $order->set_total( $order_type['total'] );
- $order->set_date_created( $time );
- $order->set_date_paid( $time );
- $order->set_shipping_total( 10 );
- $order->set_cart_tax( 10 );
- $order->save();
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Refund the order completely by changing the order status to refunded.
- $order->set_status( OrderStatus::REFUNDED );
- $order->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', strtotime( '+1 day', $order->get_date_created()->getOffsetTimestamp() ) );
-
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $expected_totals = array(
- 'orders_count' => 2,
- 'num_items_sold' => 8, // 4 per order.
- 'avg_items_per_order' => 4, // 8 / 2 orders.
- 'avg_order_value' => 55, // 110 / 2 orders.
- 'total_sales' => 50, // 50 + 100 - 100.
- 'gross_sales' => 110, // 150 - 40 ( 10 + 10 + 10 + 10 ).
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 100,
- 'taxes' => 20,
- 'shipping' => 20,
- 'net_revenue' => 10, // 50 + 100 - 100 - 20 - 20.
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- );
-
- $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
-
- // Query full refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'full',
- );
- $expected_totals = array(
- 'orders_count' => 0,
- 'num_items_sold' => 0, // bug fixed by PR #58744.
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_sales' => -100,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 100, // bug fixed by PR #58744.
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => -100, // @todo - does this value make sense?
- 'total_customers' => 1,
- 'products' => 0, // bug fixed by PR #58744.
- 'segments' => array(),
- );
-
- $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
-
- delete_option( 'woocommerce_analytics_uses_old_full_refund_data' );
- }
-
- /**
- * Test refund type filtering.
- */
- public function test_populate_and_query_refunds_with_new_full_refund_data() {
- WC_Helper_Reports::reset_stats_dbs();
-
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $order_types = array(
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 50,
- ),
- array(
- 'status' => OrderStatus::COMPLETED,
- 'total' => 100,
- ),
- );
-
- $time = time();
-
- foreach ( $order_types as $order_type ) {
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->set_status( $order_type['status'] );
- $order->set_total( $order_type['total'] );
- $order->set_date_created( $time );
- $order->set_date_paid( $time );
- $order->set_shipping_total( 10 );
- $order->set_cart_tax( 10 );
- $order->save();
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Refund the order completely by changing the order status to refunded.
- $order->set_status( OrderStatus::REFUNDED );
- $order->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', strtotime( '+1 day', $order->get_date_created()->getOffsetTimestamp() ) );
-
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $expected_totals = array(
- 'orders_count' => 2,
- 'num_items_sold' => 4, // 4 per order.
- 'avg_items_per_order' => 4, // 8 / 2 orders.
- 'avg_order_value' => 55, // 110 / 2 orders.
- 'total_sales' => 50, // 50 + 100 - 100.
- 'gross_sales' => 110, // 150 - 40 ( 10 + 10 + 10 + 10 ).
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 100,
- 'taxes' => 10,
- 'shipping' => 10,
- 'net_revenue' => 30,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- );
-
- $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
-
- // Query full refunds.
- $args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- 'refunds' => 'full',
- );
- $expected_totals = array(
- 'orders_count' => 0,
- 'num_items_sold' => -4,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_sales' => -100,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 100,
- 'taxes' => -10,
- 'shipping' => -10,
- 'net_revenue' => -80,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- );
-
- $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
-
- delete_option( 'woocommerce_analytics_uses_old_full_refund_data' );
- }
-
- /**
- * Test the calculation of multiple coupons on orders.
- */
- public function test_populate_and_query_multiple_coupons() {
- global $wpdb;
- WC_Helper_Reports::reset_stats_dbs();
-
- // Create a product and customer.
- $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
- $product_price = 23.45;
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( $product_price );
- $product->save();
-
- // create 3 coupons valued 1,2,3.
- $coupons = array();
- foreach ( range( 1, 3 ) as $amount ) {
- $coupon = WC_Helper_Coupon::create_coupon( 'coupon_' . $amount );
- $coupon->set_amount( $amount );
- $coupon->save();
- $coupons[ $amount ] = $coupon;
- }
-
- // Create 3 orders with 1, 2, 3 coupons applied respectively.
- $minute_ago = time() - MINUTE_IN_SECONDS;
- $report_start_time = $minute_ago - ( $minute_ago % HOUR_IN_SECONDS );
- $order_time = $report_start_time + 1;
- $applied_coupons = 0;
- $applied_amount = 0;
- $orders_total = 0;
- $orders = array();
-
- foreach ( range( 1, 3 ) as $order_number ) {
- $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
- $order_date = $order_time++;
- $order->set_date_created( $order_date );
- $order->set_date_paid( $order_date );
- $order->set_status( OrderStatus::COMPLETED );
-
- foreach ( $coupons as $amount => $coupon ) {
- if ( $amount >= $order_number ) {
- $order->apply_coupon( $coupon );
- ++$applied_coupons;
- $applied_amount += $amount;
- }
- }
-
- $order->calculate_totals();
- $orders_total += $order->get_total();
- $order->save();
-
- $orders[] = $order;
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- // Test for the current hour.
- $current_hour_start = new DateTime();
- $current_hour_start->setTimestamp( $report_start_time );
-
- $current_hour_end = new DateTime();
- $current_hour_end->setTimestamp( $report_start_time + HOUR_IN_SECONDS - 1 );
-
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- );
-
- $order_shipping = 10; // Hardcoded in WC_Helper_Order::create_order.
- $qty_per_product = 4; // Hardcoded in WC_Helper_Order::create_order.
- $total_customers = 1;
- $orders_count = count( $orders );
- $coupons_count = count( $coupons );
- $num_items_sold = $orders_count * $qty_per_product;
- $shipping = $orders_count * $order_shipping;
- $net_revenue = $orders_total - $shipping;
- $gross_sales = $product_price * $num_items_sold;
- $subtotals = array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $orders_total,
- 'gross_sales' => $gross_sales,
- 'coupons' => $applied_amount,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- );
- $totals = array_merge( $subtotals, array( 'products' => 1 ) );
-
- $expected_stats = array(
- 'totals' => $totals,
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => $subtotals,
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
- }
-
- /**
- * Test the calculations and querying works correctly for the case of multiple orders.
- */
- public function test_populate_and_query_multiple_intervals() {
- global $wpdb;
- WC_Helper_Reports::reset_stats_dbs();
-
- // 4 different products.
- $product_1_price = 25;
- $product_1 = new WC_Product_Simple();
- $product_1->set_name( 'Test Product' );
- $product_1->set_regular_price( $product_1_price );
- $product_1->save();
-
- $product_2_price = 10;
- $product_2 = new WC_Product_Simple();
- $product_2->set_name( 'Test Product 2' );
- $product_2->set_regular_price( $product_2_price );
- $product_2->save();
-
- $product_3_price = 13;
- $product_3 = new WC_Product_Simple();
- $product_3->set_name( 'Test Product 3' );
- $product_3->set_regular_price( $product_3_price );
- $product_3->save();
-
- $product_4_price = 1;
- $product_4 = new WC_Product_Simple();
- $product_4->set_name( 'Test Product 4' );
- $product_4->set_regular_price( $product_4_price );
- $product_4->save();
-
- // 2 different coupons
- $coupon_1_amount = 1; // by default in create_coupon.
- $coupon_1 = WC_Helper_Coupon::create_coupon( 'coupon_1' );
-
- $coupon_2_amount = 2;
- $coupon_2 = WC_Helper_Coupon::create_coupon( 'coupon_2' );
- $coupon_2->set_amount( $coupon_2_amount );
- $coupon_2->save();
-
- $order_status_1 = OrderStatus::COMPLETED;
- $order_status_2 = OrderStatus::PROCESSING;
-
- $customer_1 = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
-
- $order_1_datetime = new DateTime();
- $order_1_hour = (int) $order_1_datetime->format( 'H' );
- $order_1_datetime->setTime( $order_1_hour, 10, 0 ); // Set a time near the top of the hour.
- $order_1_time = $order_1_datetime->format( 'U' );
-
- $this_['hour'] = array( 1 );
- $this_['day'] = array( 1, 2 );
- $this_['week'] = array( 1, 2 );
- $this_['month'] = array( 1, 2 );
- $this_['year'] = array( 1, 2 );
-
- $order[1]['year'] = (int) $order_1_datetime->format( 'Y' );
- $order[1]['month'] = (int) $order_1_datetime->format( 'm' );
- $order[1]['week'] = (int) $order_1_datetime->format( 'W' );
- $order[1]['day'] = (int) $order_1_datetime->format( 'd' );
- $order[1]['hour'] = (int) $order_1_datetime->format( 'H' );
-
- // same day, different hour.
- $order_3_datetime = new DateTime();
- $order_3_datetime = $order_3_datetime->setTimestamp( $order_1_time - HOUR_IN_SECONDS );
- $order_3_time = $order_3_datetime->format( 'U' );
- $order[3]['year'] = (int) $order_3_datetime->format( 'Y' );
- $order[3]['month'] = (int) $order_3_datetime->format( 'm' );
- $order[3]['week'] = (int) $order_3_datetime->format( 'W' );
- $order[3]['day'] = (int) $order_3_datetime->format( 'd' );
-
- // Previous day.
- $order_4_datetime = new DateTime();
- $order_4_datetime = $order_4_datetime->setTimestamp( $order_1_time - DAY_IN_SECONDS );
- $order_4_time = $order_4_datetime->format( 'U' );
- $order[4]['year'] = (int) $order_4_datetime->format( 'Y' );
- $order[4]['month'] = (int) $order_4_datetime->format( 'm' );
- $order[4]['week'] = (int) $order_4_datetime->format( 'W' );
- $order[4]['day'] = (int) $order_4_datetime->format( 'd' );
-
- // Previous week.
- $order_5_datetime = new DateTime();
- $order_5_datetime = $order_5_datetime->setTimestamp( $order_1_time - WEEK_IN_SECONDS );
- $order_5_time = $order_5_datetime->format( 'U' );
- $order[5]['year'] = (int) $order_5_datetime->format( 'Y' );
- $order[5]['month'] = (int) $order_5_datetime->format( 'm' );
- $order[5]['week'] = (int) $order_5_datetime->format( 'W' );
- $order[5]['day'] = (int) $order_5_datetime->format( 'd' );
-
- // Previous month.
- $order_6_datetime = new DateTime();
- $order_6_datetime = $order_6_datetime->setTimestamp( $order_1_time - MONTH_IN_SECONDS );
- $order_6_time = $order_6_datetime->format( 'U' );
- $order[6]['year'] = (int) $order_6_datetime->format( 'Y' );
- $order[6]['month'] = (int) $order_6_datetime->format( 'm' );
- $order[6]['week'] = (int) $order_6_datetime->format( 'W' );
- $order[6]['day'] = (int) $order_6_datetime->format( 'd' );
-
- // Previous year.
- $order_7_datetime = new DateTime();
- $order_7_datetime = $order_7_datetime->setTimestamp( $order_1_time - YEAR_IN_SECONDS );
- $order_7_time = $order_7_datetime->format( 'U' );
- $order[7]['year'] = (int) $order_7_datetime->format( 'Y' );
- $order[7]['month'] = (int) $order_7_datetime->format( 'm' );
- $order[7]['week'] = (int) $order_7_datetime->format( 'W' );
- $order[7]['day'] = (int) $order_7_datetime->format( 'd' );
-
- foreach ( array( 3, 4, 5, 6, 7 ) as $order_no ) {
- if ( $order[ $order_no ]['day'] === $order[1]['day'] && $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $this_['day'][] = $order_no;
- }
- if ( $order[ $order_no ]['week'] === $order[1]['week'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $this_['week'][] = $order_no;
- }
- if ( $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $this_['month'][] = $order_no;
- }
- if ( $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $this_['year'][] = $order_no;
- }
- }
-
- $orders = array();
- // 2 different order statuses, plus new vs returning customer
- $qty_per_product = 4; // Hardcoded in WC_Helper_Order::create_order.
- $iterations = 1;
-
- foreach ( array( $product_1, $product_2, $product_3 ) as $product ) {
- foreach ( array( null, $coupon_1, $coupon_2 ) as $coupon ) {
- foreach ( array( $order_status_1, $order_status_2 ) as $order_status ) {
- foreach ( array( $customer_1 ) as $customer ) {
- foreach (
- array(
- $order_1_time,
- ) as $order_time
- ) { // As there are no tests for different timeframes, ignore these for now: $order_3_time, $order_4_time, $order_5_time, $order_6_time, $order_7_time
- // One order with only 1 product.
- $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
- $order->set_date_created( $order_time + $iterations++ ); // offset each order by 1 second.
- $order->set_status( $order_status );
-
- if ( $coupon ) {
- $order->apply_coupon( $coupon );
- } else {
- $order->calculate_totals();
- }
-
- $orders[] = $order;
-
- // One order with 2 products: product_4 and selected product.
- $order_2 = WC_Helper_Order::create_order( $customer->get_id(), $product_4 );
-
- $item = new WC_Order_Item_Product();
- $item->set_props(
- array(
- 'product' => $product,
- 'quantity' => 4,
- 'subtotal' => wc_get_price_excluding_tax( $product, array( 'qty' => 4 ) ),
- 'total' => wc_get_price_excluding_tax( $product, array( 'qty' => 4 ) ),
- )
- );
- $item->save();
- $order_2->add_item( $item );
- $order_2->set_date_created( $order_time + $iterations++ ); // offset each order by 1 second.
- $order_2->set_status( $order_status );
-
- if ( $coupon ) {
- $order_2->apply_coupon( $coupon );
- } else {
- $order_2->calculate_totals();
- }
-
- $orders[] = $order_2;
- }
- }
- }
- }
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- // Tests for before & after set to current hour.
- $current_hour_start = new DateTime();
- $current_hour_start->setTimestamp( $order_1_time );
- $current_hour_minutes = (int) $current_hour_start->format( 'i' );
- $current_hour_start->setTimestamp( $order_1_time - $current_hour_minutes * MINUTE_IN_SECONDS );
-
- $current_hour_end = new DateTime();
- $current_hour_end->setTimestamp( $order_1_time );
- $order_1_seconds = (int) $current_hour_end->format( 'U' ) % HOUR_IN_SECONDS;
- $current_hour_end->setTimestamp( $order_1_time + ( HOUR_IN_SECONDS - $order_1_seconds ) - 1 );
-
- // All orders, no filters.
- // 36 orders in one batch (3 products * 3 coupon options * 2 order statuses * 2 order shapes), 4 items of each product per order.
- // 12 orders without coupons, 24 with coupons: 12 with $1 coupon and 12 with $2 coupon.
- // shipping is $10 per order.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- );
-
- $order_permutations = 36;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $total_customers = 1;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // * Order status filter
- // ** Status is, positive filter for 2 statuses, i.e. all orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Status is, positive filter for 1 status -> half orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- ),
- );
-
- $order_permutations = 18;
- $order_w_coupon_1_perms = 6;
- $order_w_coupon_2_perms = 6;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Status is not, negative filter for 1 status -> half orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is_not' => array(
- $order_status_2,
- ),
- );
-
- $order_permutations = 18;
- $order_w_coupon_1_perms = 6;
- $order_w_coupon_2_perms = 6;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Status is not, negative filter for 2 statuses -> no orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is_not' => array(
- $order_status_1,
- $order_status_2,
- ),
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- 'products' => 0,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Status is + Status is not, positive filter for 2 statuses, negative for 1 -> half of orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- );
-
- $order_permutations = 18;
- $order_w_coupon_1_perms = 6;
- $order_w_coupon_2_perms = 6;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // * Product filter
- // ** Product includes, positive filter for 2 products, i.e. 2 orders out of 3.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_includes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 4 )
- + $product_2_price * $qty_per_product * ( $orders_count / 4 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 4 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 4 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 3,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Product includes, positive filter for 1 product, 1/3 of orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_includes' => array(
- $product_3->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 4;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_3_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- // product 3 and product 4 (that is sometimes included in the orders with product 3).
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Product excludes, negative filter for 1 product, 2/3 of orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_excludes' => array(
- $product_1->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_3_price * $qty_per_product * ( $orders_count / 4 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 4 )
- + $product_2_price * $qty_per_product * ( $orders_count / 4 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 4 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 3,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Product excludes, negative filter for 2 products, 1/3 of orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_excludes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 4;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_3_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Product includes + product excludes, positive filter for 2 products, negative for 1 -> 1/3 of orders, only orders with product 2 and product 2 + product 4
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_includes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- 'product_excludes' => array(
- $product_1->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 4;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_2_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // * Coupon filters
- // ** Coupon includes, positive filter for 2 coupons, i.e. 2/3 of orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- $coupon_2->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Coupon includes, positive filter for 1 coupon, 1/3 of orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Coupon excludes, negative filter for 1 coupon, 2/3 of orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'coupon_excludes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 0;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Coupon excludes, negative filter for 2 coupons, 1/3 of orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'coupon_excludes' => array(
- $coupon_1->get_id(),
- $coupon_2->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 0;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Coupon includes + coupon excludes, positive filter for 2 coupon, negative for 1, 1/3 orders
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- $coupon_2->get_id(),
- ),
- 'coupon_excludes' => array(
- $coupon_2->get_id(),
- ),
- );
-
- $order_permutations = 12;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // * Customer filters
- // ** Customer new
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'customer_type' => 'new',
- );
-
- $orders_count = 1;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * $orders_count;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ** Customer returning
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ), // I don't think this makes sense.... gmdate( 'Y-m-d H:i:s', $orders[0]->get_date_created()->getOffsetTimestamp() + 1 ), // Date after initial order to get a returning customer.
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'customer_type' => 'returning',
- );
-
- $total_orders_count = count( $this_['hour'] ) * 36;
- $returning_orders_count = 1;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = $total_orders_count - $returning_orders_count;
- $num_items_sold = $total_orders_count * 6 - ( $returning_orders_count * 4 );
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $total_orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $total_orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $total_orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
- - $product_1_price * $qty_per_product * $returning_orders_count
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $returning_orders_count,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'date_start_gmt' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $returning_orders_count,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query};" );
-
- // Combinations: match all
- // status_is + product_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- );
-
- $order_permutations = 6;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 2;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 6;
- $order_w_coupon_1_perms = 6;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // product_includes + coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 4;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + product_includes + coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 2;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + status_is_not + product_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 2;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + status_is_not + product_includes + product_excludes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- 'product_excludes' => array(
- $product_4->get_id(),
- ),
- );
-
- $order_permutations = 6;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 2;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count * $qty_per_product; // No 2-item-orders here.
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + $product_2_price * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- // Prod_1, status_1, no coupon orders included here, so 2 new cust orders.
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + status_is_not + product_includes + product_excludes + coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- 'product_excludes' => array(
- $product_4->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 2;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + $product_2_price * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is + status_is_not + product_includes + product_excludes + coupon_includes + coupon_excludes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'status_is' => array(
- $order_status_1,
- $order_status_2,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- $product_2->get_id(),
- ),
- 'product_excludes' => array(
- $product_4->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- $coupon_2->get_id(),
- ),
- 'coupon_excludes' => array(
- $coupon_2->get_id(),
- ),
- );
-
- $order_permutations = 2;
- $order_w_coupon_1_perms = 2;
- $order_w_coupon_2_perms = 0;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 2 )
- + $product_2_price * $qty_per_product * ( $orders_count / 2 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Combinations: match any
- // status_is + status_is_not, all orders.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'status_is_not' => array(
- $order_status_1,
- ),
- );
-
- $order_permutations = 36;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR product_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 4 )
- + $product_2_price * $qty_per_product * ( $orders_count / 8 )
- + $product_3_price * $qty_per_product * ( $orders_count / 8 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 4 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 8 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 8 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 24;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 6;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR coupon_excludes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'coupon_excludes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 30;
- $order_w_coupon_1_perms = 6;
- $order_w_coupon_2_perms = 12;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
- + $product_2_price * $qty_per_product * ( $orders_count / 6 )
- + $product_3_price * $qty_per_product * ( $orders_count / 6 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // product_includes OR coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 20;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 4;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 3 / 10 )
- + $product_2_price * $qty_per_product * ( $orders_count * 1 / 10 )
- + $product_3_price * $qty_per_product * ( $orders_count * 1 / 10 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 10 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 10 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 10 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR product_includes OR coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 28;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 8;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 3 / 14 )
- + $product_2_price * $qty_per_product * ( $orders_count * 1 / 7 )
- + $product_3_price * $qty_per_product * ( $orders_count * 1 / 7 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 14 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 7 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 7 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR status_is_not OR product_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 28;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 8;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 3 / 14 )
- + $product_2_price * $qty_per_product * ( $orders_count * 1 / 7 )
- + $product_3_price * $qty_per_product * ( $orders_count * 1 / 7 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 14 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 7 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 7 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR status_is_not OR product_includes OR product_excludes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'product_excludes' => array(
- $product_2->get_id(),
- ),
- );
-
- $order_permutations = 30;
- $order_w_coupon_1_perms = 10;
- $order_w_coupon_2_perms = 10;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 1 / 5 )
- + $product_2_price * $qty_per_product * ( $orders_count * 1 / 10 )
- + $product_3_price * $qty_per_product * ( $orders_count * 1 / 5 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 5 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 10 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 5 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR status_is_not OR product_includes OR product_excludes OR coupon_includes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'product_excludes' => array(
- $product_2->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- );
-
- $order_permutations = 32;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 10;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 3 / 16 )
- + $product_2_price * $qty_per_product * ( $orders_count * 1 / 8 )
- + $product_3_price * $qty_per_product * ( $orders_count * 3 / 16 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 16 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 1 / 8 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 16 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // status_is OR status_is_not OR product_includes OR product_excludes OR coupon_includes OR coupon_excludes.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'match' => 'any',
- 'status_is' => array(
- $order_status_1,
- ),
- 'status_is_not' => array(
- $order_status_2,
- ),
- 'product_includes' => array(
- $product_1->get_id(),
- ),
- 'product_excludes' => array(
- $product_2->get_id(),
- ),
- 'coupon_includes' => array(
- $coupon_1->get_id(),
- ),
- 'coupon_excludes' => array(
- $coupon_2->get_id(),
- ),
- );
-
- $order_permutations = 34;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 10;
-
- $orders_count = count( $this_['hour'] ) * $order_permutations;
- $num_items_sold = $orders_count / 2 * $qty_per_product
- + $orders_count / 2 * $qty_per_product * 2;
- $coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
- $coupons_count = ( $order_w_coupon_1_perms ? 1 : 0 ) + ( $order_w_coupon_2_perms ? 1 : 0 );
- $shipping = $orders_count * 10;
- $net_revenue = $product_1_price * $qty_per_product * ( $orders_count * 3 / 17 )
- + $product_2_price * $qty_per_product * ( $orders_count * 5 / 34 )
- + $product_3_price * $qty_per_product * ( $orders_count * 3 / 17 )
- + ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 17 )
- + ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count * 5 / 34 )
- + ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count * 3 / 17 )
- - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 4,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
- }
-
- /**
- * Test that distinct customer counts and same-hour interval aggregation work with multiple customers.
- *
- * @testdox Distinct customer counts and same-hour interval aggregation work with multiple customers.
- */
- public function test_populate_and_query_multiple_customers_same_hour() {
- WC_Helper_Reports::reset_stats_dbs();
-
- $customer_1 = WC_Helper_Customer::create_customer( 'cust_multi_1', 'pwd_1', 'multi_user_1@mail.com' );
- $customer_2 = WC_Helper_Customer::create_customer( 'cust_multi_2', 'pwd_2', 'multi_user_2@mail.com' );
-
- // Two completed orders by different customers within the same hourly interval.
- // Set a time near the top of the hour so both orders stay within it.
- $order_datetime = new DateTime();
- $order_datetime->setTime( (int) $order_datetime->format( 'H' ), 10, 0 );
- $order_time = (int) $order_datetime->format( 'U' );
-
- $order_1 = WC_Helper_Order::create_order( $customer_1->get_id() );
- $order_1->set_date_created( $order_time );
- $order_1->set_status( OrderStatus::COMPLETED );
- $order_1->save();
-
- // Offset by 1 second to keep both orders in the same hour but distinct.
- $order_2 = WC_Helper_Order::create_order( $customer_2->get_id() );
- $order_2->set_date_created( $order_time + 1 );
- $order_2->set_status( OrderStatus::COMPLETED );
- $order_2->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
- $start_time = gmdate( 'Y-m-d H:00:00', $order_1->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
-
- $data = json_decode(
- wp_json_encode(
- $data_store->get_data(
- array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- )
- )
- ),
- true
- );
-
- $this->assertEquals( 2, $data['totals']['orders_count'] );
- $this->assertEquals( 2, $data['totals']['total_customers'] );
- $this->assertCount( 1, $data['intervals'] );
- $this->assertEquals( 2, $data['intervals'][0]['subtotals']['orders_count'] );
- $this->assertEquals( 2, $data['intervals'][0]['subtotals']['total_customers'] );
- }
-
- /**
- * Test if lookup tables are cleaned after delete an order.
- *
- * @covers \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::delete_order
- * @covers \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::sync_on_order_delete
- * @covers \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::sync_on_order_delete
- * @covers \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore::sync_on_order_delete
- */
- public function test_order_deletion() {
- global $wpdb;
-
- WC_Helper_Reports::reset_stats_dbs();
-
- // Tables to check.
- $tables = array(
- 'wc_order_coupon_lookup',
- 'wc_order_product_lookup',
- 'wc_order_stats',
- 'wc_order_tax_lookup',
- );
-
- // Enable taxes.
- $default_calc_taxes = get_option( 'woocommerce_calc_taxes', 'no' );
- $default_customer_address = get_option( 'woocommerce_default_customer_address', 'geolocation' );
- $default_tax_based_on = get_option( 'woocommerce_tax_based_on', 'shipping' );
- update_option( 'woocommerce_calc_taxes', 'yes' );
- update_option( 'woocommerce_default_customer_address', 'base' );
- update_option( 'woocommerce_tax_based_on', 'base' );
-
- // Create tax.
- $tax_id = WC_Tax::_insert_tax_rate(
- array(
- 'tax_rate_country' => '',
- 'tax_rate_state' => '',
- 'tax_rate' => '10.0000',
- 'tax_rate_name' => 'VAT',
- 'tax_rate_priority' => '1',
- 'tax_rate_compound' => '0',
- 'tax_rate_shipping' => '1',
- 'tax_rate_order' => '1',
- 'tax_rate_class' => '',
- )
- );
-
- // Create product.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- // Create coupon.
- $coupon = new WC_Coupon();
- $coupon->set_code( '20fixed' );
- $coupon->set_discount_type( 'fixed_cart' );
- $coupon->set_amount( 20 );
- $coupon->save();
-
- // Create order.
- $order = WC_Helper_Order::create_order();
- $order->add_product( $product, 1 );
- $order->set_status( OrderStatus::COMPLETED );
- $order->set_shipping_total( 10 );
- $order->apply_coupon( $coupon );
- $order->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Check if lookup tables are populated.
- foreach ( $tables as $table ) {
- $results = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT * FROM {$wpdb->prefix}{$table} WHERE order_id = %d", // phpcs:ignore
- $order->get_id()
- )
- );
-
- $this->assertTrue( is_array( $results ) && (bool) count( $results ) );
- }
-
- // Delete order and clean all other objects too.
- $order->delete( true );
- $product->delete( true );
- $coupon->delete( true );
- WC_Tax::_delete_tax_rate( $tax_id );
-
- // Check if results are empty now.
- foreach ( $tables as $table ) {
- $results = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT * FROM {$wpdb->prefix}{$table} WHERE order_id = %d", // phpcs:ignore
- $order->get_id()
- )
- );
-
- $this->assertEmpty( $results );
- }
-
- // Reset taxes settings.
- update_option( 'woocommerce_calc_taxes', $default_customer_address );
- update_option( 'woocommerce_default_customer_address', $default_calc_taxes );
- update_option( 'woocommerce_tax_based_on', $default_tax_based_on );
- }
-
- /**
- * Test segmenting by product id and by variation id.
- */
- public function test_segmenting_by_product_and_variation() {
- // Simple product.
- $product_1_price = 25;
- $product_1 = new WC_Product_Simple();
- $product_1->set_name( 'Simple Product' );
- $product_1->set_regular_price( $product_1_price );
- $product_1->save();
-
- // Variable product.
- $product_2 = new WC_Product_Variable();
- $product_2->set_name( 'Variable Product' );
- $product_2->save();
-
- $child_1 = new WC_Product_Variation();
- $child_1->set_parent_id( $product_2->get_id() );
- $child_1->set_regular_price( 23 );
- $child_1->save();
-
- $child_2 = new WC_Product_Variation();
- $child_2->set_parent_id( $product_2->get_id() );
- $child_2->set_regular_price( 27 );
- $child_2->save();
-
- $product_2->set_children( array( $child_1->get_id(), $child_2->get_id() ) );
-
- $child_1->set_stock_status( ProductStockStatus::IN_STOCK );
- $child_1->save();
- $child_2->set_stock_status( ProductStockStatus::IN_STOCK );
- $child_2->save();
- WC_Product_Variable::sync( $product_2 );
-
- // Simple product, not used.
- $product_3_price = 17;
- $product_3 = new WC_Product_Simple();
- $product_3->set_name( 'Simple Product not used' );
- $product_3->set_regular_price( $product_3_price );
- $product_3->save();
-
- $order_status = OrderStatus::COMPLETED;
-
- $customer_1 = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
-
- $order_1_time = time();
- $order_3_time = $order_1_time - 1 * HOUR_IN_SECONDS;
-
- // Order 3: 4 x product 1, done one hour earlier.
- $order_3 = WC_Helper_Order::create_order( $customer_1->get_id(), $product_1 );
- $order_3->set_date_created( $order_3_time );
- $order_3->set_date_paid( $order_3_time );
- $order_3->set_status( $order_status );
- $order_3->calculate_totals();
- $order_3->save();
-
- // Order 1: 4 x product 1 & 3 x product 2-child 1.
- $order_1 = WC_Helper_Order::create_order( $customer_1->get_id(), $product_1 );
- $item = new WC_Order_Item_Product();
-
- $item->set_props(
- array(
- 'product_id' => $product_2->get_id(),
- 'variation_id' => $child_1->get_id(),
- 'quantity' => 3,
- 'subtotal' => 3 * floatval( $child_1->get_price() ),
- 'total' => 3 * floatval( $child_1->get_price() ),
- )
- );
- $item->save();
- $order_1->add_item( $item );
- $order_1->set_status( $order_status );
- $order_1->calculate_totals();
- $order_1->save();
-
- // Order 2: 4 x product 2-child 1 & 1 x product 2-child 2.
- $order_2 = WC_Helper_Order::create_order( $customer_1->get_id(), $child_1 );
- $item = new WC_Order_Item_Product();
- $item->set_props(
- array(
- 'product_id' => $product_2->get_id(),
- 'variation_id' => $child_2->get_id(),
- 'quantity' => 1,
- 'subtotal' => floatval( $child_2->get_price() ),
- 'total' => floatval( $child_2->get_price() ),
- )
- );
- $item->save();
- $order_2->add_item( $item );
- $order_2->set_status( $order_status );
- $order_2->calculate_totals();
- $order_2->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- // Tests for before & after set to current hour.
- $now = new DateTime();
-
- $two_hours_back = new DateTime();
- $i1_start_timestamp = $order_1_time - 2 * HOUR_IN_SECONDS;
- $two_hours_back->setTimestamp( $i1_start_timestamp );
- $i1_end_timestamp = $i1_start_timestamp + ( 3600 - ( $i1_start_timestamp % 3600 ) ) - 1;
- $i1_start = new DateTime();
- $i1_start->setTimestamp( $i1_start_timestamp );
- $i1_end = new DateTime();
- $i1_end->setTimestamp( $i1_end_timestamp );
-
- $i2_start_timestamp = $i1_end_timestamp + 1;
- $i2_end_timestamp = $i1_end_timestamp + 3600;
- $i2_start = new DateTime();
- $i2_start->setTimestamp( $i2_start_timestamp );
- $i2_end = new DateTime();
- $i2_end->setTimestamp( $i2_end_timestamp );
-
- $i3_start_timestamp = $i2_end_timestamp + 1;
- $i3_end_timestamp = $now->format( 'U' );
- $i3_start = new DateTime();
- $i3_start->setTimestamp( $i3_start_timestamp );
- $i3_end = new DateTime();
- $i3_end->setTimestamp( $i3_end_timestamp );
-
- $query_args = array(
- 'after' => $two_hours_back->format( TimeInterval::$sql_datetime_format ),
- 'before' => $now->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'segmentby' => 'product',
- 'product_includes' => array( $product_1->get_id(), $product_2->get_id(), $product_3->get_id() ),
- );
-
- $shipping_amnt = 10;
- $o1_net_revenue = 4 * $product_1_price + 3 * intval( $child_1->get_price() );
- $o2_net_revenue = 4 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() );
- $o3_net_revenue = 4 * $product_1_price;
- $o1_num_items = 4 + 3;
- $o2_num_items = 4 + 1;
- $o3_num_items = 4;
-
- // Totals.
- $orders_count = 3;
- $num_items_sold = 7 + 5 + 4;
- $shipping = $orders_count * $shipping_amnt;
- $net_revenue = $o1_net_revenue + $o2_net_revenue + $o3_net_revenue;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue;
- $total_customers = 1;
- // Totals segments.
- $p1_orders_count = 2;
- $p1_num_items_sold = 8;
- $p1_shipping = round( $shipping_amnt / $o1_num_items * 4, 6 ) + round( $shipping_amnt / $o3_num_items * 4, 6 );
- $p1_net_revenue = 8 * $product_1_price;
- $p1_total_sales = $p1_net_revenue + $p1_shipping;
-
- $p2_orders_count = 2;
- $p2_num_items_sold = 8;
- $p2_shipping = round( $shipping_amnt / $o1_num_items * 3, 6 ) + $shipping_amnt;
- $p2_net_revenue = 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() );
- $p2_total_sales = $p2_net_revenue + $p2_shipping;
-
- // Interval 3.
- // I3 Subtotals.
- $i3_tot_orders_count = 2;
- $i3_tot_num_items_sold = 4 + 3 + 4 + 1;
- $i3_tot_shipping = $i3_tot_orders_count * $shipping_amnt;
- $i3_tot_net_revenue = 4 * $product_1_price + 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() );
- $i3_tot_total_sales = $i3_tot_net_revenue + $i3_tot_shipping;
-
- // I3 Segments.
- $i3_p1_orders_count = 1;
- $i3_p1_num_items_sold = 4;
- $i3_p1_shipping = round( $shipping_amnt / $o1_num_items * 4, 6 );
- $i3_p1_net_revenue = $i3_p1_num_items_sold * $product_1_price;
- $i3_p1_total_sales = $i3_p1_net_revenue + $i3_p1_shipping;
-
- $i3_p2_orders_count = 2;
- $i3_p2_num_items_sold = 8;
- $i3_p2_shipping = round( $shipping_amnt / $o1_num_items * 3, 6 ) + $shipping_amnt;
- $i3_p2_net_revenue = 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() );
- $i3_p2_total_sales = $i3_p2_net_revenue + $i3_p2_shipping;
-
- // Interval 2
- // I2 Subtotals.
- $i2_tot_orders_count = 1;
- $i2_tot_num_items_sold = 4;
- $i2_tot_shipping = $i2_tot_orders_count * $shipping_amnt;
- $i2_tot_net_revenue = 4 * $product_1_price;
- $i2_tot_total_sales = $i2_tot_net_revenue + $i2_tot_shipping;
-
- // I2 Segments.
- $i2_p1_orders_count = 1;
- $i2_p1_num_items_sold = 4;
- $i2_p1_shipping = $shipping_amnt;
- $i2_p1_net_revenue = 4 * $product_1_price;
- $i2_p1_total_sales = $i2_p1_net_revenue + $i2_p1_shipping;
-
- $i2_p2_orders_count = 0;
- $i2_p2_num_items_sold = 0;
- $i2_p2_shipping = 0;
- $i2_p2_net_revenue = 0;
- $i2_p2_total_sales = $i2_p2_net_revenue + $i2_p2_shipping;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'products' => 2,
- 'segments' => array(
- array(
- 'segment_id' => $product_1->get_id(),
- 'segment_label' => $product_1->get_name(),
- 'subtotals' => array(
- 'orders_count' => $p1_orders_count,
- 'num_items_sold' => $p1_num_items_sold,
- 'total_sales' => $p1_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $p1_shipping,
- 'net_revenue' => $p1_net_revenue,
- 'avg_items_per_order' => ( $o1_num_items + $o3_num_items ) / $p1_orders_count,
- 'avg_order_value' => ( $o1_net_revenue + $o3_net_revenue ) / $p1_orders_count,
- 'total_customers' => $total_customers,
- ),
- ),
- array(
- 'segment_id' => $product_2->get_id(),
- 'segment_label' => $product_2->get_name(),
- 'subtotals' => array(
- 'orders_count' => $p2_orders_count,
- 'num_items_sold' => $p2_num_items_sold,
- 'total_sales' => $p2_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $p2_shipping,
- 'net_revenue' => $p2_net_revenue,
- 'avg_items_per_order' => ( $o1_num_items + $o2_num_items ) / $p2_orders_count,
- 'avg_order_value' => ( $o1_net_revenue + $o2_net_revenue ) / $p2_orders_count,
- 'total_customers' => $total_customers,
- ),
- ),
- array(
- 'segment_id' => $product_3->get_id(),
- 'segment_label' => $product_3->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- ),
- ),
- 'intervals' => array(
- array(
- 'interval' => $i3_start->format( 'Y-m-d H' ),
- 'date_start' => $i3_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $i3_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $i3_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $i3_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $i3_tot_orders_count,
- 'num_items_sold' => $i3_tot_num_items_sold,
- 'total_sales' => $i3_tot_total_sales,
- 'gross_sales' => $i3_tot_net_revenue, // no coupons or refunds.
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i3_tot_shipping,
- 'net_revenue' => $i3_tot_net_revenue,
- 'avg_items_per_order' => $i3_tot_num_items_sold / $i3_tot_orders_count,
- 'avg_order_value' => $i3_tot_net_revenue / $i3_tot_orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(
- array(
- 'segment_id' => $product_1->get_id(),
- 'segment_label' => $product_1->get_name(),
- 'subtotals' => array(
- 'orders_count' => $i3_p1_orders_count,
- 'num_items_sold' => $i3_p1_num_items_sold,
- 'total_sales' => $i3_p1_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i3_p1_shipping,
- 'net_revenue' => $i3_p1_net_revenue,
- 'avg_items_per_order' => $o1_num_items / $i3_p1_orders_count,
- 'avg_order_value' => $o1_net_revenue / $i3_p1_orders_count,
- 'total_customers' => $total_customers,
- ),
- ),
- array(
- 'segment_id' => $product_2->get_id(),
- 'segment_label' => $product_2->get_name(),
- 'subtotals' => array(
- 'orders_count' => $i3_p2_orders_count,
- 'num_items_sold' => $i3_p2_num_items_sold,
- 'total_sales' => $i3_p2_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i3_p2_shipping,
- 'net_revenue' => $i3_p2_net_revenue,
- 'avg_items_per_order' => ( $o1_num_items + $o2_num_items ) / $i3_p2_orders_count,
- 'avg_order_value' => ( $o1_net_revenue + $o2_net_revenue ) / $i3_p2_orders_count,
- 'total_customers' => $total_customers,
- ),
- ),
- array(
- 'segment_id' => $product_3->get_id(),
- 'segment_label' => $product_3->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- ),
- ),
- ),
- array(
- 'interval' => $i2_start->format( 'Y-m-d H' ),
- 'date_start' => $i2_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $i2_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $i2_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $i2_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $i2_tot_orders_count,
- 'num_items_sold' => $i2_tot_num_items_sold,
- 'total_sales' => $i2_tot_total_sales,
- 'gross_sales' => $i2_tot_net_revenue, // no coupons or refunds.
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i2_tot_shipping,
- 'net_revenue' => $i2_tot_net_revenue,
- 'avg_items_per_order' => $i2_tot_num_items_sold / $i2_tot_orders_count,
- 'avg_order_value' => $i2_tot_net_revenue / $i2_tot_orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(
- array(
- 'segment_id' => $product_1->get_id(),
- 'segment_label' => $product_1->get_name(),
- 'subtotals' => array(
- 'orders_count' => $i2_p1_orders_count,
- 'num_items_sold' => $i2_p1_num_items_sold,
- 'total_sales' => $i2_p1_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i2_p1_shipping,
- 'net_revenue' => $i2_p1_net_revenue,
- 'avg_items_per_order' => $o3_num_items / $i2_p1_orders_count,
- 'avg_order_value' => $o3_net_revenue / $i2_p1_orders_count,
- 'total_customers' => $total_customers,
- ),
- ),
- array(
- 'segment_id' => $product_2->get_id(),
- 'segment_label' => $product_2->get_name(),
- 'subtotals' => array(
- 'orders_count' => $i2_p2_orders_count,
- 'num_items_sold' => $i2_p2_num_items_sold,
- 'total_sales' => $i2_p2_total_sales,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $i2_p2_shipping,
- 'net_revenue' => $i2_p2_net_revenue,
- 'avg_items_per_order' => $i2_p2_orders_count ? $o3_num_items / $i2_p2_orders_count : 0,
- 'avg_order_value' => $i2_p2_orders_count ? $o3_net_revenue / $i2_p2_orders_count : 0,
- 'total_customers' => $i2_p2_orders_count ? $total_customers : 0,
- ),
- ),
- array(
- 'segment_id' => $product_3->get_id(),
- 'segment_label' => $product_3->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- ),
- ),
- ),
- array(
- 'interval' => $i1_start->format( 'Y-m-d H' ),
- 'date_start' => $i1_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $i1_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $i1_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $i1_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'gross_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- 'segments' => array(
- array(
- 'segment_id' => $product_1->get_id(),
- 'segment_label' => $product_1->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- array(
- 'segment_id' => $product_2->get_id(),
- 'segment_label' => $product_2->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- array(
- 'segment_id' => $product_3->get_id(),
- 'segment_label' => $product_3->get_name(),
- 'subtotals' => array(
- 'orders_count' => 0,
- 'num_items_sold' => 0,
- 'total_sales' => 0,
- 'coupons' => 0,
- 'coupons_count' => 0,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => 0,
- 'net_revenue' => 0,
- 'avg_items_per_order' => 0,
- 'avg_order_value' => 0,
- 'total_customers' => 0,
- ),
- ),
- ),
- ),
- ),
- ),
- 'total' => 3,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
-
- $this->assertEquals( $expected_stats, $actual, 'Segmenting by product, expected: ' . $this->return_print_r( $expected_stats ) . '; actual: ' . $this->return_print_r( $actual ) );
- }
-
- /**
- * Test that product segmentation without product_includes enumerates the whole catalog.
- *
- * @testdox Product segmentation without product_includes enumerates the whole catalog.
- */
- public function test_segmenting_by_product_without_includes() {
- WC_Helper_Reports::reset_stats_dbs();
-
- $sold_product = new WC_Product_Simple();
- $sold_product->set_name( 'Segmented Sold Product' );
- $sold_product->set_regular_price( 10 );
- $sold_product->save();
-
- $unsold_product = new WC_Product_Simple();
- $unsold_product->set_name( 'Segmented Unsold Product' );
- $unsold_product->set_regular_price( 15 );
- $unsold_product->save();
-
- $order = WC_Helper_Order::create_order( 1, $sold_product );
- $order->set_status( OrderStatus::COMPLETED );
- $order->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
- $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
-
- // Without product_includes the segmenter must return a segment for
- // every store product, not only the ones with orders.
- $data = json_decode(
- wp_json_encode(
- $data_store->get_data(
- array(
- 'after' => $start_time,
- 'before' => $end_time,
- 'segmentby' => 'product',
- )
- )
- ),
- true
- );
-
- $segments = array_column( $data['totals']['segments'], 'subtotals', 'segment_id' );
- $expected_product_ids = wc_get_products(
- array(
- 'return' => 'ids',
- 'limit' => -1,
- )
- );
-
- $this->assertEqualsCanonicalizing( $expected_product_ids, array_keys( $segments ) );
- $this->assertEquals( 1, $segments[ $sold_product->get_id() ]['orders_count'] );
- $this->assertEquals( 4, $segments[ $sold_product->get_id() ]['num_items_sold'] );
- $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['orders_count'] );
- $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['num_items_sold'] );
- }
-
- /**
- * Test zero filling when ordering by date in descending and ascending order.
- */
- public function test_zero_fill_order_by_date() {
- global $wpdb;
- WC_Helper_Reports::reset_stats_dbs();
-
- $product_price = 11;
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( $product_price );
- $product->save();
-
- $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
-
- $order_1_time = time();
- $order_2_time = $order_1_time;
-
- $order_during_this_['hour'] = array( 1, 2 );
- $order_during_this_['day'] = array( 1, 2 );
- $order_during_this_['week'] = array( 1, 2 );
- $order_during_this_['month'] = array( 1, 2 );
- $order_during_this_['year'] = array( 1, 2 );
-
- $order_1_datetime = new DateTime();
- $order_1_datetime = $order_1_datetime->setTimestamp( $order_1_time );
-
- // Previous year.
- $order_7_datetime = new DateTime();
- $order_7_datetime = $order_7_datetime->setTimestamp( $order_1_time - YEAR_IN_SECONDS );
- $order_7_time = $order_7_datetime->format( 'U' );
- $order[7]['year'] = (int) $order_7_datetime->format( 'Y' );
- $order[7]['month'] = (int) $order_7_datetime->format( 'm' );
- $order[7]['week'] = (int) $order_7_datetime->format( 'W' );
- $order[7]['day'] = (int) $order_7_datetime->format( 'd' );
-
- // Previous month.
- $order_6_datetime = new DateTime();
- $order_6_datetime = $order_6_datetime->setTimestamp( $order_1_time - MONTH_IN_SECONDS );
- $order_6_time = $order_6_datetime->format( 'U' );
- $order[6]['year'] = (int) $order_6_datetime->format( 'Y' );
- $order[6]['month'] = (int) $order_6_datetime->format( 'm' );
- $order[6]['week'] = (int) $order_6_datetime->format( 'W' );
- $order[6]['day'] = (int) $order_6_datetime->format( 'd' );
-
- // Previous week.
- $order_5_datetime = new DateTime();
- $order_5_datetime = $order_5_datetime->setTimestamp( $order_1_time - WEEK_IN_SECONDS );
- $order_5_time = $order_5_datetime->format( 'U' );
- $order[5]['year'] = (int) $order_5_datetime->format( 'Y' );
- $order[5]['month'] = (int) $order_5_datetime->format( 'm' );
- $order[5]['week'] = (int) $order_5_datetime->format( 'W' );
- $order[5]['day'] = (int) $order_5_datetime->format( 'd' );
-
- // Previous day.
- $order_4_datetime = new DateTime();
- $order_4_datetime = $order_4_datetime->setTimestamp( $order_1_time - DAY_IN_SECONDS );
- $order_4_time = $order_4_datetime->format( 'U' );
- $order[4]['year'] = (int) $order_4_datetime->format( 'Y' );
- $order[4]['month'] = (int) $order_4_datetime->format( 'm' );
- $order[4]['week'] = (int) $order_4_datetime->format( 'W' );
- $order[4]['day'] = (int) $order_4_datetime->format( 'd' );
-
- // same day, -1 hour.
- $order_3_datetime = new DateTime();
- $order_3_datetime = $order_3_datetime->setTimestamp( $order_1_time - HOUR_IN_SECONDS );
- $order_3_time = $order_3_datetime->format( 'U' );
- $order[3]['year'] = (int) $order_3_datetime->format( 'Y' );
- $order[3]['month'] = (int) $order_3_datetime->format( 'm' );
- $order[3]['week'] = (int) $order_3_datetime->format( 'W' );
- $order[3]['day'] = (int) $order_3_datetime->format( 'd' );
-
- // Current order.
- $order[1]['year'] = (int) $order_1_datetime->format( 'Y' );
- $order[1]['month'] = (int) $order_1_datetime->format( 'm' );
- $order[1]['week'] = (int) $order_1_datetime->format( 'W' );
- $order[1]['day'] = (int) $order_1_datetime->format( 'd' );
- $order[1]['hour'] = (int) $order_1_datetime->format( 'H' );
-
- foreach ( array( 3, 4, 5, 6, 7 ) as $order_no ) {
- if ( $order[ $order_no ]['day'] === $order[1]['day'] && $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['day'][] = $order_no;
- }
- if ( $order[ $order_no ]['week'] === $order[1]['week'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['week'][] = $order_no;
- }
- if ( $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['month'][] = $order_no;
- }
- if ( $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['year'][] = $order_no;
- }
- }
-
- $order_status = OrderStatus::COMPLETED;
- $qty_per_product = 4; // Hardcoded in WC_Helper_Order::create_order.
-
- $orders = array();
- foreach (
- array(
- $order_7_time,
- $order_6_time,
- $order_5_time,
- $order_4_time,
- $order_3_time,
- $order_2_time,
- $order_1_time,
- ) as $order_time
- ) {
- // Order with 1 product.
- $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
- $order->set_date_created( $order_time );
- $order->set_date_paid( $order_time );
- $order->set_status( $order_status );
-
- $order->calculate_totals();
- $order->save();
-
- $orders[] = $order;
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $data_store = new OrdersStatsDataStore();
-
- // Tests for before & after set to current hour.
- $current_hour_start = new DateTime();
- $current_hour_start->setTimestamp( $order_1_time );
- $current_hour_minutes = (int) $current_hour_start->format( 'i' );
- $current_hour_start->setTimestamp( $order_1_time - $current_hour_minutes * MINUTE_IN_SECONDS );
-
- $current_hour_end = new DateTime();
- $current_hour_end->setTimestamp( $order_1_time );
- $seconds_into_hour = (int) $current_hour_end->format( 'U' ) % HOUR_IN_SECONDS;
- $current_hour_end->setTimestamp( $order_1_time + ( HOUR_IN_SECONDS - $seconds_into_hour ) - 1 );
-
- // Test for current hour--only 1 hour visible.
- // DESC.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'desc',
- );
-
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $coupons_count = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ASC -- only 1 interval, so should be the same.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'asc',
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-5 hours, now] -- partial 1 page.
- * bottom ... top
- * | --------------------|------|-----|-----|
- * minus 5 hours | | order1 now
- * | | order2
- * | now-1 hour
- * order 3
- */
- // DESC.
- $hour_offset = 5;
- $minus_5_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_5_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
-
- $query_args = array(
- 'after' => $minus_5_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'desc',
- );
-
- // Expected Intervals section construction.
- $expected_intervals = array();
- // Even in case this runs exactly at the hour turn second, there should still be 6 intervals:
- // e.g. 20:30:51 -(minus 5 hours)- 15:30:51 means intervals 15:30:51--15:59:59, 16:00-16:59, 17, 18, 19, 20:00-20:30, i.e. 6 intervals
- // also if this run exactly at 20:00 -(minus 5 hours)- 15:00, then intervals should be 15:00-15:59, 16, 17, 18, 19, 20:00-20:00.
- $interval_count = $hour_offset + 1;
- for ( $i = 0; $i < $interval_count; $i++ ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_5_hours;
- $date_end = new DateTime( $minus_5_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } elseif ( 1 === $i ) {
- $orders_count = 1;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $returning_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + 1; // order 3 is 1 hour before order 1 & 2, so include it.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => $expected_intervals,
- 'total' => $interval_count,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ASC -- reverse the intervals array, but numbers stay the same.
- $query_args = array(
- 'after' => $minus_5_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'asc',
- );
- $expected_stats['intervals'] = array_reverse( $expected_stats['intervals'] );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-9 hours, now] -- full 1 page.
- * DESC:
- * bottom ... top
- * | --------------------|------|-----|-----|
- * minus 9 hours | | order1 now
- * | | order2
- * | now-1 hour
- * order 3
- */
- $hour_offset = 9;
- $minus_9_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_9_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
-
- $query_args = array(
- 'after' => $minus_9_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'desc',
- );
-
- // Expected Intervals section construction.
- $expected_intervals = array();
- $interval_count = $hour_offset + 1;
- for ( $i = 0; $i < $interval_count; $i++ ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_9_hours;
- $date_end = new DateTime( $minus_9_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $total_customers = 1;
- } elseif ( 1 === $i ) {
- $orders_count = 1;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $total_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $total_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $total_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + 1; // order 3 is 1 hour before order 1 & 2, so include it.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => $expected_intervals,
- 'total' => $interval_count,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ASC -- same values, just reverse order of intervals.
- $query_args = array(
- 'after' => $minus_9_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'asc',
- );
-
- $expected_stats['intervals'] = array_reverse( $expected_stats['intervals'] );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-10 hours, now] -- 1 page full, 1 interval on 2nd page.
- * DESC:
- * bottom ... top
- * page 1
- * | --------------------|------|-----|-----|
- * minus 9 hours | | order1 now
- * | | order2
- * | now-1 hour
- * order 3
- *
- * page 2
- * |---|
- * minus 10 hours
- * minus 9 hours
- */
- $hour_offset = 10;
- $minus_10_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_10_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
- $per_page = 10;
-
- // Page 1.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'desc',
- 'page' => 1,
- 'per_page' => $per_page,
- );
-
- // Expected Intervals section construction.
- $expected_intervals = array();
- $interval_count = 11;
- for ( $i = 0; $i < $interval_count; $i++ ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_10_hours;
- $date_end = new DateTime( $minus_10_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } elseif ( 1 === $i ) {
- $orders_count = 1;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $returning_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + 1; // order 3 is 1 hour before order 1 & 2, so include it.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, 0, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Page 2.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'desc',
- 'page' => 2,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 2,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-10 hours, now] -- 1 page full, 1 interval on 2nd page.
- * ASC:
- * bottom ... top
- * page 1
- * | -----------|-----|-----------|-----------|
- * minus 1 hour | | | minus 10 hours
- * order 3 | |
- * | minus 9 hours
- * minus 2 hours
- *
- * page 2
- * |-----------|
- * now hours
- * minus 1 hour
- */
- $expected_intervals = array_reverse( $expected_intervals );
-
- // Page 1.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'asc',
- 'page' => 1,
- 'per_page' => $per_page,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, 0, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Page 2.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'date',
- 'order' => 'asc',
- 'page' => 2,
- 'per_page' => $per_page,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 2,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Now the same should be done for days, weeks, months and years. But I'm too low on mana to do that.
- }
-
- /**
- * Test zero filling when ordering by non-date property, in this case orders_count.
- */
- public function test_zero_fill_order_by_orders_count() {
- global $wpdb;
- WC_Helper_Reports::reset_stats_dbs();
-
- $product_price = 11;
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( $product_price );
- $product->save();
-
- $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.test' );
-
- // 1 order within the current hour.
- $order_1_time = time();
-
- $order_during_this_['hour'] = array( 1 );
- $order_during_this_['day'] = array( 1 );
- $order_during_this_['week'] = array( 1 );
- $order_during_this_['month'] = array( 1 );
- $order_during_this_['year'] = array( 1 );
-
- $order_1_datetime = new DateTime();
- $order_1_datetime = $order_1_datetime->setTimestamp( $order_1_time );
-
- // Create one order in previous year.
- $order_7_datetime = new DateTime();
- $order_7_datetime = $order_7_datetime->setTimestamp( $order_1_time - YEAR_IN_SECONDS );
- $order_7_time = $order_7_datetime->format( 'U' );
- $order[7]['year'] = (int) $order_7_datetime->format( 'Y' );
- $order[7]['month'] = (int) $order_7_datetime->format( 'm' );
- $order[7]['week'] = (int) $order_7_datetime->format( 'W' );
- $order[7]['day'] = (int) $order_7_datetime->format( 'd' );
-
- // Create one order in previous month.
- $order_6_datetime = new DateTime();
- $order_6_datetime = $order_6_datetime->setTimestamp( $order_1_time - MONTH_IN_SECONDS );
- $order_6_time = $order_6_datetime->format( 'U' );
- $order[6]['year'] = (int) $order_6_datetime->format( 'Y' );
- $order[6]['month'] = (int) $order_6_datetime->format( 'm' );
- $order[6]['week'] = (int) $order_6_datetime->format( 'W' );
- $order[6]['day'] = (int) $order_6_datetime->format( 'd' );
-
- // Create one order in previous week.
- $order_5_datetime = new DateTime();
- $order_5_datetime = $order_5_datetime->setTimestamp( $order_1_time - WEEK_IN_SECONDS );
- $order_5_time = $order_5_datetime->format( 'U' );
- $order[5]['year'] = (int) $order_5_datetime->format( 'Y' );
- $order[5]['month'] = (int) $order_5_datetime->format( 'm' );
- $order[5]['week'] = (int) $order_5_datetime->format( 'W' );
- $order[5]['day'] = (int) $order_5_datetime->format( 'd' );
-
- // Create one order in previous day.
- $order_4_datetime = new DateTime();
- $order_4_datetime = $order_4_datetime->setTimestamp( $order_1_time - DAY_IN_SECONDS );
- $order_4_time = $order_4_datetime->format( 'U' );
- $order[4]['year'] = (int) $order_4_datetime->format( 'Y' );
- $order[4]['month'] = (int) $order_4_datetime->format( 'm' );
- $order[4]['week'] = (int) $order_4_datetime->format( 'W' );
- $order[4]['day'] = (int) $order_4_datetime->format( 'd' );
-
- // Create one order in same day, -1 hour.
- $order_3_datetime = new DateTime();
- $order_3_datetime = $order_3_datetime->setTimestamp( $order_1_time - HOUR_IN_SECONDS );
- $order_3_time = $order_3_datetime->format( 'U' );
- $order[3]['year'] = (int) $order_3_datetime->format( 'Y' );
- $order[3]['month'] = (int) $order_3_datetime->format( 'm' );
- $order[3]['week'] = (int) $order_3_datetime->format( 'W' );
- $order[3]['day'] = (int) $order_3_datetime->format( 'd' );
-
- // Current order.
- $order[1]['year'] = (int) $order_1_datetime->format( 'Y' );
- $order[1]['month'] = (int) $order_1_datetime->format( 'm' );
- $order[1]['week'] = (int) $order_1_datetime->format( 'W' );
- $order[1]['day'] = (int) $order_1_datetime->format( 'd' );
- $order[1]['hour'] = (int) $order_1_datetime->format( 'H' );
-
- // 2 orders within 1 hour before now to test multiple orders within one time interval.
- $order_2_time = $order_3_time;
- $order_during_this_['hour-1'] = array( 2, 3 );
-
- // In case some of the orders end up on different day/hour/month/year, we need to find out where exactly they ended up.
- foreach ( array( 3, 4, 5, 6, 7 ) as $order_no ) {
- if ( $order[ $order_no ]['day'] === $order[1]['day'] && $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['day'][] = $order_no;
- }
- if ( $order[ $order_no ]['week'] === $order[1]['week'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['week'][] = $order_no;
- }
- if ( $order[ $order_no ]['month'] === $order[1]['month'] && $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['month'][] = $order_no;
- }
- if ( $order[ $order_no ]['year'] === $order[1]['year'] ) {
- $order_during_this_['year'][] = $order_no;
- }
- }
-
- $order_status = OrderStatus::COMPLETED;
- $qty_per_product = 4; // Hardcoded in WC_Helper_Order::create_order.
-
- // Create orders for the test cases.
- $orders = array();
- foreach (
- array(
- $order_7_time,
- $order_6_time,
- $order_5_time,
- $order_4_time,
- $order_3_time,
- $order_2_time,
- $order_1_time,
- ) as $order_time
- ) {
- // Order with 1 product.
- $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
- $order->set_date_created( $order_time );
- $order->set_date_paid( $order_time );
- $order->set_status( $order_status );
-
- $order->calculate_totals();
- $order->save();
-
- $orders[] = $order;
- }
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- global $wpdb;
- $res = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wc_order_stats" );
-
- $data_store = new OrdersStatsDataStore();
-
- // Tests for before & after set to current hour.
- // (this sets minutes for current hour to 0, seconds are left as they arem e.g. 15:23:43 becomes 15:00:43).
- $current_hour_start = new DateTime();
- $current_hour_start->setTimestamp( $order_1_time );
- $current_hour_minutes = (int) $current_hour_start->format( 'i' );
- $current_hour_start->setTimestamp( $order_1_time - $current_hour_minutes * MINUTE_IN_SECONDS );
-
- // This is the last second of the current hour, e.g. 15:23:43 becomes 15:59:59.
- $current_hour_end = new DateTime();
- $current_hour_end->setTimestamp( $order_1_time );
- $seconds_into_hour = (int) $current_hour_end->format( 'U' ) % HOUR_IN_SECONDS;
- $current_hour_end->setTimestamp( $order_1_time + ( HOUR_IN_SECONDS - $seconds_into_hour ) - 1 );
-
- // Test 1: only one hour visible, so only 1 interval in the response, no real ordering.
- // DESC.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'desc',
- );
-
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $coupons_count = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array(
- array(
- 'interval' => $current_hour_start->format( 'Y-m-d H' ),
- 'date_start' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $current_hour_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $current_hour_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'segments' => array(),
- ),
- ),
- ),
- 'total' => 1,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // ASC -- only 1 interval, so should be the same.
- $query_args = array(
- 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'asc',
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-5 hours, now] -- partial 1 page.
- * This should include 3 orders, 2 done at hour-1 and one in the current hour.
- *
- * DESC ordering by order count =>
- * previous hour with 2 orders at the top,
- * then current hour with 1 order,
- * then zero-filled intervals.
- */
- $hour_offset = 5;
- $minus_5_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_5_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
-
- $query_args = array(
- 'after' => $minus_5_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'desc',
- );
-
- // Expected Intervals section construction.
- $expected_intervals = array();
- // Even in case this runs exactly at the hour turn second, there should still be 6 intervals:
- // e.g. 20:30:51 -(minus 5 hours)- 15:30:51 means intervals 15:30:51--15:59:59, 16:00-16:59, 17, 18, 19, 20:00-20:30, i.e. 6 intervals
- // also if this run exactly at 20:00 -(minus 5 hours)- 15:00, then intervals should be 15:00-15:59, 16, 17, 18, 19, 20:00-20:00.
- $interval_count = $hour_offset + 1;
- for ( $i = $interval_count - 1; $i >= 0; $i-- ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_5_hours;
- $date_end = new DateTime( $minus_5_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 1 === $i ) {
- $orders_count = count( $order_during_this_['hour-1'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } elseif ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $returning_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
-
- /*
- * The zero-filled intervals are actually always ordered by time ASC if the primary sorting condition is the same.
- * $expected_intervals now:
- * - [0] => 0 orders, hour - 5
- * - [1] => 0 orders, hour - 4
- * - [2] => 0 orders, hour - 3
- * - [3] => 0 orders, hour - 2
- * - [4] => 2 orders, hour - 1
- * - [5] => 1 orders, hour - 0
- *
- * This means we need to put last two to the beginning of the array.
- */
- $to_be_second = array_pop( $expected_intervals );
- $to_be_first = array_pop( $expected_intervals );
- array_unshift( $expected_intervals, $to_be_second );
- array_unshift( $expected_intervals, $to_be_first );
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + count( $order_during_this_['hour-1'] ); // order 2 & 3 is 1 hour before order 1, so include all of those.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => $expected_intervals,
- 'total' => $interval_count,
- 'pages' => 1,
- 'page_no' => 1,
- );
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * 5 hour time window, ASC ordering -- numbers stay the same, but first include zero intervals in asc order, then the rest.
- * $expected_intervals now:
- * - [0] => 2 orders, hour - 1
- * - [1] => 1 orders, hour - 0
- * - [2] => 0 orders, hour - 2
- * - [3] => 0 orders, hour - 3
- * - [4] => 0 orders, hour - 4
- * - [5] => 0 orders, hour - 5
- * so we need to revert first two and put them at the bottom of the array.
- */
- $to_be_last = array_shift( $expected_stats['intervals'] );
- $to_be_second_last = array_shift( $expected_stats['intervals'] );
- array_push( $expected_stats['intervals'], $to_be_second_last );
- array_push( $expected_stats['intervals'], $to_be_last );
-
- $query_args = array(
- 'after' => $minus_5_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'asc',
- );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-9 hours, now] -- full 1 page.
- * Very similar to -5 hours.
- *
- */
- $hour_offset = 9;
- $minus_9_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_9_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
-
- $query_args = array(
- 'after' => $minus_9_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'desc',
- );
-
- /*
- * Expected Intervals section construction.
- * Initially intervals are ordered from the least recent to most recent, i.e.:
- * - [0] => 0 orders, hour - 9
- * - [1] => 0 orders, hour - 8
- * ...
- * - [7] => 0 orders, hour - 2
- * - [8] => 2 orders, hour - 1
- * - [9] => 1 orders, hour - 0
- *
- * To change ordering by orders_count, just pop last two and push them to the front (aka unshift in php).
- */
- $expected_intervals = array();
- $interval_count = $hour_offset + 1;
- for ( $i = $interval_count - 1; $i >= 0; $i-- ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_9_hours;
- $date_end = new DateTime( $minus_9_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } elseif ( 1 === $i ) {
- $orders_count = count( $order_during_this_['hour-1'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $returning_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
- $to_be_second = array_pop( $expected_intervals );
- $to_be_first = array_pop( $expected_intervals );
- array_unshift( $expected_intervals, $to_be_second );
- array_unshift( $expected_intervals, $to_be_first );
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + count( $order_during_this_['hour-1'] ); // order 3 is 1 hour before order 1 & 2, so include it.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => $expected_intervals,
- 'total' => $interval_count,
- 'pages' => 1,
- 'page_no' => 1,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * ASC ordering -- same values, different intervals order.
- *
- * Now the intervals are ordered by order count desc, i.e.:
- * - [0] => 2 orders, hour - 1
- * - [1] => 1 orders, hour - 0
- * - [2] => 0 orders, hour - 9
- * ...
- * - [8] => 0 orders, hour - 3
- * - [9] => 0 orders, hour - 2
- *
- * To change ordering to orders_count ASC, just shift first two and push them to the back in reversed order.
- */
- $to_be_last = array_shift( $expected_stats['intervals'] );
- $to_be_second_last = array_shift( $expected_stats['intervals'] );
- array_push( $expected_stats['intervals'], $to_be_second_last );
- array_push( $expected_stats['intervals'], $to_be_last );
-
- $query_args = array(
- 'after' => $minus_9_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'asc',
- );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-10 hours, now] -- 1 page full, 1 interval on 2nd page.
- * DESC.
- */
- $hour_offset = 10;
- $minus_10_hours = new DateTime();
- $now_timestamp = (int) $current_hour_end->format( 'U' );
- $minus_10_hours->setTimestamp( $now_timestamp - $hour_offset * HOUR_IN_SECONDS );
- $per_page = 10;
-
- // Page 1.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'desc',
- 'page' => 1,
- 'per_page' => $per_page,
- );
-
- /**
- * Expected Intervals section construction.
- * Initially created as:
- * - [0] => 0 orders, hour - 10
- * - [1] => 0 orders, hour - 9
- * ...
- * - [8] => 0 orders, hour - 2
- * - [9] => 2 orders, hour - 1
- * - [10] => 1 orders, hour - 0
- *
- * so last two need to be put first (last one second, second last one first).
- */
- $expected_intervals = array();
- $interval_count = 11;
- for ( $i = $interval_count - 1; $i >= 0; $i-- ) {
- if ( 0 === $i ) {
- $date_start = new DateTime( $current_hour_end->format( 'Y-m-d H:00:00' ) );
- $date_end = $current_hour_end;
- } elseif ( $hour_offset === $i ) {
- $date_start = $minus_10_hours;
- $date_end = new DateTime( $minus_10_hours->format( 'Y-m-d H:59:59' ) );
- } else {
- $hour_anchor = new DateTime();
- $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
- $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
- $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
- }
-
- if ( 0 === $i ) {
- $orders_count = count( $order_during_this_['hour'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } elseif ( 1 === $i ) {
- $orders_count = count( $order_during_this_['hour-1'] );
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 1;
- } else {
- $orders_count = 0;
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
- $returning_customers = 0;
- }
-
- $expected_interval = array(
- 'interval' => $date_start->format( 'Y-m-d H' ),
- 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
- 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
- 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
- 'subtotals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => 0 === $orders_count ? 0 : $num_items_sold / $orders_count,
- 'avg_order_value' => 0 === $orders_count ? 0 : $net_revenue / $orders_count,
- 'total_customers' => $returning_customers,
- 'segments' => array(),
- ),
- );
-
- $expected_intervals[] = $expected_interval;
- }
- $to_be_second = array_pop( $expected_intervals );
- $to_be_first = array_pop( $expected_intervals );
- array_unshift( $expected_intervals, $to_be_second );
- array_unshift( $expected_intervals, $to_be_first );
-
- // Totals section.
- $orders_count = count( $order_during_this_['hour'] ) + count( $order_during_this_['hour-1'] ); // orders 2 & 3 are 1 hour before order 1, so include all of those.
- $num_items_sold = $orders_count * $qty_per_product;
- $coupons = 0;
- $shipping = $orders_count * 10;
- $net_revenue = $product_price * $qty_per_product * $orders_count - $coupons;
- $total_sales = $net_revenue + $shipping;
- $gross_sales = $net_revenue + $coupons;
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, 0, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 1,
- );
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Page 2.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'desc',
- 'page' => 2,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 2,
- );
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- /*
- * Test for [-10 hours, now] -- 1 page full, 1 interval on 2nd page.
- * ASC
- *
- * Currently, the intervals are in DESC order by orders_count:
- * - [0] => 2 orders, hour - 1
- * - [1] => 1 orders, hour - 0
- * - [2] => 0 orders, hour - 10
- * - [3] => 0 orders, hour - 9
- * ...
- * - [9] => 0 orders, hour - 3
- * - [10] => 0 orders, hour - 2
- * so first one needs to become the last one, second one the second last.
- */
- $to_be_last = array_shift( $expected_intervals );
- $to_be_second_last = array_shift( $expected_intervals );
- array_push( $expected_intervals, $to_be_second_last );
- array_push( $expected_intervals, $to_be_last );
-
- // Page 1.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'asc',
- 'page' => 1,
- 'per_page' => $per_page,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, 0, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 1,
- );
-
- $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
- $this->assertEquals( $expected_stats, $actual, 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
-
- // Page 2.
- $query_args = array(
- 'after' => $minus_10_hours->format( TimeInterval::$sql_datetime_format ),
- 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'orderby' => 'orders_count',
- 'order' => 'asc',
- 'page' => 2,
- 'per_page' => $per_page,
- );
-
- $expected_stats = array(
- 'totals' => array(
- 'orders_count' => $orders_count,
- 'num_items_sold' => $num_items_sold,
- 'total_sales' => $total_sales,
- 'gross_sales' => $gross_sales,
- 'coupons' => $coupons,
- 'coupons_count' => $coupons_count,
- 'refunds' => 0,
- 'taxes' => 0,
- 'shipping' => $shipping,
- 'net_revenue' => $net_revenue,
- 'avg_items_per_order' => $num_items_sold / $orders_count,
- 'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 1,
- 'products' => 1,
- 'segments' => array(),
- ),
- 'intervals' => array_slice( $expected_intervals, $per_page ),
- 'total' => 11,
- 'pages' => 2,
- 'page_no' => 2,
- );
-
- $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
- }
-
- /**
- * Pass input variable through print_r.
- *
- * @since 3.5.4
- * @param string|array|object $data Variable data to be passed through print_r.
- * @return string
- */
- private function return_print_r( $data ) {
- // @codingStandardsIgnoreStart
- return print_r( $data, true );
- // @codingStandardsIgnoreEnd
- }
-
- /**
- * Test new and returning guest customer.
- */
- public function test_guest_returning_customer() {
- WC_Helper_Reports::reset_stats_dbs();
-
- // Create a test product for use in an order.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $data_store = new OrdersStatsDataStore();
-
- // All empty in the beginning.
- $query_args = array(
- 'interval' => 'hour',
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 0, $actual_data->totals->total_customers );
-
- // Create an order an hour before the order 1, so that the guest customer will become returning customer later.
- $order_1_time = time();
-
- $order_0_time = $order_1_time - HOUR_IN_SECONDS;
-
- $order_0 = WC_Helper_Order::create_order( 0, $product );
- $order_0->set_date_created( $order_0_time );
- $order_0->set_date_paid( $order_0_time );
- $order_0->set_status( OrderStatus::PROCESSING );
- $order_0->set_total( 100 );
- $order_0->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_0->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- // Place an order 'one hour later', 2 orders, but still just one customer.
- $order_1 = WC_Helper_Order::create_order( 0, $product );
- $order_1->set_date_created( $order_1_time );
- $order_1->set_date_paid( $order_1_time );
- $order_1->set_status( OrderStatus::PROCESSING );
- $order_1->set_total( 100 );
- $order_1->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Time frame includes both orders -> customer is a new customer.
- $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
-
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- // Time frame includes only second order -> customer is a returning customer.
- $after_order_0 = new DateTime();
- $after_order_0->setTimestamp( $order_0_time + 1 );
-
- $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
-
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- $order_2 = WC_Helper_Order::create_order( 0, $product );
- $order_2->set_date_created( $order_1_time );
- $order_2->set_date_modified( $order_1_time + 1 );
- $order_2->set_date_paid( $order_1_time );
- $order_2->set_status( OrderStatus::PROCESSING );
- $order_2->set_total( 100 );
- $order_2->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Time frame includes second and third order -> there is one returning customer.
- $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_2->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'day', // to skip cache.
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- // It's still the same customer who ordered for the first time in this hour, they just placed 2 orders.
- $this->assertEquals( 1, $actual_data->totals->total_customers );
- }
-
- /**
- * Test new and returning registered customer.
- */
- public function test_registered_returning_customer() {
- WC_Helper_Reports::reset_stats_dbs();
-
- $customer_1 = WC_Helper_Customer::create_customer( 'cust_1_new', 'pwd_1', 'new_customer@mail.com' );
-
- // Create a test product for use in an order.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
-
- $data_store = new OrdersStatsDataStore();
-
- // All empty in the beginning.
- $query_args = array(
- 'interval' => 'hour',
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 0, $actual_data->totals->total_customers );
-
- // Create an order an hour before the order 1, so that the guest customer will become returning customer later.
- $order_1_time = time();
-
- $order_0_time = $order_1_time - HOUR_IN_SECONDS;
-
- $order_0 = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
- $order_0->set_date_created( $order_0_time );
- $order_0->set_date_paid( $order_0_time );
- $order_0->set_status( OrderStatus::PROCESSING );
- $order_0->set_total( 100 );
- $order_0->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_0->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- // Place an order 'one hour later', 2 orders, but still just one customer.
- $order_1 = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
- $order_1->set_date_created( $order_1_time );
- $order_1->set_date_paid( $order_1_time );
- $order_1->set_status( OrderStatus::PROCESSING );
- $order_1->set_total( 100 );
- $order_1->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Time frame includes both orders -> customer is a new customer.
- $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
-
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- // Time frame includes only second order -> customer is a returning customer.
- $after_order_0 = new DateTime();
- $after_order_0->setTimestamp( $order_0_time + 1 );
-
- $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'hour',
- 'after' => $start_time,
- 'before' => $end_time,
- );
-
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- $this->assertEquals( 1, $actual_data->totals->total_customers );
-
- $order_2 = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
- $order_2->set_date_created( $order_1_time );
- $order_2->set_date_modified( $order_1_time + 1 );
- $order_2->set_date_paid( $order_1_time );
- $order_2->set_status( OrderStatus::PROCESSING );
- $order_2->set_total( 100 );
- $order_2->save();
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Time frame includes second and third order -> there is one returning customer.
- $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 );
- $end_time = gmdate( 'Y-m-d H:59:59', $order_2->get_date_created()->getOffsetTimestamp() );
- $query_args = array(
- 'interval' => 'day', // to skip cache.
- 'after' => $start_time,
- 'before' => $end_time,
- );
- $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
- // It's still the same customer who ordered for the first time in this hour, they just placed 2 orders.
- $this->assertEquals( 1, $actual_data->totals->total_customers );
- }
-
- /**
- * Test that migration updates fulfillment status for orders with fulfillments.
- *
- * Creates 5 orders where only 3 have fulfillments, then runs migration
- * and verifies the correct orders are updated.
- */
- public function test_regenerate_order_fulfillment_status_updates_orders_with_fulfillments() {
- global $wpdb;
-
- // Reset analytics lookup tables for a clean slate.
- WC_Helper_Reports::reset_stats_dbs();
-
- $prev_fulfillments_opt = get_option( 'woocommerce_feature_fulfillments_enabled', null );
- update_option( 'woocommerce_feature_fulfillments_enabled', 'yes' );
-
- try {
- // Enable fulfillments feature.
- $controller = wc_get_container()->get( \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentsController::class );
- $controller->register();
- $controller->initialize_fulfillments();
-
- // Arrange: Reset migration state.
- delete_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
- delete_transient( 'woocommerce_analytics_fulfillment_status_progress' );
-
- // Ensure column exists.
- OrdersStatsDataStore::add_fulfillment_status_column();
-
- $product = WC_Helper_Product::create_simple_product();
-
- // Create 5 orders.
- $order_1 = WC_Helper_Order::create_order( get_current_user_id(), $product );
- $order_2 = WC_Helper_Order::create_order( get_current_user_id(), $product );
- $order_3 = WC_Helper_Order::create_order( get_current_user_id(), $product );
- $order_4 = WC_Helper_Order::create_order( get_current_user_id(), $product );
- $order_5 = WC_Helper_Order::create_order( get_current_user_id(), $product );
-
- WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
-
- // Add fulfillments for only orders 1, 2, 3.
- $this->add_fulfillment_to_order( $order_1, 'fulfilled', $product );
- $this->add_fulfillment_to_order( $order_2, 'partially_fulfilled', $product );
- $this->add_fulfillment_to_order( $order_3, 'unfulfilled', $product );
- // Orders 4 and 5 have no fulfillments.
-
- // Run the migration tool.
- Analytics::get_instance()->run_regenerate_order_fulfillment_status_tool();
-
- // Assert: Verify orders with fulfillments are updated.
- // Fetch all fulfillment statuses in a single query.
- $statuses = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT order_id, fulfillment_status
- FROM {$wpdb->prefix}wc_order_stats
- WHERE order_id IN (%d, %d, %d, %d, %d)
- ORDER BY order_id ASC",
- $order_1->get_id(),
- $order_2->get_id(),
- $order_3->get_id(),
- $order_4->get_id(),
- $order_5->get_id()
- ),
- OBJECT_K
- );
-
- // Verify orders with fulfillments.
- $this->assertEquals( 'fulfilled', $statuses[ $order_1->get_id() ]->fulfillment_status, 'Order 1 should have fulfilled status' );
- $this->assertEquals( 'partially_fulfilled', $statuses[ $order_2->get_id() ]->fulfillment_status, 'Order 2 should have partially_fulfilled status' );
- $this->assertEquals( 'unfulfilled', $statuses[ $order_3->get_id() ]->fulfillment_status, 'Order 3 should have unfulfilled status' );
-
- // Verify orders without fulfillments remain NULL.
- $this->assertNull( $statuses[ $order_4->get_id() ]->fulfillment_status, 'Order 4 should have NULL fulfillment_status' );
- $this->assertNull( $statuses[ $order_5->get_id() ]->fulfillment_status, 'Order 5 should have NULL fulfillment_status' );
-
- // Verify completion.
- $regenerated = get_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
- $this->assertTrue( (bool) $regenerated, 'Migration should be marked as completed' );
-
- // Verify transient cleanup.
- $progress = get_transient( 'woocommerce_analytics_fulfillment_status_progress' );
- $this->assertFalse( $progress, 'Progress transient should be deleted after completion' );
- } finally {
- // Cleanup.
- delete_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
- $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_fulfillment_meta" );
- $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_fulfillments" );
- delete_transient( 'woocommerce_analytics_fulfillment_status_progress' );
-
- if ( null === $prev_fulfillments_opt ) {
- delete_option( 'woocommerce_feature_fulfillments_enabled' );
- } else {
- update_option( 'woocommerce_feature_fulfillments_enabled', $prev_fulfillments_opt );
- }
- }
- }
-
- /**
- * Helper: Add fulfillment record and set order fulfillment status.
- *
- * This directly inserts into the fulfillments table and sets the order meta
- * to simulate what the fulfillments system would do in production.
- *
- * @param WC_Order $order Order object.
- * @param string $fulfillment_status Fulfillment status (fulfilled, partially_fulfilled, unfulfilled).
- * @param WC_Product $product Product to add to the fulfillment.
- *
- * @return Fulfillment The created fulfillment object.
- */
- private function add_fulfillment_to_order( $order, $fulfillment_status, $product ) {
- $fulfillment = new Fulfillment();
- $fulfillment->set_entity_type( WC_Order::class );
- $fulfillment->set_entity_id( (string) $order->get_id() );
-
- $fulfillment->set_items(
- array(
- array(
- 'item_id' => $product->get_id(),
- 'qty' => 4,
- ),
- )
- );
- $fulfillment->set_status( $fulfillment_status );
- $fulfillment->save();
-
- // Re-fetch the order to pick up meta changes made by hooks during save,
- // avoiding duplicate _fulfillment_status entries from separate instances.
- $order = wc_get_order( $order->get_id() );
- $order->update_meta_data( '_fulfillment_status', $fulfillment_status );
- $order->save();
-
- return $fulfillment;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreBasicsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreBasicsTest.php
new file mode 100644
index 00000000000..837b8bfaec7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreBasicsTest.php
@@ -0,0 +1,456 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query as OrdersStatsQuery;
+use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use DateTime;
+use WC_Coupon;
+use WC_Helper_Coupon;
+use WC_Helper_Customer;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Product_Simple;
+use WC_Tax;
+
+/**
+ * Tests for the basic querying behavior of the Orders Stats DataStore.
+ */
+class DataStoreBasicsTest extends OrdersStatsTestCase {
+
+ /**
+ * @testdox Stats for a single order are calculated correctly, both through the data store and the query class.
+ */
+ public function test_populate_and_query(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $coupon = WC_Helper_Coupon::create_coupon( 'test-coupon' );
+ $coupon->set_amount( 20 );
+ $coupon->save();
+
+ $order = WC_Helper_Order::create_order( 1, $product );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->set_shipping_total( 10 );
+ $order->apply_coupon( $coupon );
+ $order->set_cart_tax( 5 );
+ $order->set_shipping_tax( 2 );
+ // Total: $25x4 products + $10 shipping - $20 discount + $7 tax.
+ $order->set_total( 97 );
+ $order->save();
+
+ wc_create_refund(
+ array(
+ 'amount' => 12,
+ 'order_id' => $order->get_id(),
+ )
+ );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
+
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => 1,
+ 'num_items_sold' => 4,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 80,
+ 'total_sales' => 85,
+ 'gross_sales' => 100,
+ 'coupons' => 20,
+ 'coupons_count' => 1,
+ 'refunds' => 12,
+ 'taxes' => 7,
+ 'shipping' => 10,
+ 'net_revenue' => 68,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ ),
+ $start_time,
+ $end_time
+ );
+
+ $this->assert_report_data( $expected_stats, $args );
+
+ // The query class returns a reduced set of totals keys.
+ $query = new OrdersStatsQuery( $args );
+ $expected_stats = $this->expected_stats_single_interval(
+ array(
+ 'net_revenue' => 68,
+ 'avg_order_value' => 80,
+ 'orders_count' => 1,
+ 'avg_items_per_order' => 4,
+ 'num_items_sold' => 4,
+ 'coupons' => 20,
+ 'coupons_count' => 1,
+ 'total_customers' => 1,
+ 'products' => 1,
+ 'segments' => array(),
+ ),
+ $start_time,
+ $end_time
+ );
+ $this->assertEquals( $expected_stats, json_decode( wp_json_encode( $query->get_data() ), true ) );
+ }
+
+ /**
+ * @testdox Querying statuses includes the default or query-specific statuses.
+ */
+ public function test_populate_and_query_statuses(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $order_types = array(
+ array(
+ 'status' => OrderStatus::REFUNDED,
+ 'total' => 50,
+ ),
+ array(
+ 'status' => OrderStatus::COMPLETED,
+ 'total' => 100,
+ ),
+ array(
+ 'status' => OrderStatus::FAILED,
+ 'total' => 75,
+ ),
+ );
+
+ $time = time();
+
+ foreach ( $order_types as $order_type ) {
+ $order = WC_Helper_Order::create_order( 1, $product );
+ $order->set_status( $order_type['status'] );
+ $order->set_total( $order_type['total'] );
+ $order->set_date_created( $time );
+ $order->set_date_paid( $time );
+ $order->set_shipping_total( 0 );
+ $order->set_cart_tax( 0 );
+ $order->save();
+ }
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
+
+ // Query default statuses that should not include excluded or refunded order statuses.
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => 2,
+ // 4 items sold in the completed order, none in failed and refunded.
+ 'num_items_sold' => 4,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 75,
+ 'total_sales' => 100,
+ 'gross_sales' => 150,
+ 'refunds' => 50,
+ 'net_revenue' => 100,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ ),
+ $start_time,
+ $end_time
+ );
+
+ $this->assert_report_data( $expected_stats, $args );
+
+ // Query an excluded status which should still return orders with the queried status.
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'status_is' => array( OrderStatus::FAILED ),
+ );
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => 1,
+ 'num_items_sold' => 4,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 75,
+ 'total_sales' => 75,
+ 'gross_sales' => 75,
+ 'net_revenue' => 75,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ ),
+ $start_time,
+ $end_time
+ );
+
+ $this->assert_report_data( $expected_stats, $args );
+ }
+
+ /**
+ * @testdox Multiple coupons on orders are all included in the totals.
+ */
+ public function test_populate_and_query_multiple_coupons(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
+ $product_price = 23.45;
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( $product_price );
+ $product->save();
+
+ // Create 3 coupons valued 1, 2, 3.
+ $coupons = array();
+ foreach ( range( 1, 3 ) as $amount ) {
+ $coupon = WC_Helper_Coupon::create_coupon( 'coupon_' . $amount );
+ $coupon->set_amount( $amount );
+ $coupon->save();
+ $coupons[ $amount ] = $coupon;
+ }
+
+ // Create 3 orders with 3, 2, 1 coupons applied respectively.
+ $minute_ago = time() - MINUTE_IN_SECONDS;
+ $report_start_time = $minute_ago - ( $minute_ago % HOUR_IN_SECONDS );
+ $order_time = $report_start_time + 1;
+ $applied_amount = 0;
+ $orders_total = 0;
+ $orders = array();
+
+ foreach ( range( 1, 3 ) as $order_number ) {
+ $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
+ $order_date = $order_time++;
+ $order->set_date_created( $order_date );
+ $order->set_date_paid( $order_date );
+ $order->set_status( OrderStatus::COMPLETED );
+
+ foreach ( $coupons as $amount => $coupon ) {
+ if ( $amount >= $order_number ) {
+ $order->apply_coupon( $coupon );
+ $applied_amount += $amount;
+ }
+ }
+
+ $order->calculate_totals();
+ $orders_total += $order->get_total();
+ $order->save();
+
+ $orders[] = $order;
+ }
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $current_hour_start = new DateTime();
+ $current_hour_start->setTimestamp( $report_start_time );
+
+ $current_hour_end = new DateTime();
+ $current_hour_end->setTimestamp( $report_start_time + HOUR_IN_SECONDS - 1 );
+
+ $query_args = array(
+ 'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
+ 'before' => $current_hour_end->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ );
+
+ $orders_count = count( $orders );
+ $num_items_sold = $orders_count * self::QTY_PER_PRODUCT;
+ $shipping = $orders_count * self::SHIPPING_AMOUNT;
+ $net_revenue = $orders_total - $shipping;
+
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => $orders_count,
+ 'num_items_sold' => $num_items_sold,
+ 'total_sales' => $orders_total,
+ 'gross_sales' => $product_price * $num_items_sold,
+ 'coupons' => $applied_amount,
+ 'coupons_count' => count( $coupons ),
+ 'shipping' => $shipping,
+ 'net_revenue' => $net_revenue,
+ 'avg_items_per_order' => $num_items_sold / $orders_count,
+ 'avg_order_value' => $net_revenue / $orders_count,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ ),
+ $current_hour_start->format( 'Y-m-d H:i:s' ),
+ $current_hour_end->format( 'Y-m-d H:i:s' )
+ );
+
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+
+ /**
+ * @testdox Orders by different customers within the same hour count each customer.
+ */
+ public function test_populate_and_query_multiple_customers_same_hour(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $customer_1 = WC_Helper_Customer::create_customer( 'cust_multi_1', 'pwd_1', 'multi_user_1@mail.com' );
+ $customer_2 = WC_Helper_Customer::create_customer( 'cust_multi_2', 'pwd_2', 'multi_user_2@mail.com' );
+
+ // Two completed orders by different customers within the same hourly interval.
+ // Set a time near the top of the hour so both orders stay within it.
+ $order_datetime = new DateTime();
+ $order_datetime->setTime( (int) $order_datetime->format( 'H' ), 10, 0 );
+ $order_time = (int) $order_datetime->format( 'U' );
+
+ $customer_1_order = WC_Helper_Order::create_order( $customer_1->get_id() );
+ $customer_1_order->set_date_created( $order_time );
+ $customer_1_order->set_status( OrderStatus::COMPLETED );
+ $customer_1_order->save();
+
+ // Offset by 1 second to keep both orders in the same hour but distinct.
+ $customer_2_order = WC_Helper_Order::create_order( $customer_2->get_id() );
+ $customer_2_order->set_date_created( $order_time + 1 );
+ $customer_2_order->set_status( OrderStatus::COMPLETED );
+ $customer_2_order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+ $start_time = gmdate( 'Y-m-d H:00:00', $customer_1_order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $customer_1_order->get_date_created()->getOffsetTimestamp() );
+
+ $data = json_decode(
+ wp_json_encode(
+ $data_store->get_data(
+ array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ )
+ )
+ ),
+ true
+ );
+
+ $this->assertEquals( 2, $data['totals']['orders_count'] );
+ $this->assertEquals( 2, $data['totals']['total_customers'] );
+ $this->assertCount( 1, $data['intervals'] );
+ $this->assertEquals( 2, $data['intervals'][0]['subtotals']['orders_count'] );
+ $this->assertEquals( 2, $data['intervals'][0]['subtotals']['total_customers'] );
+ }
+
+ /**
+ * @testdox Lookup tables are cleaned after deleting an order.
+ *
+ * @covers \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::delete_order
+ * @covers \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::sync_on_order_delete
+ * @covers \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::sync_on_order_delete
+ * @covers \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore::sync_on_order_delete
+ */
+ public function test_order_deletion(): void {
+ global $wpdb;
+
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $tables = array(
+ 'wc_order_coupon_lookup',
+ 'wc_order_product_lookup',
+ 'wc_order_stats',
+ 'wc_order_tax_lookup',
+ );
+
+ // Enable taxes.
+ $default_calc_taxes = get_option( 'woocommerce_calc_taxes', 'no' );
+ $default_customer_address = get_option( 'woocommerce_default_customer_address', 'geolocation' );
+ $default_tax_based_on = get_option( 'woocommerce_tax_based_on', 'shipping' );
+ update_option( 'woocommerce_calc_taxes', 'yes' );
+ update_option( 'woocommerce_default_customer_address', 'base' );
+ update_option( 'woocommerce_tax_based_on', 'base' );
+
+ $tax_id = WC_Tax::_insert_tax_rate(
+ array(
+ 'tax_rate_country' => '',
+ 'tax_rate_state' => '',
+ 'tax_rate' => '10.0000',
+ 'tax_rate_name' => 'VAT',
+ 'tax_rate_priority' => '1',
+ 'tax_rate_compound' => '0',
+ 'tax_rate_shipping' => '1',
+ 'tax_rate_order' => '1',
+ 'tax_rate_class' => '',
+ )
+ );
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $coupon = new WC_Coupon();
+ $coupon->set_code( '20fixed' );
+ $coupon->set_discount_type( 'fixed_cart' );
+ $coupon->set_amount( 20 );
+ $coupon->save();
+
+ $order = WC_Helper_Order::create_order();
+ $order->add_product( $product, 1 );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->set_shipping_total( 10 );
+ $order->apply_coupon( $coupon );
+ $order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ // Check if lookup tables are populated.
+ foreach ( $tables as $table ) {
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}{$table} WHERE order_id = %d", // phpcs:ignore
+ $order->get_id()
+ )
+ );
+
+ $this->assertTrue( is_array( $results ) && (bool) count( $results ) );
+ }
+
+ // Delete order and clean all other objects too.
+ $order->delete( true );
+ $product->delete( true );
+ $coupon->delete( true );
+ WC_Tax::_delete_tax_rate( $tax_id );
+
+ // Check if results are empty now.
+ foreach ( $tables as $table ) {
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}{$table} WHERE order_id = %d", // phpcs:ignore
+ $order->get_id()
+ )
+ );
+
+ $this->assertEmpty( $results );
+ }
+
+ // Reset taxes settings.
+ update_option( 'woocommerce_calc_taxes', $default_calc_taxes );
+ update_option( 'woocommerce_default_customer_address', $default_customer_address );
+ update_option( 'woocommerce_tax_based_on', $default_tax_based_on );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFilterQueriesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFilterQueriesTest.php
new file mode 100644
index 00000000000..e3f4b2b94eb
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFilterQueriesTest.php
@@ -0,0 +1,708 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use DateTime;
+use WC_Helper_Coupon;
+use WC_Helper_Customer;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Order_Item_Product;
+use WC_Product_Simple;
+
+/**
+ * Tests for the Orders Stats DataStore filter handling ({@see OrdersStatsDataStore::get_data()}).
+ *
+ * A shared fixture of 36 orders is created once for the class: for each of 3 primary
+ * products, 3 coupon options (none, the small coupon, the large coupon) and 2 order
+ * statuses (completed, processing), one order with just that product and one order with
+ * that product plus a shared add-on product. Each order contains 4 items of each of its
+ * products and $10 shipping.
+ *
+ * Every filter scenario describes the orders it matches as per-product-mix order counts
+ * (the 6 "mixes" are: each primary product alone, then each with the add-on product),
+ * from which all expected totals derive.
+ */
+class DataStoreFilterQueriesTest extends OrdersStatsTestCase {
+
+ // The two fixture coupons differ only in their fixed discount amount.
+ const SMALL_COUPON_AMOUNT = 1;
+ const LARGE_COUPON_AMOUNT = 2;
+
+ // The add-on product is the second product in every two-product order.
+ const ADD_ON_PRODUCT_PRICE = 1;
+
+ /**
+ * Prices of the three primary products.
+ *
+ * @var array
+ */
+ private static $primary_product_prices = array( 25, 10, 13 );
+
+ /**
+ * IDs of the three primary products.
+ *
+ * @var array
+ */
+ private static $primary_product_ids = array();
+
+ /**
+ * ID of the add-on product.
+ *
+ * @var int
+ */
+ private static $add_on_product_id;
+
+ /**
+ * ID of the small fixture coupon.
+ *
+ * @var int
+ */
+ private static $small_coupon_id;
+
+ /**
+ * ID of the large fixture coupon.
+ *
+ * @var int
+ */
+ private static $large_coupon_id;
+
+ /**
+ * Start of the hour all fixture orders were created in.
+ *
+ * @var DateTime
+ */
+ private static $current_hour_start;
+
+ /**
+ * End of the hour all fixture orders were created in.
+ *
+ * @var DateTime
+ */
+ private static $current_hour_end;
+
+ /**
+ * IDs of the fixture orders.
+ *
+ * @var array
+ */
+ private static $fixture_order_ids = array();
+
+ /**
+ * Create the shared order fixture once for the whole class.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $primary_products = array();
+ foreach ( self::$primary_product_prices as $index => $price ) {
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product ' . ( $index + 1 ) );
+ $product->set_regular_price( $price );
+ $product->save();
+ $primary_products[] = $product;
+ self::$primary_product_ids[] = $product->get_id();
+ }
+
+ $add_on_product = new WC_Product_Simple();
+ $add_on_product->set_name( 'Test Add-on Product' );
+ $add_on_product->set_regular_price( self::ADD_ON_PRODUCT_PRICE );
+ $add_on_product->save();
+ self::$add_on_product_id = $add_on_product->get_id();
+
+ $small_coupon = WC_Helper_Coupon::create_coupon( 'small_coupon' );
+ $small_coupon->set_amount( self::SMALL_COUPON_AMOUNT );
+ $small_coupon->save();
+
+ $large_coupon = WC_Helper_Coupon::create_coupon( 'large_coupon' );
+ $large_coupon->set_amount( self::LARGE_COUPON_AMOUNT );
+ $large_coupon->save();
+
+ self::$small_coupon_id = $small_coupon->get_id();
+ self::$large_coupon_id = $large_coupon->get_id();
+
+ $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
+
+ $order_datetime = new DateTime();
+ // Time near the top of the hour, so the +1s offsets below stay within it.
+ $order_datetime->setTime( (int) $order_datetime->format( 'H' ), 10, 0 );
+ $order_time = (int) $order_datetime->format( 'U' );
+
+ $iterations = 1;
+ foreach ( $primary_products as $product ) {
+ foreach ( array( null, $small_coupon, $large_coupon ) as $coupon ) {
+ foreach ( array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ) as $order_status ) {
+ $single_product_order = WC_Helper_Order::create_order( $customer->get_id(), $product );
+ // Offset each order by 1 second.
+ $single_product_order->set_date_created( $order_time + $iterations++ );
+ $single_product_order->set_status( $order_status );
+
+ if ( $coupon ) {
+ $single_product_order->apply_coupon( $coupon );
+ } else {
+ $single_product_order->calculate_totals();
+ }
+
+ $two_product_order = WC_Helper_Order::create_order( $customer->get_id(), $add_on_product );
+
+ $item = new WC_Order_Item_Product();
+ $item->set_props(
+ array(
+ 'product' => $product,
+ 'quantity' => self::QTY_PER_PRODUCT,
+ 'subtotal' => wc_get_price_excluding_tax( $product, array( 'qty' => self::QTY_PER_PRODUCT ) ),
+ 'total' => wc_get_price_excluding_tax( $product, array( 'qty' => self::QTY_PER_PRODUCT ) ),
+ )
+ );
+ $item->save();
+ $two_product_order->add_item( $item );
+ // Offset each order by 1 second.
+ $two_product_order->set_date_created( $order_time + $iterations++ );
+ $two_product_order->set_status( $order_status );
+
+ if ( $coupon ) {
+ $two_product_order->apply_coupon( $coupon );
+ } else {
+ $two_product_order->calculate_totals();
+ }
+
+ self::$fixture_order_ids[] = $single_product_order->get_id();
+ self::$fixture_order_ids[] = $two_product_order->get_id();
+ }
+ }
+ }
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ self::$current_hour_start = new DateTime();
+ self::$current_hour_start->setTimestamp( $order_time - ( (int) $order_datetime->format( 'i' ) * MINUTE_IN_SECONDS ) );
+
+ self::$current_hour_end = new DateTime();
+ self::$current_hour_end->setTimestamp( $order_time + ( HOUR_IN_SECONDS - ( $order_time % HOUR_IN_SECONDS ) ) - 1 );
+ }
+
+ /**
+ * Remove the persistent class fixture data.
+ */
+ public static function wpTearDownAfterClass(): void {
+ foreach ( self::$fixture_order_ids as $order_id ) {
+ $order = wc_get_order( $order_id );
+ if ( $order ) {
+ $order->delete( true );
+ }
+ }
+ self::$fixture_order_ids = array();
+
+ // Delete the products through CRUD so their wc_product_meta_lookup rows go too.
+ foreach ( array_merge( self::$primary_product_ids, array( self::$add_on_product_id ) ) as $product_id ) {
+ $product = wc_get_product( $product_id );
+ if ( $product ) {
+ $product->delete( true );
+ }
+ }
+ self::$primary_product_ids = array();
+
+ WC_Helper_Reports::reset_stats_dbs();
+ }
+
+ /**
+ * @testdox Filter scenarios return the expected totals and intervals.
+ * @dataProvider filter_scenarios
+ *
+ * @param array $filter_args Filter arguments added to the query, with placeholder values resolved by resolve_filter_args().
+ * @param array $mix_counts Matched-order counts per product mix: ( p1, p2, p3, p1+p4, p2+p4, p3+p4 ).
+ * @param array $coupon_counts Matched-order counts using the small and large coupon, respectively.
+ * @param int $products_count Expected count of distinct products in the matched orders.
+ */
+ public function test_filter_scenario( array $filter_args, array $mix_counts, array $coupon_counts, int $products_count ): void {
+ $query_args = array_merge( $this->base_query_args(), $this->resolve_filter_args( $filter_args ) );
+
+ $expected_stats = $this->build_expected_stats( $mix_counts, $coupon_counts, $products_count );
+
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+
+ /**
+ * Filter scenarios, as migrated from the legacy multiple-intervals test.
+ *
+ * In filter args, products are given as 'product_1' ... 'product_3' or
+ * 'add_on_product', and coupons as 'small_coupon' / 'large_coupon'; these placeholders are
+ * resolved to the fixture IDs by resolve_filter_args().
+ *
+ * @return array
+ */
+ public function filter_scenarios(): array {
+ return array(
+ 'no filters matches all 36 orders' => array(
+ array(),
+ array( 6, 6, 6, 6, 6, 6 ),
+ array( 12, 12 ),
+ 4,
+ ),
+ 'status_is with both statuses matches all orders' => array(
+ array( 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ) ),
+ array( 6, 6, 6, 6, 6, 6 ),
+ array( 12, 12 ),
+ 4,
+ ),
+ 'status_is completed matches the completed half' => array(
+ array( 'status_is' => array( OrderStatus::COMPLETED ) ),
+ array( 3, 3, 3, 3, 3, 3 ),
+ array( 6, 6 ),
+ 4,
+ ),
+ 'status_is_not processing matches the completed half' => array(
+ array( 'status_is_not' => array( OrderStatus::PROCESSING ) ),
+ array( 3, 3, 3, 3, 3, 3 ),
+ array( 6, 6 ),
+ 4,
+ ),
+ 'status_is_not with both statuses matches nothing' => array(
+ array( 'status_is_not' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ) ),
+ array( 0, 0, 0, 0, 0, 0 ),
+ array( 0, 0 ),
+ 0,
+ ),
+ 'status_is both minus processing matches the completed half' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ ),
+ array( 3, 3, 3, 3, 3, 3 ),
+ array( 6, 6 ),
+ 4,
+ ),
+ 'product_includes products 1 and 2 matches two thirds' => array(
+ array( 'product_includes' => array( 'product_1', 'product_2' ) ),
+ array( 6, 6, 0, 6, 6, 0 ),
+ array( 8, 8 ),
+ 3,
+ ),
+ 'product_includes product 3 matches its third of orders' => array(
+ array( 'product_includes' => array( 'product_3' ) ),
+ array( 0, 0, 6, 0, 0, 6 ),
+ array( 4, 4 ),
+ 2,
+ ),
+ 'product_excludes product 1 matches the other two thirds' => array(
+ array( 'product_excludes' => array( 'product_1' ) ),
+ array( 0, 6, 6, 0, 6, 6 ),
+ array( 8, 8 ),
+ 3,
+ ),
+ 'product_excludes products 1 and 2 matches the remaining third' => array(
+ array( 'product_excludes' => array( 'product_1', 'product_2' ) ),
+ array( 0, 0, 6, 0, 0, 6 ),
+ array( 4, 4 ),
+ 2,
+ ),
+ 'include products 1 and 2 minus product 1 leaves product 2 orders' => array(
+ array(
+ 'product_includes' => array( 'product_1', 'product_2' ),
+ 'product_excludes' => array( 'product_1' ),
+ ),
+ array( 0, 6, 0, 0, 6, 0 ),
+ array( 4, 4 ),
+ 2,
+ ),
+ 'coupon_includes both matches the couponed two thirds' => array(
+ array( 'coupon_includes' => array( 'small_coupon', 'large_coupon' ) ),
+ array( 4, 4, 4, 4, 4, 4 ),
+ array( 12, 12 ),
+ 4,
+ ),
+ 'coupon_includes the small coupon matches its third' => array(
+ array( 'coupon_includes' => array( 'small_coupon' ) ),
+ array( 2, 2, 2, 2, 2, 2 ),
+ array( 12, 0 ),
+ 4,
+ ),
+ 'coupon_excludes small matches uncouponed and large-coupon orders' => array(
+ array( 'coupon_excludes' => array( 'small_coupon' ) ),
+ array( 4, 4, 4, 4, 4, 4 ),
+ array( 0, 12 ),
+ 4,
+ ),
+ 'coupon_excludes both matches the uncouponed third' => array(
+ array( 'coupon_excludes' => array( 'small_coupon', 'large_coupon' ) ),
+ array( 2, 2, 2, 2, 2, 2 ),
+ array( 0, 0 ),
+ 4,
+ ),
+ 'coupon_includes both minus the large leaves the small third' => array(
+ array(
+ 'coupon_includes' => array( 'small_coupon', 'large_coupon' ),
+ 'coupon_excludes' => array( 'large_coupon' ),
+ ),
+ array( 2, 2, 2, 2, 2, 2 ),
+ array( 12, 0 ),
+ 4,
+ ),
+ 'customer_type new matches only the very first order' => array(
+ array( 'customer_type' => 'new' ),
+ array( 1, 0, 0, 0, 0, 0 ),
+ array( 0, 0 ),
+ 1,
+ ),
+ 'match all: completed orders containing product 1' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'product_includes' => array( 'product_1' ),
+ ),
+ array( 3, 0, 0, 3, 0, 0 ),
+ array( 2, 2 ),
+ 2,
+ ),
+ 'match all: completed orders using the small coupon' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 1, 1, 1, 1, 1, 1 ),
+ array( 6, 0 ),
+ 4,
+ ),
+ 'match all: product 1 orders using the small coupon' => array(
+ array(
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 2, 0, 0, 2, 0, 0 ),
+ array( 4, 0 ),
+ 2,
+ ),
+ 'match all: completed product 1 orders using the small coupon' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 1, 0, 0, 1, 0, 0 ),
+ array( 2, 0 ),
+ 2,
+ ),
+ 'match all: status_is_not processing keeps the same orders' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 1, 0, 0, 1, 0, 0 ),
+ array( 2, 0 ),
+ 2,
+ ),
+ 'match all: excluding the add-on leaves single-product orders' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1', 'product_2' ),
+ 'product_excludes' => array( 'add_on_product' ),
+ ),
+ array( 3, 3, 0, 0, 0, 0 ),
+ array( 2, 2 ),
+ 2,
+ ),
+ 'match all: five filters leave two small-coupon orders' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1', 'product_2' ),
+ 'product_excludes' => array( 'add_on_product' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 1, 1, 0, 0, 0, 0 ),
+ array( 2, 0 ),
+ 2,
+ ),
+ 'match all: six filters keep the same two orders' => array(
+ array(
+ 'status_is' => array( OrderStatus::COMPLETED, OrderStatus::PROCESSING ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1', 'product_2' ),
+ 'product_excludes' => array( 'add_on_product' ),
+ 'coupon_includes' => array( 'small_coupon', 'large_coupon' ),
+ 'coupon_excludes' => array( 'large_coupon' ),
+ ),
+ array( 1, 1, 0, 0, 0, 0 ),
+ array( 2, 0 ),
+ 2,
+ ),
+ 'match any: a status and its negation together match all orders' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'status_is_not' => array( OrderStatus::COMPLETED ),
+ ),
+ array( 6, 6, 6, 6, 6, 6 ),
+ array( 12, 12 ),
+ 4,
+ ),
+ 'match any: completed or containing product 1' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'product_includes' => array( 'product_1' ),
+ ),
+ array( 6, 3, 3, 6, 3, 3 ),
+ array( 8, 8 ),
+ 4,
+ ),
+ 'match any: completed or using the small coupon' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 4, 4, 4, 4, 4, 4 ),
+ array( 12, 6 ),
+ 4,
+ ),
+ 'match any: completed or not using the small coupon' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'coupon_excludes' => array( 'small_coupon' ),
+ ),
+ array( 5, 5, 5, 5, 5, 5 ),
+ array( 6, 12 ),
+ 4,
+ ),
+ 'match any: product 1 orders or small-coupon orders' => array(
+ array(
+ 'match' => 'any',
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 6, 2, 2, 6, 2, 2 ),
+ array( 12, 4 ),
+ 4,
+ ),
+ 'match any: completed, product 1 or small-coupon orders' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 6, 4, 4, 6, 4, 4 ),
+ array( 12, 8 ),
+ 4,
+ ),
+ 'match any: status_is_not processing adds no new orders' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 6, 4, 4, 6, 4, 4 ),
+ array( 12, 8 ),
+ 4,
+ ),
+ 'match any: orders without product 2 widen the union' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1' ),
+ 'product_excludes' => array( 'product_2' ),
+ ),
+ array( 6, 3, 6, 6, 3, 6 ),
+ array( 10, 10 ),
+ 4,
+ ),
+ 'match any: adding the small coupon widens the union further' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1' ),
+ 'product_excludes' => array( 'product_2' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ ),
+ array( 6, 4, 6, 6, 4, 6 ),
+ array( 12, 10 ),
+ 4,
+ ),
+ 'match any: excluding the large coupon widens the union again' => array(
+ array(
+ 'match' => 'any',
+ 'status_is' => array( OrderStatus::COMPLETED ),
+ 'status_is_not' => array( OrderStatus::PROCESSING ),
+ 'product_includes' => array( 'product_1' ),
+ 'product_excludes' => array( 'product_2' ),
+ 'coupon_includes' => array( 'small_coupon' ),
+ 'coupon_excludes' => array( 'large_coupon' ),
+ ),
+ array( 6, 5, 6, 6, 5, 6 ),
+ array( 12, 10 ),
+ 4,
+ ),
+ );
+ }
+
+ /**
+ * @testdox The returning-customer filter matches all fixture orders except the customer's first.
+ */
+ public function test_customer_type_returning(): void {
+ $query_args = array_merge(
+ $this->base_query_args(),
+ array( 'customer_type' => 'returning' )
+ );
+
+ // All 36 fixture orders match except the customer's first: half hold one product, half hold two.
+ $total_orders_count = 36;
+ $returning_orders_count = 1;
+
+ $orders_count = $total_orders_count - $returning_orders_count;
+ $num_items_sold = ( $total_orders_count / 2 ) * self::QTY_PER_PRODUCT
+ + ( $total_orders_count / 2 ) * self::QTY_PER_PRODUCT * 2
+ - $returning_orders_count * self::QTY_PER_PRODUCT;
+ $coupons = 12 * self::SMALL_COUPON_AMOUNT + 12 * self::LARGE_COUPON_AMOUNT;
+ $shipping = $orders_count * self::SHIPPING_AMOUNT;
+ $net_revenue = $this->net_revenue_for_mix_counts( array( 6, 6, 6, 6, 6, 6 ) )
+ - self::$primary_product_prices[0] * self::QTY_PER_PRODUCT * $returning_orders_count
+ - $coupons;
+
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => $orders_count,
+ 'num_items_sold' => $num_items_sold,
+ 'total_sales' => $net_revenue + $shipping,
+ 'gross_sales' => $net_revenue + $coupons,
+ 'coupons' => $coupons,
+ // Both fixture coupons appear across the matched orders.
+ 'coupons_count' => 2,
+ 'shipping' => $shipping,
+ 'net_revenue' => $net_revenue,
+ 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
+ 'avg_order_value' => $net_revenue / $orders_count,
+ // All fixture orders belong to the one fixture customer.
+ 'total_customers' => 1,
+ 'products' => 4,
+ )
+ ),
+ self::$current_hour_start->format( 'Y-m-d H:i:s' ),
+ self::$current_hour_end->format( 'Y-m-d H:i:s' )
+ );
+
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+
+ /**
+ * Query args shared by every scenario: the fixture hour with hourly intervals.
+ *
+ * @return array
+ */
+ private function base_query_args(): array {
+ return array(
+ 'after' => self::$current_hour_start->format( TimeInterval::$sql_datetime_format ),
+ 'before' => self::$current_hour_end->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ );
+ }
+
+ /**
+ * Replace status/product/coupon placeholders in filter args with fixture values.
+ *
+ * @param array $filter_args Filter args with placeholder values.
+ * @return array
+ */
+ private function resolve_filter_args( array $filter_args ): array {
+ $map = array(
+ 'product_1' => self::$primary_product_ids[0] ?? null,
+ 'product_2' => self::$primary_product_ids[1] ?? null,
+ 'product_3' => self::$primary_product_ids[2] ?? null,
+ 'add_on_product' => self::$add_on_product_id,
+ 'small_coupon' => self::$small_coupon_id,
+ 'large_coupon' => self::$large_coupon_id,
+ );
+
+ $resolved = array();
+ foreach ( $filter_args as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $resolved[ $key ] = array_map(
+ function ( $placeholder ) use ( $map ) {
+ return $map[ $placeholder ] ?? $placeholder;
+ },
+ $value
+ );
+ } else {
+ $resolved[ $key ] = $value;
+ }
+ }
+
+ return $resolved;
+ }
+
+ /**
+ * Sum of the matched orders' product revenue (before coupon deduction).
+ *
+ * @param array $mix_counts Matched-order counts per product mix: ( p1, p2, p3, p1+p4, p2+p4, p3+p4 ).
+ * @return float
+ */
+ private function net_revenue_for_mix_counts( array $mix_counts ): float {
+ $prices = self::$primary_product_prices;
+ $mix_prices = array(
+ $prices[0],
+ $prices[1],
+ $prices[2],
+ $prices[0] + self::ADD_ON_PRODUCT_PRICE,
+ $prices[1] + self::ADD_ON_PRODUCT_PRICE,
+ $prices[2] + self::ADD_ON_PRODUCT_PRICE,
+ );
+ $total_revenue = 0;
+ foreach ( $mix_prices as $index => $price ) {
+ $total_revenue += $price * self::QTY_PER_PRODUCT * $mix_counts[ $index ];
+ }
+
+ return (float) $total_revenue;
+ }
+
+ /**
+ * Build the full expected get_data() result from a scenario description.
+ *
+ * @param array $mix_counts Matched-order counts per product mix: ( p1, p2, p3, p1+p4, p2+p4, p3+p4 ).
+ * @param array $coupon_counts Matched-order counts using the small and large coupon, respectively.
+ * @param int $products_count Expected count of distinct products in the matched orders.
+ * @return array
+ */
+ private function build_expected_stats( array $mix_counts, array $coupon_counts, int $products_count ): array {
+ $orders_count = array_sum( $mix_counts );
+ $single_item_orders = $mix_counts[0] + $mix_counts[1] + $mix_counts[2];
+ $two_item_orders = $mix_counts[3] + $mix_counts[4] + $mix_counts[5];
+ $num_items_sold = $single_item_orders * self::QTY_PER_PRODUCT + $two_item_orders * self::QTY_PER_PRODUCT * 2;
+ $coupons = $coupon_counts[0] * self::SMALL_COUPON_AMOUNT + $coupon_counts[1] * self::LARGE_COUPON_AMOUNT;
+ $coupons_count = ( $coupon_counts[0] ? 1 : 0 ) + ( $coupon_counts[1] ? 1 : 0 );
+ $shipping = $orders_count * self::SHIPPING_AMOUNT;
+ $net_revenue = $orders_count ? $this->net_revenue_for_mix_counts( $mix_counts ) - $coupons : 0;
+
+ return $this->expected_stats_single_interval(
+ $this->expected_totals(
+ array(
+ 'orders_count' => $orders_count,
+ 'num_items_sold' => $num_items_sold,
+ 'total_sales' => $net_revenue + $shipping,
+ 'gross_sales' => $net_revenue + $coupons,
+ 'coupons' => $coupons,
+ 'coupons_count' => $coupons_count,
+ 'shipping' => $shipping,
+ 'net_revenue' => $net_revenue,
+ 'avg_items_per_order' => $orders_count ? $num_items_sold / $orders_count : 0,
+ 'avg_order_value' => $orders_count ? $net_revenue / $orders_count : 0,
+ 'total_customers' => $orders_count ? 1 : 0,
+ 'products' => $products_count,
+ )
+ ),
+ self::$current_hour_start->format( 'Y-m-d H:i:s' ),
+ self::$current_hour_end->format( 'Y-m-d H:i:s' )
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFulfillmentsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFulfillmentsTest.php
new file mode 100644
index 00000000000..18512a37ead
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreFulfillmentsTest.php
@@ -0,0 +1,159 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentsController;
+use Automattic\WooCommerce\Internal\Admin\Analytics;
+use WC_Helper_Order;
+use WC_Helper_Product;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Order;
+use WC_Product;
+
+/**
+ * Tests for the order fulfillment status regeneration tool of the Orders Stats DataStore.
+ */
+class DataStoreFulfillmentsTest extends OrdersStatsTestCase {
+
+ /**
+ * Set the database version and clear the fulfillment-status column flag for this class.
+ */
+ public static function setUpBeforeClass(): void {
+ // Must come first: the parent reconnects `$wpdb`, which discards anything this
+ // method has written but not committed.
+ parent::setUpBeforeClass();
+
+ $db_version = strstr( WC()->version, '-', true );
+ $db_version = $db_version ? $db_version : WC()->version;
+ update_option( 'woocommerce_db_version', $db_version );
+
+ delete_option( OrdersStatsDataStore::OPTION_ORDER_STATS_TABLE_HAS_COLUMN_ORDER_FULFILLMENT_STATUS );
+ }
+
+ /**
+ * @testdox The regeneration tool updates fulfillment status only for orders with fulfillments.
+ */
+ public function test_regenerate_order_fulfillment_status_updates_orders_with_fulfillments(): void {
+ global $wpdb;
+
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $prev_fulfillments_opt = get_option( 'woocommerce_feature_fulfillments_enabled', null );
+ update_option( 'woocommerce_feature_fulfillments_enabled', 'yes' );
+
+ try {
+ // Enable fulfillments feature.
+ $controller = wc_get_container()->get( FulfillmentsController::class );
+ $controller->register();
+ $controller->initialize_fulfillments();
+
+ // Reset migration state.
+ delete_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
+ delete_transient( 'woocommerce_analytics_fulfillment_status_progress' );
+
+ // Ensure column exists.
+ OrdersStatsDataStore::add_fulfillment_status_column();
+
+ $product = WC_Helper_Product::create_simple_product();
+
+ $fulfilled_order = WC_Helper_Order::create_order( get_current_user_id(), $product );
+ $partially_fulfilled_order = WC_Helper_Order::create_order( get_current_user_id(), $product );
+ $unfulfilled_order = WC_Helper_Order::create_order( get_current_user_id(), $product );
+ $no_fulfillment_order_1 = WC_Helper_Order::create_order( get_current_user_id(), $product );
+ $no_fulfillment_order_2 = WC_Helper_Order::create_order( get_current_user_id(), $product );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $this->add_fulfillment_to_order( $fulfilled_order, 'fulfilled', $product );
+ $this->add_fulfillment_to_order( $partially_fulfilled_order, 'partially_fulfilled', $product );
+ $this->add_fulfillment_to_order( $unfulfilled_order, 'unfulfilled', $product );
+
+ Analytics::get_instance()->run_regenerate_order_fulfillment_status_tool();
+
+ // Fetch all fulfillment statuses in a single query.
+ $statuses = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT order_id, fulfillment_status
+ FROM {$wpdb->prefix}wc_order_stats
+ WHERE order_id IN (%d, %d, %d, %d, %d)
+ ORDER BY order_id ASC",
+ $fulfilled_order->get_id(),
+ $partially_fulfilled_order->get_id(),
+ $unfulfilled_order->get_id(),
+ $no_fulfillment_order_1->get_id(),
+ $no_fulfillment_order_2->get_id()
+ ),
+ OBJECT_K
+ );
+
+ // Verify orders with fulfillments.
+ $this->assertEquals( 'fulfilled', $statuses[ $fulfilled_order->get_id() ]->fulfillment_status, 'The fulfilled order should have fulfilled status' );
+ $this->assertEquals( 'partially_fulfilled', $statuses[ $partially_fulfilled_order->get_id() ]->fulfillment_status, 'The partially fulfilled order should have partially_fulfilled status' );
+ $this->assertEquals( 'unfulfilled', $statuses[ $unfulfilled_order->get_id() ]->fulfillment_status, 'The unfulfilled order should have unfulfilled status' );
+
+ // Verify orders without fulfillments remain NULL.
+ $this->assertNull( $statuses[ $no_fulfillment_order_1->get_id() ]->fulfillment_status, 'Orders without fulfillments should have a NULL fulfillment_status' );
+ $this->assertNull( $statuses[ $no_fulfillment_order_2->get_id() ]->fulfillment_status, 'Orders without fulfillments should have a NULL fulfillment_status' );
+
+ // Verify completion.
+ $regenerated = get_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
+ $this->assertTrue( (bool) $regenerated, 'Migration should be marked as completed' );
+
+ // Verify transient cleanup.
+ $progress = get_transient( 'woocommerce_analytics_fulfillment_status_progress' );
+ $this->assertFalse( $progress, 'Progress transient should be deleted after completion' );
+ } finally {
+ delete_option( 'woocommerce_analytics_order_fulfillment_status_regenerated' );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_fulfillment_meta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_fulfillments" );
+ delete_transient( 'woocommerce_analytics_fulfillment_status_progress' );
+
+ if ( null === $prev_fulfillments_opt ) {
+ delete_option( 'woocommerce_feature_fulfillments_enabled' );
+ } else {
+ update_option( 'woocommerce_feature_fulfillments_enabled', $prev_fulfillments_opt );
+ }
+ }
+ }
+
+ /**
+ * Add a fulfillment record and set the order fulfillment status.
+ *
+ * This directly inserts into the fulfillments table and sets the order meta
+ * to simulate what the fulfillments system would do in production.
+ *
+ * @param WC_Order $order Order object.
+ * @param string $fulfillment_status Fulfillment status (fulfilled, partially_fulfilled, unfulfilled).
+ * @param WC_Product $product Product to add to the fulfillment.
+ *
+ * @return Fulfillment The created fulfillment object.
+ */
+ private function add_fulfillment_to_order( $order, $fulfillment_status, $product ) {
+ $fulfillment = new Fulfillment();
+ $fulfillment->set_entity_type( WC_Order::class );
+ $fulfillment->set_entity_id( (string) $order->get_id() );
+
+ $fulfillment->set_items(
+ array(
+ array(
+ 'item_id' => $product->get_id(),
+ 'qty' => 4,
+ ),
+ )
+ );
+ $fulfillment->set_status( $fulfillment_status );
+ $fulfillment->save();
+
+ // Re-fetch the order to pick up meta changes made by hooks during save,
+ // avoiding duplicate _fulfillment_status entries from separate instances.
+ $order = wc_get_order( $order->get_id() );
+ $order->update_meta_data( '_fulfillment_status', $fulfillment_status );
+ $order->save();
+
+ return $fulfillment;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreRefundFiltersTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreRefundFiltersTest.php
new file mode 100644
index 00000000000..00218f0d002
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreRefundFiltersTest.php
@@ -0,0 +1,306 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Product_Simple;
+
+/**
+ * Tests for the refunds filter of the Orders Stats DataStore.
+ */
+class DataStoreRefundFiltersTest extends OrdersStatsTestCase {
+
+ /**
+ * Remove the full-refund data format option after each test, including failed ones.
+ */
+ public function tearDown(): void {
+ delete_option( 'woocommerce_analytics_uses_old_full_refund_data' );
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox The refunds filter distinguishes all, none, partial and full refunds.
+ */
+ public function test_populate_and_query_refunds(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $order_types = array(
+ array(
+ 'status' => OrderStatus::REFUNDED,
+ 'total' => 50,
+ ),
+ array(
+ 'status' => OrderStatus::COMPLETED,
+ 'total' => 100,
+ ),
+ array(
+ 'status' => OrderStatus::COMPLETED,
+ 'total' => 75,
+ ),
+ );
+
+ $time = time();
+
+ foreach ( $order_types as $order_type ) {
+ $order = WC_Helper_Order::create_order( 1, $product );
+ $order->set_status( $order_type['status'] );
+ $order->set_total( $order_type['total'] );
+ $order->set_date_created( $time );
+ $order->set_date_paid( $time );
+ $order->set_shipping_total( 0 );
+ $order->set_cart_tax( 0 );
+ $order->save();
+ }
+
+ // Add a partial refund on the first item of the last order.
+ $item_id = current( $order->get_items() )->get_id();
+ wc_create_refund(
+ array(
+ 'amount' => 10,
+ 'order_id' => $order->get_id(),
+ 'line_items' => array(
+ $item_id => array(
+ 'qty' => 0,
+ 'refund_total' => 10,
+ ),
+ ),
+ )
+ );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
+
+ $refund_filter_expectations = array(
+ 'all' => array(
+ 'num_items_sold' => -4,
+ 'total_sales' => -60,
+ 'refunds' => 60,
+ 'net_revenue' => -60,
+ 'total_customers' => 1,
+ ),
+ 'none' => array(
+ 'orders_count' => 3,
+ 'num_items_sold' => 12,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 75,
+ 'total_sales' => 225,
+ 'gross_sales' => 225,
+ 'net_revenue' => 225,
+ 'total_customers' => 1,
+ ),
+ 'partial' => array(
+ 'total_sales' => -10,
+ 'refunds' => 10,
+ 'net_revenue' => -10,
+ 'total_customers' => 1,
+ ),
+ 'full' => array(
+ 'num_items_sold' => -4,
+ 'total_sales' => -50,
+ 'refunds' => 50,
+ 'net_revenue' => -50,
+ 'total_customers' => 1,
+ ),
+ );
+
+ foreach ( $refund_filter_expectations as $refund_filter => $expected_values ) {
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'refunds' => $refund_filter,
+ );
+
+ $expected_stats = $this->expected_stats_single_interval(
+ $this->expected_totals( array_merge( $expected_values, array( 'products' => 1 ) ) ),
+ $start_time,
+ $end_time
+ );
+
+ $this->assertEquals(
+ $expected_stats,
+ json_decode( wp_json_encode( $data_store->get_data( $args ) ), true ),
+ "Unexpected stats for refunds filter '{$refund_filter}'"
+ );
+ }
+ }
+
+ /**
+ * @testdox Full refunds via status change are reported with the old full refund data format.
+ */
+ public function test_populate_and_query_refunds_with_old_full_refund_data(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+ update_option( 'woocommerce_analytics_uses_old_full_refund_data', 'yes' );
+
+ $order = $this->create_orders_with_full_refund_via_status_change();
+
+ $data_store = new OrdersStatsDataStore();
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', strtotime( '+1 day', $order->get_date_created()->getOffsetTimestamp() ) );
+
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $expected_totals = $this->expected_totals(
+ array(
+ 'orders_count' => 2,
+ 'num_items_sold' => 8,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 55,
+ 'total_sales' => 50,
+ 'gross_sales' => 110,
+ 'refunds' => 100,
+ 'taxes' => 20,
+ 'shipping' => 20,
+ 'net_revenue' => 10,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ );
+
+ $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
+
+ // Query full refunds.
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'refunds' => 'full',
+ );
+ // num_items_sold, refunds and products expectations reflect the bug fixed by PR #58744.
+ $expected_totals = $this->expected_totals(
+ array(
+ 'total_sales' => -100,
+ 'refunds' => 100,
+ 'net_revenue' => -100,
+ 'total_customers' => 1,
+ 'products' => 0,
+ )
+ );
+
+ $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
+ }
+
+ /**
+ * @testdox Full refunds via status change are reported with the new full refund data format.
+ */
+ public function test_populate_and_query_refunds_with_new_full_refund_data(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $order = $this->create_orders_with_full_refund_via_status_change();
+
+ $data_store = new OrdersStatsDataStore();
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', strtotime( '+1 day', $order->get_date_created()->getOffsetTimestamp() ) );
+
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $expected_totals = $this->expected_totals(
+ array(
+ 'orders_count' => 2,
+ 'num_items_sold' => 4,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => 55,
+ 'total_sales' => 50,
+ 'gross_sales' => 110,
+ 'refunds' => 100,
+ 'taxes' => 10,
+ 'shipping' => 10,
+ 'net_revenue' => 30,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ );
+
+ $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
+
+ // Query full refunds.
+ $args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'refunds' => 'full',
+ );
+ $expected_totals = $this->expected_totals(
+ array(
+ 'num_items_sold' => -4,
+ 'total_sales' => -100,
+ 'refunds' => 100,
+ 'taxes' => -10,
+ 'shipping' => -10,
+ 'net_revenue' => -80,
+ 'total_customers' => 1,
+ 'products' => 1,
+ )
+ );
+
+ $this->assertEquals( $expected_totals, json_decode( wp_json_encode( $data_store->get_data( $args ) ), true )['totals'] );
+ }
+
+ /**
+ * Create two completed orders and fully refund the second by changing its status.
+ *
+ * @return \WC_Order The refunded order.
+ */
+ private function create_orders_with_full_refund_via_status_change() {
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $order_types = array(
+ array(
+ 'status' => OrderStatus::COMPLETED,
+ 'total' => 50,
+ ),
+ array(
+ 'status' => OrderStatus::COMPLETED,
+ 'total' => 100,
+ ),
+ );
+
+ $time = time();
+
+ foreach ( $order_types as $order_type ) {
+ $order = WC_Helper_Order::create_order( 1, $product );
+ $order->set_status( $order_type['status'] );
+ $order->set_total( $order_type['total'] );
+ $order->set_date_created( $time );
+ $order->set_date_paid( $time );
+ $order->set_shipping_total( 10 );
+ $order->set_cart_tax( 10 );
+ $order->save();
+ }
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ // Refund the order completely by changing the order status to refunded.
+ $order->set_status( OrderStatus::REFUNDED );
+ $order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreReturningCustomersTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreReturningCustomersTest.php
new file mode 100644
index 00000000000..634b6e85d87
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreReturningCustomersTest.php
@@ -0,0 +1,145 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use WC_Helper_Customer;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Product_Simple;
+
+/**
+ * Tests for new vs returning customer counting in the Orders Stats DataStore.
+ */
+class DataStoreReturningCustomersTest extends OrdersStatsTestCase {
+
+ /**
+ * @testdox A guest customer with multiple orders is counted once, as new or returning depending on the time frame.
+ */
+ public function test_guest_returning_customer(): void {
+ $this->assert_returning_customer_counting( 0 );
+ }
+
+ /**
+ * @testdox A registered customer with multiple orders is counted once, as new or returning depending on the time frame.
+ */
+ public function test_registered_returning_customer(): void {
+ $customer = WC_Helper_Customer::create_customer( 'cust_1_new', 'pwd_1', 'new_customer@mail.com' );
+
+ $this->assert_returning_customer_counting( $customer->get_id() );
+ }
+
+ /**
+ * Assert total_customers over a sequence of orders placed by one customer.
+ *
+ * @param int $customer_id The ordering customer, 0 for a guest.
+ */
+ private function assert_returning_customer_counting( int $customer_id ): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $data_store = new OrdersStatsDataStore();
+
+ // All empty in the beginning.
+ $query_args = array(
+ 'interval' => 'hour',
+ );
+ $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
+ $this->assertEquals( 0, $actual_data->totals->total_customers );
+
+ // The first order lands an hour before the later ones, so its customer becomes a returning customer.
+ $later_orders_time = time();
+ $first_order_time = $later_orders_time - HOUR_IN_SECONDS;
+
+ $first_order = $this->create_order_at( $customer_id, $product, $first_order_time );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $start_time = gmdate( 'Y-m-d H:00:00', $first_order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $first_order->get_date_created()->getOffsetTimestamp() );
+ $query_args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
+ $this->assertEquals( 1, $actual_data->totals->total_customers );
+
+ // Place a second order an hour later: 2 orders, but still just one customer.
+ $second_order = $this->create_order_at( $customer_id, $product, $later_orders_time );
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ // Time frame includes both orders -> customer is a new customer.
+ $start_time = gmdate( 'Y-m-d H:00:00', $first_order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $second_order->get_date_created()->getOffsetTimestamp() );
+ $query_args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+
+ $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
+ $this->assertEquals( 1, $actual_data->totals->total_customers );
+
+ // Time frame includes only the second order -> customer is a returning customer.
+ $start_time = gmdate( 'Y-m-d H:i:s', $first_order_time + 1 );
+ $end_time = gmdate( 'Y-m-d H:59:59', $second_order->get_date_created()->getOffsetTimestamp() );
+ $query_args = array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+
+ $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
+ $this->assertEquals( 1, $actual_data->totals->total_customers );
+
+ $third_order = $this->create_order_at( $customer_id, $product, $later_orders_time );
+ $third_order->set_date_modified( $later_orders_time + 1 );
+ $third_order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ // Time frame includes the second and third order -> there is one returning customer.
+ $start_time = gmdate( 'Y-m-d H:i:s', $first_order_time + 1 );
+ $end_time = gmdate( 'Y-m-d H:59:59', $third_order->get_date_created()->getOffsetTimestamp() );
+ $query_args = array(
+ 'interval' => 'day',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ );
+ $actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
+
+ $this->assertEquals(
+ 1,
+ $actual_data->totals->total_customers,
+ 'The same customer placing multiple orders in the time frame should be counted once'
+ );
+ }
+
+ /**
+ * Create a processing order for the customer at the given time.
+ *
+ * @param int $customer_id The ordering customer, 0 for a guest.
+ * @param WC_Product_Simple $product Product to order.
+ * @param int $order_time Order creation/payment timestamp.
+ * @return \WC_Order
+ */
+ private function create_order_at( int $customer_id, WC_Product_Simple $product, int $order_time ) {
+ $order = WC_Helper_Order::create_order( $customer_id, $product );
+ $order->set_date_created( $order_time );
+ $order->set_date_paid( $order_time );
+ $order->set_status( OrderStatus::PROCESSING );
+ $order->set_total( 100 );
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreSegmentingTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreSegmentingTest.php
new file mode 100644
index 00000000000..068abc35a8c
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreSegmentingTest.php
@@ -0,0 +1,447 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Enums\ProductStockStatus;
+use DateTime;
+use WC_Helper_Customer;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Order_Item_Product;
+use WC_Product;
+use WC_Product_Simple;
+use WC_Product_Variable;
+use WC_Product_Variation;
+
+/**
+ * Tests for product segmentation in the Orders Stats DataStore.
+ */
+class DataStoreSegmentingTest extends OrdersStatsTestCase {
+
+ /**
+ * @testdox Segmenting by product id reports per-product subtotals in totals and intervals.
+ */
+ public function test_segmenting_by_product_and_variation(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ // Simple product.
+ $simple_product_price = 25;
+ $simple_product = new WC_Product_Simple();
+ $simple_product->set_name( 'Simple Product' );
+ $simple_product->set_regular_price( $simple_product_price );
+ $simple_product->save();
+
+ // Variable product.
+ $variable_product = new WC_Product_Variable();
+ $variable_product->set_name( 'Variable Product' );
+ $variable_product->save();
+
+ $variation_1 = new WC_Product_Variation();
+ $variation_1->set_parent_id( $variable_product->get_id() );
+ $variation_1->set_regular_price( 23 );
+ $variation_1->save();
+
+ $variation_2 = new WC_Product_Variation();
+ $variation_2->set_parent_id( $variable_product->get_id() );
+ $variation_2->set_regular_price( 27 );
+ $variation_2->save();
+
+ $variable_product->set_children( array( $variation_1->get_id(), $variation_2->get_id() ) );
+
+ $variation_1->set_stock_status( ProductStockStatus::IN_STOCK );
+ $variation_1->save();
+ $variation_2->set_stock_status( ProductStockStatus::IN_STOCK );
+ $variation_2->save();
+ WC_Product_Variable::sync( $variable_product );
+
+ // A product no order contains, so its segments stay empty.
+ $unused_product = new WC_Product_Simple();
+ $unused_product->set_name( 'Simple Product not used' );
+ $unused_product->set_regular_price( 17 );
+ $unused_product->save();
+
+ $order_status = OrderStatus::COMPLETED;
+
+ $customer_1 = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
+
+ $latest_orders_time = time();
+ $earlier_order_time = $latest_orders_time - 1 * HOUR_IN_SECONDS;
+
+ // The earlier order: 4 x the simple product, one hour before the others.
+ $earlier_order = WC_Helper_Order::create_order( $customer_1->get_id(), $simple_product );
+ $earlier_order->set_date_created( $earlier_order_time );
+ $earlier_order->set_date_paid( $earlier_order_time );
+ $earlier_order->set_status( $order_status );
+ $earlier_order->calculate_totals();
+ $earlier_order->save();
+
+ // The mixed order: 4 x the simple product & 3 x variation 1.
+ $mixed_order = WC_Helper_Order::create_order( $customer_1->get_id(), $simple_product );
+ $mixed_order->set_date_created( $latest_orders_time );
+ $mixed_order->set_date_paid( $latest_orders_time );
+ $item = new WC_Order_Item_Product();
+ $item->set_props(
+ array(
+ 'product_id' => $variable_product->get_id(),
+ 'variation_id' => $variation_1->get_id(),
+ 'quantity' => 3,
+ 'subtotal' => 3 * floatval( $variation_1->get_price() ),
+ 'total' => 3 * floatval( $variation_1->get_price() ),
+ )
+ );
+ $item->save();
+ $mixed_order->add_item( $item );
+ $mixed_order->set_status( $order_status );
+ $mixed_order->calculate_totals();
+ $mixed_order->save();
+
+ // The variations order: 4 x variation 1 & 1 x variation 2.
+ $variations_order = WC_Helper_Order::create_order( $customer_1->get_id(), $variation_1 );
+ $variations_order->set_date_created( $latest_orders_time );
+ $variations_order->set_date_paid( $latest_orders_time );
+ $item = new WC_Order_Item_Product();
+ $item->set_props(
+ array(
+ 'product_id' => $variable_product->get_id(),
+ 'variation_id' => $variation_2->get_id(),
+ 'quantity' => 1,
+ 'subtotal' => floatval( $variation_2->get_price() ),
+ 'total' => floatval( $variation_2->get_price() ),
+ )
+ );
+ $item->save();
+ $variations_order->add_item( $item );
+ $variations_order->set_status( $order_status );
+ $variations_order->calculate_totals();
+ $variations_order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+
+ // Three hourly intervals ending at the captured order time; reading the clock here
+ // could cross an hour boundary and add a fourth.
+ $now = new DateTime();
+ $now->setTimestamp( $latest_orders_time );
+
+ $two_hours_back = new DateTime();
+ $oldest_interval_start_timestamp = $latest_orders_time - 2 * HOUR_IN_SECONDS;
+ $two_hours_back->setTimestamp( $oldest_interval_start_timestamp );
+ $oldest_interval_end_timestamp = $oldest_interval_start_timestamp + ( 3600 - ( $oldest_interval_start_timestamp % 3600 ) ) - 1;
+ $oldest_interval_start = new DateTime();
+ $oldest_interval_start->setTimestamp( $oldest_interval_start_timestamp );
+ $oldest_interval_end = new DateTime();
+ $oldest_interval_end->setTimestamp( $oldest_interval_end_timestamp );
+
+ $middle_interval_start_timestamp = $oldest_interval_end_timestamp + 1;
+ $middle_interval_end_timestamp = $oldest_interval_end_timestamp + 3600;
+ $middle_interval_start = new DateTime();
+ $middle_interval_start->setTimestamp( $middle_interval_start_timestamp );
+ $middle_interval_end = new DateTime();
+ $middle_interval_end->setTimestamp( $middle_interval_end_timestamp );
+
+ $latest_interval_start_timestamp = $middle_interval_end_timestamp + 1;
+ $latest_interval_end_timestamp = (int) $now->format( 'U' );
+ $latest_interval_start = new DateTime();
+ $latest_interval_start->setTimestamp( $latest_interval_start_timestamp );
+ $latest_interval_end = new DateTime();
+ $latest_interval_end->setTimestamp( $latest_interval_end_timestamp );
+
+ $query_args = array(
+ 'after' => $two_hours_back->format( TimeInterval::$sql_datetime_format ),
+ 'before' => $now->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ 'segmentby' => 'product',
+ 'product_includes' => array( $simple_product->get_id(), $variable_product->get_id(), $unused_product->get_id() ),
+ );
+
+ $mixed_order_revenue = self::QTY_PER_PRODUCT * $simple_product_price + 3 * intval( $variation_1->get_price() );
+ $variations_order_revenue = self::QTY_PER_PRODUCT * intval( $variation_1->get_price() ) + 1 * intval( $variation_2->get_price() );
+ $earlier_order_revenue = self::QTY_PER_PRODUCT * $simple_product_price;
+ $mixed_order_items = self::QTY_PER_PRODUCT + 3;
+ $variations_order_items = self::QTY_PER_PRODUCT + 1;
+ $earlier_order_items = self::QTY_PER_PRODUCT;
+
+ // Totals.
+ $orders_count = 3;
+ $num_items_sold = $mixed_order_items + $variations_order_items + $earlier_order_items;
+ $shipping = $orders_count * self::SHIPPING_AMOUNT;
+ $net_revenue = $mixed_order_revenue + $variations_order_revenue + $earlier_order_revenue;
+ $total_customers = 1;
+
+ // Totals segments. The segmenter allocates shipping per item and rounds to 6 decimals.
+ $simple_product_orders = 2;
+ $simple_product_shipping = round( self::SHIPPING_AMOUNT / $mixed_order_items * self::QTY_PER_PRODUCT, 6 ) + round( self::SHIPPING_AMOUNT / $earlier_order_items * self::QTY_PER_PRODUCT, 6 );
+ $simple_product_revenue = 2 * self::QTY_PER_PRODUCT * $simple_product_price;
+
+ $variable_product_orders = 2;
+ $variable_product_shipping = round( self::SHIPPING_AMOUNT / $mixed_order_items * 3, 6 ) + self::SHIPPING_AMOUNT;
+ $variable_product_revenue = 7 * intval( $variation_1->get_price() ) + 1 * intval( $variation_2->get_price() );
+
+ // Interval 3 subtotals and segments.
+ $latest_interval_orders_count = 2;
+ $latest_interval_items = $mixed_order_items + $variations_order_items;
+ $latest_interval_shipping = $latest_interval_orders_count * self::SHIPPING_AMOUNT;
+ $latest_interval_revenue = self::QTY_PER_PRODUCT * $simple_product_price + 7 * intval( $variation_1->get_price() ) + 1 * intval( $variation_2->get_price() );
+
+ $latest_interval_simple_shipping = round( self::SHIPPING_AMOUNT / $mixed_order_items * self::QTY_PER_PRODUCT, 6 );
+ $latest_interval_simple_revenue = self::QTY_PER_PRODUCT * $simple_product_price;
+
+ $latest_interval_variable_orders = 2;
+ $latest_interval_variable_shipping = round( self::SHIPPING_AMOUNT / $mixed_order_items * 3, 6 ) + self::SHIPPING_AMOUNT;
+ $latest_interval_variable_revenue = 7 * intval( $variation_1->get_price() ) + 1 * intval( $variation_2->get_price() );
+
+ // Interval 2 subtotals and segments.
+ $middle_interval_orders_count = 1;
+ $middle_interval_revenue = self::QTY_PER_PRODUCT * $simple_product_price;
+
+ $expected_stats = array(
+ 'totals' => $this->expected_totals(
+ array(
+ 'orders_count' => $orders_count,
+ 'num_items_sold' => $num_items_sold,
+ 'total_sales' => $net_revenue + $shipping,
+ 'gross_sales' => $net_revenue,
+ 'shipping' => $shipping,
+ 'net_revenue' => $net_revenue,
+ 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
+ 'avg_order_value' => $net_revenue / $orders_count,
+ 'total_customers' => $total_customers,
+ 'products' => 2,
+ 'segments' => array(
+ $this->build_segment(
+ $simple_product,
+ array(
+ 'orders_count' => $simple_product_orders,
+ 'num_items_sold' => 8,
+ 'total_sales' => $simple_product_revenue + $simple_product_shipping,
+ 'shipping' => $simple_product_shipping,
+ 'net_revenue' => $simple_product_revenue,
+ 'avg_items_per_order' => ( $mixed_order_items + $earlier_order_items ) / $simple_product_orders,
+ 'avg_order_value' => ( $mixed_order_revenue + $earlier_order_revenue ) / $simple_product_orders,
+ 'total_customers' => $total_customers,
+ )
+ ),
+ $this->build_segment(
+ $variable_product,
+ array(
+ 'orders_count' => $variable_product_orders,
+ 'num_items_sold' => 8,
+ 'total_sales' => $variable_product_revenue + $variable_product_shipping,
+ 'shipping' => $variable_product_shipping,
+ 'net_revenue' => $variable_product_revenue,
+ 'avg_items_per_order' => ( $mixed_order_items + $variations_order_items ) / $variable_product_orders,
+ 'avg_order_value' => ( $mixed_order_revenue + $variations_order_revenue ) / $variable_product_orders,
+ 'total_customers' => $total_customers,
+ )
+ ),
+ $this->build_segment( $unused_product, array() ),
+ ),
+ )
+ ),
+ 'intervals' => array(
+ array(
+ 'interval' => $latest_interval_start->format( 'Y-m-d H' ),
+ 'date_start' => $latest_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_start_gmt' => $latest_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_end' => $latest_interval_end->format( 'Y-m-d H:i:s' ),
+ 'date_end_gmt' => $latest_interval_end->format( 'Y-m-d H:i:s' ),
+ 'subtotals' => $this->expected_totals(
+ array(
+ 'orders_count' => $latest_interval_orders_count,
+ 'num_items_sold' => $latest_interval_items,
+ 'total_sales' => $latest_interval_revenue + $latest_interval_shipping,
+ 'gross_sales' => $latest_interval_revenue,
+ 'shipping' => $latest_interval_shipping,
+ 'net_revenue' => $latest_interval_revenue,
+ 'avg_items_per_order' => $latest_interval_items / $latest_interval_orders_count,
+ 'avg_order_value' => $latest_interval_revenue / $latest_interval_orders_count,
+ 'total_customers' => $total_customers,
+ 'segments' => array(
+ $this->build_segment(
+ $simple_product,
+ array(
+ 'orders_count' => 1,
+ 'num_items_sold' => 4,
+ 'total_sales' => $latest_interval_simple_revenue + $latest_interval_simple_shipping,
+ 'shipping' => $latest_interval_simple_shipping,
+ 'net_revenue' => $latest_interval_simple_revenue,
+ 'avg_items_per_order' => $mixed_order_items,
+ 'avg_order_value' => $mixed_order_revenue,
+ 'total_customers' => $total_customers,
+ )
+ ),
+ $this->build_segment(
+ $variable_product,
+ array(
+ 'orders_count' => $latest_interval_variable_orders,
+ 'num_items_sold' => 8,
+ 'total_sales' => $latest_interval_variable_revenue + $latest_interval_variable_shipping,
+ 'shipping' => $latest_interval_variable_shipping,
+ 'net_revenue' => $latest_interval_variable_revenue,
+ 'avg_items_per_order' => ( $mixed_order_items + $variations_order_items ) / $latest_interval_variable_orders,
+ 'avg_order_value' => ( $mixed_order_revenue + $variations_order_revenue ) / $latest_interval_variable_orders,
+ 'total_customers' => $total_customers,
+ )
+ ),
+ $this->build_segment( $unused_product, array() ),
+ ),
+ )
+ ),
+ ),
+ array(
+ 'interval' => $middle_interval_start->format( 'Y-m-d H' ),
+ 'date_start' => $middle_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_start_gmt' => $middle_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_end' => $middle_interval_end->format( 'Y-m-d H:i:s' ),
+ 'date_end_gmt' => $middle_interval_end->format( 'Y-m-d H:i:s' ),
+ 'subtotals' => $this->expected_totals(
+ array(
+ 'orders_count' => $middle_interval_orders_count,
+ 'num_items_sold' => 4,
+ 'total_sales' => $middle_interval_revenue + self::SHIPPING_AMOUNT,
+ 'gross_sales' => $middle_interval_revenue,
+ 'shipping' => self::SHIPPING_AMOUNT,
+ 'net_revenue' => $middle_interval_revenue,
+ 'avg_items_per_order' => 4,
+ 'avg_order_value' => $middle_interval_revenue,
+ 'total_customers' => $total_customers,
+ 'segments' => array(
+ $this->build_segment(
+ $simple_product,
+ array(
+ 'orders_count' => 1,
+ 'num_items_sold' => 4,
+ 'total_sales' => self::QTY_PER_PRODUCT * $simple_product_price + self::SHIPPING_AMOUNT,
+ 'shipping' => self::SHIPPING_AMOUNT,
+ 'net_revenue' => self::QTY_PER_PRODUCT * $simple_product_price,
+ 'avg_items_per_order' => $earlier_order_items,
+ 'avg_order_value' => $earlier_order_revenue,
+ 'total_customers' => $total_customers,
+ )
+ ),
+ $this->build_segment( $variable_product, array() ),
+ $this->build_segment( $unused_product, array() ),
+ ),
+ )
+ ),
+ ),
+ array(
+ 'interval' => $oldest_interval_start->format( 'Y-m-d H' ),
+ 'date_start' => $oldest_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_start_gmt' => $oldest_interval_start->format( 'Y-m-d H:i:s' ),
+ 'date_end' => $oldest_interval_end->format( 'Y-m-d H:i:s' ),
+ 'date_end_gmt' => $oldest_interval_end->format( 'Y-m-d H:i:s' ),
+ 'subtotals' => $this->expected_totals(
+ array(
+ 'segments' => array(
+ $this->build_segment( $simple_product, array() ),
+ $this->build_segment( $variable_product, array() ),
+ $this->build_segment( $unused_product, array() ),
+ ),
+ )
+ ),
+ ),
+ ),
+ 'total' => 3,
+ 'pages' => 1,
+ 'page_no' => 1,
+ );
+
+ $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
+
+ $this->assertEquals( $expected_stats, $actual, 'Unexpected result for segmenting by product' );
+ }
+
+ /**
+ * @testdox Product segmentation without product_includes enumerates the whole catalog.
+ */
+ public function test_segmenting_by_product_without_includes(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $sold_product = new WC_Product_Simple();
+ $sold_product->set_name( 'Segmented Sold Product' );
+ $sold_product->set_regular_price( 10 );
+ $sold_product->save();
+
+ $unsold_product = new WC_Product_Simple();
+ $unsold_product->set_name( 'Segmented Unsold Product' );
+ $unsold_product->set_regular_price( 15 );
+ $unsold_product->save();
+
+ $order = WC_Helper_Order::create_order( 1, $sold_product );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
+
+ // Without product_includes the segmenter must return a segment for
+ // every store product, not only the ones with orders.
+ $data = json_decode(
+ wp_json_encode(
+ $data_store->get_data(
+ array(
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'segmentby' => 'product',
+ )
+ )
+ ),
+ true
+ );
+
+ $segments = array_column( $data['totals']['segments'], 'subtotals', 'segment_id' );
+ $expected_product_ids = wc_get_products(
+ array(
+ 'return' => 'ids',
+ 'limit' => -1,
+ )
+ );
+
+ $this->assertEqualsCanonicalizing( $expected_product_ids, array_keys( $segments ) );
+ $this->assertEquals( 1, $segments[ $sold_product->get_id() ]['orders_count'] );
+ $this->assertEquals( 4, $segments[ $sold_product->get_id() ]['num_items_sold'] );
+ $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['orders_count'] );
+ $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['num_items_sold'] );
+ }
+
+ /**
+ * Build one expected segment entry, with zero values unless overridden.
+ *
+ * @param WC_Product $product The product the segment belongs to.
+ * @param array $subtotals Non-zero subtotal values.
+ * @return array
+ */
+ private function build_segment( WC_Product $product, array $subtotals ): array {
+ return array(
+ 'segment_id' => $product->get_id(),
+ 'segment_label' => $product->get_name(),
+ 'subtotals' => array_merge(
+ array(
+ 'orders_count' => 0,
+ 'num_items_sold' => 0,
+ 'total_sales' => 0,
+ 'coupons' => 0,
+ 'coupons_count' => 0,
+ 'refunds' => 0,
+ 'taxes' => 0,
+ 'shipping' => 0,
+ 'net_revenue' => 0,
+ 'avg_items_per_order' => 0,
+ 'avg_order_value' => 0,
+ 'total_customers' => 0,
+ ),
+ $subtotals
+ ),
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreZeroFillTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreZeroFillTest.php
new file mode 100644
index 00000000000..83ba28256cc
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/DataStoreZeroFillTest.php
@@ -0,0 +1,381 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
+use Automattic\WooCommerce\Enums\OrderStatus;
+use DateTime;
+use WC_Helper_Customer;
+use WC_Helper_Order;
+use WC_Helper_Queue;
+use WC_Helper_Reports;
+use WC_Product_Simple;
+
+/**
+ * Tests for zero-filling of empty intervals in the Orders Stats DataStore.
+ *
+ * A shared fixture is created once for the class: two orders in the current hour, three
+ * in the previous hour (so ordering by orders_count differs from ordering by date), and
+ * one order each in the previous day, week, month and year. Queries then cover windows of
+ * the last 1, 6, 10 and 11 hours, so all intervals beyond the two busy hours must be
+ * zero-filled, in the position the ordering demands.
+ */
+class DataStoreZeroFillTest extends OrdersStatsTestCase {
+
+ const PRODUCT_PRICE = 11;
+ const ORDERS_THIS_HOUR = 2;
+ const ORDERS_PREVIOUS_HOUR = 3;
+
+ /**
+ * Query start inside the hour the newest fixture order was created in.
+ *
+ * Keeps the seconds-of-minute part of the order time; the data store clamps the
+ * first interval's date_start to it.
+ *
+ * @var DateTime
+ */
+ private static $current_hour_start;
+
+ /**
+ * End of the hour the newest fixture order was created in.
+ *
+ * @var DateTime
+ */
+ private static $current_hour_end;
+
+ /**
+ * IDs of the fixture orders.
+ *
+ * @var array
+ */
+ private static $fixture_order_ids = array();
+
+ /**
+ * ID of the fixture product.
+ *
+ * @var int
+ */
+ private static $fixture_product_id;
+
+ /**
+ * Create the fixture orders once for the whole class.
+ *
+ * Besides the orders in the current and previous hour, one order each is created in
+ * the previous day, week, month and year, all outside every window these tests query.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( self::PRODUCT_PRICE );
+ $product->save();
+ self::$fixture_product_id = $product->get_id();
+
+ $customer = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
+
+ $newest_order_time = time();
+
+ $order_times = array_merge(
+ array(
+ $newest_order_time - YEAR_IN_SECONDS,
+ $newest_order_time - MONTH_IN_SECONDS,
+ $newest_order_time - WEEK_IN_SECONDS,
+ $newest_order_time - DAY_IN_SECONDS,
+ ),
+ array_fill( 0, self::ORDERS_PREVIOUS_HOUR, $newest_order_time - HOUR_IN_SECONDS ),
+ array_fill( 0, self::ORDERS_THIS_HOUR, $newest_order_time )
+ );
+
+ foreach ( $order_times as $order_time ) {
+ $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
+ $order->set_date_created( $order_time );
+ $order->set_date_paid( $order_time );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->calculate_totals();
+ $order->save();
+
+ self::$fixture_order_ids[] = $order->get_id();
+ }
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ self::$current_hour_start = new DateTime();
+ self::$current_hour_start->setTimestamp( $newest_order_time - ( ( $newest_order_time % HOUR_IN_SECONDS ) - ( $newest_order_time % MINUTE_IN_SECONDS ) ) );
+
+ self::$current_hour_end = new DateTime();
+ self::$current_hour_end->setTimestamp( $newest_order_time + ( HOUR_IN_SECONDS - ( $newest_order_time % HOUR_IN_SECONDS ) ) - 1 );
+ }
+
+ /**
+ * Remove the persistent class fixture data.
+ */
+ public static function wpTearDownAfterClass(): void {
+ foreach ( self::$fixture_order_ids as $order_id ) {
+ $order = wc_get_order( $order_id );
+ if ( $order ) {
+ $order->delete( true );
+ }
+ }
+ self::$fixture_order_ids = array();
+
+ // Delete the product through CRUD so its wc_product_meta_lookup row goes too.
+ $product = wc_get_product( self::$fixture_product_id );
+ if ( $product ) {
+ $product->delete( true );
+ }
+
+ WC_Helper_Reports::reset_stats_dbs();
+ }
+
+ /**
+ * @testdox A single-hour window returns one interval, identically for both sort directions.
+ * @dataProvider orderby_variants
+ *
+ * @param string $orderby Report orderby argument.
+ */
+ public function test_single_hour_window( string $orderby ): void {
+ $expected_stats = array(
+ 'totals' => $this->build_totals( self::ORDERS_THIS_HOUR, true ),
+ 'intervals' => array(
+ $this->build_interval( self::$current_hour_start, self::$current_hour_end, self::ORDERS_THIS_HOUR ),
+ ),
+ 'total' => 1,
+ 'pages' => 1,
+ 'page_no' => 1,
+ );
+
+ foreach ( array( 'desc', 'asc' ) as $order ) {
+ $query_args = array(
+ 'after' => self::$current_hour_start->format( TimeInterval::$sql_datetime_format ),
+ 'before' => self::$current_hour_end->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ 'orderby' => $orderby,
+ 'order' => $order,
+ );
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+ }
+
+ /**
+ * @testdox A partial-page window zero-fills the empty hours in the position the ordering demands.
+ * @dataProvider orderby_variants
+ *
+ * @param string $orderby Report orderby argument.
+ */
+ public function test_five_hour_window( string $orderby ): void {
+ $this->assert_windowed_report( $orderby, 5 );
+ }
+
+ /**
+ * @testdox A window that exactly fills one page zero-fills the empty hours in the position the ordering demands.
+ * @dataProvider orderby_variants
+ *
+ * @param string $orderby Report orderby argument.
+ */
+ public function test_nine_hour_window( string $orderby ): void {
+ $this->assert_windowed_report( $orderby, 9 );
+ }
+
+ /**
+ * @testdox Zero-filled intervals paginate correctly in both sort directions.
+ * @dataProvider orderby_variants
+ *
+ * @param string $orderby Report orderby argument.
+ */
+ public function test_pagination( string $orderby ): void {
+ $hour_offset = 10;
+ $per_page = 10;
+ $totals = $this->build_totals( self::ORDERS_THIS_HOUR + self::ORDERS_PREVIOUS_HOUR, true );
+
+ foreach ( array( 'desc', 'asc' ) as $order ) {
+ $expected_intervals = $this->build_expected_intervals( $hour_offset, $orderby, $order );
+
+ foreach ( array( 1, 2 ) as $page ) {
+ $query_args = array(
+ 'after' => $this->window_start( $hour_offset )->format( TimeInterval::$sql_datetime_format ),
+ 'before' => self::$current_hour_end->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ 'orderby' => $orderby,
+ 'order' => $order,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ );
+
+ $expected_stats = array(
+ 'totals' => $totals,
+ 'intervals' => array_slice( $expected_intervals, ( $page - 1 ) * $per_page, $per_page ),
+ 'total' => $hour_offset + 1,
+ 'pages' => 2,
+ 'page_no' => $page,
+ );
+
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+ }
+ }
+
+ /**
+ * The orderby variants under test.
+ *
+ * @return array
+ */
+ public function orderby_variants(): array {
+ return array(
+ 'order by date' => array( 'date' ),
+ 'order by orders_count' => array( 'orders_count' ),
+ );
+ }
+
+ /**
+ * Assert the report for an N-hour window, in both sort directions.
+ *
+ * @param string $orderby Report orderby argument.
+ * @param int $hour_offset Hours before the current hour's end the window starts at.
+ */
+ private function assert_windowed_report( string $orderby, int $hour_offset ): void {
+ $totals = $this->build_totals( self::ORDERS_THIS_HOUR + self::ORDERS_PREVIOUS_HOUR, true );
+
+ foreach ( array( 'desc', 'asc' ) as $order ) {
+ $query_args = array(
+ 'after' => $this->window_start( $hour_offset )->format( TimeInterval::$sql_datetime_format ),
+ 'before' => self::$current_hour_end->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ 'orderby' => $orderby,
+ 'order' => $order,
+ );
+
+ $expected_stats = array(
+ 'totals' => $totals,
+ 'intervals' => $this->build_expected_intervals( $hour_offset, $orderby, $order ),
+ 'total' => $hour_offset + 1,
+ 'pages' => 1,
+ 'page_no' => 1,
+ );
+
+ $this->assert_report_data( $expected_stats, $query_args );
+ }
+ }
+
+ /**
+ * The start of an N-hours-before-window-end query window.
+ *
+ * @param int $hour_offset Hours before the current hour's end the window starts at.
+ * @return DateTime
+ */
+ private function window_start( int $hour_offset ): DateTime {
+ $window_start = new DateTime();
+ $window_start->setTimestamp( (int) self::$current_hour_end->format( 'U' ) - $hour_offset * HOUR_IN_SECONDS );
+
+ return $window_start;
+ }
+
+ /**
+ * Build the expected intervals for an N-hour window in the requested ordering.
+ *
+ * Zero-filled intervals always sort among themselves by time ascending, so for
+ * orderby=orders_count the busy hours move to the front (desc) or back (asc) while
+ * the zero-filled block keeps its time-ascending order.
+ *
+ * @param int $hour_offset Hours before the current hour's end the window starts at.
+ * @param string $orderby Report orderby argument.
+ * @param string $order 'asc' or 'desc'.
+ * @return array
+ */
+ private function build_expected_intervals( int $hour_offset, string $orderby, string $order ): array {
+ $now_timestamp = (int) self::$current_hour_end->format( 'U' );
+
+ // Time-descending: index 0 is the current hour, index $hour_offset the partial oldest hour.
+ $intervals_by_recency = array();
+ for ( $i = 0; $i <= $hour_offset; $i++ ) {
+ if ( 0 === $i ) {
+ $date_start = new DateTime( self::$current_hour_end->format( 'Y-m-d H:00:00' ) );
+ $date_end = self::$current_hour_end;
+ } elseif ( $hour_offset === $i ) {
+ $date_start = $this->window_start( $hour_offset );
+ $date_end = new DateTime( $date_start->format( 'Y-m-d H:59:59' ) );
+ } else {
+ $hour_anchor = new DateTime();
+ $hour_anchor->setTimestamp( $now_timestamp - $i * HOUR_IN_SECONDS );
+ $date_start = new DateTime( $hour_anchor->format( 'Y-m-d H:00:00' ) );
+ $date_end = new DateTime( $hour_anchor->format( 'Y-m-d H:59:59' ) );
+ }
+
+ if ( 0 === $i ) {
+ $orders_count = self::ORDERS_THIS_HOUR;
+ } elseif ( 1 === $i ) {
+ $orders_count = self::ORDERS_PREVIOUS_HOUR;
+ } else {
+ $orders_count = 0;
+ }
+
+ $intervals_by_recency[] = $this->build_interval( $date_start, $date_end, $orders_count );
+ }
+
+ if ( 'date' === $orderby ) {
+ return 'desc' === $order ? $intervals_by_recency : array_reverse( $intervals_by_recency );
+ }
+
+ // orderby=orders_count: previous hour (most orders) and current hour lead or trail
+ // the time-ascending zero block, depending on direction.
+ $busy_hours_by_count = array( $intervals_by_recency[1], $intervals_by_recency[0] );
+ $zero_hours_by_time = array_reverse( array_slice( $intervals_by_recency, 2 ) );
+
+ return 'desc' === $order
+ ? array_merge( $busy_hours_by_count, $zero_hours_by_time )
+ : array_merge( $zero_hours_by_time, array_reverse( $busy_hours_by_count ) );
+ }
+
+ /**
+ * Build one expected interval entry.
+ *
+ * @param DateTime $date_start Interval start.
+ * @param DateTime $date_end Interval end.
+ * @param int $orders_count Orders inside the interval.
+ * @return array
+ */
+ private function build_interval( DateTime $date_start, DateTime $date_end, int $orders_count ): array {
+ return array(
+ 'interval' => $date_start->format( 'Y-m-d H' ),
+ 'date_start' => $date_start->format( 'Y-m-d H:i:s' ),
+ 'date_start_gmt' => $date_start->format( 'Y-m-d H:i:s' ),
+ 'date_end' => $date_end->format( 'Y-m-d H:i:s' ),
+ 'date_end_gmt' => $date_end->format( 'Y-m-d H:i:s' ),
+ 'subtotals' => $this->build_totals( $orders_count, false ),
+ );
+ }
+
+ /**
+ * Build a totals (or subtotals) block for a number of single-product orders.
+ *
+ * @param int $orders_count Orders included.
+ * @param bool $include_products Whether to include the totals-only 'products' key.
+ * @return array
+ */
+ private function build_totals( int $orders_count, bool $include_products ): array {
+ $num_items_sold = $orders_count * self::QTY_PER_PRODUCT;
+ $shipping = $orders_count * self::SHIPPING_AMOUNT;
+ $net_revenue = self::PRODUCT_PRICE * self::QTY_PER_PRODUCT * $orders_count;
+
+ $totals = $this->expected_totals(
+ array(
+ 'orders_count' => $orders_count,
+ 'num_items_sold' => $num_items_sold,
+ 'total_sales' => $net_revenue + $shipping,
+ 'gross_sales' => $net_revenue,
+ 'shipping' => $shipping,
+ 'net_revenue' => $net_revenue,
+ 'avg_items_per_order' => $orders_count ? $num_items_sold / $orders_count : 0,
+ 'avg_order_value' => $orders_count ? $net_revenue / $orders_count : 0,
+ 'total_customers' => $orders_count ? 1 : 0,
+ )
+ );
+
+ if ( $include_products ) {
+ $totals['products'] = 1;
+ }
+
+ return $totals;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/OrdersStatsTestCase.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/OrdersStatsTestCase.php
new file mode 100644
index 00000000000..c30666a8f97
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/Stats/OrdersStatsTestCase.php
@@ -0,0 +1,114 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders\Stats;
+
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use WC_Unit_Test_Case;
+
+/**
+ * Base class for the Orders Stats report tests.
+ *
+ * Disables report caching and provides builders for the expected get_data()
+ * result shape, so tests only spell out the values that differ from zero.
+ */
+abstract class OrdersStatsTestCase extends WC_Unit_Test_Case {
+
+ // Per-product item quantity and per-order shipping hardcoded in WC_Helper_Order::create_order.
+ const QTY_PER_PRODUCT = 4;
+ const SHIPPING_AMOUNT = 10;
+
+ /**
+ * Don't cache report data during these tests.
+ *
+ * The filter must be added after parent::setUp(): WordPress snapshots the hook
+ * globals per test and restores that snapshot afterwards, which both removes the
+ * filter again and would drop one registered any earlier.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ add_filter( 'woocommerce_analytics_report_should_use_cache', '__return_false' );
+ }
+
+ /**
+ * Build a totals (or subtotals) block, zero-valued except for the given values.
+ *
+ * Pass a 'products' value for totals-level blocks; interval subtotals omit it.
+ *
+ * @param array $values Non-zero values, keyed like the get_data() totals.
+ * @return array
+ */
+ protected function expected_totals( array $values ): array {
+ return array_merge(
+ array(
+ 'orders_count' => 0,
+ 'num_items_sold' => 0,
+ 'total_sales' => 0,
+ 'gross_sales' => 0,
+ 'coupons' => 0,
+ 'coupons_count' => 0,
+ 'refunds' => 0,
+ 'taxes' => 0,
+ 'shipping' => 0,
+ 'net_revenue' => 0,
+ 'avg_items_per_order' => 0,
+ 'avg_order_value' => 0,
+ 'total_customers' => 0,
+ 'segments' => array(),
+ ),
+ $values
+ );
+ }
+
+ /**
+ * Wrap a totals block into the full get_data() result shape for a single interval.
+ *
+ * The interval's subtotals are the totals without the totals-only 'products' key.
+ *
+ * @param array $totals The expected totals, including 'products' where applicable.
+ * @param string $date_start Interval start, in 'Y-m-d H:i:s' format.
+ * @param string $date_end Interval end, in 'Y-m-d H:i:s' format.
+ * @return array
+ */
+ protected function expected_stats_single_interval( array $totals, string $date_start, string $date_end ): array {
+ $subtotals = $totals;
+ unset( $subtotals['products'] );
+
+ return array(
+ 'totals' => $totals,
+ 'intervals' => array(
+ array(
+ 'interval' => substr( $date_start, 0, strlen( 'YYYY-MM-DD HH' ) ),
+ 'date_start' => $date_start,
+ 'date_start_gmt' => $date_start,
+ 'date_end' => $date_end,
+ 'date_end_gmt' => $date_end,
+ 'subtotals' => $subtotals,
+ ),
+ ),
+ 'total' => 1,
+ 'pages' => 1,
+ 'page_no' => 1,
+ );
+ }
+
+ /**
+ * Assert that get_data() returns the expected result for the query args.
+ *
+ * @param array $expected_stats Expected get_data() result.
+ * @param array $query_args Query arguments passed to get_data().
+ */
+ protected function assert_report_data( array $expected_stats, array $query_args ): void {
+ global $wpdb;
+
+ $data_store = new OrdersStatsDataStore();
+ $actual = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true );
+
+ $this->assertEquals(
+ $expected_stats,
+ $actual,
+ 'Query args: ' . print_r( $query_args, true ) . "; query: {$wpdb->last_query}" // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ );
+ }
+}