Merge branch 'master' into fix/1012-2
# Conflicts: # tests/reports/class-wc-tests-reports-orders-stats.php
This commit is contained in:
commit
b69d2aa2ab
|
@ -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.
|
|
@ -103,7 +103,7 @@ describe( '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 };
|
||||
|
||||
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
|
||||
|
|
|
@ -20,6 +20,7 @@ import { onQueryChange } from '@woocommerce/navigation';
|
|||
*/
|
||||
import ReportError from 'analytics/components/report-error';
|
||||
import { getReportChartData, getReportTableData } from 'wc-api/reports/utils';
|
||||
import { QUERY_DEFAULTS } from 'wc-api/constants';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import { extendTableData } from './utils';
|
||||
|
||||
|
@ -84,12 +85,12 @@ class ReportTable extends Component {
|
|||
}
|
||||
|
||||
const isRequesting = tableData.isRequesting || primaryData.isRequesting;
|
||||
const totals = get( primaryData, [ 'data', 'totals' ], null );
|
||||
const totalResults = items.totalResults || 0;
|
||||
const totals = get( primaryData, [ 'data', 'totals' ], {} );
|
||||
const totalResults = items.totalResults;
|
||||
const { headers, ids, rows, summary } = applyFilters( TABLE_FILTER, {
|
||||
endpoint: endpoint,
|
||||
headers: getHeadersContent(),
|
||||
ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : null,
|
||||
ids: itemIdField ? items.data.map( item => item[ itemIdField ] ) : [],
|
||||
rows: getRowsContent( items.data ),
|
||||
totals: totals,
|
||||
summary: getSummary ? getSummary( totals, totalResults ) : null,
|
||||
|
@ -107,7 +108,7 @@ class ReportTable extends Component {
|
|||
onQueryChange={ onQueryChange }
|
||||
onColumnsChange={ this.onColumnsChange }
|
||||
rows={ rows }
|
||||
rowsPerPage={ parseInt( query.per_page ) }
|
||||
rowsPerPage={ parseInt( query.per_page ) || QUERY_DEFAULTS.pageSize }
|
||||
summary={ summary }
|
||||
totalRows={ totalResults }
|
||||
{ ...tableProps }
|
||||
|
@ -159,7 +160,7 @@ ReportTable.propTypes = {
|
|||
* Primary data of that report. If it's not provided, it will be automatically
|
||||
* 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
|
||||
* loaded via the provided `endpoint`.
|
||||
|
@ -176,13 +177,23 @@ ReportTable.propTypes = {
|
|||
};
|
||||
|
||||
ReportTable.defaultProps = {
|
||||
tableData: {},
|
||||
primaryData: {},
|
||||
tableData: {
|
||||
items: {
|
||||
data: [],
|
||||
totalResults: 0,
|
||||
},
|
||||
query: {},
|
||||
},
|
||||
tableQuery: {},
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSelect( ( select, 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 primaryData = getSummary
|
||||
? getReportChartData( chartEndpoint, 'primary', query, select )
|
||||
|
|
|
@ -111,27 +111,24 @@ class CategoriesReportTable extends Component {
|
|||
} );
|
||||
}
|
||||
|
||||
getSummary( totals, totalResults ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
getSummary( totals, totalResults = 0 ) {
|
||||
const { items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'category', 'categories', totalResults, 'wc-admin' ),
|
||||
value: numberFormat( totalResults ),
|
||||
},
|
||||
{
|
||||
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
|
||||
value: numberFormat( totals.items_sold ),
|
||||
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
|
||||
value: numberFormat( items_sold ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.net_revenue ),
|
||||
value: formatCurrency( net_revenue ),
|
||||
},
|
||||
{
|
||||
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -168,7 +165,12 @@ class CategoriesReportTable extends Component {
|
|||
}
|
||||
|
||||
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 tableQuery = {
|
||||
per_page: -1,
|
||||
|
|
|
@ -129,21 +129,19 @@ export default class CouponsReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const { coupons_count = 0, orders_count = 0, amount = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'coupon', 'coupons', totals.coupons_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.coupons_count ),
|
||||
label: _n( 'coupon', 'coupons', coupons_count, 'wc-admin' ),
|
||||
value: numberFormat( coupons_count ),
|
||||
},
|
||||
{
|
||||
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
{
|
||||
label: __( 'amount discounted', 'wc-admin' ),
|
||||
value: formatCurrency( totals.amount ),
|
||||
value: formatCurrency( amount ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const advancedFilters = {
|
|||
input: {
|
||||
component: 'Search',
|
||||
type: 'customers',
|
||||
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
|
||||
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
||||
id: customer.id,
|
||||
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
|
||||
} ) ),
|
||||
|
@ -157,7 +157,7 @@ export const advancedFilters = {
|
|||
input: {
|
||||
component: 'Search',
|
||||
type: 'emails',
|
||||
getLabels: getRequestByIdString( NAMESPACE + 'customers', customer => ( {
|
||||
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
||||
id: customer.id,
|
||||
label: customer.email,
|
||||
} ) ),
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, _n } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { Tooltip } from '@wordpress/components';
|
||||
|
||||
|
@ -191,25 +191,28 @@ export default class CustomersReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const {
|
||||
customers_count = 0,
|
||||
avg_orders_count = 0,
|
||||
avg_total_spend = 0,
|
||||
avg_avg_order_value = 0,
|
||||
} = totals;
|
||||
return [
|
||||
{
|
||||
label: __( 'customers', 'wc-admin' ),
|
||||
value: numberFormat( totals.customers_count ),
|
||||
label: _n( 'customer', 'customers', customers_count, 'wc-admin' ),
|
||||
value: numberFormat( customers_count ),
|
||||
},
|
||||
{
|
||||
label: __( 'average orders', 'wc-admin' ),
|
||||
value: numberFormat( totals.avg_orders_count ),
|
||||
label: _n( 'average order', 'average orders', avg_orders_count, 'wc-admin' ),
|
||||
value: numberFormat( avg_orders_count ),
|
||||
},
|
||||
{
|
||||
label: __( 'average lifetime spend', 'wc-admin' ),
|
||||
value: formatCurrency( totals.avg_total_spend ),
|
||||
value: formatCurrency( avg_total_spend ),
|
||||
},
|
||||
{
|
||||
label: __( 'average order value', 'wc-admin' ),
|
||||
value: formatCurrency( totals.avg_avg_order_value ),
|
||||
value: formatCurrency( avg_avg_order_value ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -121,9 +121,7 @@ export default class CouponsReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const { download_count = 0 } = totals;
|
||||
const { query } = this.props;
|
||||
const dates = getCurrentDates( query );
|
||||
const after = moment( dates.primary.after );
|
||||
|
@ -136,8 +134,8 @@ export default class CouponsReportTable extends Component {
|
|||
value: numberFormat( days ),
|
||||
},
|
||||
{
|
||||
label: _n( 'download', 'downloads', totals.download_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.download_count ),
|
||||
label: _n( 'download', 'downloads', download_count, 'wc-admin' ),
|
||||
value: numberFormat( download_count ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ export default compose(
|
|||
const items = searchItemsByString( select, report, search );
|
||||
const ids = Object.keys( items );
|
||||
if ( ! ids.length ) {
|
||||
return {}; // @TODO if no results were found, we should avoid making a server request.
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -182,42 +182,48 @@ export default class OrdersReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const {
|
||||
orders_count = 0,
|
||||
num_new_customers = 0,
|
||||
num_returning_customers = 0,
|
||||
products = 0,
|
||||
num_items_sold = 0,
|
||||
coupons = 0,
|
||||
net_revenue = 0,
|
||||
} = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
{
|
||||
label: _n( 'new customer', 'new customers', totals.num_new_customers, 'wc-admin' ),
|
||||
value: numberFormat( totals.num_new_customers ),
|
||||
label: _n( 'new customer', 'new customers', num_new_customers, 'wc-admin' ),
|
||||
value: numberFormat( num_new_customers ),
|
||||
},
|
||||
{
|
||||
label: _n(
|
||||
'returning customer',
|
||||
'returning customers',
|
||||
totals.num_returning_customers,
|
||||
num_returning_customers,
|
||||
'wc-admin'
|
||||
),
|
||||
value: numberFormat( totals.num_returning_customers ),
|
||||
value: numberFormat( num_returning_customers ),
|
||||
},
|
||||
{
|
||||
label: _n( 'product', 'products', totals.products, 'wc-admin' ),
|
||||
value: numberFormat( totals.products ),
|
||||
label: _n( 'product', 'products', products, 'wc-admin' ),
|
||||
value: numberFormat( products ),
|
||||
},
|
||||
{
|
||||
label: _n( 'item sold', 'items sold', totals.num_items_sold, 'wc-admin' ),
|
||||
value: numberFormat( totals.num_items_sold ),
|
||||
label: _n( 'item sold', 'items sold', num_items_sold, 'wc-admin' ),
|
||||
value: numberFormat( num_items_sold ),
|
||||
},
|
||||
{
|
||||
label: _n( 'coupon', 'coupons', totals.coupons, 'wc-admin' ),
|
||||
value: numberFormat( totals.coupons ),
|
||||
label: _n( 'coupon', 'coupons', coupons, 'wc-admin' ),
|
||||
value: numberFormat( coupons ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.net_revenue ),
|
||||
value: formatCurrency( net_revenue ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -139,26 +139,24 @@ export default class VariationsReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
// @TODO: When primaryData is segmented, fix this to reflect variations, not products.
|
||||
label: _n( 'variation sold', 'variations sold', totals.products_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.products_count ),
|
||||
label: _n( 'variation sold', 'variations sold', products_count, 'wc-admin' ),
|
||||
value: numberFormat( products_count ),
|
||||
},
|
||||
{
|
||||
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
|
||||
value: numberFormat( totals.items_sold ),
|
||||
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
|
||||
value: numberFormat( items_sold ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.net_revenue ),
|
||||
value: formatCurrency( net_revenue ),
|
||||
},
|
||||
{
|
||||
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -204,25 +204,23 @@ class ProductsReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const { products_count = 0, items_sold = 0, net_revenue = 0, orders_count = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'product sold', 'products sold', totals.products_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.products_count ),
|
||||
label: _n( 'product sold', 'products sold', products_count, 'wc-admin' ),
|
||||
value: numberFormat( products_count ),
|
||||
},
|
||||
{
|
||||
label: _n( 'item sold', 'items sold', totals.items_sold, 'wc-admin' ),
|
||||
value: numberFormat( totals.items_sold ),
|
||||
label: _n( 'item sold', 'items sold', items_sold, 'wc-admin' ),
|
||||
value: numberFormat( items_sold ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.net_revenue ),
|
||||
value: formatCurrency( net_revenue ),
|
||||
},
|
||||
{
|
||||
label: _n( 'orders', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'orders', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -259,7 +257,12 @@ class ProductsReportTable extends Component {
|
|||
}
|
||||
|
||||
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 tableQuery = {
|
||||
per_page: -1,
|
||||
|
|
|
@ -153,43 +153,48 @@ class RevenueReportTable extends Component {
|
|||
} );
|
||||
}
|
||||
|
||||
getSummary( totals, totalResults ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
getSummary( totals, totalResults = 0 ) {
|
||||
const {
|
||||
orders_count = 0,
|
||||
gross_revenue = 0,
|
||||
refunds = 0,
|
||||
coupons = 0,
|
||||
taxes = 0,
|
||||
shipping = 0,
|
||||
net_revenue = 0,
|
||||
} = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'day', 'days', totalResults, 'wc-admin' ),
|
||||
value: numberFormat( totalResults ),
|
||||
},
|
||||
{
|
||||
label: _n( 'order', 'orders', totals.orders_count, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
{
|
||||
label: __( 'gross revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.gross_revenue ),
|
||||
value: formatCurrency( gross_revenue ),
|
||||
},
|
||||
{
|
||||
label: __( 'refunds', 'wc-admin' ),
|
||||
value: formatCurrency( totals.refunds ),
|
||||
value: formatCurrency( refunds ),
|
||||
},
|
||||
{
|
||||
label: __( 'coupons', 'wc-admin' ),
|
||||
value: formatCurrency( totals.coupons ),
|
||||
value: formatCurrency( coupons ),
|
||||
},
|
||||
{
|
||||
label: __( 'taxes', 'wc-admin' ),
|
||||
value: formatCurrency( totals.taxes ),
|
||||
value: formatCurrency( taxes ),
|
||||
},
|
||||
{
|
||||
label: __( 'shipping', 'wc-admin' ),
|
||||
value: formatCurrency( totals.shipping ),
|
||||
value: formatCurrency( shipping ),
|
||||
},
|
||||
{
|
||||
label: __( 'net revenue', 'wc-admin' ),
|
||||
value: formatCurrency( totals.net_revenue ),
|
||||
value: formatCurrency( net_revenue ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -103,25 +103,23 @@ export default class StockReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'product', 'products', totals.products, 'wc-admin' ),
|
||||
value: numberFormat( totals.products ),
|
||||
label: _n( 'product', 'products', products, 'wc-admin' ),
|
||||
value: numberFormat( products ),
|
||||
},
|
||||
{
|
||||
label: __( 'out of stock', totals.out_of_stock, 'wc-admin' ),
|
||||
value: numberFormat( totals.out_of_stock ),
|
||||
label: __( 'out of stock', out_of_stock, 'wc-admin' ),
|
||||
value: numberFormat( out_of_stock ),
|
||||
},
|
||||
{
|
||||
label: __( 'low stock', totals.low_stock, 'wc-admin' ),
|
||||
value: numberFormat( totals.low_stock ),
|
||||
label: __( 'low stock', low_stock, 'wc-admin' ),
|
||||
value: numberFormat( low_stock ),
|
||||
},
|
||||
{
|
||||
label: __( 'in stock', totals.in_stock, 'wc-admin' ),
|
||||
value: numberFormat( totals.in_stock ),
|
||||
label: __( 'in stock', in_stock, 'wc-admin' ),
|
||||
value: numberFormat( in_stock ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export const filters = [
|
|||
settings: {
|
||||
type: 'taxes',
|
||||
param: 'taxes',
|
||||
getLabels: getRequestByIdString( NAMESPACE + 'taxes', tax => ( {
|
||||
getLabels: getRequestByIdString( NAMESPACE + '/taxes', tax => ( {
|
||||
id: tax.id,
|
||||
label: getTaxCode( tax ),
|
||||
} ) ),
|
||||
|
|
|
@ -111,29 +111,33 @@ export default class TaxesReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
if ( ! totals ) {
|
||||
return [];
|
||||
}
|
||||
const {
|
||||
tax_codes = 0,
|
||||
total_tax = 0,
|
||||
order_tax = 0,
|
||||
shipping_tax = 0,
|
||||
orders_count = 0,
|
||||
} = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'tax code', 'tax codes', totals.tax_codes, 'wc-admin' ),
|
||||
value: numberFormat( totals.tax_codes ),
|
||||
label: _n( 'tax code', 'tax codes', tax_codes, 'wc-admin' ),
|
||||
value: numberFormat( tax_codes ),
|
||||
},
|
||||
{
|
||||
label: __( 'total tax', 'wc-admin' ),
|
||||
value: formatCurrency( totals.total_tax ),
|
||||
value: formatCurrency( total_tax ),
|
||||
},
|
||||
{
|
||||
label: __( 'order tax', 'wc-admin' ),
|
||||
value: formatCurrency( totals.order_tax ),
|
||||
value: formatCurrency( order_tax ),
|
||||
},
|
||||
{
|
||||
label: __( 'shipping tax', 'wc-admin' ),
|
||||
value: formatCurrency( totals.shipping_tax ),
|
||||
value: formatCurrency( shipping_tax ),
|
||||
},
|
||||
{
|
||||
label: _n( 'order', 'orders', totals.orders, 'wc-admin' ),
|
||||
value: numberFormat( totals.orders_count ),
|
||||
label: _n( 'order', 'orders', orders_count, 'wc-admin' ),
|
||||
value: numberFormat( orders_count ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
import { Component, createElement } from '@wordpress/element';
|
||||
import { parse } from 'qs';
|
||||
import { find, last } from 'lodash';
|
||||
import { find, last, isEqual } from 'lodash';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { getPersistedQuery, stringifyQuery } from '@woocommerce/navigation';
|
||||
import { getNewPath, getPersistedQuery, history, stringifyQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -64,11 +64,35 @@ const getPages = () => {
|
|||
};
|
||||
|
||||
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() {
|
||||
// Pass URL parameters (example :report -> params.report) and query string parameters
|
||||
const { path, url, params } = this.props.match;
|
||||
const search = this.props.location.search.substring( 1 );
|
||||
const query = parse( search );
|
||||
const query = this.getQuery( this.props.location.search );
|
||||
const page = find( getPages(), { path } );
|
||||
window.wpNavMenuUrlUpdate( page, query );
|
||||
window.wpNavMenuClassChange( page );
|
||||
|
|
|
@ -39,30 +39,30 @@ export function getRequestByIdString( path, handleData = identity ) {
|
|||
}
|
||||
|
||||
export const getCategoryLabels = getRequestByIdString(
|
||||
NAMESPACE + 'products/categories',
|
||||
NAMESPACE + '/products/categories',
|
||||
category => ( {
|
||||
id: category.id,
|
||||
label: category.name,
|
||||
} )
|
||||
);
|
||||
|
||||
export const getCouponLabels = getRequestByIdString( NAMESPACE + 'coupons', coupon => ( {
|
||||
export const getCouponLabels = getRequestByIdString( NAMESPACE + '/coupons', coupon => ( {
|
||||
id: coupon.id,
|
||||
label: coupon.code,
|
||||
} ) );
|
||||
|
||||
export const getCustomerLabels = getRequestByIdString( NAMESPACE + 'customers', customer => ( {
|
||||
export const getCustomerLabels = getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
||||
id: customer.id,
|
||||
label: customer.username,
|
||||
} ) );
|
||||
|
||||
export const getProductLabels = getRequestByIdString( NAMESPACE + 'products', product => ( {
|
||||
export const getProductLabels = getRequestByIdString( NAMESPACE + '/products', product => ( {
|
||||
id: product.id,
|
||||
label: product.name,
|
||||
} ) );
|
||||
|
||||
export const getVariationLabels = getRequestByIdString(
|
||||
query => NAMESPACE + `products/${ query.products }/variations`,
|
||||
query => NAMESPACE + `/products/${ query.products }/variations`,
|
||||
variation => {
|
||||
return {
|
||||
id: variation.id,
|
||||
|
|
|
@ -239,7 +239,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
|
|||
isError: false,
|
||||
isRequesting: false,
|
||||
data: {
|
||||
totals: null,
|
||||
totals: {},
|
||||
intervals: [],
|
||||
},
|
||||
};
|
||||
|
@ -355,6 +355,7 @@ export function getReportTableData( endpoint, urlQuery, select, query = {} ) {
|
|||
isError: false,
|
||||
items: {
|
||||
data: [],
|
||||
totalResults: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
*/
|
||||
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.
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -39,15 +39,16 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['coupons'] = (array) $request['coupons'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['coupons'] = (array) $request['coupons'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -61,7 +62,11 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
|
|||
public function get_items( $request ) {
|
||||
$query_args = $this->prepare_reports_query( $request );
|
||||
$coupons_query = new WC_Admin_Reports_Coupons_Stats_Query( $query_args );
|
||||
$report_data = $coupons_query->get_data();
|
||||
try {
|
||||
$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(
|
||||
'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
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
$data_values = array(
|
||||
'amount' => array(
|
||||
'description' => __( 'Net discount amount.', 'wc-admin' ),
|
||||
'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' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_coupons_stats',
|
||||
|
@ -302,6 +336,17 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con
|
|||
'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;
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
$report_data = $customers_query->get_data();
|
||||
$out_data = array(
|
||||
'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() ),
|
||||
);
|
||||
|
||||
|
@ -119,7 +119,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
// TODO: should any of these be 'indicator's?
|
||||
// @todo: should any of these be 'indicator's?
|
||||
$totals = array(
|
||||
'customers_count' => array(
|
||||
'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,
|
||||
'properties' => $totals,
|
||||
),
|
||||
'intervals' => array( // TODO: remove this?
|
||||
'intervals' => array( // @todo: remove this?
|
||||
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
|
|
|
@ -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['customer'] = $request['customer'];
|
||||
$args['categories'] = (array) $request['categories'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -69,7 +70,11 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
public function get_items( $request ) {
|
||||
$query_args = $this->prepare_reports_query( $request );
|
||||
$orders_query = new WC_Admin_Reports_Orders_Stats_Query( $query_args );
|
||||
$report_data = $orders_query->get_data();
|
||||
try {
|
||||
$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(
|
||||
'totals' => get_object_vars( $report_data->totals ),
|
||||
|
@ -140,15 +145,15 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
'net_revenue' => array(
|
||||
$data_values = array(
|
||||
'net_revenue' => array(
|
||||
'description' => __( 'Net revenue.', 'wc-admin' ),
|
||||
'type' => 'number',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'format' => 'currency',
|
||||
),
|
||||
'orders_count' => array(
|
||||
'orders_count' => array(
|
||||
'description' => __( 'Amount of orders', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
|
@ -163,14 +168,78 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
'indicator' => true,
|
||||
'format' => 'currency',
|
||||
),
|
||||
'avg_items_per_order' => array(
|
||||
'avg_items_per_order' => array(
|
||||
'description' => __( 'Average items per order', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'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' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_orders_stats',
|
||||
|
@ -227,7 +296,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
'properties' => $intervals,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -358,7 +427,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_includes'] = array(
|
||||
$params['coupon_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -367,7 +436,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_excludes'] = array(
|
||||
$params['coupon_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -385,6 +454,18 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
),
|
||||
'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;
|
||||
}
|
||||
|
|
|
@ -74,8 +74,12 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
|
|||
}
|
||||
}
|
||||
|
||||
$query = new WC_Admin_Reports_Products_Stats_Query( $query_args );
|
||||
$report_data = $query->get_data();
|
||||
$query = new WC_Admin_Reports_Products_Stats_Query( $query_args );
|
||||
try {
|
||||
$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(
|
||||
'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
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
$data_values = array(
|
||||
'items_sold' => array(
|
||||
'description' => __( 'Number of items sold.', 'wc-admin' ),
|
||||
'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' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_products_stats',
|
||||
|
@ -350,6 +383,16 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co
|
|||
'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;
|
||||
}
|
||||
|
|
|
@ -38,14 +38,15 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -59,7 +60,11 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
|
|||
public function get_items( $request ) {
|
||||
$query_args = $this->prepare_reports_query( $request );
|
||||
$reports_revenue = new WC_Admin_Reports_Revenue_Query( $query_args );
|
||||
$report_data = $reports_revenue->get_data();
|
||||
try {
|
||||
$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(
|
||||
'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
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
$data_values = array(
|
||||
'gross_revenue' => array(
|
||||
'description' => __( 'Gross revenue.', 'wc-admin' ),
|
||||
'type' => 'number',
|
||||
|
@ -197,8 +202,39 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con
|
|||
),
|
||||
);
|
||||
|
||||
$intervals = $totals;
|
||||
unset( $intervals['products'] );
|
||||
$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' => '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',
|
||||
);
|
||||
$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;
|
||||
}
|
||||
|
|
|
@ -68,15 +68,16 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['taxes'] = (array) $request['taxes'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['taxes'] = (array) $request['taxes'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -161,7 +162,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
$data_values = array(
|
||||
'total_tax' => array(
|
||||
'description' => __( 'Total tax.', 'wc-admin' ),
|
||||
'type' => 'number',
|
||||
|
@ -192,7 +193,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'tax_codes' => array(
|
||||
'tax_codes' => array(
|
||||
'description' => __( 'Amount of tax codes.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
|
@ -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' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_taxes_stats',
|
||||
|
@ -273,9 +303,9 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
'description' => __( 'Current page of the collection.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
|
@ -283,7 +313,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'minimum' => 1,
|
||||
);
|
||||
$params['per_page'] = array(
|
||||
$params['per_page'] = array(
|
||||
'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 10,
|
||||
|
@ -292,26 +322,26 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['after'] = array(
|
||||
$params['after'] = array(
|
||||
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['before'] = array(
|
||||
$params['before'] = array(
|
||||
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order'] = array(
|
||||
$params['order'] = array(
|
||||
'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'desc',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby'] = array(
|
||||
$params['orderby'] = array(
|
||||
'description' => __( 'Sort collection by object attribute.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'date',
|
||||
|
@ -324,7 +354,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['interval'] = array(
|
||||
$params['interval'] = array(
|
||||
'description' => __( 'Time interval to use for buckets in the returned data.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'week',
|
||||
|
@ -338,7 +368,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['taxes'] = array(
|
||||
$params['taxes'] = array(
|
||||
'description' => __( 'Limit result set to all items that have the specified term assigned in the taxes taxonomy.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
|
@ -347,6 +377,14 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr
|
|||
'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;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,11 @@ class WC_Admin_Api_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.
|
||||
*
|
||||
|
@ -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( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
|
||||
|
||||
// Initialize Orders data store class's static vars.
|
||||
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 );
|
||||
// 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 syncing hooks.
|
||||
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_init' ) );
|
||||
|
||||
// Initialize scheduled action handlers.
|
||||
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::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::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
|
||||
|
||||
// Add currency symbol to orders endpoint 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.
|
||||
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.
|
||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-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-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-variations-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-system-status-tools-controller.php';
|
||||
|
@ -202,6 +218,7 @@ class WC_Admin_Api_Init {
|
|||
'WC_Admin_REST_Products_Controller',
|
||||
'WC_Admin_REST_Product_Categories_Controller',
|
||||
'WC_Admin_REST_Product_Reviews_Controller',
|
||||
'WC_Admin_REST_Product_Variations_Controller',
|
||||
'WC_Admin_REST_Reports_Controller',
|
||||
'WC_Admin_REST_Setting_Options_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];
|
||||
}
|
||||
|
||||
// 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.
|
||||
if ( isset( $endpoints['/wc/v4/taxes'] )
|
||||
&& 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.
|
||||
self::customer_lookup_batch_init();
|
||||
// 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_Products_Data_Store::init();
|
||||
WC_Admin_Reports_Taxes_Data_Store::init();
|
||||
WC_Admin_Reports_Coupons_Data_Store::init();
|
||||
WC_Admin_Reports_Customers_Data_Store::init();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -483,19 +556,35 @@ class WC_Admin_Api_Init {
|
|||
$order_ids = $order_query->get_orders();
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
// TODO: schedule single order update if this fails?
|
||||
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 );
|
||||
self::orders_lookup_process_order( $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() {
|
||||
WC_Admin_Reports_Customers_Data_Store::init();
|
||||
public static function orders_lookup_process_order( $order_id ) {
|
||||
$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.
|
||||
*
|
||||
* @param string $action Action to run after prerequisite.
|
||||
* @param array $action_args Action arguments.
|
||||
* @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(
|
||||
array(
|
||||
'status' => 'pending',
|
||||
|
@ -584,10 +674,10 @@ class WC_Admin_Api_Init {
|
|||
self::queue()->schedule_single(
|
||||
$after_blocking_job,
|
||||
self::QUEUE_DEPEDENT_ACTION,
|
||||
array( $action, $prerequisite_action )
|
||||
array( $action, $action_args, $prerequisite_action )
|
||||
);
|
||||
} 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();
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
@ -681,7 +771,7 @@ class WC_Admin_Api_Init {
|
|||
return array_merge(
|
||||
$wc_tables,
|
||||
array(
|
||||
// TODO: will this work on multisite?
|
||||
// @todo: will this work on multisite?
|
||||
"{$wpdb->prefix}wc_order_stats",
|
||||
"{$wpdb->prefix}wc_order_product_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,
|
||||
product_qty INT UNSIGNED 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),
|
||||
KEY order_id (order_id),
|
||||
KEY product_id (product_id),
|
||||
|
@ -746,7 +842,7 @@ class WC_Admin_Api_Init {
|
|||
shipping_tax double DEFAULT 0 NOT NULL,
|
||||
order_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 date_created (date_created)
|
||||
) $collate;
|
||||
|
|
|
@ -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' ) );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ class WC_Admin_Reports_Customers_Stats_Query extends WC_Admin_Reports_Query {
|
|||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date_registered',
|
||||
'fields' => '*', // TODO: needed?
|
||||
'fields' => '*', // @todo: needed?
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ class WC_Admin_Reports_Interval {
|
|||
|
||||
return (int) floor( ( (int) $diff_timestamp ) / DAY_IN_SECONDS ) + 1 + $addendum;
|
||||
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;
|
||||
do {
|
||||
$start_datetime = self::next_week_start( $start_datetime );
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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})";
|
||||
}
|
||||
|
||||
// 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 );
|
||||
if ( $included_products ) {
|
||||
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
|
||||
|
|
|
@ -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'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -315,19 +306,21 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
|
|||
*
|
||||
* @since 3.5.0
|
||||
* @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 ) {
|
||||
global $wpdb;
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
$coupon_items = $order->get_items( 'coupon' );
|
||||
$num_updated = 0;
|
||||
|
||||
foreach ( $coupon_items as $coupon_item ) {
|
||||
$wpdb->replace(
|
||||
$result = $wpdb->replace(
|
||||
$wpdb->prefix . self::TABLE_NAME,
|
||||
array(
|
||||
'order_id' => $order_id,
|
||||
|
@ -342,7 +335,11 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
|
|||
'%s',
|
||||
)
|
||||
);
|
||||
|
||||
$num_updated += intval( $result );
|
||||
}
|
||||
|
||||
return ( count( $coupon_items ) === $num_updated );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -165,7 +165,9 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
|
|||
if ( null === $totals ) {
|
||||
return $data;
|
||||
}
|
||||
$totals = (object) $this->cast_numbers( $totals[0] );
|
||||
$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] );
|
||||
|
||||
// Intervals.
|
||||
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name );
|
||||
|
@ -213,6 +215,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
|
|||
} else {
|
||||
$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 );
|
||||
|
||||
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||
|
|
|
@ -41,7 +41,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
'customer_id' => 'customer_id',
|
||||
'user_id' => 'user_id',
|
||||
'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',
|
||||
'country' => 'country',
|
||||
'city' => 'city',
|
||||
|
|
|
@ -49,7 +49,7 @@ class WC_Admin_Reports_Data_Store {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -73,7 +73,7 @@ class WC_Admin_Reports_Data_Store {
|
|||
private function interval_cmp( $a, $b ) {
|
||||
if ( '' === $this->order_by || '' === $this->order ) {
|
||||
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 ] ) {
|
||||
// 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.
|
||||
*/
|
||||
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 = $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
|
||||
*/
|
||||
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.
|
||||
$end_datetime = new DateTime( $datetime_end );
|
||||
$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 ) {
|
||||
$totals_arr[ $key ] = 0;
|
||||
}
|
||||
// TODO: should 'products' be in intervals?
|
||||
// @todo: should 'products' be in intervals?
|
||||
unset( $totals_arr['products'] );
|
||||
while ( $datetime <= $end_datetime ) {
|
||||
$next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
|
||||
|
@ -338,7 +349,7 @@ class WC_Admin_Reports_Data_Store {
|
|||
$start_iteration = 0;
|
||||
}
|
||||
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->setTimestamp( $new_start_date_timestamp );
|
||||
}
|
||||
|
@ -419,8 +430,8 @@ class WC_Admin_Reports_Data_Store {
|
|||
* @return array
|
||||
*/
|
||||
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[] = 'refunded';
|
||||
$excluded_statuses = WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) );
|
||||
$excluded_statuses = array_merge( array( 'refunded', 'trash' ), $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'] );
|
||||
|
||||
$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->setTimestamp( $prev_start_timestamp );
|
||||
if ( $datetime_start ) {
|
||||
|
|
|
@ -45,6 +45,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
|||
'num_returning_customers' => 'intval',
|
||||
'num_new_customers' => '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.
|
||||
*/
|
||||
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( '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.
|
||||
*/
|
||||
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;
|
||||
|
||||
$from_clause = '';
|
||||
|
@ -97,7 +94,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
|||
|
||||
$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.
|
||||
$included_products = $this->get_included_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.
|
||||
*
|
||||
* @since 3.5.0
|
||||
* @param array $query_args Query parameters.
|
||||
* @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 ),
|
||||
'interval' => 'week',
|
||||
'fields' => '*',
|
||||
'segmentby' => '',
|
||||
|
||||
'match' => 'all',
|
||||
'status_is' => array(),
|
||||
|
@ -248,7 +245,10 @@ 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'] );
|
||||
$totals[0]['products'] = $unique_products;
|
||||
|
||||
$totals = (object) $this->cast_numbers( $totals[0] );
|
||||
$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] );
|
||||
|
||||
$db_intervals = $wpdb->get_col(
|
||||
"SELECT
|
||||
|
@ -317,6 +317,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
|||
} else {
|
||||
$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 );
|
||||
|
||||
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.
|
||||
*
|
||||
* @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 ) {
|
||||
if ( 'shop_order' !== get_post_type( $post_id ) ) {
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $post_id );
|
||||
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.
|
||||
*
|
||||
* @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 ) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . self::TABLE_NAME;
|
||||
|
||||
if ( ! $order->get_id() || ! $order->get_date_created() ) {
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
$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.
|
||||
return $wpdb->replace( $table_name, $data, $format );
|
||||
$result = $wpdb->replace( $table_name, $data, $format );
|
||||
|
||||
return ( 1 === $result );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -319,7 +310,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
|
|||
*
|
||||
* @since 3.5.0
|
||||
* @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 ) {
|
||||
global $wpdb;
|
||||
|
@ -328,70 +319,84 @@ 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.
|
||||
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_items as $order_item ) {
|
||||
$order_item_id = $order_item->get_id();
|
||||
$quantity_refunded = $order->get_item_quantity_refunded( $order_item );
|
||||
$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;
|
||||
|
||||
foreach ( $order->get_items() as $order_item ) {
|
||||
$order_item_id = $order_item->get_id();
|
||||
$quantity_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['quantity'] : 0;
|
||||
$amount_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['subtotal'] : 0;
|
||||
if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) {
|
||||
$wpdb->delete(
|
||||
$result = $wpdb->delete(
|
||||
$wpdb->prefix . self::TABLE_NAME,
|
||||
array( 'order_item_id' => $order_item_id ),
|
||||
array( '%d' )
|
||||
);
|
||||
); // WPCS: cache ok, DB call ok.
|
||||
} else {
|
||||
$wpdb->replace(
|
||||
$result = $wpdb->replace(
|
||||
$wpdb->prefix . self::TABLE_NAME,
|
||||
array(
|
||||
'order_item_id' => $order_item_id,
|
||||
'order_id' => $order->get_id(),
|
||||
'product_id' => $order_item->get_product_id( 'edit' ),
|
||||
'variation_id' => $order_item->get_variation_id( 'edit' ),
|
||||
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
|
||||
'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded,
|
||||
'product_net_revenue' => $order_item->get_subtotal( 'edit' ) - $amount_refunded,
|
||||
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
||||
'order_item_id' => $order_item_id,
|
||||
'order_id' => $order->get_id(),
|
||||
'product_id' => $order_item->get_product_id( 'edit' ),
|
||||
'variation_id' => $order_item->get_variation_id( 'edit' ),
|
||||
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
|
||||
'product_qty' => $product_qty,
|
||||
'product_net_revenue' => $net_revenue,
|
||||
'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(
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
'%f',
|
||||
'%s',
|
||||
'%d', // order_item_id.
|
||||
'%d', // order_id.
|
||||
'%d', // product_id.
|
||||
'%d', // variation_id.
|
||||
'%d', // customer_id.
|
||||
'%d', // product_qty.
|
||||
'%f', // product_net_revenue.
|
||||
'%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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] );
|
||||
}
|
||||
$num_updated += intval( $result );
|
||||
}
|
||||
return $refunded_line_items;
|
||||
|
||||
return ( count( $order_items ) === $num_updated );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -159,6 +159,9 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
|
|||
ARRAY_A
|
||||
); // 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 ) {
|
||||
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 {
|
||||
$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 );
|
||||
|
||||
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||
|
|
|
@ -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'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
* @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 ) {
|
||||
global $wpdb;
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
foreach ( $order->get_items( 'tax' ) as $tax_item ) {
|
||||
$wpdb->replace(
|
||||
$tax_items = $order->get_items( 'tax' );
|
||||
$num_updated = 0;
|
||||
|
||||
foreach ( $tax_items as $tax_item ) {
|
||||
$result = $wpdb->replace(
|
||||
$wpdb->prefix . self::TABLE_NAME,
|
||||
array(
|
||||
'order_id' => $order->get_id(),
|
||||
|
@ -284,7 +278,11 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
|
|||
'%f',
|
||||
)
|
||||
);
|
||||
|
||||
$num_updated += intval( $result );
|
||||
}
|
||||
|
||||
return ( count( $tax_items ) === $num_updated );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -183,6 +183,8 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
|
|||
if ( null === $totals ) {
|
||||
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 );
|
||||
|
||||
|
@ -231,6 +233,7 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
|
|||
} else {
|
||||
$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 );
|
||||
|
||||
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||
|
|
|
@ -60,11 +60,11 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz",
|
||||
"integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz",
|
||||
"integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.3.0",
|
||||
"@babel/types": "^7.3.2",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.10",
|
||||
"source-map": "^0.5.0",
|
||||
|
@ -274,9 +274,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz",
|
||||
"integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA=="
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz",
|
||||
"integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ=="
|
||||
},
|
||||
"@babel/plugin-proposal-async-generator-functions": {
|
||||
"version": "7.2.0",
|
||||
|
@ -298,9 +298,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-proposal-object-rest-spread": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz",
|
||||
"integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz",
|
||||
"integrity": "sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@babel/plugin-syntax-object-rest-spread": "^7.2.0"
|
||||
|
@ -424,9 +424,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-transform-destructuring": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz",
|
||||
"integrity": "sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz",
|
||||
"integrity": "sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
|
@ -730,9 +730,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz",
|
||||
"integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz",
|
||||
"integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==",
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.10",
|
||||
|
@ -1765,21 +1765,21 @@
|
|||
}
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz",
|
||||
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
|
||||
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-escapes": "^3.0.0",
|
||||
"chalk": "^2.0.0",
|
||||
"ansi-escapes": "^3.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-cursor": "^2.1.0",
|
||||
"cli-width": "^2.0.0",
|
||||
"external-editor": "^3.0.0",
|
||||
"external-editor": "^3.0.3",
|
||||
"figures": "^2.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"lodash": "^4.17.11",
|
||||
"mute-stream": "0.0.7",
|
||||
"run-async": "^2.2.0",
|
||||
"rxjs": "^6.1.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"string-width": "^2.1.0",
|
||||
"strip-ansi": "^5.0.0",
|
||||
"through": "^2.3.6"
|
||||
|
@ -2113,9 +2113,9 @@
|
|||
"integrity": "sha512-y+h7tNlxDPDrH/TrSw1wCSm6FoEAY8FrbUxYng3BMSYBTUsX1utLooizk9v8J1yy6F9AioXNnPZ1qiu2vsa08Q=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.12.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz",
|
||||
"integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA=="
|
||||
"version": "10.12.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz",
|
||||
"integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ=="
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.7.11",
|
||||
|
@ -2999,9 +2999,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz",
|
||||
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA=="
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
|
||||
"integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3060,9 +3060,9 @@
|
|||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
|
||||
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
|
||||
"integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
@ -4262,9 +4262,9 @@
|
|||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
|
||||
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz",
|
||||
"integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
|
@ -4632,9 +4632,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30000932",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz",
|
||||
"integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A=="
|
||||
"version": "1.0.30000934",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000934.tgz",
|
||||
"integrity": "sha512-o7yfZn0R9N+mWAuksDsdLsb1gu9o//XK0QSU0zSSReKNRsXsFc/n/psxi0YSPNiqlKxImp5h4DHnAPdwYJ8nNA=="
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "1.2.0",
|
||||
|
@ -6357,9 +6357,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz",
|
||||
"integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog=="
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz",
|
||||
"integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A=="
|
||||
},
|
||||
"cssstyle": {
|
||||
"version": "1.1.1",
|
||||
|
@ -6456,17 +6456,17 @@
|
|||
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
|
||||
},
|
||||
"d3-shape": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.3.tgz",
|
||||
"integrity": "sha512-f7V9wHQCmv4s4N7EmD5i0mwJ5y09L8r1rWVrzH1Av0YfgBKJCnTJGho76rS4HNUIw6tTBbWfFcs4ntP/MKWF4A==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.4.tgz",
|
||||
"integrity": "sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg==",
|
||||
"requires": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"d3-time": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz",
|
||||
"integrity": "sha512-hF+NTLCaJHF/JqHN5hE8HVGAXPStEq6/omumPE/SxyHVrR7/qQxusFDo0t0c/44+sCGHthC7yNGFZIEgju0P8g=="
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
|
||||
"integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
|
||||
},
|
||||
"d3-time-format": {
|
||||
"version": "2.1.3",
|
||||
|
@ -7234,9 +7234,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
|
||||
"integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.0.0",
|
||||
|
@ -7317,9 +7317,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.109",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz",
|
||||
"integrity": "sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow=="
|
||||
"version": "1.3.113",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz",
|
||||
"integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g=="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.4.1",
|
||||
|
@ -7422,9 +7422,9 @@
|
|||
}
|
||||
},
|
||||
"enzyme-adapter-react-16": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.8.0.tgz",
|
||||
"integrity": "sha512-7cVHIKutqnesGeM3CjNFHSvktpypSWBokrBO8wIW+BVx+HGxWCF87W9TpkIIYJqgCtdw9FQGFrAbLg8kSwPRuQ==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.0.tgz",
|
||||
"integrity": "sha512-tUqmeLi0Y3PxuiPSykjn8ZMqzCnaRIVywNx0i50+nhd4y/b3JtXRbsvIc8HKxn3heE4t969EI2461Kc9FYxjdw==",
|
||||
"requires": {
|
||||
"enzyme-adapter-utils": "^1.10.0",
|
||||
"function.prototype.name": "^1.1.0",
|
||||
|
@ -7607,9 +7607,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz",
|
||||
"integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==",
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
|
||||
"integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-jsx": {
|
||||
|
@ -7682,21 +7682,21 @@
|
|||
"dev": true
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz",
|
||||
"integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
|
||||
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-escapes": "^3.0.0",
|
||||
"chalk": "^2.0.0",
|
||||
"ansi-escapes": "^3.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-cursor": "^2.1.0",
|
||||
"cli-width": "^2.0.0",
|
||||
"external-editor": "^3.0.0",
|
||||
"external-editor": "^3.0.3",
|
||||
"figures": "^2.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"lodash": "^4.17.11",
|
||||
"mute-stream": "0.0.7",
|
||||
"run-async": "^2.2.0",
|
||||
"rxjs": "^6.1.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"string-width": "^2.1.0",
|
||||
"strip-ansi": "^5.0.0",
|
||||
"through": "^2.3.6"
|
||||
|
@ -8656,39 +8656,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"flush-write-stream": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
|
||||
"integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.0.tgz",
|
||||
"integrity": "sha512-6MHED/cmsyux1G4/Cek2Z776y9t7WCNd3h2h/HW91vFeU7pzMhA8XvAlDhHcanG5IWuIh/xcC7JASY4WQpG6xg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^2.0.4"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"for-in": {
|
||||
|
@ -11339,8 +11313,7 @@
|
|||
"is-wsl": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
|
@ -13263,11 +13236,11 @@
|
|||
}
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz",
|
||||
"integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==",
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz",
|
||||
"integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==",
|
||||
"requires": {
|
||||
"sourcemap-codec": "^1.4.1"
|
||||
"sourcemap-codec": "^1.4.4"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
|
@ -14163,20 +14136,21 @@
|
|||
}
|
||||
},
|
||||
"node-notifier": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz",
|
||||
"integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz",
|
||||
"integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==",
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"semver": "^5.5.0",
|
||||
"shellwords": "^0.1.1",
|
||||
"which": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.6.tgz",
|
||||
"integrity": "sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.7.tgz",
|
||||
"integrity": "sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==",
|
||||
"requires": {
|
||||
"semver": "^5.3.0"
|
||||
}
|
||||
|
@ -14457,13 +14431,14 @@
|
|||
}
|
||||
},
|
||||
"npm-package-json-lint": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.4.1.tgz",
|
||||
"integrity": "sha512-W4xlmeFRAY34GQoHUywqoI3PxVZ0hugjbZLiGnVgFjgmvRRcmxKwwmubMe0lAD78vgOHgJZRGubdVXwkp9d3QA==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-3.5.0.tgz",
|
||||
"integrity": "sha512-MELethOnZW5uVzP65oTQEH2fI6eS/BQEXjvOTyQkUQqGHP9si5pxCWcO+Q4dsahb+4yG7GMxFhpF42AjhCbgRA==",
|
||||
"requires": {
|
||||
"ajv": "^6.5.4",
|
||||
"chalk": "^2.4.1",
|
||||
"ajv": "^6.7.0",
|
||||
"chalk": "^2.4.2",
|
||||
"glob": "^7.1.3",
|
||||
"ignore": "^5.0.5",
|
||||
"is-path-inside": "^2.0.0",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"is-resolvable": "^1.1.0",
|
||||
|
@ -14472,7 +14447,14 @@
|
|||
"plur": "^3.0.1",
|
||||
"semver": "^5.6.0",
|
||||
"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": {
|
||||
|
@ -14561,9 +14543,9 @@
|
|||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||
},
|
||||
"nwsapi": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz",
|
||||
"integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz",
|
||||
"integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg=="
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
|
@ -19717,9 +19699,9 @@
|
|||
}
|
||||
},
|
||||
"stylelint-scss": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.1.tgz",
|
||||
"integrity": "sha512-XNWKTU1a2EUNWdauxHPTJlGNNQzIbg48OTTIdBs5xTXxpbYAGtX/J+jBqMPjxfdySXijc/mexubuZ+ZinUGGgw==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.2.tgz",
|
||||
"integrity": "sha512-HL95s8Q6wihbJe7c7z6rL9GHVHOF3H3tXkVmGutitwn14LYR52JYMwCkcifqlf4nRsvXrUDaoH6OHOdilifyjw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.11",
|
||||
|
@ -19931,14 +19913,14 @@
|
|||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz",
|
||||
"integrity": "sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw==",
|
||||
"version": "3.16.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.16.1.tgz",
|
||||
"integrity": "sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "~2.17.1",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.6"
|
||||
"source-map-support": "~0.5.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
|
@ -19966,9 +19948,9 @@
|
|||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz",
|
||||
"integrity": "sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz",
|
||||
"integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cacache": "^11.0.2",
|
||||
|
@ -19976,7 +19958,7 @@
|
|||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^1.4.0",
|
||||
"source-map": "^0.6.1",
|
||||
"terser": "^3.8.1",
|
||||
"terser": "^3.16.1",
|
||||
"webpack-sources": "^1.1.0",
|
||||
"worker-farm": "^1.5.2"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
- Add emoji-flags dependency
|
||||
|
||||
|
@ -5,7 +10,6 @@
|
|||
- Chart component: format numbers and prices using store currency settings.
|
||||
- Make `href`/linking optional in SummaryNumber.
|
||||
- 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
|
||||
- Add download log ip address autocompleter to search component
|
||||
|
|
|
@ -197,7 +197,7 @@ export const drawAxis = ( node, params, xOffset ) => {
|
|||
: 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 ) );
|
||||
|
||||
|
@ -210,7 +210,7 @@ export const drawAxis = ( node, params, xOffset ) => {
|
|||
d3AxisBottom( xScale )
|
||||
.tickValues( ticks )
|
||||
.tickFormat( ( d, i ) => params.interval === 'hour'
|
||||
? params.xFormat( d )
|
||||
? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
|
||||
: removeDuplicateDates( d, i, ticks, params.xFormat ) )
|
||||
);
|
||||
|
||||
|
@ -257,7 +257,7 @@ export const drawAxis = ( node, params, xOffset ) => {
|
|||
.attr( 'text-anchor', 'start' )
|
||||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( yGrids )
|
||||
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
|
||||
);
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export const getYMax = lineData => {
|
|||
*/
|
||||
export const getYScale = ( height, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax ] )
|
||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
||||
.rangeRound( [ height, 0 ] );
|
||||
|
||||
/**
|
||||
|
@ -83,5 +83,5 @@ export const getYScale = ( height, yMax ) =>
|
|||
*/
|
||||
export const getYTickOffset = ( height, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax ] )
|
||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
||||
.rangeRound( [ height + 12, 12 ] );
|
||||
|
|
|
@ -107,6 +107,14 @@ describe( 'Y scales', () => {
|
|||
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
|
||||
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', () => {
|
||||
|
@ -116,5 +124,13 @@ describe( 'Y scales', () => {
|
|||
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
|
||||
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 ] );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
.woocommerce-search__clear {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: calc( 50% - 10px );
|
||||
top: calc(50% - 10px);
|
||||
|
||||
& > .dashicon {
|
||||
color: #c9c9c9;
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -64,6 +64,8 @@ class WC_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 100 ); // $25 x 4.
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
|
||||
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
|
||||
|
|
|
@ -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->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$request->set_query_params(
|
||||
array(
|
||||
|
@ -101,9 +103,10 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case {
|
|||
|
||||
$expected_reports = array(
|
||||
'totals' => array(
|
||||
'amount' => 4,
|
||||
'amount' => 4.0,
|
||||
'coupons_count' => 2,
|
||||
'orders_count' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => 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_gmt' => date( 'Y-m-d 23:59:59', $time ),
|
||||
'subtotals' => (object) array(
|
||||
'amount' => 4,
|
||||
'amount' => 4.0,
|
||||
'coupons_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 );
|
||||
|
||||
$totals = $properties['totals']['properties'];
|
||||
$this->assertEquals( 3, count( $totals ) );
|
||||
$this->assertEquals( 4, count( $totals ) );
|
||||
$this->assertArrayHasKey( 'amount', $totals );
|
||||
$this->assertArrayHasKey( 'coupons_count', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertArrayHasKey( 'segments', $totals );
|
||||
|
||||
$intervals = $properties['intervals']['items']['properties'];
|
||||
$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 );
|
||||
|
||||
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
|
||||
$this->assertEquals( 3, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'amount', $totals );
|
||||
$this->assertArrayHasKey( 'coupons_count', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertEquals( 4, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'amount', $subtotals );
|
||||
$this->assertArrayHasKey( 'coupons_count', $subtotals );
|
||||
$this->assertArrayHasKey( 'orders_count', $subtotals );
|
||||
$this->assertArrayHasKey( 'segments', $subtotals );
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,8 @@ class WC_Tests_API_Reports_Coupons extends WC_REST_Unit_Test_Case {
|
|||
$order_2c->calculate_totals();
|
||||
$order_2c->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
|
||||
$coupon_reports = $response->get_data();
|
||||
|
||||
|
|
|
@ -126,6 +126,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 9.12 );
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
|
|
@ -134,6 +134,8 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 100 );
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$request->set_query_params(
|
||||
array(
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
* Reports Orders Stats REST API Test
|
||||
*
|
||||
* @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 );
|
||||
|
||||
$totals = $properties['totals']['properties'];
|
||||
$this->assertEquals( 4, count( $totals ) );
|
||||
$this->assertEquals( 10, count( $totals ) );
|
||||
$this->assertArrayHasKey( 'net_revenue', $totals );
|
||||
$this->assertArrayHasKey( 'avg_order_value', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $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'];
|
||||
$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 );
|
||||
|
||||
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
|
||||
$this->assertEquals( 4, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'net_revenue', $totals );
|
||||
$this->assertArrayHasKey( 'avg_order_value', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertArrayHasKey( 'avg_items_per_order', $totals );
|
||||
$this->assertEquals( 9, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'net_revenue', $subtotals );
|
||||
$this->assertArrayHasKey( 'avg_order_value', $subtotals );
|
||||
$this->assertArrayHasKey( 'orders_count', $subtotals );
|
||||
$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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 100 ); // $25 x 4.
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$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 ) );
|
||||
|
|
|
@ -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->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$time = time();
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$request->set_query_params(
|
||||
|
|
|
@ -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->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$request->set_query_params(
|
||||
array(
|
||||
|
@ -89,6 +91,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
|
|||
'net_revenue' => 100.0,
|
||||
'orders_count' => 1,
|
||||
'products_count' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
array(
|
||||
|
@ -102,6 +105,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case {
|
|||
'net_revenue' => 100.0,
|
||||
'orders_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 );
|
||||
|
||||
$totals = $properties['totals']['properties'];
|
||||
$this->assertEquals( 3, count( $totals ) );
|
||||
$this->assertEquals( 4, count( $totals ) );
|
||||
$this->assertArrayHasKey( 'net_revenue', $totals );
|
||||
$this->assertArrayHasKey( 'items_sold', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertArrayHasKey( 'segments', $totals );
|
||||
|
||||
$intervals = $properties['intervals']['items']['properties'];
|
||||
$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 );
|
||||
|
||||
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
|
||||
$this->assertEquals( 3, count( $subtotals ) );
|
||||
$this->assertEquals( 4, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'net_revenue', $subtotals );
|
||||
$this->assertArrayHasKey( 'items_sold', $subtotals );
|
||||
$this->assertArrayHasKey( 'orders_count', $subtotals );
|
||||
$this->assertArrayHasKey( 'segments', $subtotals );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ class WC_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 100 ); // $25 x 4.
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
|
||||
$reports = $response->get_data();
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case {
|
|||
$this->assertArrayHasKey( 'intervals', $properties );
|
||||
|
||||
$totals = $properties['totals']['properties'];
|
||||
$this->assertEquals( 9, count( $totals ) );
|
||||
$this->assertEquals( 10, count( $totals ) );
|
||||
$this->assertArrayHasKey( 'gross_revenue', $totals );
|
||||
$this->assertArrayHasKey( 'net_revenue', $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( 'num_items_sold', $totals );
|
||||
$this->assertArrayHasKey( 'products', $totals );
|
||||
$this->assertArrayHasKey( 'segments', $totals );
|
||||
|
||||
$intervals = $properties['intervals']['items']['properties'];
|
||||
$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 );
|
||||
|
||||
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
|
||||
$this->assertEquals( 8, count( $subtotals ) );
|
||||
$this->assertEquals( 9, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'gross_revenue', $subtotals );
|
||||
$this->assertArrayHasKey( 'net_revenue', $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( 'orders_count', $subtotals );
|
||||
$this->assertArrayHasKey( 'num_items_sold', $subtotals );
|
||||
$this->assertArrayHasKey( 'segments', $subtotals );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,12 +143,13 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case {
|
|||
|
||||
$totals = $properties['totals']['properties'];
|
||||
|
||||
$this->assertEquals( 5, count( $totals ) );
|
||||
$this->assertEquals( 6, count( $totals ) );
|
||||
$this->assertArrayHasKey( 'order_tax', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertArrayHasKey( 'shipping_tax', $totals );
|
||||
$this->assertArrayHasKey( 'tax_codes', $totals );
|
||||
$this->assertArrayHasKey( 'total_tax', $totals );
|
||||
$this->assertArrayHasKey( 'segments', $totals );
|
||||
|
||||
$intervals = $properties['intervals']['items']['properties'];
|
||||
$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 );
|
||||
|
||||
$subtotals = $properties['intervals']['items']['properties']['subtotals']['properties'];
|
||||
$this->assertEquals( 5, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'order_tax', $totals );
|
||||
$this->assertArrayHasKey( 'orders_count', $totals );
|
||||
$this->assertArrayHasKey( 'shipping_tax', $totals );
|
||||
$this->assertArrayHasKey( 'tax_codes', $totals );
|
||||
$this->assertArrayHasKey( 'total_tax', $totals );
|
||||
$this->assertEquals( 6, count( $subtotals ) );
|
||||
$this->assertArrayHasKey( 'order_tax', $subtotals );
|
||||
$this->assertArrayHasKey( 'orders_count', $subtotals );
|
||||
$this->assertArrayHasKey( 'shipping_tax', $subtotals );
|
||||
$this->assertArrayHasKey( 'tax_codes', $subtotals );
|
||||
$this->assertArrayHasKey( 'total_tax', $subtotals );
|
||||
$this->assertArrayHasKey( 'segments', $subtotals );
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) );
|
||||
$reports = $response->get_data();
|
||||
|
||||
|
|
|
@ -65,6 +65,8 @@ class WC_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case {
|
|||
$order->set_total( 100 ); // $25 x 4.
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
|
||||
$reports = $response->get_data();
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
|
|||
// insert a blocking job.
|
||||
WC_Admin_Api_Init::queue()->schedule_single( time(), 'blocking_job', array( 'stuff' ) );
|
||||
// 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.
|
||||
$this->assertEmpty(
|
||||
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(
|
||||
array(
|
||||
'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.
|
||||
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.
|
||||
$this->assertCount(
|
||||
1,
|
||||
|
@ -172,7 +172,7 @@ class WC_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case {
|
|||
WC_Admin_Api_Init::queue()->search(
|
||||
array(
|
||||
'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' ),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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-admin-notes.php';
|
||||
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-test-action-queue.php';
|
||||
require_once dirname( __FILE__ ) . '/framework/helpers/class-wc-helper-queue.php';
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
|
|||
$order_2c->calculate_totals();
|
||||
$order_2c->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$data_store = new WC_Admin_Reports_Coupons_Stats_Data_Store();
|
||||
$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() );
|
||||
|
@ -72,9 +74,10 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
|
|||
'pages' => 1,
|
||||
'page_no' => 1,
|
||||
'totals' => (object) array(
|
||||
'amount' => 2 * $coupon_1_amount + $coupon_2_amount,
|
||||
'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ),
|
||||
'coupons_count' => 2,
|
||||
'orders_count' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => 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_gmt' => $end_datetime->format( 'Y-m-d H:i:s' ),
|
||||
'subtotals' => (object) array(
|
||||
'amount' => 2 * $coupon_1_amount + $coupon_2_amount,
|
||||
'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ),
|
||||
'coupons_count' => 2,
|
||||
'orders_count' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -54,6 +54,8 @@ class WC_Tests_Reports_Coupons extends WC_Unit_Test_Case {
|
|||
$order_2c->calculate_totals();
|
||||
$order_2c->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$data_store = new WC_Admin_Reports_Coupons_Data_Store();
|
||||
$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() );
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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->save();
|
||||
|
||||
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 );
|
||||
|
@ -113,6 +115,8 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
|
|||
$order_2->set_date_created( $date_created_2 );
|
||||
$order_2->save();
|
||||
|
||||
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_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_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax.
|
||||
$order->save();
|
||||
|
||||
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 );
|
||||
|
@ -249,4 +256,76 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case {
|
|||
);
|
||||
$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() );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
// /reports/revenue/stats is mapped to Orders_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,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 1,
|
||||
'products' => '1',
|
||||
'products' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
array(
|
||||
|
@ -84,6 +87,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
|
|||
'avg_order_value' => 80,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -107,6 +111,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
|
|||
'shipping' => 10,
|
||||
'net_revenue' => 80,
|
||||
'products' => '1',
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
array(
|
||||
|
@ -124,6 +129,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case {
|
|||
'taxes' => 7,
|
||||
'shipping' => 10,
|
||||
'net_revenue' => 80,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -39,6 +39,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
|
|||
$order->set_status( 'completed' );
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$data_store = new WC_Admin_Reports_Variations_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 );
|
||||
|
@ -106,6 +108,8 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case {
|
|||
$order->set_status( 'completed' );
|
||||
$order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$data_store = new WC_Admin_Reports_Variations_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 );
|
||||
|
|
Loading…
Reference in New Issue