Merge branch 'master' into fix/1012-2

# Conflicts:
#	tests/reports/class-wc-tests-reports-orders-stats.php
This commit is contained in:
Peter Fabian 2019-02-05 18:34:15 +01:00
commit b69d2aa2ab
78 changed files with 4144 additions and 495 deletions

View File

@ -0,0 +1,398 @@
### WooCommerce - eCommerce for WordPress
Copyright 2015 by the contributors
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
This program incorporates work covered by the following copyright and
permission notices:
Jigoshop is Copyright (c) 2011 Jigowatt Ltd.
http://jigowatt.com - http://jigoshop.com
Jigoshop is released under the GPL
and
WooCommerce - eCommerce for WordPress
Copyright 2015 by the contributors
WooCommerce is released under the GPL
---
### GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
### Preamble
The licenses for most software are designed to take away your freedom
to share and change it. By contrast, the GNU General Public License is
intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on,
we want its recipients to know that what they have is not the
original, so that any problems introduced by others will not reflect
on the original authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at
all.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
**0.** This License applies to any program or other work which
contains a notice placed by the copyright holder saying it may be
distributed under the terms of this General Public License. The
"Program", below, refers to any such program or work, and a "work
based on the Program" means either the Program or any derivative work
under copyright law: that is to say, a work containing the Program or
a portion of it, either verbatim or with modifications and/or
translated into another language. (Hereinafter, translation is
included without limitation in the term "modification".) Each licensee
is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the Program
(independent of having been made by running the Program). Whether that
is true depends on what the Program does.
**1.** You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a
fee.
**2.** You may modify your copy or copies of the Program or any
portion of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
**a)** You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
**b)** You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any part
thereof, to be licensed as a whole at no charge to all third parties
under the terms of this License.
**c)** If the modified program normally reads commands interactively
when run, you must cause it, when started running for such interactive
use in the most ordinary way, to print or display an announcement
including an appropriate copyright notice and a notice that there is
no warranty (or else, saying that you provide a warranty) and that
users may redistribute the program under these conditions, and telling
the user how to view a copy of this License. (Exception: if the
Program itself is interactive but does not normally print such an
announcement, your work based on the Program is not required to print
an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
**3.** You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
**a)** Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections 1
and 2 above on a medium customarily used for software interchange; or,
**b)** Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your cost of
physically performing source distribution, a complete machine-readable
copy of the corresponding source code, to be distributed under the
terms of Sections 1 and 2 above on a medium customarily used for
software interchange; or,
**c)** Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is allowed
only for noncommercial distribution and only if you received the
program in object code or executable form with such an offer, in
accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
**4.** You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt otherwise
to copy, modify, sublicense or distribute the Program is void, and
will automatically terminate your rights under this License. However,
parties who have received copies, or rights, from you under this
License will not have their licenses terminated so long as such
parties remain in full compliance.
**5.** You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
**6.** Each time you redistribute the Program (or any work based on
the Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
**7.** If, as a consequence of a court judgment or allegation of
patent infringement or for any other reason (not limited to patent
issues), conditions are imposed on you (whether by court order,
agreement or otherwise) that contradict the conditions of this
License, they do not excuse you from the conditions of this License.
If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations,
then as a consequence you may not distribute the Program at all. For
example, if a patent license would not permit royalty-free
redistribution of the Program by all those who receive copies directly
or indirectly through you, then the only way you could satisfy both it
and this License would be to refrain entirely from distribution of the
Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
**8.** If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
**9.** The Free Software Foundation may publish revised and/or new
versions of the General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Program does not specify a
version number of this License, you may choose any version ever
published by the Free Software Foundation.
**10.** If you wish to incorporate parts of the Program into other
free programs whose distribution conditions are different, write to
the author to ask for permission. For software which is copyrighted by
the Free Software Foundation, write to the Free Software Foundation;
we sometimes make exceptions for this. Our decision will be guided by
the two goals of preserving the free status of all derivatives of our
free software and of promoting the sharing and reuse of software
generally.
**NO WARRANTY**
**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
### END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
one line to give the program's name and an idea of what it does.
Copyright (C) yyyy name of author
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper
mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details
type `show w'. This is free software, and you are welcome
to redistribute it under certain conditions; type `show c'
for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, the
commands you use may be called something other than \`show w' and
\`show c'; they could even be mouse-clicks or menu items--whatever
suits your program.
You should also get your employer (if you work as a programmer) or
your school, if any, to sign a "copyright disclaimer" for the program,
if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright
interest in the program `Gnomovision'
(which makes passes at compilers) written
by James Hacker.
signature of Ty Coon, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library,
you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
[GNU Lesser General Public
License](http://www.gnu.org/licenses/lgpl.html) instead of this
License.

View File

@ -103,7 +103,7 @@ describe( 'Leaderboard', () => {
); );
const leaderboard = leaderboardWrapper.root.findByType( Leaderboard ); const leaderboard = leaderboardWrapper.root.findByType( Leaderboard );
const endpoint = NAMESPACE + 'reports/products'; const endpoint = NAMESPACE + '/reports/products';
const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 }; const query = { orderby: 'items_sold', per_page: 5, extended_info: 1 };
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint ); expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );

View File

@ -20,6 +20,7 @@ import { onQueryChange } from '@woocommerce/navigation';
*/ */
import ReportError from 'analytics/components/report-error'; import ReportError from 'analytics/components/report-error';
import { getReportChartData, getReportTableData } from 'wc-api/reports/utils'; import { getReportChartData, getReportTableData } from 'wc-api/reports/utils';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import { extendTableData } from './utils'; import { extendTableData } from './utils';
@ -84,12 +85,12 @@ class ReportTable extends Component {
} }
const isRequesting = tableData.isRequesting || primaryData.isRequesting; const isRequesting = tableData.isRequesting || primaryData.isRequesting;
const totals = get( primaryData, [ 'data', 'totals' ], null ); const totals = get( primaryData, [ 'data', 'totals' ], {} );
const totalResults = items.totalResults || 0; const totalResults = items.totalResults;
const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, { const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, {
endpoint: endpoint, endpoint: endpoint,
headers: getHeadersContent(), headers: getHeadersContent(),
ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : null, ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : [],
rows: getRowsContent( items.data ), rows: getRowsContent( items.data ),
totals: totals, totals: totals,
summary: getSummary ? getSummary( totals, totalResults ) : null, summary: getSummary ? getSummary( totals, totalResults ) : null,
@ -107,7 +108,7 @@ class ReportTable extends Component {
onQueryChange={ onQueryChange } onQueryChange={ onQueryChange }
onColumnsChange={ this.onColumnsChange } onColumnsChange={ this.onColumnsChange }
rows={ rows } rows={ rows }
rowsPerPage={ parseInt( query.per_page ) } rowsPerPage={ parseInt( query.per_page ) || QUERY_DEFAULTS.pageSize }
summary={ summary } summary={ summary }
totalRows={ totalResults } totalRows={ totalResults }
{ ...tableProps } { ...tableProps }
@ -159,7 +160,7 @@ ReportTable.propTypes = {
* Primary data of that report. If it's not provided, it will be automatically * Primary data of that report. If it's not provided, it will be automatically
* loaded via the provided `endpoint`. * loaded via the provided `endpoint`.
*/ */
primaryData: PropTypes.object.isRequired, primaryData: PropTypes.object,
/** /**
* Table data of that report. If it's not provided, it will be automatically * Table data of that report. If it's not provided, it will be automatically
* loaded via the provided `endpoint`. * loaded via the provided `endpoint`.
@ -176,13 +177,23 @@ ReportTable.propTypes = {
}; };
ReportTable.defaultProps = { ReportTable.defaultProps = {
tableData: {}, primaryData: {},
tableData: {
items: {
data: [],
totalResults: 0,
},
query: {},
},
tableQuery: {}, tableQuery: {},
}; };
export default compose( export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { endpoint, getSummary, query, tableData, tableQuery, columnPrefsKey } = props; const { endpoint, getSummary, query, tableData, tableQuery, columnPrefsKey } = props;
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
return {};
}
const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint; const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint;
const primaryData = getSummary const primaryData = getSummary
? getReportChartData( chartEndpoint, 'primary', query, select ) ? getReportChartData( chartEndpoint, 'primary', query, select )

View File

@ -111,27 +111,24 @@ class CategoriesReportTable extends Component {
} ); } );
} }
getSummary( totals, totalResults ) { getSummary( totals, totalResults = 0 ) {
if ( ! totals ) { const { items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [];
}
return [ return [
{ {
label: _n( 'category', 'categories', totalResults, 'wc-admin' ), label: _n( 'category', 'categories', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ), value: numberFormat( totalResults ),
}, },
{ {
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ), label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ), value: numberFormat( items_sold ),
}, },
{ {
label: __( 'net revenue', 'wc-admin' ), label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ), value: formatCurrency( net_revenue ),
}, },
{ {
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
]; ];
} }
@ -168,7 +165,12 @@ class CategoriesReportTable extends Component {
} }
export default compose( export default compose(
withSelect( select => { withSelect( ( select, props ) => {
const { query } = props;
if ( query.search && ! ( query.categories && query.categories.length ) ) {
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' ); const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = { const tableQuery = {
per_page: -1, per_page: -1,

View File

@ -129,21 +129,19 @@ export default class CouponsReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const { coupons_count = 0, orders_count = 0, amount = 0 } = totals;
return [];
}
return [ return [
{ {
label: _n( 'coupon', 'coupons', totals.coupons_count, 'wc-admin' ), label: _n( 'coupon', 'coupons', coupons_count, 'wc-admin' ),
value: numberFormat( totals.coupons_count ), value: numberFormat( coupons_count ),
}, },
{ {
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
{ {
label: __( 'amount discounted', 'wc-admin' ), label: __( 'amount discounted', 'wc-admin' ),
value: formatCurrency( totals.amount ), value: formatCurrency( amount ),
}, },
]; ];
} }

View File

@ -57,7 +57,7 @@ export const advancedFilters = {
input: { input: {
component: 'Search', component: 'Search',
type: 'customers', type: 'customers',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( { getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id, id: customer.id,
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ), label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
} ) ), } ) ),
@ -157,7 +157,7 @@ export const advancedFilters = {
input: { input: {
component: 'Search', component: 'Search',
type: 'emails', type: 'emails',
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( { getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id, id: customer.id,
label: customer.email, label: customer.email,
} ) ), } ) ),

View File

@ -2,7 +2,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, _n } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { Tooltip } from '@wordpress/components'; import { Tooltip } from '@wordpress/components';
@ -191,25 +191,28 @@ export default class CustomersReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const {
return []; customers_count = 0,
} avg_orders_count = 0,
avg_total_spend = 0,
avg_avg_order_value = 0,
} = totals;
return [ return [
{ {
label: __( 'customers', 'wc-admin' ), label: _n( 'customer', 'customers', customers_count, 'wc-admin' ),
value: numberFormat( totals.customers_count ), value: numberFormat( customers_count ),
}, },
{ {
label: __( 'average orders', 'wc-admin' ), label: _n( 'average order', 'average orders', avg_orders_count, 'wc-admin' ),
value: numberFormat( totals.avg_orders_count ), value: numberFormat( avg_orders_count ),
}, },
{ {
label: __( 'average lifetime spend', 'wc-admin' ), label: __( 'average lifetime spend', 'wc-admin' ),
value: formatCurrency( totals.avg_total_spend ), value: formatCurrency( avg_total_spend ),
}, },
{ {
label: __( 'average order value', 'wc-admin' ), label: __( 'average order value', 'wc-admin' ),
value: formatCurrency( totals.avg_avg_order_value ), value: formatCurrency( avg_avg_order_value ),
}, },
]; ];
} }

View File

@ -121,9 +121,7 @@ export default class CouponsReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const { download_count = 0 } = totals;
return [];
}
const { query } = this.props; const { query } = this.props;
const dates = getCurrentDates( query ); const dates = getCurrentDates( query );
const after = moment( dates.primary.after ); const after = moment( dates.primary.after );
@ -136,8 +134,8 @@ export default class CouponsReportTable extends Component {
value: numberFormat( days ), value: numberFormat( days ),
}, },
{ {
label: _n( 'download', 'downloads', totals.download_count, 'wc-admin' ), label: _n( 'download', 'downloads', download_count, 'wc-admin' ),
value: numberFormat( totals.download_count ), value: numberFormat( download_count ),
}, },
]; ];
} }

View File

@ -148,7 +148,7 @@ export default compose(
const items = searchItemsByString( select, report, search ); const items = searchItemsByString( select, report, search );
const ids = Object.keys( items ); const ids = Object.keys( items );
if ( ! ids.length ) { if ( ! ids.length ) {
return {}; // @TODO if no results were found, we should avoid making a server request. return {};
} }
return { return {

View File

@ -182,42 +182,48 @@ export default class OrdersReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const {
return []; orders_count = 0,
} num_new_customers = 0,
num_returning_customers = 0,
products = 0,
num_items_sold = 0,
coupons = 0,
net_revenue = 0,
} = totals;
return [ return [
{ {
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
{ {
label: _n( 'new customer', 'new customers', totals.num_new_customers, 'wc-admin' ), label: _n( 'new customer', 'new customers', num_new_customers, 'wc-admin' ),
value: numberFormat( totals.num_new_customers ), value: numberFormat( num_new_customers ),
}, },
{ {
label: _n( label: _n(
'returning customer', 'returning customer',
'returning customers', 'returning customers',
totals.num_returning_customers, num_returning_customers,
'wc-admin' 'wc-admin'
), ),
value: numberFormat( totals.num_returning_customers ), value: numberFormat( num_returning_customers ),
}, },
{ {
label: _n( 'product', 'products', totals.products, 'wc-admin' ), label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( totals.products ), value: numberFormat( products ),
}, },
{ {
label: _n( 'item sold', 'items sold', totals.num_items_sold, 'wc-admin' ), label: _n( 'item sold', 'items sold', num_items_sold, 'wc-admin' ),
value: numberFormat( totals.num_items_sold ), value: numberFormat( num_items_sold ),
}, },
{ {
label: _n( 'coupon', 'coupons', totals.coupons, 'wc-admin' ), label: _n( 'coupon', 'coupons', coupons, 'wc-admin' ),
value: numberFormat( totals.coupons ), value: numberFormat( coupons ),
}, },
{ {
label: __( 'net revenue', 'wc-admin' ), label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ), value: formatCurrency( net_revenue ),
}, },
]; ];
} }

View File

@ -139,26 +139,24 @@ export default class VariationsReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [];
}
return [ return [
{ {
// @TODO: When primaryData is segmented, fix this to reflect variations, not products. // @TODO: When primaryData is segmented, fix this to reflect variations, not products.
label: _n( 'variation sold', 'variations sold', totals.products_count, 'wc-admin' ), label: _n( 'variation sold', 'variations sold', products_count, 'wc-admin' ),
value: numberFormat( totals.products_count ), value: numberFormat( products_count ),
}, },
{ {
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ), label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ), value: numberFormat( items_sold ),
}, },
{ {
label: __( 'net revenue', 'wc-admin' ), label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ), value: formatCurrency( net_revenue ),
}, },
{ {
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
]; ];
} }

View File

@ -204,25 +204,23 @@ class ProductsReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
return [];
}
return [ return [
{ {
label: _n( 'product sold', 'products sold', totals.products_count, 'wc-admin' ), label: _n( 'product sold', 'products sold', products_count, 'wc-admin' ),
value: numberFormat( totals.products_count ), value: numberFormat( products_count ),
}, },
{ {
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ), label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
value: numberFormat( totals.items_sold ), value: numberFormat( items_sold ),
}, },
{ {
label: __( 'net revenue', 'wc-admin' ), label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ), value: formatCurrency( net_revenue ),
}, },
{ {
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
]; ];
} }
@ -259,7 +257,12 @@ class ProductsReportTable extends Component {
} }
export default compose( export default compose(
withSelect( select => { withSelect( ( select, props ) => {
const { query } = props;
if ( query.search && ! ( query.products && query.products.length ) ) {
return {};
}
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' ); const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const tableQuery = { const tableQuery = {
per_page: -1, per_page: -1,

View File

@ -153,43 +153,48 @@ class RevenueReportTable extends Component {
} ); } );
} }
getSummary( totals, totalResults ) { getSummary( totals, totalResults = 0 ) {
if ( ! totals ) { const {
return []; orders_count = 0,
} gross_revenue = 0,
refunds = 0,
coupons = 0,
taxes = 0,
shipping = 0,
net_revenue = 0,
} = totals;
return [ return [
{ {
label: _n( 'day', 'days', totalResults, 'wc-admin' ), label: _n( 'day', 'days', totalResults, 'wc-admin' ),
value: numberFormat( totalResults ), value: numberFormat( totalResults ),
}, },
{ {
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ), label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
{ {
label: __( 'gross revenue', 'wc-admin' ), label: __( 'gross revenue', 'wc-admin' ),
value: formatCurrency( totals.gross_revenue ), value: formatCurrency( gross_revenue ),
}, },
{ {
label: __( 'refunds', 'wc-admin' ), label: __( 'refunds', 'wc-admin' ),
value: formatCurrency( totals.refunds ), value: formatCurrency( refunds ),
}, },
{ {
label: __( 'coupons', 'wc-admin' ), label: __( 'coupons', 'wc-admin' ),
value: formatCurrency( totals.coupons ), value: formatCurrency( coupons ),
}, },
{ {
label: __( 'taxes', 'wc-admin' ), label: __( 'taxes', 'wc-admin' ),
value: formatCurrency( totals.taxes ), value: formatCurrency( taxes ),
}, },
{ {
label: __( 'shipping', 'wc-admin' ), label: __( 'shipping', 'wc-admin' ),
value: formatCurrency( totals.shipping ), value: formatCurrency( shipping ),
}, },
{ {
label: __( 'net revenue', 'wc-admin' ), label: __( 'net revenue', 'wc-admin' ),
value: formatCurrency( totals.net_revenue ), value: formatCurrency( net_revenue ),
}, },
]; ];
} }

View File

@ -103,25 +103,23 @@ export default class StockReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
return [];
}
return [ return [
{ {
label: _n( 'product', 'products', totals.products, 'wc-admin' ), label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( totals.products ), value: numberFormat( products ),
}, },
{ {
label: __( 'out of stock', totals.out_of_stock, 'wc-admin' ), label: __( 'out of stock', out_of_stock, 'wc-admin' ),
value: numberFormat( totals.out_of_stock ), value: numberFormat( out_of_stock ),
}, },
{ {
label: __( 'low stock', totals.low_stock, 'wc-admin' ), label: __( 'low stock', low_stock, 'wc-admin' ),
value: numberFormat( totals.low_stock ), value: numberFormat( low_stock ),
}, },
{ {
label: __( 'in stock', totals.in_stock, 'wc-admin' ), label: __( 'in stock', in_stock, 'wc-admin' ),
value: numberFormat( totals.in_stock ), value: numberFormat( in_stock ),
}, },
]; ];
} }

View File

@ -49,7 +49,7 @@ export const filters = [
settings: { settings: {
type: 'taxes', type: 'taxes',
param: 'taxes', param: 'taxes',
getLabels: getRequestByIdString( NAMESPACE + 'taxes', tax => ( { getLabels: getRequestByIdString( NAMESPACE + '/taxes', tax => ( {
id: tax.id, id: tax.id,
label: getTaxCode( tax ), label: getTaxCode( tax ),
} ) ), } ) ),

View File

@ -111,29 +111,33 @@ export default class TaxesReportTable extends Component {
} }
getSummary( totals ) { getSummary( totals ) {
if ( ! totals ) { const {
return []; tax_codes = 0,
} total_tax = 0,
order_tax = 0,
shipping_tax = 0,
orders_count = 0,
} = totals;
return [ return [
{ {
label: _n( 'tax code', 'tax codes', totals.tax_codes, 'wc-admin' ), label: _n( 'tax code', 'tax codes', tax_codes, 'wc-admin' ),
value: numberFormat( totals.tax_codes ), value: numberFormat( tax_codes ),
}, },
{ {
label: __( 'total tax', 'wc-admin' ), label: __( 'total tax', 'wc-admin' ),
value: formatCurrency( totals.total_tax ), value: formatCurrency( total_tax ),
}, },
{ {
label: __( 'order tax', 'wc-admin' ), label: __( 'order tax', 'wc-admin' ),
value: formatCurrency( totals.order_tax ), value: formatCurrency( order_tax ),
}, },
{ {
label: __( 'shipping tax', 'wc-admin' ), label: __( 'shipping tax', 'wc-admin' ),
value: formatCurrency( totals.shipping_tax ), value: formatCurrency( shipping_tax ),
}, },
{ {
label: _n( 'order', 'orders', totals.orders, 'wc-admin' ), label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
value: numberFormat( totals.orders_count ), value: numberFormat( orders_count ),
}, },
]; ];
} }

View File

@ -4,12 +4,12 @@
*/ */
import { Component, createElement } from '@wordpress/element'; import { Component, createElement } from '@wordpress/element';
import { parse } from 'qs'; import { parse } from 'qs';
import { find, last } from 'lodash'; import { find, last, isEqual } from 'lodash';
/** /**
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { getPersistedQuery, stringifyQuery } from '@woocommerce/navigation'; import { getNewPath, getPersistedQuery, history, stringifyQuery } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
@ -64,11 +64,35 @@ const getPages = () => {
}; };
class Controller extends Component { class Controller extends Component {
componentDidUpdate( prevProps ) {
const prevQuery = this.getQuery( prevProps.location.search );
const prevBaseQuery = this.getBaseQuery( prevProps.location.search );
const baseQuery = this.getBaseQuery( this.props.location.search );
if ( prevQuery.page > 1 && ! isEqual( prevBaseQuery, baseQuery ) ) {
history.replace( getNewPath( { page: 1 } ) );
}
}
getQuery( searchString ) {
if ( ! searchString ) {
return {};
}
const search = searchString.substring( 1 );
return parse( search );
}
getBaseQuery( searchString ) {
const query = this.getQuery( searchString );
delete query.page;
return query;
}
render() { render() {
// Pass URL parameters (example :report -> params.report) and query string parameters // Pass URL parameters (example :report -> params.report) and query string parameters
const { path, url, params } = this.props.match; const { path, url, params } = this.props.match;
const search = this.props.location.search.substring( 1 ); const query = this.getQuery( this.props.location.search );
const query = parse( search );
const page = find( getPages(), { path } ); const page = find( getPages(), { path } );
window.wpNavMenuUrlUpdate( page, query ); window.wpNavMenuUrlUpdate( page, query );
window.wpNavMenuClassChange( page ); window.wpNavMenuClassChange( page );

View File

@ -39,30 +39,30 @@ export function getRequestByIdString( path, handleData = identity ) {
} }
export const getCategoryLabels = getRequestByIdString( export const getCategoryLabels = getRequestByIdString(
NAMESPACE + 'products/categories', NAMESPACE + '/products/categories',
category => ( { category => ( {
id: category.id, id: category.id,
label: category.name, label: category.name,
} ) } )
); );
export const getCouponLabels = getRequestByIdString( NAMESPACE + 'coupons', coupon => ( { export const getCouponLabels = getRequestByIdString( NAMESPACE + '/coupons', coupon => ( {
id: coupon.id, id: coupon.id,
label: coupon.code, label: coupon.code,
} ) ); } ) );
export const getCustomerLabels = getRequestByIdString( NAMESPACE + 'customers', customer => ( { export const getCustomerLabels = getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id, id: customer.id,
label: customer.username, label: customer.username,
} ) ); } ) );
export const getProductLabels = getRequestByIdString( NAMESPACE + 'products', product => ( { export const getProductLabels = getRequestByIdString( NAMESPACE + '/products', product => ( {
id: product.id, id: product.id,
label: product.name, label: product.name,
} ) ); } ) );
export const getVariationLabels = getRequestByIdString( export const getVariationLabels = getRequestByIdString(
query => NAMESPACE + `products/${ query.products }/variations`, query => NAMESPACE + `/products/${ query.products }/variations`,
variation => { variation => {
return { return {
id: variation.id, id: variation.id,

View File

@ -239,7 +239,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
isError: false, isError: false,
isRequesting: false, isRequesting: false,
data: { data: {
totals: null, totals: {},
intervals: [], intervals: [],
}, },
}; };
@ -355,6 +355,7 @@ export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
isError: false, isError: false,
items: { items: {
data: [], data: [],
totalResults: 0,
}, },
}; };

View File

@ -17,7 +17,7 @@ defined( 'ABSPATH' ) || exit;
*/ */
class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller { class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
// TODO Add support for guests here. See https://wp.me/p7bje6-1dM. // @todo Add support for guests here. See https://wp.me/p7bje6-1dM.
/** /**
* Endpoint namespace. * Endpoint namespace.

View File

@ -0,0 +1,25 @@
<?php
/**
* REST API Product Variations Controller
*
* Handles requests to /products/variations.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* Product variations controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Product_Variations_Controller
*/
class WC_Admin_REST_Product_Variations_Controller extends WC_REST_Product_Variations_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
}

View File

@ -48,6 +48,7 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
$args['orderby'] = $request['orderby']; $args['orderby'] = $request['orderby'];
$args['order'] = $request['order']; $args['order'] = $request['order'];
$args['coupons'] = (array) $request['coupons']; $args['coupons'] = (array) $request['coupons'];
$args['segmentby'] = $request['segmentby'];
return $args; return $args;
} }
@ -61,7 +62,11 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
public function get_items( $request ) { public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request ); $query_args = $this->prepare_reports_query( $request );
$coupons_query = new WC_Admin_Reports_Coupons_Stats_Query( $query_args ); $coupons_query = new WC_Admin_Reports_Coupons_Stats_Query( $query_args );
try {
$report_data = $coupons_query->get_data(); $report_data = $coupons_query->get_data();
} catch ( WC_Admin_Reports_Parameter_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array( $out_data = array(
'totals' => get_object_vars( $report_data->totals ), 'totals' => get_object_vars( $report_data->totals ),
@ -132,7 +137,7 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$totals = array( $data_values = array(
'amount' => array( 'amount' => array(
'description' => __( 'Net discount amount.', 'wc-admin' ), 'description' => __( 'Net discount amount.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
@ -156,6 +161,35 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
), ),
); );
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_coupons_stats', 'title' => 'report_coupons_stats',
@ -302,6 +336,17 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',
'enum' => array(
'product',
'variation',
'category',
'coupon',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -77,7 +77,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$report_data = $customers_query->get_data(); $report_data = $customers_query->get_data();
$out_data = array( $out_data = array(
'totals' => $report_data, 'totals' => $report_data,
// TODO: is this needed? the single element array tricks the isReportDataEmpty() selector. // @todo: is this needed? the single element array tricks the isReportDataEmpty() selector.
'intervals' => array( (object) array() ), 'intervals' => array( (object) array() ),
); );
@ -119,7 +119,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
// TODO: should any of these be 'indicator's? // @todo: should any of these be 'indicator's?
$totals = array( $totals = array(
'customers_count' => array( 'customers_count' => array(
'description' => __( 'Number of customers.', 'wc-admin' ), 'description' => __( 'Number of customers.', 'wc-admin' ),
@ -161,7 +161,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
'readonly' => true, 'readonly' => true,
'properties' => $totals, 'properties' => $totals,
), ),
'intervals' => array( // TODO: remove this? 'intervals' => array( // @todo: remove this?
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ), 'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
'type' => 'array', 'type' => 'array',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),

View File

@ -56,6 +56,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
$args['coupon_excludes'] = (array) $request['coupon_excludes']; $args['coupon_excludes'] = (array) $request['coupon_excludes'];
$args['customer'] = $request['customer']; $args['customer'] = $request['customer'];
$args['categories'] = (array) $request['categories']; $args['categories'] = (array) $request['categories'];
$args['segmentby'] = $request['segmentby'];
return $args; return $args;
} }
@ -69,7 +70,11 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
public function get_items( $request ) { public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request ); $query_args = $this->prepare_reports_query( $request );
$orders_query = new WC_Admin_Reports_Orders_Stats_Query( $query_args ); $orders_query = new WC_Admin_Reports_Orders_Stats_Query( $query_args );
try {
$report_data = $orders_query->get_data(); $report_data = $orders_query->get_data();
} catch ( WC_Admin_Reports_Parameter_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array( $out_data = array(
'totals' => get_object_vars( $report_data->totals ), 'totals' => get_object_vars( $report_data->totals ),
@ -140,7 +145,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$totals = array( $data_values = array(
'net_revenue' => array( 'net_revenue' => array(
'description' => __( 'Net revenue.', 'wc-admin' ), 'description' => __( 'Net revenue.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
@ -169,8 +174,72 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), ),
'num_items_sold' => array(
'description' => __( 'Number of items sold', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'coupons' => array(
'description' => __( 'Amount discounted by coupons', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_returning_customers' => array(
'description' => __( 'Number of orders done by returning customers', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_new_customers' => array(
'description' => __( 'Number of orders done by new customers', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'products' => array(
'description' => __( 'Number of distinct products sold.', 'wc-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
); );
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
// Products is not shown in intervals.
unset( $data_values['products'] );
$intervals = array_merge( $data_values, $segments );
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_orders_stats', 'title' => 'report_orders_stats',
@ -227,7 +296,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
'type' => 'object', 'type' => 'object',
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
'properties' => $totals, 'properties' => $intervals,
), ),
), ),
), ),
@ -385,6 +454,18 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',
'enum' => array(
'product',
'category',
'variation',
'coupon',
'customer_type', // new vs returning.
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -75,7 +75,11 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
} }
$query = new WC_Admin_Reports_Products_Stats_Query( $query_args ); $query = new WC_Admin_Reports_Products_Stats_Query( $query_args );
try {
$report_data = $query->get_data(); $report_data = $query->get_data();
} catch ( WC_Admin_Reports_Parameter_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array( $out_data = array(
'totals' => get_object_vars( $report_data->totals ), 'totals' => get_object_vars( $report_data->totals ),
@ -146,7 +150,7 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$totals = array( $data_values = array(
'items_sold' => array( 'items_sold' => array(
'description' => __( 'Number of items sold.', 'wc-admin' ), 'description' => __( 'Number of items sold.', 'wc-admin' ),
'type' => 'integer', 'type' => 'integer',
@ -169,6 +173,35 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
), ),
); );
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_products_stats', 'title' => 'report_products_stats',
@ -350,6 +383,16 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',
'enum' => array(
'product',
'category',
'variation',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -46,6 +46,7 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
$args['per_page'] = $request['per_page']; $args['per_page'] = $request['per_page'];
$args['orderby'] = $request['orderby']; $args['orderby'] = $request['orderby'];
$args['order'] = $request['order']; $args['order'] = $request['order'];
$args['segmentby'] = $request['segmentby'];
return $args; return $args;
} }
@ -59,7 +60,11 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
public function get_items( $request ) { public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request ); $query_args = $this->prepare_reports_query( $request );
$reports_revenue = new WC_Admin_Reports_Revenue_Query( $query_args ); $reports_revenue = new WC_Admin_Reports_Revenue_Query( $query_args );
try {
$report_data = $reports_revenue->get_data(); $report_data = $reports_revenue->get_data();
} catch ( WC_Admin_Reports_Parameter_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array( $out_data = array(
'totals' => get_object_vars( $report_data->totals ), 'totals' => get_object_vars( $report_data->totals ),
@ -130,7 +135,7 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$totals = array( $data_values = array(
'gross_revenue' => array( 'gross_revenue' => array(
'description' => __( 'Gross revenue.', 'wc-admin' ), 'description' => __( 'Gross revenue.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
@ -197,8 +202,39 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
), ),
); );
$intervals = $totals; $segments = array(
unset( $intervals['products'] ); 'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
// Products is not shown in intervals.
unset( $data_values['products'] );
$intervals = array_merge( $data_values, $segments );
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
@ -342,6 +378,18 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',
'enum' => array(
'product',
'category',
'variation',
'coupon',
'customer_type', // new vs returning.
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -77,6 +77,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
$args['orderby'] = $request['orderby']; $args['orderby'] = $request['orderby'];
$args['order'] = $request['order']; $args['order'] = $request['order'];
$args['taxes'] = (array) $request['taxes']; $args['taxes'] = (array) $request['taxes'];
$args['segmentby'] = $request['segmentby'];
return $args; return $args;
} }
@ -161,7 +162,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
$totals = array( $data_values = array(
'total_tax' => array( 'total_tax' => array(
'description' => __( 'Total tax.', 'wc-admin' ), 'description' => __( 'Total tax.', 'wc-admin' ),
'type' => 'number', 'type' => 'number',
@ -200,6 +201,35 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
), ),
); );
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
$schema = array( $schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_taxes_stats', 'title' => 'report_taxes_stats',
@ -347,6 +377,14 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ),
'type' => 'string',
'enum' => array(
'tax_rate_id',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -37,6 +37,11 @@ class WC_Admin_Api_Init {
*/ */
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init'; const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
/**
* Action hook for processing a batch of orders.
*/
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
/** /**
* Queue instance. * Queue instance.
* *
@ -59,19 +64,16 @@ class WC_Admin_Api_Init {
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 ); add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) ); add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
// Initialize Orders data store class's static vars. // Initialize syncing hooks.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 ); add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_init' ) );
// Initialize Customers Report data store sync hooks.
// Note: we need to hook in before `wc_current_user_is_active`.
// See: https://github.com/woocommerce/woocommerce/blob/942615101ba00c939c107c3a4820c3d466864872/includes/wc-user-functions.php#L749.
add_action( 'wp_loaded', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ) );
// Initialize scheduled action handlers. // Initialize scheduled action handlers.
add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 ); add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 2 ); add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 3 );
add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) ); add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) );
add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) ); add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) );
add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) ); add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) );
add_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
// Add currency symbol to orders endpoint response. // Add currency symbol to orders endpoint response.
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) ); add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
@ -110,6 +112,19 @@ class WC_Admin_Api_Init {
// Common date time code. // Common date time code.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.php';
// Exceptions.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-parameter-exception.php';
// WC Class extensions.
require_once dirname( __FILE__ ) . '/class-wc-admin-order.php';
// Segmentation.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-segmenting.php';
// Query classes for reports. // Query classes for reports.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-query.php';
@ -166,6 +181,7 @@ class WC_Admin_Api_Init {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-setting-options-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-setting-options-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php';
@ -202,6 +218,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Products_Controller', 'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Categories_Controller', 'WC_Admin_REST_Product_Categories_Controller',
'WC_Admin_REST_Product_Reviews_Controller', 'WC_Admin_REST_Product_Reviews_Controller',
'WC_Admin_REST_Product_Variations_Controller',
'WC_Admin_REST_Reports_Controller', 'WC_Admin_REST_Reports_Controller',
'WC_Admin_REST_Setting_Options_Controller', 'WC_Admin_REST_Setting_Options_Controller',
'WC_Admin_REST_System_Status_Tools_Controller', 'WC_Admin_REST_System_Status_Tools_Controller',
@ -371,6 +388,17 @@ class WC_Admin_Api_Init {
$endpoints['/wc/v4/products/reviews'][1] = $endpoints['/wc/v4/products/reviews'][3]; $endpoints['/wc/v4/products/reviews'][1] = $endpoints['/wc/v4/products/reviews'][3];
} }
// Override /wc/v4/products/$product_id/variations.
if ( isset( $endpoints['products/(?P<product_id>[\d]+)/variations'] )
&& isset( $endpoints['products/(?P<product_id>[\d]+)/variations'][3] )
&& isset( $endpoints['products/(?P<product_id>[\d]+)/variations'][2] )
&& $endpoints['products/(?P<product_id>[\d]+)/variations'][2]['callback'][0] instanceof WC_Admin_REST_Product_Variations_Controller
&& $endpoints['products/(?P<product_id>[\d]+)/variations'][3]['callback'][0] instanceof WC_Admin_REST_Product_Variations_Controller
) {
$endpoints['products/(?P<product_id>[\d]+)/variations'][0] = $endpoints['products/(?P<product_id>[\d]+)/variations'][2];
$endpoints['products/(?P<product_id>[\d]+)/variations'][1] = $endpoints['products/(?P<product_id>[\d]+)/variations'][3];
}
// Override /wc/v4/taxes. // Override /wc/v4/taxes.
if ( isset( $endpoints['/wc/v4/taxes'] ) if ( isset( $endpoints['/wc/v4/taxes'] )
&& isset( $endpoints['/wc/v4/taxes'][3] ) && isset( $endpoints['/wc/v4/taxes'][3] )
@ -407,7 +435,7 @@ class WC_Admin_Api_Init {
// so that the orders can be associated with the `customer_id` column. // so that the orders can be associated with the `customer_id` column.
self::customer_lookup_batch_init(); self::customer_lookup_batch_init();
// Queue orders lookup to occur after customers lookup generation is done. // Queue orders lookup to occur after customers lookup generation is done.
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, self::CUSTOMERS_BATCH_ACTION ); self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
} }
/** /**
@ -431,13 +459,58 @@ class WC_Admin_Api_Init {
} }
/** /**
* Init orders data store. * Schedule an action to process a single Order.
*
* @param int $order_id Order ID.
* @return void
*/ */
public static function orders_data_store_init() { public static function schedule_single_order_process( $order_id ) {
if ( 'shop_order' !== get_post_type( $order_id ) ) {
return;
}
// This can get called multiple times for a single order, so we look
// for existing pending jobs for the same order to avoid duplicating efforts.
$existing_jobs = self::queue()->search(
array(
'status' => 'pending',
'per_page' => 1,
'claimed' => false,
'search' => "[{$order_id}]",
)
);
if ( $existing_jobs ) {
$existing_job = current( $existing_jobs );
// Bail out if there's a pending single order action, or a pending dependent action.
if (
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
(
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
)
) {
return;
}
}
// We want to ensure that customer lookup updates are scheduled before order updates.
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Attach order lookup update hooks.
*/
public static function orders_lookup_update_init() {
// Activate WC_Order extension.
WC_Admin_Order::add_filters();
add_action( 'save_post_shop_order', array( __CLASS__, 'schedule_single_order_process' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
WC_Admin_Reports_Orders_Stats_Data_Store::init(); WC_Admin_Reports_Orders_Stats_Data_Store::init();
WC_Admin_Reports_Products_Data_Store::init(); WC_Admin_Reports_Customers_Data_Store::init();
WC_Admin_Reports_Taxes_Data_Store::init();
WC_Admin_Reports_Coupons_Data_Store::init();
} }
/** /**
@ -483,19 +556,35 @@ class WC_Admin_Api_Init {
$order_ids = $order_query->get_orders(); $order_ids = $order_query->get_orders();
foreach ( $order_ids as $order_id ) { foreach ( $order_ids as $order_id ) {
// TODO: schedule single order update if this fails? self::orders_lookup_process_order( $order_id );
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id );
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id );
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id );
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id );
} }
} }
/** /**
* Init customers report data store. * Process a single order to update lookup tables for.
* If an error is encountered in one of the updates, a retry action is scheduled.
*
* @param int $order_id Order ID.
* @return void
*/ */
public static function customers_report_data_store_init() { public static function orders_lookup_process_order( $order_id ) {
WC_Admin_Reports_Customers_Data_Store::init(); $result = array_sum(
array(
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
)
);
// If all updates were either skipped or successful, we're done.
// The update methods return -1 for skip, or a boolean success indicator.
if ( 4 === absint( $result ) ) {
return;
}
// Otherwise assume an error occurred and reschedule.
self::schedule_single_order_process( $order_id );
} }
/** /**
@ -563,9 +652,10 @@ class WC_Admin_Api_Init {
* Queue an action to run after another. * Queue an action to run after another.
* *
* @param string $action Action to run after prerequisite. * @param string $action Action to run after prerequisite.
* @param array $action_args Action arguments.
* @param string $prerequisite_action Prerequisite action. * @param string $prerequisite_action Prerequisite action.
*/ */
public static function queue_dependent_action( $action, $prerequisite_action ) { public static function queue_dependent_action( $action, $action_args, $prerequisite_action ) {
$blocking_jobs = self::queue()->search( $blocking_jobs = self::queue()->search(
array( array(
'status' => 'pending', 'status' => 'pending',
@ -584,10 +674,10 @@ class WC_Admin_Api_Init {
self::queue()->schedule_single( self::queue()->schedule_single(
$after_blocking_job, $after_blocking_job,
self::QUEUE_DEPEDENT_ACTION, self::QUEUE_DEPEDENT_ACTION,
array( $action, $prerequisite_action ) array( $action, $action_args, $prerequisite_action )
); );
} else { } else {
self::queue()->schedule_single( time() + 5, $action ); self::queue()->schedule_single( time() + 5, $action, $action_args );
} }
} }
@ -634,7 +724,7 @@ class WC_Admin_Api_Init {
$customer_ids = $customer_query->get_results(); $customer_ids = $customer_query->get_results();
foreach ( $customer_ids as $customer_id ) { foreach ( $customer_ids as $customer_id ) {
// TODO: schedule single customer update if this fails? // @todo: schedule single customer update if this fails?
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id ); WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
} }
} }
@ -681,7 +771,7 @@ class WC_Admin_Api_Init {
return array_merge( return array_merge(
$wc_tables, $wc_tables,
array( array(
// TODO: will this work on multisite? // @todo: will this work on multisite?
"{$wpdb->prefix}wc_order_stats", "{$wpdb->prefix}wc_order_stats",
"{$wpdb->prefix}wc_order_product_lookup", "{$wpdb->prefix}wc_order_product_lookup",
"{$wpdb->prefix}wc_order_tax_lookup", "{$wpdb->prefix}wc_order_tax_lookup",
@ -733,6 +823,12 @@ class WC_Admin_Api_Init {
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT UNSIGNED NOT NULL, product_qty INT UNSIGNED NOT NULL,
product_net_revenue double DEFAULT 0 NOT NULL, product_net_revenue double DEFAULT 0 NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
coupon_amount double DEFAULT 0 NOT NULL,
tax_amount double DEFAULT 0 NOT NULL,
shipping_amount double DEFAULT 0 NOT NULL,
shipping_tax_amount double DEFAULT 0 NOT NULL,
refund_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_item_id), PRIMARY KEY (order_item_id),
KEY order_id (order_id), KEY order_id (order_id),
KEY product_id (product_id), KEY product_id (product_id),
@ -746,7 +842,7 @@ class WC_Admin_Api_Init {
shipping_tax double DEFAULT 0 NOT NULL, shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL, order_tax double DEFAULT 0 NOT NULL,
total_tax double DEFAULT 0 NOT NULL, total_tax double DEFAULT 0 NOT NULL,
KEY order_id (order_id), PRIMARY KEY (order_id, tax_rate_id),
KEY tax_rate_id (tax_rate_id), KEY tax_rate_id (tax_rate_id),
KEY date_created (date_created) KEY date_created (date_created)
) $collate; ) $collate;

View File

@ -0,0 +1,189 @@
<?php
/**
* WC Admin Order
*
* WC Admin Order class that adds some functionality on top of general WooCommerce WC_Order.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Order class.
*/
class WC_Admin_Order extends WC_Order {
/**
* Holds refund amounts and quantities for the order.
*
* @var void|array
*/
protected $refunded_line_items;
/**
* Add filter(s) required to hook WC_Admin_Order class to substitute WC_Order.
*/
public static function add_filters() {
add_filter( 'woocommerce_order_class', array( __CLASS__, 'order_class_name' ), 10, 3 );
}
/**
* Filter function to swap class WC_Order for WC_Admin_Order in cases when it's suitable.
*
* @param string $classname Name of the class to be created.
* @param string $order_type Type of order object to be created.
* @param number $order_id Order id to create.
*
* @return string
*/
public static function order_class_name( $classname, $order_type, $order_id ) {
if ( 'WC_Order' === $classname ) {
return 'WC_Admin_Order';
} else {
return $classname;
}
}
/**
* Calculate shipping amount for line item/product as a total shipping amount ratio based on quantity.
*
* @param WC_Order_Item $item Line item from order.
*
* @return float|int
*/
public function get_item_shipping_amount( $item ) {
// Shipping amount loosely based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php
// distributed simply based on number of line items.
$quantity_refunded = $this->get_item_quantity_refunded( $item );
$product_qty = $item->get_quantity( 'edit' ) - $quantity_refunded;
$order_items = $this->get_item_count();
if ( 0 === $order_items ) {
return 0;
}
$refunded = $this->get_total_shipping_refunded();
if ( $refunded > 0 ) {
$total_shipping_amount = $this->get_shipping_total() - $refunded;
} else {
$total_shipping_amount = $this->get_shipping_total();
}
return $total_shipping_amount / $order_items * $product_qty;
}
/**
* Save refund amounts and quantities for the order in an array for later use in calculations.
*/
protected function set_order_refund_items() {
if ( ! isset( $this->refunded_line_items ) ) {
$refunds = $this->get_refunds();
$refunded_line_items = array();
foreach ( $refunds as $refund ) {
foreach ( $refund->get_items() as $refunded_item ) {
$line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true );
if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) {
$refunded_line_items[ $line_item_id ]['quantity'] = 0;
$refunded_line_items[ $line_item_id ]['subtotal'] = 0;
}
$refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] );
$refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] );
}
}
$this->refunded_line_items = $refunded_line_items;
}
}
/**
* Get quantity refunded for the line item.
*
* @param WC_Order_Item $item Line item from order.
*
* @return int
*/
public function get_item_quantity_refunded( $item ) {
$this->set_order_refund_items();
$order_item_id = $item->get_id();
return isset( $this->refunded_line_items[ $order_item_id ] ) ? $this->refunded_line_items[ $order_item_id ]['quantity'] : 0;
}
/**
* Get amount refunded for the line item.
*
* @param WC_Order_Item $item Line item from order.
*
* @return int
*/
public function get_item_amount_refunded( $item ) {
$this->set_order_refund_items();
$order_item_id = $item->get_id();
return isset( $this->refunded_line_items[ $order_item_id ] ) ? $this->refunded_line_items[ $order_item_id ]['subtotal'] : 0;
}
/**
* Get item quantity minus refunded quantity for the line item.
*
* @param WC_Order_Item $item Line item from order.
*
* @return int
*/
public function get_item_quantity_minus_refunded( $item ) {
return $item->get_quantity( 'edit' ) - $this->get_item_quantity_refunded( $item );
}
/**
* Calculate shipping tax amount for line item/product as a total shipping tax amount ratio based on quantity.
*
* Loosely based on code in includes/admin/meta-boxes/views/html-order-item(s).php.
*
* @todo: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly?
*
* @param WC_Order_Item $item Line item from order.
*
* @return float|int
*/
public function get_item_shipping_tax_amount( $item ) {
$order_items = $this->get_item_count();
if ( 0 === $order_items ) {
return 0;
}
$quantity_refunded = $this->get_item_quantity_refunded( $item );
$product_qty = $item->get_quantity( 'edit' ) - $quantity_refunded;
$order_taxes = $this->get_taxes();
$line_items_shipping = $this->get_items( 'shipping' );
$total_shipping_tax_amount = 0;
foreach ( $line_items_shipping as $item_id => $shipping_item ) {
$tax_data = $shipping_item->get_taxes();
if ( $tax_data ) {
foreach ( $order_taxes as $tax_item ) {
$tax_item_id = $tax_item->get_rate_id();
$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
$refunded = $this->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
if ( $refunded ) {
$total_shipping_tax_amount += $tax_item_total - $refunded;
} else {
$total_shipping_tax_amount += $tax_item_total;
}
}
}
}
return $total_shipping_tax_amount / $order_items * $product_qty;
}
/**
* Calculates coupon amount for specified line item/product.
*
* Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php.
*
* @param WC_Order_Item $item Line item from order.
*
* @return float
*/
public function get_item_coupon_amount( $item ) {
return floatval( $item->get_subtotal( 'edit' ) - $item->get_total( 'edit' ) );
}
}

View File

@ -0,0 +1,312 @@
<?php
/**
* Class for adding segmenting support to coupons/stats without cluttering the data store.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class WC_Admin_Reports_Coupons_Stats_Segmenting extends WC_Admin_Reports_Segmenting {
/**
* Returns SELECT clause statements to be used for product-related product-level segmenting query (e.g. coupon discount amount for product X when segmenting by product id or category).
*
* @param string $products_table Name of SQL table containing the product-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'amount' => "SUM($products_table.coupon_amount) as amount",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Returns SELECT clause statements to be used for order-related product-level segmenting query (e.g. orders_count when segmented by category).
*
* @param string $coupons_lookup_table Name of SQL table containing the order-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_order_level( $coupons_lookup_table ) {
$columns_mapping = array(
'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count",
'orders_count' => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Returns SELECT clause statements to be used for order-level segmenting query (e.g. discount amount when segmented by coupons).
*
* @param string $coupons_lookup_table Name of SQL table containing the order-level info.
* @param array $overrides Array of overrides for default column calculations.
*
* @return string
*/
protected function segment_selections_orders( $coupons_lookup_table, $overrides = array() ) {
$columns_mapping = array(
'amount' => "SUM($coupons_lookup_table.discount_amount) as amount",
'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count",
'orders_count' => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count",
);
if ( $overrides ) {
$columns_mapping = array_merge( $columns_mapping, $overrides );
}
return $this->prepare_selections( $columns_mapping );
}
/**
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for totals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
global $wpdb;
// Product-level numbers and order-level numbers can be fetched by the same query.
$segments_products = $wpdb->get_results(
"SELECT
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
{$segmenting_selections['order_level']}
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
return $totals_segments;
}
/**
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
global $wpdb;
// LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments.
$limit_parts = explode( ',', $intervals_query['limit'] );
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
// Product-level numbers and order-level numbers can be fetched by the same query.
$segments_products = $wpdb->get_results(
"SELECT
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
{$segmenting_selections['order_level']}
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
return $intervals_segments;
}
/**
* Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
global $wpdb;
$totals_segments = $wpdb->get_results(
"SELECT
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
return $totals_segments;
}
/**
* Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
global $wpdb;
$limit_parts = explode( ',', $intervals_query['limit'] );
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
$intervals_segments = $wpdb->get_results(
"SELECT
MAX($table_name.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
return $intervals_segments;
}
/**
* Return array of segments formatted for REST response.
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $query_params SQL query parameter array.
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
*
* @return array
* @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified.
*/
protected function get_segments( $type, $query_params, $table_name ) {
global $wpdb;
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
return array();
}
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
$unique_orders_table = '';
$segmenting_where = '';
// Product, variation, and category are bound to product, so here product segmenting table is required,
// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
// This also means that segment selections need to be calculated differently.
if ( 'product' === $this->query_args['segmentby'] ) {
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $table_name ),
);
$segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
$segmenting_groupby = $product_segmenting_table . '.product_id';
$segmenting_dimension_name = 'product_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) );
}
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $table_name ),
);
$segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
$segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
$segmenting_groupby = $product_segmenting_table . '.variation_id';
$segmenting_dimension_name = 'variation_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $table_name ),
);
$segmenting_from = "
INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)
LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id
RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id
";
$segmenting_where = " AND taxonomy = 'product_cat'";
$segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id';
$segmenting_dimension_name = 'category_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
$segmenting_selections = $this->segment_selections_orders( $table_name );
$segmenting_from = '';
$segmenting_groupby = "$table_name.coupon_id";
$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
return $segments;
}
}

View File

@ -34,7 +34,7 @@ class WC_Admin_Reports_Customers_Stats_Query extends WC_Admin_Reports_Query {
'page' => 1, 'page' => 1,
'order' => 'DESC', 'order' => 'DESC',
'orderby' => 'date_registered', 'orderby' => 'date_registered',
'fields' => '*', // TODO: needed? 'fields' => '*', // @todo: needed?
); );
} }

View File

@ -194,7 +194,7 @@ class WC_Admin_Reports_Interval {
return (int) floor( ( (int) $diff_timestamp ) / DAY_IN_SECONDS ) + 1 + $addendum; return (int) floor( ( (int) $diff_timestamp ) / DAY_IN_SECONDS ) + 1 + $addendum;
case 'week': case 'week':
// TODO: optimize? approximately day count / 7, but year end is tricky, a week can have fewer days. // @todo: optimize? approximately day count / 7, but year end is tricky, a week can have fewer days.
$week_count = 0; $week_count = 0;
do { do {
$start_datetime = self::next_week_start( $start_datetime ); $start_datetime = self::next_week_start( $start_datetime );

View File

@ -0,0 +1,419 @@
<?php
/**
* Class for adding segmenting support without cluttering the data stores.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenting {
/**
* Returns SELECT clause statements to be used for product-related product-level segmenting query (e.g. products sold, revenue from product X when segmenting by category).
*
* @param string $products_table Name of SQL table containing the product-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'num_items_sold' => "SUM($products_table.product_qty) as num_items_sold",
'gross_revenue' => "SUM($products_table.product_gross_revenue) AS gross_revenue",
'coupons' => "SUM($products_table.coupon_amount) AS coupons",
'refunds' => "SUM($products_table.refund_amount) AS refunds",
'taxes' => "SUM($products_table.tax_amount) AS taxes",
'shipping' => "SUM($products_table.shipping_amount) AS shipping",
// @todo: product_net_revenue should already have refunds subtracted, so it should not be here. Pls check.
'net_revenue' => "SUM($products_table.product_net_revenue) AS net_revenue",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Returns SELECT clause statements to be used for order-related product-level segmenting query (e.g. avg items per order when segmented by category).
*
* @param string $unique_orders_table Name of SQL table containing the order-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_order_level( $unique_orders_table ) {
$columns_mapping = array(
'orders_count' => "COUNT($unique_orders_table.order_id) AS orders_count",
'avg_items_per_order' => "AVG($unique_orders_table.num_items_sold) AS avg_items_per_order",
'avg_order_value' => "(SUM($unique_orders_table.net_total) - SUM($unique_orders_table.refund_total))/COUNT($unique_orders_table.order_id) AS avg_order_value",
'num_returning_customers' => "SUM($unique_orders_table.returning_customer) AS num_returning_customers",
'num_new_customers' => "COUNT($unique_orders_table.returning_customer) - SUM($unique_orders_table.returning_customer) AS num_new_customers",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Returns SELECT clause statements to be used for order-level segmenting query (e.g. avg items per order or net revenue when segmented by coupons).
*
* @param string $order_stats_table Name of SQL table containing the order-level info.
* @param array $overrides Array of overrides for default column calculations.
*
* @return string
*/
protected function segment_selections_orders( $order_stats_table, $overrides = array() ) {
$columns_mapping = array(
'num_items_sold' => "SUM($order_stats_table.num_items_sold) as num_items_sold",
'gross_revenue' => "SUM($order_stats_table.gross_total) AS gross_revenue",
'coupons' => "SUM($order_stats_table.coupon_total) AS coupons",
'refunds' => "SUM($order_stats_table.refund_total) AS refunds",
'taxes' => "SUM($order_stats_table.tax_total) AS taxes",
'shipping' => "SUM($order_stats_table.shipping_total) AS shipping",
'net_revenue' => "SUM($order_stats_table.net_total) - SUM($order_stats_table.refund_total) AS net_revenue",
'orders_count' => "COUNT($order_stats_table.order_id) AS orders_count",
'avg_items_per_order' => "AVG($order_stats_table.num_items_sold) AS avg_items_per_order",
'avg_order_value' => "(SUM($order_stats_table.net_total) - SUM($order_stats_table.refund_total))/COUNT($order_stats_table.order_id) AS avg_order_value",
'num_returning_customers' => "SUM($order_stats_table.returning_customer) AS num_returning_customers",
'num_new_customers' => "COUNT($order_stats_table.returning_customer) - SUM($order_stats_table.returning_customer) AS num_new_customers",
);
if ( $overrides ) {
$columns_mapping = array_merge( $columns_mapping, $overrides );
}
return $this->prepare_selections( $columns_mapping );
}
/**
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for totals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
$segments_products = $wpdb->get_results(
"SELECT
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Order level numbers.
// As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc.
$segments_orders = $wpdb->get_results(
"SELECT
$unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name
{$segmenting_selections['order_level']}
FROM
(
SELECT
$table_name.order_id,
$segmenting_groupby AS $segmenting_dimension_name,
MAX( num_items_sold ) AS num_items_sold,
MAX( net_total ) as net_total,
MAX( refund_total ) as refund_total,
MAX( returning_customer ) AS returning_customer
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$product_segmenting_table.order_id, $segmenting_groupby
) AS $unique_orders_table
GROUP BY
$unique_orders_table.$segmenting_dimension_name",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, $segments_orders );
return $totals_segments;
}
/**
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments.
$limit_parts = explode( ',', $intervals_query['limit'] );
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
$segments_products = $wpdb->get_results(
"SELECT
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Order level numbers.
// As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc.
$segments_orders = $wpdb->get_results(
"SELECT
$unique_orders_table.time_interval AS time_interval,
$unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name
{$segmenting_selections['order_level']}
FROM
(
SELECT
MAX( $table_name.date_created ) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval,
$table_name.order_id,
$segmenting_groupby AS $segmenting_dimension_name,
MAX( num_items_sold ) AS num_items_sold,
MAX( net_total ) as net_total,
MAX( refund_total ) as refund_total,
MAX( returning_customer ) AS returning_customer
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $product_segmenting_table.order_id, $segmenting_groupby
) AS $unique_orders_table
GROUP BY
time_interval, $unique_orders_table.$segmenting_dimension_name
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, $segments_orders );
return $intervals_segments;
}
/**
* Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
global $wpdb;
$totals_segments = $wpdb->get_results(
"SELECT
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
return $totals_segments;
}
/**
* Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
global $wpdb;
$segmenting_limit = '';
$limit_parts = explode( ',', $intervals_query['limit'] );
if ( 2 === count( $limit_parts ) ) {
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
}
$intervals_segments = $wpdb->get_results(
"SELECT
MAX($table_name.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
return $intervals_segments;
}
/**
* Return array of segments formatted for REST response.
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $query_params SQL query parameter array.
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
*
* @return array
* @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified.
*/
protected function get_segments( $type, $query_params, $table_name ) {
global $wpdb;
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
return array();
}
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
$unique_orders_table = 'uniq_orders';
$segmenting_where = '';
// Product, variation, and category are bound to product, so here product segmenting table is required,
// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
// This also means that segment selections need to be calculated differently.
if ( 'product' === $this->query_args['segmentby'] ) {
// @todo: how to handle shipping taxes when grouped by product?
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ),
);
$segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
$segmenting_groupby = $product_segmenting_table . '.product_id';
$segmenting_dimension_name = 'product_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) );
}
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ),
);
$segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)";
$segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
$segmenting_groupby = $product_segmenting_table . '.variation_id';
$segmenting_dimension_name = 'variation_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ),
);
$segmenting_from = "
INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)
LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id
RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id
";
$segmenting_where = " AND taxonomy = 'product_cat'";
$segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id';
$segmenting_dimension_name = 'category_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
// As there can be 2 or more coupons applied per one order, coupon amount needs to be split.
$coupon_override = array(
'coupons' => 'SUM(coupon_lookup.discount_amount) AS coupons',
);
$segmenting_selections = $this->segment_selections_orders( $table_name, $coupon_override );
$segmenting_from = "
INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($table_name.order_id = coupon_lookup.order_id)
";
$segmenting_groupby = 'coupon_lookup.coupon_id';
$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
$segmenting_selections = $this->segment_selections_orders( $table_name );
$segmenting_from = '';
$segmenting_groupby = "$table_name.returning_customer";
$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
return $segments;
}
}

View File

@ -0,0 +1,15 @@
<?php
/**
* WooCommerce Admin Input Parameter Exception Class
*
* Exception class thrown when user provides incorrect parameters.
*
* @package WooCommerce Admin
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Parameter_Exception class.
*/
class WC_Admin_Reports_Parameter_Exception extends WC_Data_Exception {}

View File

@ -0,0 +1,189 @@
<?php
/**
* Class for adding segmenting support without cluttering the data stores.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class WC_Admin_Reports_Products_Stats_Segmenting extends WC_Admin_Reports_Segmenting {
/**
* Returns SELECT clause statements to be used for product-related product-level segmenting query (e.g. products sold, revenue from product X when segmenting by category).
*
* @param string $products_table Name of SQL table containing the product-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'items_sold' => "SUM($products_table.product_qty) as items_sold",
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for totals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
$segments_products = $wpdb->get_results(
"SELECT
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
return $totals_segments;
}
/**
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments.
$limit_parts = explode( ',', $intervals_query['limit'] );
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
$segments_products = $wpdb->get_results(
"SELECT
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
return $intervals_segments;
}
/**
* Return array of segments formatted for REST response.
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $query_params SQL query parameter array.
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
*
* @return array
* @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified.
*/
protected function get_segments( $type, $query_params, $table_name ) {
global $wpdb;
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
return array();
}
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
$unique_orders_table = 'uniq_orders';
$segmenting_where = '';
// Product, variation, and category are bound to product, so here product segmenting table is required,
// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
// This also means that segment selections need to be calculated differently.
if ( 'product' === $this->query_args['segmentby'] ) {
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
);
$segmenting_from = '';
$segmenting_groupby = $product_segmenting_table . '.product_id';
$segmenting_dimension_name = 'product_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) );
}
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
);
$segmenting_from = '';
$segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}";
$segmenting_groupby = $product_segmenting_table . '.variation_id';
$segmenting_dimension_name = 'variation_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$segmenting_selections = array(
'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ),
);
$segmenting_from = "
LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id
RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id
";
$segmenting_where = " AND taxonomy = 'product_cat'";
$segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id';
$segmenting_dimension_name = 'category_id';
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
}
return $segments;
}
}

View File

@ -0,0 +1,520 @@
<?php
/**
* Class for adding segmenting support without cluttering the data stores.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class WC_Admin_Reports_Segmenting {
/**
* Array of all segment ids.
*
* @var array|bool
*/
protected $all_segment_ids = false;
/**
* Query arguments supplied by the user for data store.
*
* @var array
*/
protected $query_args = '';
/**
* SQL definition for each column.
*
* @var array
*/
protected $report_columns = array();
/**
* WC_Admin_Reports_Segmenting constructor.
*
* @param array $query_args Query arguments supplied by the user for data store.
* @param array $report_columns Report columns lookup from data store.
*/
public function __construct( $query_args, $report_columns ) {
$this->query_args = $query_args;
$this->report_columns = $report_columns;
}
/**
* Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause.
*
* @param array $columns_mapping Column name -> SQL statememt mapping.
*
* @return string to be used in SELECT clause statements.
*/
protected function prepare_selections( $columns_mapping ) {
if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) {
$keep = array();
foreach ( $this->query_args['fields'] as $field ) {
if ( isset( $columns_mapping[ $field ] ) ) {
$keep[ $field ] = $columns_mapping[ $field ];
}
}
$selections = implode( ', ', $keep );
} else {
$selections = implode( ', ', $columns_mapping );
}
if ( $selections ) {
$selections = ',' . $selections;
}
return $selections;
}
/**
* Update row-level db result for segments in 'totals' section to the format used for output.
*
* @param array $segments_db_result Results from the SQL db query for segmenting.
* @param string $segment_dimension Name of column used for grouping the result.
*
* @return array Reformatted array.
*/
protected function reformat_totals_segments( $segments_db_result, $segment_dimension ) {
$segment_result = array();
if ( strpos( $segment_dimension, '.' ) ) {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
}
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
$segment_result[ $segment_id ] = $segment_datum;
}
return $segment_result;
}
/**
* Merges segmented results for totals response part.
*
* E.g. $r1 = array(
* 0 => array(
* 'product_id' => 3,
* 'net_amount' => 15,
* ),
* );
* $r2 = array(
* 0 => array(
* 'product_id' => 3,
* 'avg_order_value' => 25,
* ),
* );
*
* $merged = array(
* 3 => array(
* 'segment_id' => 3,
* 'subtotals' => array(
* 'net_amount' => 15,
* 'avg_order_value' => 25,
* )
* ),
* );
*
* @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
* @param array $result1 Array 1 of segmented figures.
* @param array $result2 Array 2 of segmented figures.
*
* @return array
*/
protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
foreach ( $result1 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$result_segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
}
foreach ( $result2 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
if ( ! isset( $result_segments[ $segment_id ] ) ) {
$result_segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => array(),
);
}
$result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data );
}
return $result_segments;
}
/**
* Merges segmented results for intervals response part.
*
* E.g. $r1 = array(
* 0 => array(
* 'product_id' => 3,
* 'time_interval' => '2018-12'
* 'net_amount' => 15,
* ),
* );
* $r2 = array(
* 0 => array(
* 'product_id' => 3,
* 'time_interval' => '2018-12'
* 'avg_order_value' => 25,
* ),
* );
*
* $merged = array(
* '2018-12' => array(
* 'segments' => array(
* 3 => array(
* 'segment_id' => 3,
* 'subtotals' => array(
* 'net_amount' => 15,
* 'avg_order_value' => 25,
* ),
* ),
* ),
* ),
* );
*
* @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
* @param array $result1 Array 1 of segmented figures.
* @param array $result2 Array 2 of segmented figures.
*
* @return array
*/
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
foreach ( $result1 as $segment_data ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $result_segments[ $time_interval ] ) ) {
$result_segments[ $time_interval ] = array();
$result_segments[ $time_interval ]['segments'] = array();
}
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
$result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
}
foreach ( $result2 as $segment_data ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $result_segments[ $time_interval ] ) ) {
$result_segments[ $time_interval ] = array();
$result_segments[ $time_interval ]['segments'] = array();
}
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) {
$result_segments[ $time_interval ]['segments'][ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => array(),
);
}
$result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data );
}
return $result_segments;
}
/**
* Update row-level db result for segments in 'intervals' section to the format used for output.
*
* @param array $segments_db_result Results from the SQL db query for segmenting.
* @param string $segment_dimension Name of column used for grouping the result.
*
* @return array Reformatted array.
*/
protected function reformat_intervals_segments( $segments_db_result, $segment_dimension ) {
$aggregated_segment_result = array();
if ( strpos( $segment_dimension, '.' ) ) {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
}
foreach ( $segments_db_result as $segment_data ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
$aggregated_segment_result[ $time_interval ] = array();
$aggregated_segment_result[ $time_interval ]['segments'] = array();
}
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
$segment_id = $segment_data[ $segment_dimension ];
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
'subtotals' => $segment_data,
);
$aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
}
return $aggregated_segment_result;
}
/**
* Fetches all segment ids from db and stores it for later use.
*
* @return void
*/
protected function set_all_segments() {
global $wpdb;
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
$this->all_segment_ids = array();
return;
}
if ( 'product' === $this->query_args['segmentby'] ) {
$segments = wc_get_products(
array(
'return' => 'ids',
'limit' => -1,
)
);
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
// @todo: assuming that this will only be used for one product, check assumption.
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
$this->all_segment_ids = array();
return;
}
$segments = wc_get_products(
array(
'return' => 'ids',
'limit' => - 1,
'type' => 'variation',
'parent' => $this->query_args['product_includes'][0],
)
);
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$categories = get_categories(
array(
'taxonomy' => 'product_cat',
)
);
$segments = wp_list_pluck( $categories, 'cat_ID' );
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
// @todo: switch to a non-direct-SQL way to get all coupons?
// @todo: These are only currently existing coupons, but we should add also deleted ones, if they have been used at least once.
$coupon_ids = $wpdb->get_results( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_type='shop_coupon' AND post_status='publish'", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$segments = wp_list_pluck( $coupon_ids, 'ID' );
} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
// 0 -- new customer
// 1 -- returning customer
$segments = array( 0, 1 );
} elseif ( 'tax_rate_id' === $this->query_args['segmentby'] ) {
// @todo: do we need to include tax rates that existed in the past, but have never been used? I guess there are other, more pressing problems...
// Current tax rates UNION previously used tax rates.
$tax_rate_ids = $wpdb->get_results(
"SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates
UNION
SELECT DISTINCT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta where meta_key='rate_id'",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$segments = wp_list_pluck( $tax_rate_ids, 'tax_rate_id' );
} else {
// Catch all default.
$segments = array();
}
$this->all_segment_ids = $segments;
}
/**
* Return all segment ids for given segmentby query parameter.
*
* @return array
*/
protected function get_all_segments() {
if ( ! is_array( $this->all_segment_ids ) ) {
$this->set_all_segments();
}
return $this->all_segment_ids;
}
/**
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
*
* @param stdClass $a Object a.
* @param stdClass $b Object b.
* @return string
*/
private function segment_cmp( $a, $b ) {
if ( $a['segment_id'] === $b['segment_id'] ) {
return 0;
} elseif ( $a['segment_id'] > $b['segment_id'] ) {
return 1;
} elseif ( $a['segment_id'] < $b['segment_id'] ) {
return - 1;
}
}
/**
* Adds zeroes for segments not present in the data selection.
*
* @param array $segments Array of segments from the database for given data points.
*
* @return array
*/
protected function fill_in_missing_segments( $segments ) {
$segment_subtotals = array();
if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) {
foreach ( $this->query_args['fields'] as $field ) {
if ( isset( $this->report_columns[ $field ] ) ) {
$segment_subtotals[ $field ] = 0;
}
}
} else {
foreach ( $this->report_columns as $field => $sql_clause ) {
$segment_subtotals[ $field ] = 0;
}
}
if ( ! is_array( $segments ) ) {
$segments = array();
}
$all_segment_ids = $this->get_all_segments();
foreach ( $all_segment_ids as $segment_id ) {
if ( ! isset( $segments[ $segment_id ] ) ) {
$segments[ $segment_id ] = array(
'segment_id' => $segment_id,
'subtotals' => $segment_subtotals,
);
}
}
// Using array_values to remove custom keys, so that it gets later converted to JSON as an array.
$segments_no_keys = array_values( $segments );
usort( $segments_no_keys, array( $this, 'segment_cmp' ) );
return $segments_no_keys;
}
/**
* Adds missing segments to intervals, modifies $data.
*
* @param stdClass $data Response data.
*/
protected function fill_in_missing_interval_segments( &$data ) {
foreach ( $data->intervals as $order_id => $interval_data ) {
$data->intervals[ $order_id ]['segments'] = $this->fill_in_missing_segments( $data->intervals[ $order_id ]['segments'] );
}
}
/**
* Calculate segments for segmenting property bound to product (e.g. category, product_id, variation_id).
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $query_params Array of SQL clauses for intervals/totals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ) {
if ( 'totals' === $type ) {
return $this->get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
} elseif ( 'intervals' === $type ) {
return $this->get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
}
}
/**
* Calculate segments for segmenting property bound to order (e.g. coupon or customer type).
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $query_params Array of SQL clauses for intervals/totals query.
*
* @return array
*/
protected function get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ) {
if ( 'totals' === $type ) {
return $this->get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
} elseif ( 'intervals' === $type ) {
return $this->get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
}
/**
* Assign segments to time intervals by updating original $intervals array.
*
* @param array $intervals Result array from intervals SQL query.
* @param array $intervals_segments Result array from interval segments SQL query.
*/
protected function assign_segments_to_intervals( &$intervals, $intervals_segments ) {
$old_keys = array_keys( $intervals );
foreach ( $intervals as $interval ) {
$intervals[ $interval['time_interval'] ] = $interval;
$intervals[ $interval['time_interval'] ]['segments'] = array();
}
foreach ( $old_keys as $key ) {
unset( $intervals[ $key ] );
}
foreach ( $intervals_segments as $time_interval => $segment ) {
if ( ! isset( $intervals[ $time_interval ] ) ) {
$intervals[ $time_interval ]['segments'] = array();
}
$intervals[ $time_interval ]['segments'] = $segment['segments'];
}
// To remove time interval keys (so that REST response is formatted correctly).
$intervals = array_values( $intervals );
}
/**
* Returns an array of segments for totals part of REST response.
*
* @param array $query_params Totals SQL query parameters.
* @param string $table_name Name of the SQL table that is the main order stats table.
*
* @return array
*/
public function get_totals_segments( $query_params, $table_name ) {
$segments = $this->get_segments( 'totals', $query_params, $table_name );
return $this->fill_in_missing_segments( $segments );
}
/**
* Adds an array of segments to data->intervals object.
*
* @param stdClass $data Data object representing the REST response.
* @param array $intervals_query Intervals SQL query parameters.
* @param string $table_name Name of the SQL table that is the main order stats table.
*/
public function add_intervals_segments( &$data, $intervals_query, $table_name ) {
$intervals_segments = $this->get_segments( 'intervals', $intervals_query, $table_name );
$this->assign_segments_to_intervals( $data->intervals, $intervals_segments );
$this->fill_in_missing_interval_segments( $data );
}
}

View File

@ -0,0 +1,147 @@
<?php
/**
* Class for adding segmenting support without cluttering the data stores.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class WC_Admin_Reports_Taxes_Stats_Segmenting extends WC_Admin_Reports_Segmenting {
/**
* Returns SELECT clause statements to be used for order-related order-level segmenting query (e.g. tax_rate_id).
*
* @param string $lookup_table Name of SQL table containing the order-level segmenting info.
*
* @return string SELECT clause statements.
*/
protected function get_segment_selections_order_level( $lookup_table ) {
$columns_mapping = array(
'tax_codes' => "COUNT(DISTINCT $lookup_table.tax_rate_id) as tax_codes",
'total_tax' => "SUM($lookup_table.total_tax) AS total_tax",
'order_tax' => "SUM($lookup_table.order_tax) as order_tax",
'shipping_tax' => "SUM($lookup_table.shipping_tax) as shipping_tax",
'orders_count' => "COUNT(DISTINCT $lookup_table.order_id) as orders_count",
);
return $this->prepare_selections( $columns_mapping );
}
/**
* Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
global $wpdb;
$totals_segments = $wpdb->get_results(
"SELECT
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby );
return $totals_segments;
}
/**
* Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
*
* @param string $segmenting_select SELECT part of segmenting SQL query.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
*
* @return array
*/
protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
global $wpdb;
$segmenting_limit = '';
$limit_parts = explode( ',', $intervals_query['limit'] );
if ( 2 === count( $limit_parts ) ) {
$orig_rowcount = intval( $limit_parts[1] );
$segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() );
}
$intervals_segments = $wpdb->get_results(
"SELECT
MAX($table_name.date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby
$segmenting_select
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// Reformat result.
$intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby );
return $intervals_segments;
}
/**
* Return array of segments formatted for REST response.
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $query_params SQL query parameter array.
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
*
* @return array
* @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified.
*/
protected function get_segments( $type, $query_params, $table_name ) {
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
return array();
}
$segmenting_where = '';
$segmenting_from = '';
$segments = array();
if ( 'tax_rate_id' === $this->query_args['segmentby'] ) {
$segmenting_select = $this->get_segment_selections_order_level( $table_name );
$segmenting_groupby = $table_name . '.tax_rate_id';
$segments = $this->get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
return $segments;
}
}

View File

@ -95,7 +95,7 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
$sql_query_params['where_clause'] .= " AND {$wpdb->prefix}term_taxonomy.term_id IN ({$included_categories})"; $sql_query_params['where_clause'] .= " AND {$wpdb->prefix}term_taxonomy.term_id IN ({$included_categories})";
} }
// TODO: only products in the category C or orders with products from category C (and, possibly others?). // @todo: only products in the category C or orders with products from category C (and, possibly others?).
$included_products = $this->get_included_products( $query_args ); $included_products = $this->get_included_products( $query_args );
if ( $included_products ) { if ( $included_products ) {
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})"; $sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";

View File

@ -51,15 +51,6 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] ); $this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
} }
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_coupons' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_coupons' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_coupons' ) );
}
/** /**
* Returns comma separated ids of included coupons, based on query arguments from the user. * Returns comma separated ids of included coupons, based on query arguments from the user.
* *
@ -315,19 +306,21 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
* *
* @since 3.5.0 * @since 3.5.0
* @param int $order_id Order ID. * @param int $order_id Order ID.
* @return void * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/ */
public static function sync_order_coupons( $order_id ) { public static function sync_order_coupons( $order_id ) {
global $wpdb; global $wpdb;
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
if ( ! $order ) { if ( ! $order ) {
return; return -1;
} }
$coupon_items = $order->get_items( 'coupon' ); $coupon_items = $order->get_items( 'coupon' );
$num_updated = 0;
foreach ( $coupon_items as $coupon_item ) { foreach ( $coupon_items as $coupon_item ) {
$wpdb->replace( $result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME, $wpdb->prefix . self::TABLE_NAME,
array( array(
'order_id' => $order_id, 'order_id' => $order_id,
@ -342,7 +335,11 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
'%s', '%s',
) )
); );
$num_updated += intval( $result );
} }
return ( count( $coupon_items ) === $num_updated );
} }
} }

View File

@ -165,6 +165,8 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
if ( null === $totals ) { if ( null === $totals ) {
return $data; return $data;
} }
$segmenter = new WC_Admin_Reports_Coupons_Stats_Segmenting( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] ); $totals = (object) $this->cast_numbers( $totals[0] );
// Intervals. // Intervals.
@ -213,6 +215,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
} else { } else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
} }
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals ); $this->create_interval_subtotals( $data->intervals );
wp_cache_set( $cache_key, $data, $this->cache_group ); wp_cache_set( $cache_key, $data, $this->cache_group );

View File

@ -41,7 +41,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
'customer_id' => 'customer_id', 'customer_id' => 'customer_id',
'user_id' => 'user_id', 'user_id' => 'user_id',
'username' => 'username', 'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // TODO: what does this mean for RTL? 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo: what does this mean for RTL?
'email' => 'email', 'email' => 'email',
'country' => 'country', 'country' => 'country',
'city' => 'city', 'city' => 'city',

View File

@ -49,7 +49,7 @@ class WC_Admin_Reports_Data_Store {
*/ */
protected $report_columns = array(); protected $report_columns = array();
// TODO: this does not really belong here, maybe factor out the comparison as separate class? // @todo: this does not really belong here, maybe factor out the comparison as separate class?
/** /**
* Order by property, used in the cmp function. * Order by property, used in the cmp function.
* *
@ -73,7 +73,7 @@ class WC_Admin_Reports_Data_Store {
private function interval_cmp( $a, $b ) { private function interval_cmp( $a, $b ) {
if ( '' === $this->order_by || '' === $this->order ) { if ( '' === $this->order_by || '' === $this->order ) {
return 0; return 0;
// TODO: should return WP_Error here perhaps? // @todo: should return WP_Error here perhaps?
} }
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) { if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
// As relative order is undefined in case of equality in usort, second-level sorting by date needs to be enforced // As relative order is undefined in case of equality in usort, second-level sorting by date needs to be enforced
@ -102,9 +102,20 @@ class WC_Admin_Reports_Data_Store {
* @param string $direction DESC/ASC. * @param string $direction DESC/ASC.
*/ */
protected function sort_intervals( &$data, $sort_by, $direction ) { protected function sort_intervals( &$data, $sort_by, $direction ) {
$this->sort_array( $data->intervals, $sort_by, $direction );
}
/**
* Sorts array of arrays based on subarray key $sort_by.
*
* @param array $arr Array to sort.
* @param string $sort_by Ordering property.
* @param string $direction DESC/ASC.
*/
protected function sort_array( &$arr, $sort_by, $direction ) {
$this->order_by = $this->normalize_order_by( $sort_by ); $this->order_by = $this->normalize_order_by( $sort_by );
$this->order = $direction; $this->order = $direction;
usort( $data->intervals, array( $this, 'interval_cmp' ) ); usort( $arr, array( $this, 'interval_cmp' ) );
} }
/** /**
@ -118,7 +129,7 @@ class WC_Admin_Reports_Data_Store {
* @return stdClass * @return stdClass
*/ */
protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) { protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) {
// TODO: this is ugly and messy. // @todo: this is ugly and messy.
// At this point, we don't know when we can stop iterating, as the ordering can be based on any value. // At this point, we don't know when we can stop iterating, as the ordering can be based on any value.
$end_datetime = new DateTime( $datetime_end ); $end_datetime = new DateTime( $datetime_end );
$time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) ); $time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) );
@ -129,7 +140,7 @@ class WC_Admin_Reports_Data_Store {
foreach ( $totals_arr as $key => $val ) { foreach ( $totals_arr as $key => $val ) {
$totals_arr[ $key ] = 0; $totals_arr[ $key ] = 0;
} }
// TODO: should 'products' be in intervals? // @todo: should 'products' be in intervals?
unset( $totals_arr['products'] ); unset( $totals_arr['products'] );
while ( $datetime <= $end_datetime ) { while ( $datetime <= $end_datetime ) {
$next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); $next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
@ -338,7 +349,7 @@ class WC_Admin_Reports_Data_Store {
$start_iteration = 0; $start_iteration = 0;
} }
if ( $start_iteration ) { if ( $start_iteration ) {
// TODO: is this correct? should it only be added if iterate runs? other two iterate instances, too? // @todo: is this correct? should it only be added if iterate runs? other two iterate instances, too?
$new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1; $new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1;
$new_start_date->setTimestamp( $new_start_date_timestamp ); $new_start_date->setTimestamp( $new_start_date_timestamp );
} }
@ -420,7 +431,7 @@ class WC_Admin_Reports_Data_Store {
*/ */
protected static function get_excluded_report_order_statuses() { protected static function get_excluded_report_order_statuses() {
$excluded_statuses = WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) ); $excluded_statuses = WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) );
$excluded_statuses[] = 'refunded'; $excluded_statuses = array_merge( array( 'refunded', 'trash' ), $excluded_statuses );
return apply_filters( 'woocommerce_reports_excluded_order_statuses', $excluded_statuses ); return apply_filters( 'woocommerce_reports_excluded_order_statuses', $excluded_statuses );
} }
@ -464,7 +475,7 @@ class WC_Admin_Reports_Data_Store {
$datetime = new DateTime( $interval['datetime_anchor'] ); $datetime = new DateTime( $interval['datetime_anchor'] );
$prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true ); $prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true );
// TODO: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. // @todo: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below.
$prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1; $prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1;
$prev_start->setTimestamp( $prev_start_timestamp ); $prev_start->setTimestamp( $prev_start_timestamp );
if ( $datetime_start ) { if ( $datetime_start ) {

View File

@ -45,6 +45,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'num_returning_customers' => 'intval', 'num_returning_customers' => 'intval',
'num_new_customers' => 'intval', 'num_new_customers' => 'intval',
'products' => 'intval', 'products' => 'intval',
'segment_id' => 'intval',
); );
/** /**
@ -72,10 +73,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Set up all the hooks for maintaining and populating table data. * Set up all the hooks for maintaining and populating table data.
*/ */
public static function init() { public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order' ) );
// TODO: this is required as order update skips save_post.
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) );
add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 ); add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 );
add_action( 'delete_post', array( __CLASS__, 'delete_order' ) ); add_action( 'delete_post', array( __CLASS__, 'delete_order' ) );
} }
@ -88,7 +85,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* @param array $intervals_query Array of options for intervals db query. * @param array $intervals_query Array of options for intervals db query.
*/ */
protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) { protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) {
// TODO: performance of all of this? // @todo: performance of all of this?
global $wpdb; global $wpdb;
$from_clause = ''; $from_clause = '';
@ -97,7 +94,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
$where_filters = array(); $where_filters = array();
// TODO: maybe move the sql inside the get_included/excluded functions? // @todo: maybe move the sql inside the get_included/excluded functions?
// Products filters. // Products filters.
$included_products = $this->get_included_products( $query_args ); $included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args ); $excluded_products = $this->get_excluded_products( $query_args );
@ -176,7 +173,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
/** /**
* Returns the report data based on parameters supplied by the user. * Returns the report data based on parameters supplied by the user.
* *
* @since 3.5.0
* @param array $query_args Query parameters. * @param array $query_args Query parameters.
* @return stdClass|WP_Error Data. * @return stdClass|WP_Error Data.
*/ */
@ -197,6 +193,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'interval' => 'week', 'interval' => 'week',
'fields' => '*', 'fields' => '*',
'segmentby' => '',
'match' => 'all', 'match' => 'all',
'status_is' => array(), 'status_is' => array(),
@ -248,6 +245,9 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
$unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] ); $unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['products'] = $unique_products; $totals[0]['products'] = $unique_products;
$segmenting = new WC_Admin_Reports_Orders_Stats_Segmenting( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenting->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] ); $totals = (object) $this->cast_numbers( $totals[0] );
$db_intervals = $wpdb->get_col( $db_intervals = $wpdb->get_col(
@ -317,6 +317,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
} else { } else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
} }
$segmenting->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals ); $this->create_interval_subtotals( $data->intervals );
wp_cache_set( $cache_key, $data, $this->cache_group ); wp_cache_set( $cache_key, $data, $this->cache_group );
@ -355,18 +356,19 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Add order information to the lookup table when orders are created or modified. * Add order information to the lookup table when orders are created or modified.
* *
* @param int $post_id Post ID. * @param int $post_id Post ID.
* @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/ */
public static function sync_order( $post_id ) { public static function sync_order( $post_id ) {
if ( 'shop_order' !== get_post_type( $post_id ) ) { if ( 'shop_order' !== get_post_type( $post_id ) ) {
return; return -1;
} }
$order = wc_get_order( $post_id ); $order = wc_get_order( $post_id );
if ( ! $order ) { if ( ! $order ) {
return; return -1;
} }
self::update( $order ); return self::update( $order );
} }
/** /**
@ -383,14 +385,14 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* Update the database with stats data. * Update the database with stats data.
* *
* @param WC_Order $order Order to update row for. * @param WC_Order $order Order to update row for.
* @return int|bool|null Number or rows modified or false on failure. * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/ */
public static function update( $order ) { public static function update( $order ) {
global $wpdb; global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME; $table_name = $wpdb->prefix . self::TABLE_NAME;
if ( ! $order->get_id() || ! $order->get_date_created() ) { if ( ! $order->get_id() || ! $order->get_date_created() ) {
return false; return -1;
} }
$data = array( $data = array(
@ -445,7 +447,9 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
} }
// Update or add the information to the DB. // Update or add the information to the DB.
return $wpdb->replace( $table_name, $data, $format ); $result = $wpdb->replace( $table_name, $data, $format );
return ( 1 === $result );
} }
/** /**

View File

@ -82,15 +82,6 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] ); $this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
} }
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_products' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_products' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_products' ) );
}
/** /**
* Fills ORDER BY clause of SQL request based on user supplied parameters. * Fills ORDER BY clause of SQL request based on user supplied parameters.
* *
@ -319,7 +310,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
* *
* @since 3.5.0 * @since 3.5.0
* @param int $order_id Order ID. * @param int $order_id Order ID.
* @return void * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/ */
public static function sync_order_products( $order_id ) { public static function sync_order_products( $order_id ) {
global $wpdb; global $wpdb;
@ -328,23 +319,43 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
// This hook gets called on refunds as well, so return early to avoid errors. // This hook gets called on refunds as well, so return early to avoid errors.
if ( ! $order || 'shop_order_refund' === $order->get_type() ) { if ( ! $order || 'shop_order_refund' === $order->get_type() ) {
return; return -1;
} }
$refunds = self::get_order_refund_items( $order ); $order_items = $order->get_items();
$num_updated = 0;
foreach ( $order->get_items() as $order_item ) { foreach ( $order_items as $order_item ) {
$order_item_id = $order_item->get_id(); $order_item_id = $order_item->get_id();
$quantity_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['quantity'] : 0; $quantity_refunded = $order->get_item_quantity_refunded( $order_item );
$amount_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['subtotal'] : 0; $amount_refunded = $order->get_item_amount_refunded( $order_item );
$product_qty = $order->get_item_quantity_minus_refunded( $order_item );
$shipping_amount = $order->get_item_shipping_amount( $order_item );
$shipping_tax_amount = $order->get_item_shipping_tax_amount( $order_item );
$coupon_amount = $order->get_item_coupon_amount( $order_item );
// Tax amount.
// @todo: check if this calculates tax correctly with refunds.
$tax_amount = 0;
$order_taxes = $order->get_taxes();
$tax_data = $order_item->get_taxes();
foreach ( $order_taxes as $tax_item ) {
$tax_item_id = $tax_item->get_rate_id();
$tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : 0;
}
// @todo: should net revenue be affected by refunds, as refunds are tracked separately?
$net_revenue = $order_item->get_subtotal( 'edit' ) - $amount_refunded;
if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) { if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) {
$wpdb->delete( $result = $wpdb->delete(
$wpdb->prefix . self::TABLE_NAME, $wpdb->prefix . self::TABLE_NAME,
array( 'order_item_id' => $order_item_id ), array( 'order_item_id' => $order_item_id ),
array( '%d' ) array( '%d' )
); ); // WPCS: cache ok, DB call ok.
} else { } else {
$wpdb->replace( $result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME, $wpdb->prefix . self::TABLE_NAME,
array( array(
'order_item_id' => $order_item_id, 'order_item_id' => $order_item_id,
@ -352,46 +363,40 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
'product_id' => $order_item->get_product_id( 'edit' ), 'product_id' => $order_item->get_product_id( 'edit' ),
'variation_id' => $order_item->get_variation_id( 'edit' ), 'variation_id' => $order_item->get_variation_id( 'edit' ),
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null, 'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded, 'product_qty' => $product_qty,
'product_net_revenue' => $order_item->get_subtotal( 'edit' ) - $amount_refunded, 'product_net_revenue' => $net_revenue,
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'coupon_amount' => $coupon_amount,
'tax_amount' => $tax_amount,
'shipping_amount' => $shipping_amount,
'shipping_tax_amount' => $shipping_tax_amount,
// @todo: can this be incorrect if modified by filters?
'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount,
'refund_amount' => $amount_refunded,
), ),
array( array(
'%d', '%d', // order_item_id.
'%d', '%d', // order_id.
'%d', '%d', // product_id.
'%d', '%d', // variation_id.
'%d', '%d', // customer_id.
'%d', '%d', // product_qty.
'%f', '%f', // product_net_revenue.
'%s', '%s', // date_created.
'%f', // coupon_amount.
'%f', // tax_amount.
'%f', // shipping_amount.
'%f', // shipping_tax_amount.
'%f', // product_gross_revenue.
'%f', // refund_amount.
) )
); ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
}
}
} }
/** $num_updated += intval( $result );
* Get order refund items quantity and subtotal
*
* @param object $order WC Order object.
* @return array
*/
public static function get_order_refund_items( $order ) {
$refunds = $order->get_refunds();
$refunded_line_items = array();
foreach ( $refunds as $refund ) {
foreach ( $refund->get_items() as $refunded_item ) {
$line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true );
if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) {
$refunded_line_items[ $line_item_id ]['quantity'] = 0;
$refunded_line_items[ $line_item_id ]['subtotal'] = 0;
} }
$refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] );
$refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] ); return ( count( $order_items ) === $num_updated );
}
}
return $refunded_line_items;
} }
} }

View File

@ -159,6 +159,9 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
ARRAY_A ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok. ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$segmenter = new WC_Admin_Reports_Products_Stats_Segmenting( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
if ( null === $totals ) { if ( null === $totals ) {
return new WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); return new WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) );
} }
@ -208,6 +211,7 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
} else { } else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
} }
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals ); $this->create_interval_subtotals( $data->intervals );
wp_cache_set( $cache_key, $data, $this->cache_group ); wp_cache_set( $cache_key, $data, $this->cache_group );

View File

@ -66,15 +66,6 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
$this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] ); $this->report_columns['orders_count'] = str_replace( 'order_id', $table_name . '.order_id', $this->report_columns['orders_count'] );
} }
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order_taxes' ) );
add_action( 'clean_post_cache', array( __CLASS__, 'sync_order_taxes' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order_taxes' ) );
}
/** /**
* Updates the database query with parameters used for Taxes report: categories and order status. * Updates the database query with parameters used for Taxes report: categories and order status.
* *
@ -255,17 +246,20 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
* Create or update an entry in the wc_order_tax_lookup table for an order. * Create or update an entry in the wc_order_tax_lookup table for an order.
* *
* @param int $order_id Order ID. * @param int $order_id Order ID.
* @return void * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
*/ */
public static function sync_order_taxes( $order_id ) { public static function sync_order_taxes( $order_id ) {
global $wpdb; global $wpdb;
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
if ( ! $order ) { if ( ! $order ) {
return; return -1;
} }
foreach ( $order->get_items( 'tax' ) as $tax_item ) { $tax_items = $order->get_items( 'tax' );
$wpdb->replace( $num_updated = 0;
foreach ( $tax_items as $tax_item ) {
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME, $wpdb->prefix . self::TABLE_NAME,
array( array(
'order_id' => $order->get_id(), 'order_id' => $order->get_id(),
@ -284,7 +278,11 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
'%f', '%f',
) )
); );
$num_updated += intval( $result );
} }
return ( count( $tax_items ) === $num_updated );
} }
} }

View File

@ -183,6 +183,8 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
if ( null === $totals ) { if ( null === $totals ) {
return new WP_Error( 'woocommerce_reports_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); return new WP_Error( 'woocommerce_reports_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) );
} }
$segmenter = new WC_Admin_Reports_Taxes_Stats_Segmenting( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
@ -231,6 +233,7 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
} else { } else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
} }
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals ); $this->create_interval_subtotals( $data->intervals );
wp_cache_set( $cache_key, $data, $this->cache_group ); wp_cache_set( $cache_key, $data, $this->cache_group );

View File

@ -60,11 +60,11 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.3.0", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz",
"integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==", "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==",
"requires": { "requires": {
"@babel/types": "^7.3.0", "@babel/types": "^7.3.2",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"source-map": "^0.5.0", "source-map": "^0.5.0",
@ -274,9 +274,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.3.1", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz",
"integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==" "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ=="
}, },
"@babel/plugin-proposal-async-generator-functions": { "@babel/plugin-proposal-async-generator-functions": {
"version": "7.2.0", "version": "7.2.0",
@ -298,9 +298,9 @@
} }
}, },
"@babel/plugin-proposal-object-rest-spread": { "@babel/plugin-proposal-object-rest-spread": {
"version": "7.3.1", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz",
"integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==", "integrity": "sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==",
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-syntax-object-rest-spread": "^7.2.0" "@babel/plugin-syntax-object-rest-spread": "^7.2.0"
@ -424,9 +424,9 @@
} }
}, },
"@babel/plugin-transform-destructuring": { "@babel/plugin-transform-destructuring": {
"version": "7.2.0", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz",
"integrity": "sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ==", "integrity": "sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==",
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.0.0" "@babel/helper-plugin-utils": "^7.0.0"
} }
@ -730,9 +730,9 @@
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.3.0", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz",
"integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==",
"requires": { "requires": {
"esutils": "^2.0.2", "esutils": "^2.0.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
@ -1765,21 +1765,21 @@
} }
}, },
"inquirer": { "inquirer": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-escapes": "^3.0.0", "ansi-escapes": "^3.2.0",
"chalk": "^2.0.0", "chalk": "^2.4.2",
"cli-cursor": "^2.1.0", "cli-cursor": "^2.1.0",
"cli-width": "^2.0.0", "cli-width": "^2.0.0",
"external-editor": "^3.0.0", "external-editor": "^3.0.3",
"figures": "^2.0.0", "figures": "^2.0.0",
"lodash": "^4.17.10", "lodash": "^4.17.11",
"mute-stream": "0.0.7", "mute-stream": "0.0.7",
"run-async": "^2.2.0", "run-async": "^2.2.0",
"rxjs": "^6.1.0", "rxjs": "^6.4.0",
"string-width": "^2.1.0", "string-width": "^2.1.0",
"strip-ansi": "^5.0.0", "strip-ansi": "^5.0.0",
"through": "^2.3.6" "through": "^2.3.6"
@ -2113,9 +2113,9 @@
"integrity": "sha512-y+h7tNlxDPDrH/TrSw1wCSm6FoEAY8FrbUxYng3BMSYBTUsX1utLooizk9v8J1yy6F9AioXNnPZ1qiu2vsa08Q==" "integrity": "sha512-y+h7tNlxDPDrH/TrSw1wCSm6FoEAY8FrbUxYng3BMSYBTUsX1utLooizk9v8J1yy6F9AioXNnPZ1qiu2vsa08Q=="
}, },
"@types/node": { "@types/node": {
"version": "10.12.19", "version": "10.12.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz",
"integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==" "integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ=="
}, },
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.7.11", "version": "1.7.11",
@ -2999,9 +2999,9 @@
}, },
"dependencies": { "dependencies": {
"acorn": { "acorn": {
"version": "6.0.6", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==" "integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw=="
} }
} }
}, },
@ -3060,9 +3060,9 @@
} }
}, },
"ajv": { "ajv": {
"version": "6.7.0", "version": "6.8.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
"requires": { "requires": {
"fast-deep-equal": "^2.0.1", "fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@ -4262,9 +4262,9 @@
} }
}, },
"binary-extensions": { "binary-extensions": {
"version": "1.12.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz",
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==",
"dev": true "dev": true
}, },
"bindings": { "bindings": {
@ -4632,9 +4632,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30000932", "version": "1.0.30000934",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000934.tgz",
"integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==" "integrity": "sha512-o7yfZn0R9N+mWAuksDsdLsb1gu9o//XK0QSU0zSSReKNRsXsFc/n/psxi0YSPNiqlKxImp5h4DHnAPdwYJ8nNA=="
}, },
"capture-exit": { "capture-exit": {
"version": "1.2.0", "version": "1.2.0",
@ -6357,9 +6357,9 @@
"dev": true "dev": true
}, },
"cssom": { "cssom": {
"version": "0.3.4", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz",
"integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==" "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A=="
}, },
"cssstyle": { "cssstyle": {
"version": "1.1.1", "version": "1.1.1",
@ -6456,17 +6456,17 @@
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
}, },
"d3-shape": { "d3-shape": {
"version": "1.3.3", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.3.tgz", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.4.tgz",
"integrity": "sha512-f7V9wHQCmv4s4N7EmD5i0mwJ5y09L8r1rWVrzH1Av0YfgBKJCnTJGho76rS4HNUIw6tTBbWfFcs4ntP/MKWF4A==", "integrity": "sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg==",
"requires": { "requires": {
"d3-path": "1" "d3-path": "1"
} }
}, },
"d3-time": { "d3-time": {
"version": "1.0.10", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
"integrity": "sha512-hF+NTLCaJHF/JqHN5hE8HVGAXPStEq6/omumPE/SxyHVrR7/qQxusFDo0t0c/44+sCGHthC7yNGFZIEgju0P8g==" "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
}, },
"d3-time-format": { "d3-time-format": {
"version": "2.1.3", "version": "2.1.3",
@ -7234,9 +7234,9 @@
"dev": true "dev": true
}, },
"duplexify": { "duplexify": {
"version": "3.6.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"end-of-stream": "^1.0.0", "end-of-stream": "^1.0.0",
@ -7317,9 +7317,9 @@
"dev": true "dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.109", "version": "1.3.113",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz",
"integrity": "sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow==" "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g=="
}, },
"elliptic": { "elliptic": {
"version": "6.4.1", "version": "6.4.1",
@ -7422,9 +7422,9 @@
} }
}, },
"enzyme-adapter-react-16": { "enzyme-adapter-react-16": {
"version": "1.8.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.8.0.tgz", "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.0.tgz",
"integrity": "sha512-7cVHIKutqnesGeM3CjNFHSvktpypSWBokrBO8wIW+BVx+HGxWCF87W9TpkIIYJqgCtdw9FQGFrAbLg8kSwPRuQ==", "integrity": "sha512-tUqmeLi0Y3PxuiPSykjn8ZMqzCnaRIVywNx0i50+nhd4y/b3JtXRbsvIc8HKxn3heE4t969EI2461Kc9FYxjdw==",
"requires": { "requires": {
"enzyme-adapter-utils": "^1.10.0", "enzyme-adapter-utils": "^1.10.0",
"function.prototype.name": "^1.1.0", "function.prototype.name": "^1.1.0",
@ -7607,9 +7607,9 @@
}, },
"dependencies": { "dependencies": {
"acorn": { "acorn": {
"version": "6.0.6", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==", "integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==",
"dev": true "dev": true
}, },
"acorn-jsx": { "acorn-jsx": {
@ -7682,21 +7682,21 @@
"dev": true "dev": true
}, },
"inquirer": { "inquirer": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-escapes": "^3.0.0", "ansi-escapes": "^3.2.0",
"chalk": "^2.0.0", "chalk": "^2.4.2",
"cli-cursor": "^2.1.0", "cli-cursor": "^2.1.0",
"cli-width": "^2.0.0", "cli-width": "^2.0.0",
"external-editor": "^3.0.0", "external-editor": "^3.0.3",
"figures": "^2.0.0", "figures": "^2.0.0",
"lodash": "^4.17.10", "lodash": "^4.17.11",
"mute-stream": "0.0.7", "mute-stream": "0.0.7",
"run-async": "^2.2.0", "run-async": "^2.2.0",
"rxjs": "^6.1.0", "rxjs": "^6.4.0",
"string-width": "^2.1.0", "string-width": "^2.1.0",
"strip-ansi": "^5.0.0", "strip-ansi": "^5.0.0",
"through": "^2.3.6" "through": "^2.3.6"
@ -8656,39 +8656,13 @@
"dev": true "dev": true
}, },
"flush-write-stream": { "flush-write-stream": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.0.tgz",
"integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", "integrity": "sha512-6MHED/cmsyux1G4/Cek2Z776y9t7WCNd3h2h/HW91vFeU7pzMhA8XvAlDhHcanG5IWuIh/xcC7JASY4WQpG6xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"inherits": "^2.0.1", "inherits": "^2.0.3",
"readable-stream": "^2.0.4" "readable-stream": "^3.1.1"
},
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
}
} }
}, },
"for-in": { "for-in": {
@ -11339,8 +11313,7 @@
"is-wsl": { "is-wsl": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
"dev": true
}, },
"isarray": { "isarray": {
"version": "1.0.0", "version": "1.0.0",
@ -13263,11 +13236,11 @@
} }
}, },
"magic-string": { "magic-string": {
"version": "0.25.1", "version": "0.25.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz",
"integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==", "integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==",
"requires": { "requires": {
"sourcemap-codec": "^1.4.1" "sourcemap-codec": "^1.4.4"
} }
}, },
"make-dir": { "make-dir": {
@ -14163,20 +14136,21 @@
} }
}, },
"node-notifier": { "node-notifier": {
"version": "5.3.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz",
"integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==",
"requires": { "requires": {
"growly": "^1.3.0", "growly": "^1.3.0",
"is-wsl": "^1.1.0",
"semver": "^5.5.0", "semver": "^5.5.0",
"shellwords": "^0.1.1", "shellwords": "^0.1.1",
"which": "^1.3.0" "which": "^1.3.0"
} }
}, },
"node-releases": { "node-releases": {
"version": "1.1.6", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.6.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.7.tgz",
"integrity": "sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg==", "integrity": "sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==",
"requires": { "requires": {
"semver": "^5.3.0" "semver": "^5.3.0"
} }
@ -14457,13 +14431,14 @@
} }
}, },
"npm-package-json-lint": { "npm-package-json-lint": {
"version": "3.4.1", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.4.1.tgz", "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.5.0.tgz",
"integrity": "sha512-W4xlmeFRAY34GQoHUywqoI3PxVZ0hugjbZLiGnVgFjgmvRRcmxKwwmubMe0lAD78vgOHgJZRGubdVXwkp9d3QA==", "integrity": "sha512-MELethOnZW5uVzP65oTQEH2fI6eS/BQEXjvOTyQkUQqGHP9si5pxCWcO+Q4dsahb+4yG7GMxFhpF42AjhCbgRA==",
"requires": { "requires": {
"ajv": "^6.5.4", "ajv": "^6.7.0",
"chalk": "^2.4.1", "chalk": "^2.4.2",
"glob": "^7.1.3", "glob": "^7.1.3",
"ignore": "^5.0.5",
"is-path-inside": "^2.0.0", "is-path-inside": "^2.0.0",
"is-plain-obj": "^1.1.0", "is-plain-obj": "^1.1.0",
"is-resolvable": "^1.1.0", "is-resolvable": "^1.1.0",
@ -14472,7 +14447,14 @@
"plur": "^3.0.1", "plur": "^3.0.1",
"semver": "^5.6.0", "semver": "^5.6.0",
"strip-json-comments": "^2.0.1", "strip-json-comments": "^2.0.1",
"validator": "^10.8.0" "validator": "^10.11.0"
},
"dependencies": {
"ignore": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.5.tgz",
"integrity": "sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA=="
}
} }
}, },
"npm-packlist": { "npm-packlist": {
@ -14561,9 +14543,9 @@
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
}, },
"nwsapi": { "nwsapi": {
"version": "2.0.9", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz",
"integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==" "integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg=="
}, },
"oauth-sign": { "oauth-sign": {
"version": "0.9.0", "version": "0.9.0",
@ -19717,9 +19699,9 @@
} }
}, },
"stylelint-scss": { "stylelint-scss": {
"version": "3.5.1", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.1.tgz", "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.2.tgz",
"integrity": "sha512-XNWKTU1a2EUNWdauxHPTJlGNNQzIbg48OTTIdBs5xTXxpbYAGtX/J+jBqMPjxfdySXijc/mexubuZ+ZinUGGgw==", "integrity": "sha512-HL95s8Q6wihbJe7c7z6rL9GHVHOF3H3tXkVmGutitwn14LYR52JYMwCkcifqlf4nRsvXrUDaoH6OHOdilifyjw==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.11", "lodash": "^4.17.11",
@ -19931,14 +19913,14 @@
} }
}, },
"terser": { "terser": {
"version": "3.14.1", "version": "3.16.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-3.16.1.tgz",
"integrity": "sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw==", "integrity": "sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==",
"dev": true, "dev": true,
"requires": { "requires": {
"commander": "~2.17.1", "commander": "~2.17.1",
"source-map": "~0.6.1", "source-map": "~0.6.1",
"source-map-support": "~0.5.6" "source-map-support": "~0.5.9"
}, },
"dependencies": { "dependencies": {
"commander": { "commander": {
@ -19966,9 +19948,9 @@
} }
}, },
"terser-webpack-plugin": { "terser-webpack-plugin": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz",
"integrity": "sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw==", "integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==",
"dev": true, "dev": true,
"requires": { "requires": {
"cacache": "^11.0.2", "cacache": "^11.0.2",
@ -19976,7 +19958,7 @@
"schema-utils": "^1.0.0", "schema-utils": "^1.0.0",
"serialize-javascript": "^1.4.0", "serialize-javascript": "^1.4.0",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"terser": "^3.8.1", "terser": "^3.16.1",
"webpack-sources": "^1.1.0", "webpack-sources": "^1.1.0",
"worker-farm": "^1.5.2" "worker-farm": "^1.5.2"
}, },

View File

@ -1,3 +1,8 @@
# 1.5.0 (unreleased)
- Improves display of charts where all values are 0.
- Fix X-axis labels in hourly bar charts.
- New `<Search>` prop named `showClearButton`, that will display a 'Clear' button when the search box contains one or more tags.
# 1.4.2 # 1.4.2
- Add emoji-flags dependency - Add emoji-flags dependency
@ -5,7 +10,6 @@
- Chart component: format numbers and prices using store currency settings. - Chart component: format numbers and prices using store currency settings.
- Make `href`/linking optional in SummaryNumber. - Make `href`/linking optional in SummaryNumber.
- Fix SummaryNumber example code. - Fix SummaryNumber example code.
- New `<Search>` prop named `showClearButton`, that will display a 'Clear' button when the search box contains one or more tags.
# 1.4.0 # 1.4.0
- Add download log ip address autocompleter to search component - Add download log ip address autocompleter to search component

View File

@ -197,7 +197,7 @@ export const drawAxis = ( node, params, xOffset ) => {
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' ); : compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
}; };
const yGrids = getYGrids( params.yMax ); const yGrids = getYGrids( params.yMax === 0 ? 1 : params.yMax );
const ticks = params.xTicks.map( d => ( params.type === 'line' ? moment( d ).toDate() : d ) ); const ticks = params.xTicks.map( d => ( params.type === 'line' ? moment( d ).toDate() : d ) );
@ -210,7 +210,7 @@ export const drawAxis = ( node, params, xOffset ) => {
d3AxisBottom( xScale ) d3AxisBottom( xScale )
.tickValues( ticks ) .tickValues( ticks )
.tickFormat( ( d, i ) => params.interval === 'hour' .tickFormat( ( d, i ) => params.interval === 'hour'
? params.xFormat( d ) ? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
: removeDuplicateDates( d, i, ticks, params.xFormat ) ) : removeDuplicateDates( d, i, ticks, params.xFormat ) )
); );
@ -257,7 +257,7 @@ export const drawAxis = ( node, params, xOffset ) => {
.attr( 'text-anchor', 'start' ) .attr( 'text-anchor', 'start' )
.call( .call(
d3AxisLeft( params.yTickOffset ) d3AxisLeft( params.yTickOffset )
.tickValues( yGrids ) .tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) ) .tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
); );

View File

@ -72,7 +72,7 @@ export const getYMax = lineData => {
*/ */
export const getYScale = ( height, yMax ) => export const getYScale = ( height, yMax ) =>
d3ScaleLinear() d3ScaleLinear()
.domain( [ 0, yMax ] ) .domain( [ 0, yMax === 0 ? 1 : yMax ] )
.rangeRound( [ height, 0 ] ); .rangeRound( [ height, 0 ] );
/** /**
@ -83,5 +83,5 @@ export const getYScale = ( height, yMax ) =>
*/ */
export const getYTickOffset = ( height, yMax ) => export const getYTickOffset = ( height, yMax ) =>
d3ScaleLinear() d3ScaleLinear()
.domain( [ 0, yMax ] ) .domain( [ 0, yMax === 0 ? 1 : yMax ] )
.rangeRound( [ height + 12, 12 ] ); .rangeRound( [ height + 12, 12 ] );

View File

@ -107,6 +107,14 @@ describe( 'Y scales', () => {
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] ); expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 100, 0 ] ); expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 100, 0 ] );
} ); } );
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
getYScale( 100, 0 );
const args = scaleLinear().domain.mock.calls;
const lastArgs = args[ args.length - 1 ][ 0 ];
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
} );
} ); } );
describe( 'getYTickOffset', () => { describe( 'getYTickOffset', () => {
@ -116,5 +124,13 @@ describe( 'Y scales', () => {
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] ); expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] ); expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] );
} ); } );
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
getYTickOffset( 100, 0 );
const args = scaleLinear().domain.mock.calls;
const lastArgs = args[ args.length - 1 ][ 0 ];
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
} );
} ); } );
} ); } );

View File

@ -131,7 +131,7 @@
.woocommerce-search__clear { .woocommerce-search__clear {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: calc( 50% - 10px ); top: calc(50% - 10px);
& > .dashicon { & > .dashicon {
color: #c9c9c9; color: #c9c9c9;

View File

@ -0,0 +1,85 @@
<?php
/**
* REST API Init Class Test
*
* @package WooCommerce\Tests\API
* @since 3.5.0
*/
/**
* Class WC_Tests_API_Init
*/
class WC_Tests_API_Init extends WC_REST_Unit_Test_Case {
/**
* Set up.
*/
public function setUp() {
parent::setUp();
$this->queue = new WC_Admin_Test_Action_Queue();
WC_Admin_Api_Init::set_queue( $this->queue );
}
/**
* Tear down.
*/
public function tearDown() {
parent::tearDown();
WC_Admin_Api_Init::set_queue( null );
$this->queue->actions = array();
}
/**
* Cause a failure when updating order stats for the test order, without deleting it.
*
* @param string $query Query.
* @return string
*/
public function filter_order_query( $query ) {
if (
0 === strpos( $query, 'REPLACE INTO' ) &&
false !== strpos( $query, WC_Admin_Reports_Orders_Stats_Data_Store::TABLE_NAME )
) {
remove_filter( 'query', array( $this, 'filter_order_query' ) );
return 'THIS WONT MATCH';
}
return $query;
}
/**
* Test that a retry job is scheduled for a failed sync.
*
* @return void
*/
public function test_order_sync_retries_on_failure() {
// Create a test Order.
$product = new WC_Product_Simple();
$product->set_name( 'Test Product' );
$product->set_regular_price( 25 );
$product->save();
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_total( 100 ); // $25 x 4.
$order->save();
// Clear the existing action queue (the above save adds an action).
$this->queue->actions = array();
// Force a failure by sabotaging the query run after retreiving order coupons.
add_filter( 'query', array( $this, 'filter_order_query' ) );
// Initiate sync.
WC_Admin_Api_Init::orders_lookup_process_order( $order->get_id() );
// Verify that a retry job was scheduled.
$this->assertCount( 1, $this->queue->actions );
$this->assertArraySubset(
array(
'hook' => WC_Admin_Api_Init::SINGLE_ORDER_ACTION,
'args' => array( $order->get_id() ),
),
$this->queue->actions[0]
);
}
}

View File

@ -64,6 +64,8 @@ class WC_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4. $order->set_total( 100 ); // $25 x 4.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' ); $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );

View File

@ -87,6 +87,8 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
$order_2c->set_date_created( $time ); $order_2c->set_date_created( $time );
$order_2c->save(); $order_2c->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint ); $request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( $request->set_query_params(
array( array(
@ -101,9 +103,10 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
$expected_reports = array( $expected_reports = array(
'totals' => array( 'totals' => array(
'amount' => 4, 'amount' => 4.0,
'coupons_count' => 2, 'coupons_count' => 2,
'orders_count' => 2, 'orders_count' => 2,
'segments' => array(),
), ),
'intervals' => array( 'intervals' => array(
array( array(
@ -113,9 +116,10 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
'date_end' => date( 'Y-m-d 23:59:59', $time ), 'date_end' => date( 'Y-m-d 23:59:59', $time ),
'date_end_gmt' => date( 'Y-m-d 23:59:59', $time ), 'date_end_gmt' => date( 'Y-m-d 23:59:59', $time ),
'subtotals' => (object) array( 'subtotals' => (object) array(
'amount' => 4, 'amount' => 4.0,
'coupons_count' => 2, 'coupons_count' => 2,
'orders_count' => 2, 'orders_count' => 2,
'segments' => array(),
), ),
), ),
), ),
@ -150,10 +154,11 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'intervals', $properties ); $this->assertArrayHasKey( 'intervals', $properties );
$totals = $properties['totals']['properties']; $totals = $properties['totals']['properties'];
$this->assertEquals( 3, count( $totals ) ); $this->assertEquals( 4, count( $totals ) );
$this->assertArrayHasKey( 'amount', $totals ); $this->assertArrayHasKey( 'amount', $totals );
$this->assertArrayHasKey( 'coupons_count', $totals ); $this->assertArrayHasKey( 'coupons_count', $totals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'segments', $totals );
$intervals = $properties['intervals']['items']['properties']; $intervals = $properties['intervals']['items']['properties'];
$this->assertEquals( 6, count( $intervals ) ); $this->assertEquals( 6, count( $intervals ) );
@ -165,9 +170,11 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'subtotals', $intervals ); $this->assertArrayHasKey( 'subtotals', $intervals );
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 3, count( $subtotals ) ); $this->assertEquals( 4, count( $subtotals ) );
$this->assertArrayHasKey( 'amount', $totals ); $this->assertArrayHasKey( 'amount', $subtotals );
$this->assertArrayHasKey( 'coupons_count', $totals ); $this->assertArrayHasKey( 'coupons_count', $subtotals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $subtotals );
$this->assertArrayHasKey( 'segments', $subtotals );
} }
} }

View File

@ -82,6 +82,8 @@ class WC_Tests_API_Reports_Coupons extends WC_REST_Unit_Test_Case {
$order_2c->calculate_totals(); $order_2c->calculate_totals();
$order_2c->save(); $order_2c->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$coupon_reports = $response->get_data(); $coupon_reports = $response->get_data();

View File

@ -126,6 +126,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
$order->set_total( 9.12 ); $order->set_total( 9.12 );
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint ); $request = new WP_REST_Request( 'GET', $this->endpoint );
$response = $this->server->dispatch( $request ); $response = $this->server->dispatch( $request );
$reports = $response->get_data(); $reports = $response->get_data();

View File

@ -134,6 +134,8 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); $order->set_total( 100 );
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint ); $request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( $request->set_query_params(
array( array(

View File

@ -3,7 +3,10 @@
* Reports Orders Stats REST API Test * Reports Orders Stats REST API Test
* *
* @package WooCommerce\Tests\API * @package WooCommerce\Tests\API
* @since 3.5.0 */
/**
* WC_Tests_API_Reports_Orders_Stats
*/ */
/** /**
@ -90,11 +93,17 @@ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'intervals', $properties ); $this->assertArrayHasKey( 'intervals', $properties );
$totals = $properties['totals']['properties']; $totals = $properties['totals']['properties'];
$this->assertEquals( 4, count( $totals ) ); $this->assertEquals( 10, count( $totals ) );
$this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'net_revenue', $totals );
$this->assertArrayHasKey( 'avg_order_value', $totals ); $this->assertArrayHasKey( 'avg_order_value', $totals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'avg_items_per_order', $totals ); $this->assertArrayHasKey( 'avg_items_per_order', $totals );
$this->assertArrayHasKey( 'num_items_sold', $totals );
$this->assertArrayHasKey( 'coupons', $totals );
$this->assertArrayHasKey( 'num_returning_customers', $totals );
$this->assertArrayHasKey( 'num_new_customers', $totals );
$this->assertArrayHasKey( 'products', $totals );
$this->assertArrayHasKey( 'segments', $totals );
$intervals = $properties['intervals']['items']['properties']; $intervals = $properties['intervals']['items']['properties'];
$this->assertEquals( 6, count( $intervals ) ); $this->assertEquals( 6, count( $intervals ) );
@ -106,10 +115,15 @@ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'subtotals', $intervals ); $this->assertArrayHasKey( 'subtotals', $intervals );
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 4, count( $subtotals ) ); $this->assertEquals( 9, count( $subtotals ) );
$this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'net_revenue', $subtotals );
$this->assertArrayHasKey( 'avg_order_value', $totals ); $this->assertArrayHasKey( 'avg_order_value', $subtotals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $subtotals );
$this->assertArrayHasKey( 'avg_items_per_order', $totals ); $this->assertArrayHasKey( 'avg_items_per_order', $subtotals );
$this->assertArrayHasKey( 'num_items_sold', $subtotals );
$this->assertArrayHasKey( 'coupons', $subtotals );
$this->assertArrayHasKey( 'num_returning_customers', $subtotals );
$this->assertArrayHasKey( 'num_new_customers', $subtotals );
$this->assertArrayHasKey( 'segments', $subtotals );
} }
} }

View File

@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4. $order->set_total( 100 ); // $25 x 4.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$expected_customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( 1 ); $expected_customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( 1 );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );

View File

@ -84,6 +84,8 @@ class WC_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Test_Case
$object->set_user_ip_address( '1.2.3.4' ); $object->set_user_ip_address( '1.2.3.4' );
$object->save(); $object->save();
WC_Helper_Queue::run_all_pending();
$time = time(); $time = time();
$request = new WP_REST_Request( 'GET', $this->endpoint ); $request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( $request->set_query_params(

View File

@ -71,6 +71,8 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$request = new WP_REST_Request( 'GET', $this->endpoint ); $request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( $request->set_query_params(
array( array(
@ -89,6 +91,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
'net_revenue' => 100.0, 'net_revenue' => 100.0,
'orders_count' => 1, 'orders_count' => 1,
'products_count' => 1, 'products_count' => 1,
'segments' => array(),
), ),
'intervals' => array( 'intervals' => array(
array( array(
@ -102,6 +105,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
'net_revenue' => 100.0, 'net_revenue' => 100.0,
'orders_count' => 1, 'orders_count' => 1,
'products_count' => 1, 'products_count' => 1,
'segments' => array(),
), ),
), ),
), ),
@ -140,10 +144,11 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'intervals', $properties ); $this->assertArrayHasKey( 'intervals', $properties );
$totals = $properties['totals']['properties']; $totals = $properties['totals']['properties'];
$this->assertEquals( 3, count( $totals ) ); $this->assertEquals( 4, count( $totals ) );
$this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'net_revenue', $totals );
$this->assertArrayHasKey( 'items_sold', $totals ); $this->assertArrayHasKey( 'items_sold', $totals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'segments', $totals );
$intervals = $properties['intervals']['items']['properties']; $intervals = $properties['intervals']['items']['properties'];
$this->assertEquals( 6, count( $intervals ) ); $this->assertEquals( 6, count( $intervals ) );
@ -155,9 +160,10 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'subtotals', $intervals ); $this->assertArrayHasKey( 'subtotals', $intervals );
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 3, count( $subtotals ) ); $this->assertEquals( 4, count( $subtotals ) );
$this->assertArrayHasKey( 'net_revenue', $subtotals ); $this->assertArrayHasKey( 'net_revenue', $subtotals );
$this->assertArrayHasKey( 'items_sold', $subtotals ); $this->assertArrayHasKey( 'items_sold', $subtotals );
$this->assertArrayHasKey( 'orders_count', $subtotals ); $this->assertArrayHasKey( 'orders_count', $subtotals );
$this->assertArrayHasKey( 'segments', $subtotals );
} }
} }

View File

@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4. $order->set_total( 100 ); // $25 x 4.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data(); $reports = $response->get_data();

View File

@ -97,7 +97,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'intervals', $properties ); $this->assertArrayHasKey( 'intervals', $properties );
$totals = $properties['totals']['properties']; $totals = $properties['totals']['properties'];
$this->assertEquals( 9, count( $totals ) ); $this->assertEquals( 10, count( $totals ) );
$this->assertArrayHasKey( 'gross_revenue', $totals ); $this->assertArrayHasKey( 'gross_revenue', $totals );
$this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'net_revenue', $totals );
$this->assertArrayHasKey( 'coupons', $totals ); $this->assertArrayHasKey( 'coupons', $totals );
@ -107,6 +107,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'num_items_sold', $totals ); $this->assertArrayHasKey( 'num_items_sold', $totals );
$this->assertArrayHasKey( 'products', $totals ); $this->assertArrayHasKey( 'products', $totals );
$this->assertArrayHasKey( 'segments', $totals );
$intervals = $properties['intervals']['items']['properties']; $intervals = $properties['intervals']['items']['properties'];
$this->assertEquals( 6, count( $intervals ) ); $this->assertEquals( 6, count( $intervals ) );
@ -118,7 +119,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'subtotals', $intervals ); $this->assertArrayHasKey( 'subtotals', $intervals );
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 8, count( $subtotals ) ); $this->assertEquals( 9, count( $subtotals ) );
$this->assertArrayHasKey( 'gross_revenue', $subtotals ); $this->assertArrayHasKey( 'gross_revenue', $subtotals );
$this->assertArrayHasKey( 'net_revenue', $subtotals ); $this->assertArrayHasKey( 'net_revenue', $subtotals );
$this->assertArrayHasKey( 'coupons', $subtotals ); $this->assertArrayHasKey( 'coupons', $subtotals );
@ -127,5 +128,6 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'refunds', $subtotals ); $this->assertArrayHasKey( 'refunds', $subtotals );
$this->assertArrayHasKey( 'orders_count', $subtotals ); $this->assertArrayHasKey( 'orders_count', $subtotals );
$this->assertArrayHasKey( 'num_items_sold', $subtotals ); $this->assertArrayHasKey( 'num_items_sold', $subtotals );
$this->assertArrayHasKey( 'segments', $subtotals );
} }
} }

View File

@ -143,12 +143,13 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
$totals = $properties['totals']['properties']; $totals = $properties['totals']['properties'];
$this->assertEquals( 5, count( $totals ) ); $this->assertEquals( 6, count( $totals ) );
$this->assertArrayHasKey( 'order_tax', $totals ); $this->assertArrayHasKey( 'order_tax', $totals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals );
$this->assertArrayHasKey( 'shipping_tax', $totals ); $this->assertArrayHasKey( 'shipping_tax', $totals );
$this->assertArrayHasKey( 'tax_codes', $totals ); $this->assertArrayHasKey( 'tax_codes', $totals );
$this->assertArrayHasKey( 'total_tax', $totals ); $this->assertArrayHasKey( 'total_tax', $totals );
$this->assertArrayHasKey( 'segments', $totals );
$intervals = $properties['intervals']['items']['properties']; $intervals = $properties['intervals']['items']['properties'];
$this->assertEquals( 6, count( $intervals ) ); $this->assertEquals( 6, count( $intervals ) );
@ -160,12 +161,13 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'subtotals', $intervals ); $this->assertArrayHasKey( 'subtotals', $intervals );
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
$this->assertEquals( 5, count( $subtotals ) ); $this->assertEquals( 6, count( $subtotals ) );
$this->assertArrayHasKey( 'order_tax', $totals ); $this->assertArrayHasKey( 'order_tax', $subtotals );
$this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'orders_count', $subtotals );
$this->assertArrayHasKey( 'shipping_tax', $totals ); $this->assertArrayHasKey( 'shipping_tax', $subtotals );
$this->assertArrayHasKey( 'tax_codes', $totals ); $this->assertArrayHasKey( 'tax_codes', $subtotals );
$this->assertArrayHasKey( 'total_tax', $totals ); $this->assertArrayHasKey( 'total_tax', $subtotals );
$this->assertArrayHasKey( 'segments', $subtotals );
} }
} }

View File

@ -91,6 +91,8 @@ class WC_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case {
) )
); );
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data(); $reports = $response->get_data();

View File

@ -65,6 +65,8 @@ class WC_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case {
$order->set_total( 100 ); // $25 x 4. $order->set_total( 100 ); // $25 x 4.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
$reports = $response->get_data(); $reports = $response->get_data();

View File

@ -136,7 +136,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
// insert a blocking job. // insert a blocking job.
WC_Admin_Api_Init::queue()->schedule_single( time(), 'blocking_job', array( 'stuff' ) ); WC_Admin_Api_Init::queue()->schedule_single( time(), 'blocking_job', array( 'stuff' ) );
// queue an action that depends on blocking job. // queue an action that depends on blocking job.
WC_Admin_Api_Init::queue_dependent_action( 'dependent_action', 'blocking_job' ); WC_Admin_Api_Init::queue_dependent_action( 'dependent_action', array(), 'blocking_job' );
// verify that the action was properly blocked. // verify that the action was properly blocked.
$this->assertEmpty( $this->assertEmpty(
WC_Admin_Api_Init::queue()->search( WC_Admin_Api_Init::queue()->search(
@ -151,13 +151,13 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
WC_Admin_Api_Init::queue()->search( WC_Admin_Api_Init::queue()->search(
array( array(
'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION, 'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION,
'args' => array( 'dependent_action', 'blocking_job' ), 'args' => array( 'dependent_action', array(), 'blocking_job' ),
) )
) )
); );
// queue an action that isn't blocked. // queue an action that isn't blocked.
WC_Admin_Api_Init::queue_dependent_action( 'another_dependent_action', 'nonexistant_blocking_job' ); WC_Admin_Api_Init::queue_dependent_action( 'another_dependent_action', array(), 'nonexistant_blocking_job' );
// verify that the dependent action was queued. // verify that the dependent action was queued.
$this->assertCount( $this->assertCount(
1, 1,
@ -172,7 +172,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
WC_Admin_Api_Init::queue()->search( WC_Admin_Api_Init::queue()->search(
array( array(
'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION, 'hook' => WC_Admin_Api_Init::QUEUE_DEPEDENT_ACTION,
'args' => array( 'another_dependent_action', 'nonexistant_blocking_job' ), 'args' => array( 'another_dependent_action', array(), 'nonexistant_blocking_job' ),
) )
) )
); );

View File

@ -123,3 +123,4 @@ wc_test_includes();
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-reports.php'; require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-reports.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-admin-notes.php'; require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-admin-notes.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-test-action-queue.php'; require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-test-action-queue.php';
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-queue.php';

View File

@ -0,0 +1,32 @@
<?php
/**
* Helper code for wc-admin unit tests.
*
* @package WooCommerce\Tests\Framework\Helpers
*/
/**
* Class WC_Helper_Queue.
*
* This helper class should ONLY be used for unit tests!.
*/
class WC_Helper_Queue {
/**
* Run all pending queued actions.
*
* @return void
*/
public static function run_all_pending() {
$jobs = WC()->queue()->search(
array(
'per_page' => -1,
'status' => 'pending',
'claimed' => false,
)
);
foreach ( $jobs as $job ) {
$job->execute();
}
}
}

View File

@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
$order_2c->calculate_totals(); $order_2c->calculate_totals();
$order_2c->save(); $order_2c->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Coupons_Stats_Data_Store(); $data_store = new WC_Admin_Reports_Coupons_Stats_Data_Store();
$start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() ); $end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() );
@ -72,9 +74,10 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
'pages' => 1, 'pages' => 1,
'page_no' => 1, 'page_no' => 1,
'totals' => (object) array( 'totals' => (object) array(
'amount' => 2 * $coupon_1_amount + $coupon_2_amount, 'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ),
'coupons_count' => 2, 'coupons_count' => 2,
'orders_count' => 2, 'orders_count' => 2,
'segments' => array(),
), ),
'intervals' => array( 'intervals' => array(
array( array(
@ -84,9 +87,10 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
'date_end' => $end_datetime->format( 'Y-m-d H:i:s' ), 'date_end' => $end_datetime->format( 'Y-m-d H:i:s' ),
'date_end_gmt' => $end_datetime->format( 'Y-m-d H:i:s' ), 'date_end_gmt' => $end_datetime->format( 'Y-m-d H:i:s' ),
'subtotals' => (object) array( 'subtotals' => (object) array(
'amount' => 2 * $coupon_1_amount + $coupon_2_amount, 'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ),
'coupons_count' => 2, 'coupons_count' => 2,
'orders_count' => 2, 'orders_count' => 2,
'segments' => array(),
), ),
), ),
), ),

View File

@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons extends WC_Unit_Test_Case {
$order_2c->calculate_totals(); $order_2c->calculate_totals();
$order_2c->save(); $order_2c->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Coupons_Data_Store(); $data_store = new WC_Admin_Reports_Coupons_Data_Store();
$start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() ); $end_time = date( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() );

View File

@ -38,6 +38,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store(); $data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS ); $end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -113,6 +115,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order_2->set_date_created( $date_created_2 ); $order_2->set_date_created( $date_created_2 );
$order_2->save(); $order_2->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store(); $data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order_2->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS ); $end_time = date( 'Y-m-d H:00:00', $order_2->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -211,6 +215,9 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
$order->set_shipping_tax( 2 ); $order->set_shipping_tax( 2 );
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store(); $data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS ); $end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -249,4 +256,76 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
} }
/**
* Tests that line item refunds are reflected in product stats.
*/
public function test_populate_and_refund() {
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 = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_shipping_total( 10 );
$order->set_discount_total( 20 );
$order->set_discount_tax( 0 );
$order->set_cart_tax( 5 );
$order->set_shipping_tax( 2 );
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save();
foreach ( $order->get_items() as $item_key => $item_values ) {
$item_data = $item_values->get_data();
$refund = wc_create_refund(
array(
'amount' => 12,
'order_id' => $order->get_id(),
'line_items' => array(
$item_data['id'] => array(
'qty' => 1,
'refund_total' => 10,
),
),
)
);
break;
}
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Products_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
$args = array(
'after' => $start_time,
'before' => $end_time,
);
// Test retrieving the stats through the data store.
$data = $data_store->get_data( $args );
$expected_data = (object) array(
'total' => 1,
'pages' => 1,
'page_no' => 1,
'data' => array(
0 => array(
'product_id' => $product->get_id(),
'items_sold' => 3,
'net_revenue' => 90.0, // $25 * 4 - $10 refund.
'orders_count' => 1,
'extended_info' => new ArrayObject(),
),
),
);
$this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class.
$query = new WC_Admin_Reports_Products_Query( $args );
$this->assertEquals( $expected_data, $query->get_data() );
}
} }

View File

@ -37,6 +37,8 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
$order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
// /reports/revenue/stats is mapped to Orders_Data_Store. // /reports/revenue/stats is mapped to Orders_Data_Store.
$data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store();
@ -62,7 +64,8 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
'avg_order_value' => 80, 'avg_order_value' => 80,
'num_returning_customers' => 0, 'num_returning_customers' => 0,
'num_new_customers' => 1, 'num_new_customers' => 1,
'products' => '1', 'products' => 1,
'segments' => array(),
), ),
'intervals' => array( 'intervals' => array(
array( array(
@ -84,6 +87,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
'avg_order_value' => 80, 'avg_order_value' => 80,
'num_returning_customers' => 0, 'num_returning_customers' => 0,
'num_new_customers' => 1, 'num_new_customers' => 1,
'segments' => array(),
), ),
), ),
), ),
@ -107,6 +111,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
'shipping' => 10, 'shipping' => 10,
'net_revenue' => 80, 'net_revenue' => 80,
'products' => '1', 'products' => '1',
'segments' => array(),
), ),
'intervals' => array( 'intervals' => array(
array( array(
@ -124,6 +129,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
'taxes' => 7, 'taxes' => 7,
'shipping' => 10, 'shipping' => 10,
'net_revenue' => 80, 'net_revenue' => 80,
'segments' => array(),
), ),
), ),
), ),

View File

@ -39,6 +39,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
$order->set_status( 'completed' ); $order->set_status( 'completed' );
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Variations_Data_Store(); $data_store = new WC_Admin_Reports_Variations_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS ); $end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );
@ -106,6 +108,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
$order->set_status( 'completed' ); $order->set_status( 'completed' );
$order->save(); $order->save();
WC_Helper_Queue::run_all_pending();
$data_store = new WC_Admin_Reports_Variations_Data_Store(); $data_store = new WC_Admin_Reports_Variations_Data_Store();
$start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
$end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS ); $end_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() + HOUR_IN_SECONDS );