This commit is contained in:
KhaiNguyen
2020-02-13 10:39:37 +07:00
commit 59401cb805
12867 changed files with 4646216 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,987 @@
/** @format */
/** @format */
/** @format */
/* stylelint-disable block-closing-brace-newline-after */
/* stylelint-enable */
/** @format */
/**
* Breakpoint mixins
*/
/* stylelint-disable block-closing-brace-newline-after */
/* stylelint-enable */
/**
* Internal Dependencies
*/
/** @format */
.woocommerce-segmented-selection__item:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-segmented-selection__item:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-segmented-selection__item:nth-child(3) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-segmented-selection__item:nth-child(4) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-segmented-selection__item:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-segmented-selection__item:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-segmented-selection__item:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-segmented-selection__item:nth-child(8) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-segmented-selection__item:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.woocommerce-segmented-selection__item:nth-child(10) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.has-1-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-2-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-2-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 6;
grid-column-start: 6;
-ms-grid-column-span: 1;
grid-column-end: 7;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
@media (max-width: 1440px) {
.has-6-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-6-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-6-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; } }
.has-7-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-7-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-7-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
@media (max-width: 1440px) {
.has-9-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-9-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-9-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; } }
.has-10-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
@media (max-width: 1440px) {
.has-10-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-10-items .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; } }
@media (max-width: 960px) {
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 6;
grid-row-start: 6;
-ms-grid-row-span: 1;
grid-row-end: 7; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 7;
grid-row-start: 7;
-ms-grid-row-span: 1;
grid-row-end: 8; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 8;
grid-row-start: 8;
-ms-grid-row-span: 1;
grid-row-end: 9; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 9;
grid-row-start: 9;
-ms-grid-row-span: 1;
grid-row-end: 10; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 10;
grid-row-start: 10;
-ms-grid-row-span: 1;
grid-row-end: 11; } }

View File

@@ -0,0 +1,987 @@
/** @format */
/** @format */
/** @format */
/* stylelint-disable block-closing-brace-newline-after */
/* stylelint-enable */
/** @format */
/**
* Breakpoint mixins
*/
/* stylelint-disable block-closing-brace-newline-after */
/* stylelint-enable */
/**
* Internal Dependencies
*/
/** @format */
.woocommerce-segmented-selection__item:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-segmented-selection__item:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-segmented-selection__item:nth-child(3) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-segmented-selection__item:nth-child(4) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-segmented-selection__item:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-segmented-selection__item:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-segmented-selection__item:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-segmented-selection__item:nth-child(8) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-segmented-selection__item:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.woocommerce-segmented-selection__item:nth-child(10) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.has-1-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-2-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-2-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-3-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-4-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-5-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 6;
grid-column-start: 6;
-ms-grid-column-span: 1;
grid-column-end: 7;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
@media (max-width: 1440px) {
.has-6-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-6-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-6-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-6-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; } }
.has-7-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-7-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-7-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-7-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-8-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-8-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
@media (max-width: 1440px) {
.has-9-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-9-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-9-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-9-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-9-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; } }
.has-10-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 5;
grid-column-start: 5;
-ms-grid-column-span: 1;
grid-column-end: 6;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
@media (max-width: 1440px) {
.has-10-items .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.has-10-items .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 3;
grid-column-start: 3;
-ms-grid-column-span: 1;
grid-column-end: 4;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 4;
grid-column-start: 4;
-ms-grid-column-span: 1;
grid-column-end: 5;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.has-10-items .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.has-10-items .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 2;
grid-column-start: 2;
-ms-grid-column-span: 1;
grid-column-end: 3;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; } }
@media (max-width: 960px) {
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(1) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 1;
grid-row-start: 1;
-ms-grid-row-span: 1;
grid-row-end: 2; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(2) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 2;
grid-row-start: 2;
-ms-grid-row-span: 1;
grid-row-end: 3; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(3) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 3;
grid-row-start: 3;
-ms-grid-row-span: 1;
grid-row-end: 4; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(4) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 4;
grid-row-start: 4;
-ms-grid-row-span: 1;
grid-row-end: 5; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(5) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 5;
grid-row-start: 5;
-ms-grid-row-span: 1;
grid-row-end: 6; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(6) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 6;
grid-row-start: 6;
-ms-grid-row-span: 1;
grid-row-end: 7; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(7) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 7;
grid-row-start: 7;
-ms-grid-row-span: 1;
grid-row-end: 8; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(8) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 8;
grid-row-start: 8;
-ms-grid-row-span: 1;
grid-row-end: 9; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(9) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 9;
grid-row-start: 9;
-ms-grid-row-span: 1;
grid-row-end: 10; }
.woocommerce-summary > .woocommerce-summary__item-container:nth-child(10) {
-ms-grid-column: 1;
grid-column-start: 1;
-ms-grid-column-span: 1;
grid-column-end: 2;
-ms-grid-row: 10;
grid-row-start: 10;
-ms-grid-row-span: 1;
grid-row-end: 11; } }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
this.wc=this.wc||{},this.wc.csvExport=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=743)}({20:function(e,t){!function(){e.exports=this.moment}()},371:function(e,t,n){
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var r=r||function(e){"use strict";if("undefined"==typeof navigator||!/MSIE [1-9]\./.test(navigator.userAgent)){var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,i=/Version\/[\d\.]+.*Safari/.test(navigator.userAgent),a=e.webkitRequestFileSystem,u=e.requestFileSystem||a||e.mozRequestFileSystem,c=function(t){(e.setImmediate||e.setTimeout)((function(){throw t}),0)},f=0,l=function(e){setTimeout((function(){"string"==typeof e?n().revokeObjectURL(e):e.remove()}),4e4)},s=function(e,t,n){for(var r=(t=[].concat(t)).length;r--;){var o=e["on"+t[r]];if("function"==typeof o)try{o.call(e,n||e)}catch(e){c(e)}}},d=function(e){return/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)?new Blob(["\ufeff",e],{type:e.type}):e},p=function(t,c,p){p||(t=d(t));var v,y,w,m=this,b=t.type,g=!1,h=function(){s(m,"writestart progress write writeend".split(" "))},S=function(){if(y&&i&&"undefined"!=typeof FileReader){var r=new FileReader;return r.onloadend=function(){var e=r.result;y.location.href="data:attachment/file"+e.slice(e.search(/[,;]/)),m.readyState=m.DONE,h()},r.readAsDataURL(t),void(m.readyState=m.INIT)}(!g&&v||(v=n().createObjectURL(t)),y)?y.location.href=v:void 0===e.open(v,"_blank")&&i&&(e.location.href=v);m.readyState=m.DONE,h(),l(v)},O=function(e){return function(){if(m.readyState!==m.DONE)return e.apply(this,arguments)}},R={create:!0,exclusive:!1};if(m.readyState=m.INIT,c||(c="download"),o)return v=n().createObjectURL(t),void setTimeout((function(){var e,t;r.href=v,r.download=c,e=r,t=new MouseEvent("click"),e.dispatchEvent(t),h(),l(v),m.readyState=m.DONE}));e.chrome&&b&&"application/octet-stream"!==b&&(w=t.slice||t.webkitSlice,t=w.call(t,0,t.size,"application/octet-stream"),g=!0),a&&"download"!==c&&(c+=".download"),("application/octet-stream"===b||a)&&(y=e),u?(f+=t.size,u(e.TEMPORARY,f,O((function(e){e.root.getDirectory("saved",R,O((function(e){var n=function(){e.getFile(c,R,O((function(e){e.createWriter(O((function(n){n.onwriteend=function(t){y.location.href=e.toURL(),m.readyState=m.DONE,s(m,"writeend",t),l(e)},n.onerror=function(){var e=n.error;e.code!==e.ABORT_ERR&&S()},"writestart progress write abort".split(" ").forEach((function(e){n["on"+e]=m["on"+e]})),n.write(t),m.abort=function(){n.abort(),m.readyState=m.DONE},m.readyState=m.WRITING})),S)})),S)};e.getFile(c,{create:!1},O((function(e){e.remove(),n()})),O((function(e){e.code===e.NOT_FOUND_ERR?n():S()})))})),S)})),S)):S()},v=p.prototype;return"undefined"!=typeof navigator&&navigator.msSaveOrOpenBlob?function(e,t,n){return n||(e=d(e)),navigator.msSaveOrOpenBlob(e,t||"download")}:(v.abort=function(){this.readyState=this.DONE,s(this,"abort")},v.readyState=v.INIT=0,v.WRITING=1,v.DONE=2,v.error=v.onwritestart=v.onprogress=v.onwrite=v.onabort=v.onerror=v.onwriteend=null,function(e,t,n){return new p(e,t,n)})}}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this.content);e.exports?e.exports.saveAs=r:"undefined"!=typeof define&&null!==define&&null!==define.amd&&define([],(function(){return r}))},743:function(e,t,n){"use strict";n.r(t),n.d(t,"generateCSVDataFromTable",(function(){return f})),n.d(t,"generateCSVFileName",(function(){return l})),n.d(t,"downloadCSVFile",(function(){return s}));var r=n(20),o=n.n(r),i=n(371);function a(e){var t=e.toString();return["=","+","-","@"].includes(t.charAt(0))?t="'"+t:t.match(/[,"\s]/)&&(t='"'+t.replace(/"/g,'""')+'"'),t}function u(e){return Array.isArray(e)?e.map((function(e){return a(e.label)})).join(","):[]}function c(e){return Array.isArray(e)?e.map((function(e){return e.map((function(e){return void 0===e.value||null===e.value?"":a(e.value)})).join(",")})).join("\n"):[]}function f(e,t){return[u(e),c(t)].filter((function(e){return e.length})).join("\n")}function l(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[e.toLowerCase().replace(/[^a-z0-9]/g,"-"),o()().format("YYYY-MM-DD"),Object.keys(t).map((function(e){return e.toLowerCase().replace(/[^a-z0-9]/g,"-")+"-"+decodeURIComponent(t[e]).toLowerCase().replace(/[^a-z0-9]/g,"-")})).join("_")].filter((function(e){return e.length}));return n.join("_")+".csv"}function s(e,t){var n=new Blob([t],{type:"text/csv;charset=utf-8"});Object(i.saveAs)(n,e)}}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__compare,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__compare{-ms-grid-row-align:center;align-self:center;-ms-grid-column:1;grid-column-start:1;-ms-grid-column-span:1;grid-column-end:2}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-search{-ms-grid-row-align:center;align-self:center;-ms-grid-column:2;grid-column-start:2;-ms-grid-column-span:1;grid-column-end:3}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__download-button,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__download-button{-ms-grid-row-align:center;align-self:center;-ms-grid-column:3;grid-column-start:3;-ms-grid-column-span:1;grid-column-end:4}

View File

@@ -0,0 +1 @@
.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__compare,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__compare{-ms-grid-row-align:center;align-self:center;-ms-grid-column:1;grid-column-start:1;-ms-grid-column-span:1;grid-column-end:2}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-search{-ms-grid-row-align:center;align-self:center;-ms-grid-column:2;grid-column-start:2;-ms-grid-column-span:1;grid-column-end:3}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__download-button,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__download-button{-ms-grid-row-align:center;align-self:center;-ms-grid-column:3;grid-column-start:3;-ms-grid-column-span:1;grid-column-end:4}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
this.wc=this.wc||{},this.wc.number=function(t){var e={};function r(n){if(e[n])return e[n].exports;var i=e[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(n,i,function(e){return t[e]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=746)}({31:function(t,e,r){"use strict";function n(t,e,r){return e in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}r.d(e,"a",(function(){return n}))},41:function(t,e,r){"use strict";function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t)){var r=[],n=!0,i=!1,o=void 0;try{for(var u,c=t[Symbol.iterator]();!(n=(u=c.next()).done)&&(r.push(u.value),!e||r.length!==e);n=!0);}catch(t){i=!0,o=t}finally{try{n||null==c.return||c.return()}finally{if(i)throw o}}return r}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}r.d(e,"a",(function(){return n}))},746:function(t,e,r){"use strict";r.r(e),r.d(e,"numberFormat",(function(){return c})),r.d(e,"formatValue",(function(){return a})),r.d(e,"calculateDelta",(function(){return l}));var n=r(31),i=r(41);function o(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}var u=r(747);function c(t,e){var r=t.precision,n=void 0===r?null:r,o=t.decimalSeparator,c=void 0===o?".":o,a=t.thousandSeparator,l=void 0===a?",":a;if("number"!=typeof e&&(e=parseFloat(e)),isNaN(e))return"";if(n=parseInt(n),isNaN(n)){var f=e.toString().split("."),s=Object(i.a)(f,2)[1];n=s?s.length:0}return u(e,n,c,l)}function a(t,e,r){if(!Number.isFinite(r))return null;switch(e){case"average":return Math.round(r);case"number":return c(function(t){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?o(Object(r),!0).forEach((function(e){Object(n.a)(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):o(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}({},t,{precision:null}),r)}}function l(t,e){return Number.isFinite(t)&&Number.isFinite(e)?0===e?0:Math.round((t-e)/e*100):null}},747:function(t,e,r){"use strict";t.exports=function(t,e,r,n){t=(t+"").replace(/[^0-9+\-Ee.]/g,"");var i=isFinite(+t)?+t:0,o=isFinite(+e)?Math.abs(e):0,u=void 0===n?",":n,c=void 0===r?".":r,a="";return(a=(o?function(t,e){if(-1===(""+t).indexOf("e"))return+(Math.round(t+"e+"+e)+"e-"+e);var r=(""+t).split("e"),n="";return+r[1]+e>0&&(n="+"),(+(Math.round(+r[0]+"e"+n+(+r[1]+e))+"e-"+e)).toFixed(e)}(i,o).toString():""+Math.round(i)).split("."))[0].length>3&&(a[0]=a[0].replace(/\B(?=(?:\d{3})+(?!\d))/g,u)),(a[1]||"").length<o&&(a[1]=a[1]||"",a[1]+=new Array(o-a[1].length+1).join("0")),a.join(c)}}});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
this.wc=this.wc||{},this.wc.onboardingProductImportNotice=function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=750)}({1:function(t,e){!function(){t.exports=this.wp.i18n}()},12:function(t,e,n){"use strict";var r={};n.r(r),n.d(r,"ADMIN_URL",(function(){return l})),n.d(r,"COUNTRIES",(function(){return p})),n.d(r,"CURRENCY",(function(){return y})),n.d(r,"LOCALE",(function(){return b})),n.d(r,"ORDER_STATUSES",(function(){return S})),n.d(r,"SITE_TITLE",(function(){return m})),n.d(r,"WC_ASSET_URL",(function(){return g})),n.d(r,"DEFAULT_DATE_RANGE",(function(){return O})),n.d(r,"getSetting",(function(){return v})),n.d(r,"setSetting",(function(){return E})),n.d(r,"getAdminLink",(function(){return w}));var o=n(48),u=n(13),c=n.n(u),i=n(49);function a(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function f(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?a(Object(n),!0).forEach((function(e){c()(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):a(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}var s={adminUrl:"",countries:[],currency:{code:"USD",precision:2,symbol:"$",symbolPosition:"left",decimalSeparator:".",priceFormat:"%1$s%2$s",thousandSeparator:","},defaultDateRange:"period=month&compare=previous_year",locale:{siteLocale:"en_US",userLocale:"en_US",weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},orderStatuses:[],siteTitle:"",wcAssetUrl:""},d=f({},s,{},"object"===("undefined"==typeof wcSettings?"undefined":n.n(i)()(wcSettings))?wcSettings:{});d.currency=f({},s.currency,{},d.currency),d.locale=f({},s.locale,{},d.locale);var l=d.adminUrl,p=d.countries,y=d.currency,b=d.locale,S=d.orderStatuses,m=d.siteTitle,g=d.wcAssetUrl,O=d.defaultDateRange;function v(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(t){return t},r=d.hasOwnProperty(t)?d[t]:e;return n(r,e)}function E(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(t){return t};d[t]=n(e)}function w(t){return(l||"")+t}n.d(e,"a",(function(){return _})),n.d(e,"b",(function(){return j})),n.d(e,"c",(function(){return h})),n.d(e,"d",(function(){return L})),n.d(e,"e",(function(){return A})),n.d(e,"g",(function(){return U})),n.d(e,"h",(function(){return R})),n.d(e,"f",(function(){return P}));var T=o&&void 0!==o.getSetting?o:r,_=T.ADMIN_URL,j=(T.COUNTRIES,T.CURRENCY),h=T.LOCALE,L=T.ORDER_STATUSES,A=(T.SITE_TITLE,T.WC_ASSET_URL),U=(T.DEFAULT_DATE_RANGE,T.getSetting),R=T.setSetting,P=T.getAdminLink||w},13:function(t,e){t.exports=function(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}},48:function(t,e){!function(){t.exports=this.wc.wcSettings}()},49:function(t,e){function n(e){return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?t.exports=n=function(t){return typeof t}:t.exports=n=function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(e)}t.exports=n},59:function(t,e,n){"use strict";function r(t){"complete"!==document.readyState&&"interactive"!==document.readyState?document.addEventListener("DOMContentLoaded",t):t()}n.d(e,"a",(function(){return r}))},750:function(t,e,n){"use strict";n.r(e);var r=n(1),o=n(59),u=n(12);Object(o.a)((function(){var t=document.querySelector(".wc-actions");if(t){var e=document.querySelector(".wc-actions .button-primary");e&&(e.classList.remove("button"),e.classList.remove("button-primary"));var n=document.createElement("a");n.classList.add("button"),n.classList.add("button-primary"),n.setAttribute("href",Object(u.f)("admin.php?page=wc-admin")),n.innerText=Object(r.__)("Continue setup","woocommerce-admin"),t.appendChild(n)}}))}});

View File

@@ -0,0 +1 @@
this.wc=this.wc||{},this.wc.onboardingProductNotice=function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=749)}({1:function(t,e){!function(){t.exports=this.wp.i18n}()},12:function(t,e,n){"use strict";var r={};n.r(r),n.d(r,"ADMIN_URL",(function(){return l})),n.d(r,"COUNTRIES",(function(){return p})),n.d(r,"CURRENCY",(function(){return y})),n.d(r,"LOCALE",(function(){return b})),n.d(r,"ORDER_STATUSES",(function(){return S})),n.d(r,"SITE_TITLE",(function(){return O})),n.d(r,"WC_ASSET_URL",(function(){return m})),n.d(r,"DEFAULT_DATE_RANGE",(function(){return g})),n.d(r,"getSetting",(function(){return _})),n.d(r,"setSetting",(function(){return v})),n.d(r,"getAdminLink",(function(){return E}));var o=n(48),c=n(13),u=n.n(c),i=n(49);function f(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function a(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?f(Object(n),!0).forEach((function(e){u()(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):f(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}var d={adminUrl:"",countries:[],currency:{code:"USD",precision:2,symbol:"$",symbolPosition:"left",decimalSeparator:".",priceFormat:"%1$s%2$s",thousandSeparator:","},defaultDateRange:"period=month&compare=previous_year",locale:{siteLocale:"en_US",userLocale:"en_US",weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},orderStatuses:[],siteTitle:"",wcAssetUrl:""},s=a({},d,{},"object"===("undefined"==typeof wcSettings?"undefined":n.n(i)()(wcSettings))?wcSettings:{});s.currency=a({},d.currency,{},s.currency),s.locale=a({},d.locale,{},s.locale);var l=s.adminUrl,p=s.countries,y=s.currency,b=s.locale,S=s.orderStatuses,O=s.siteTitle,m=s.wcAssetUrl,g=s.defaultDateRange;function _(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(t){return t},r=s.hasOwnProperty(t)?s[t]:e;return n(r,e)}function v(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(t){return t};s[t]=n(e)}function E(t){return(l||"")+t}n.d(e,"a",(function(){return j})),n.d(e,"b",(function(){return w})),n.d(e,"c",(function(){return h})),n.d(e,"d",(function(){return R})),n.d(e,"e",(function(){return U})),n.d(e,"g",(function(){return A})),n.d(e,"h",(function(){return D})),n.d(e,"f",(function(){return L}));var T=o&&void 0!==o.getSetting?o:r,j=T.ADMIN_URL,w=(T.COUNTRIES,T.CURRENCY),h=T.LOCALE,R=T.ORDER_STATUSES,U=(T.SITE_TITLE,T.WC_ASSET_URL),A=(T.DEFAULT_DATE_RANGE,T.getSetting),D=T.setSetting,L=T.getAdminLink||E},13:function(t,e){t.exports=function(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}},24:function(t,e){!function(){t.exports=this.wp.data}()},48:function(t,e){!function(){t.exports=this.wc.wcSettings}()},49:function(t,e){function n(e){return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?t.exports=n=function(t){return typeof t}:t.exports=n=function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(e)}t.exports=n},59:function(t,e,n){"use strict";function r(t){"complete"!==document.readyState&&"interactive"!==document.readyState?document.addEventListener("DOMContentLoaded",t):t()}n.d(e,"a",(function(){return r}))},749:function(t,e,n){"use strict";n.r(e);var r=n(1),o=n(24),c=n(59),u=n(12);Object(c.a)((function(){Object(o.dispatch)("core/notices").createSuccessNotice(Object(r.__)("🎉 Congrats on adding your first product!","woocommerce-admin"),{id:"WOOCOMMERCE_ONBOARDING_PRODUCT_NOTICE",actions:[{url:Object(u.f)("admin.php?page=wc-admin"),label:Object(r.__)("Continue setup.","woocommerce-admin")}]})}))}});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"> <path d="M226.153073,88.3099993 L355.380187,301.446227 C363.970299,315.614028 359.448689,334.062961 345.280888,342.653073 C340.591108,345.496544 335.21158,347 329.727115,347 L71.2728854,347 C54.7043429,347 41.2728854,333.568542 41.2728854,317 C41.2728854,311.515534 42.7763415,306.136007 45.6198127,301.446227 L174.846927,88.3099993 C183.437039,74.1421985 201.885972,69.6205881 216.053773,78.2106999 C220.184157,80.7150022 223.64877,84.1796157 226.153073,88.3099993 Z M184.370159,153 L186.899684,255.024156 L213.459691,255.024156 L215.989216,153 L184.370159,153 Z M200.179688,307.722584 C209.770801,307.722584 217.359375,300.450201 217.359375,291.175278 C217.359375,281.900355 209.770801,274.627972 200.179688,274.627972 C190.588574,274.627972 183,281.900355 183,291.175278 C183,300.450201 190.588574,307.722584 200.179688,307.722584 Z" id="Combined-Shape" stroke="#979797" fill="#95588A" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

View File

@@ -0,0 +1,284 @@
<?php
/**
* Connect existing WooCommerce pages to WooCommerce Admin.
*
* @package Woocommerce Admin
*/
use Automattic\WooCommerce\Admin\PageController;
/**
* Returns core WC pages to connect to WC-Admin.
*
* @return array
*/
function wc_admin_get_core_pages_to_connect() {
$all_reports = WC_Admin_Reports::get_reports();
$report_tabs = array();
foreach ( $all_reports as $report_id => $report_data ) {
$report_tabs[ $report_id ] = $report_data['title'];
}
return array(
'wc-addons' => array(
'title' => __( 'Extensions', 'woocommerce-admin' ),
'tabs' => array(),
),
'wc-reports' => array(
'title' => __( 'Reports', 'woocommerce-admin' ),
'tabs' => $report_tabs,
),
'wc-settings' => array(
'title' => __( 'Settings', 'woocommerce-admin' ),
'tabs' => apply_filters( 'woocommerce_settings_tabs_array', array() ),
),
'wc-status' => array(
'title' => __( 'Status', 'woocommerce-admin' ),
'tabs' => apply_filters(
'woocommerce_admin_status_tabs',
array(
'status' => __( 'System status', 'woocommerce-admin' ),
'tools' => __( 'Tools', 'woocommerce-admin' ),
'logs' => __( 'Logs', 'woocommerce-admin' ),
)
),
),
);
}
/**
* Filter breadcrumbs for core pages that aren't explicitly connected.
*
* @param array $breadcrumbs Breadcrumb pieces.
* @return array Filtered breadcrumb pieces.
*/
function wc_admin_filter_core_page_breadcrumbs( $breadcrumbs ) {
$screen_id = PageController::get_instance()->get_current_screen_id();
$pages_to_connect = wc_admin_get_core_pages_to_connect();
foreach ( $pages_to_connect as $page_id => $page_data ) {
if ( preg_match( "/^woocommerce_page_{$page_id}\-/", $screen_id ) ) {
if ( empty( $page_data['tabs'] ) ) {
$new_breadcrumbs = array( $page_data['title'] );
} else {
$new_breadcrumbs = array(
array(
add_query_arg( 'page', $page_id, 'admin.php' ),
$page_data['title'],
),
);
if ( isset( $_GET['tab'] ) ) {
$current_tab = wc_clean( wp_unslash( $_GET['tab'] ) );
} else {
$current_tab = key( $page_data['tabs'] );
}
$new_breadcrumbs[] = $page_data['tabs'][ $current_tab ];
}
return $new_breadcrumbs;
}
}
return $breadcrumbs;
}
/**
* Render the WC-Admin header bar on all WooCommerce core pages.
*
* @param bool $is_connected Whether the current page is connected.
* @param bool $current_page The current page, if connected.
* @return bool Whether to connect the page.
*/
function wc_admin_connect_core_pages( $is_connected, $current_page ) {
if ( false === $is_connected && false === $current_page ) {
$screen_id = PageController::get_instance()->get_current_screen_id();
$pages_to_connect = wc_admin_get_core_pages_to_connect();
foreach ( $pages_to_connect as $page_id => $page_data ) {
if ( preg_match( "/^woocommerce_page_{$page_id}\-/", $screen_id ) ) {
add_filter( 'woocommerce_navigation_get_breadcrumbs', 'wc_admin_filter_core_page_breadcrumbs' );
return true;
}
}
}
return $is_connected;
}
add_filter( 'woocommerce_navigation_is_connected_page', 'wc_admin_connect_core_pages', 10, 2 );
$posttype_list_base = 'edit.php';
// WooCommerce > Orders.
wc_admin_connect_page(
array(
'id' => 'woocommerce-orders',
'screen_id' => 'edit-shop_order',
'title' => __( 'Orders', 'woocommerce-admin' ),
'path' => add_query_arg( 'post_type', 'shop_order', $posttype_list_base ),
)
);
// WooCommerce > Orders > Add New.
wc_admin_connect_page(
array(
'id' => 'woocommerce-add-order',
'parent' => 'woocommerce-orders',
'screen_id' => 'shop_order-add',
'title' => __( 'Add New', 'woocommerce-admin' ),
)
);
// WooCommerce > Orders > Edit Order.
wc_admin_connect_page(
array(
'id' => 'woocommerce-edit-order',
'parent' => 'woocommerce-orders',
'screen_id' => 'shop_order',
'title' => __( 'Edit Order', 'woocommerce-admin' ),
)
);
// WooCommerce > Coupons.
wc_admin_connect_page(
array(
'id' => 'woocommerce-coupons',
'screen_id' => 'edit-shop_coupon',
'title' => __( 'Coupons', 'woocommerce-admin' ),
'path' => add_query_arg( 'post_type', 'shop_coupon', $posttype_list_base ),
)
);
// WooCommerce > Coupons > Add New.
wc_admin_connect_page(
array(
'id' => 'woocommerce-add-coupon',
'parent' => 'woocommerce-coupons',
'screen_id' => 'shop_coupon-add',
'title' => __( 'Add New', 'woocommerce-admin' ),
)
);
// WooCommerce > Coupons > Edit Coupon.
wc_admin_connect_page(
array(
'id' => 'woocommerce-edit-coupon',
'parent' => 'woocommerce-coupons',
'screen_id' => 'shop_coupon',
'title' => __( 'Edit Coupon', 'woocommerce-admin' ),
)
);
// WooCommerce > Products.
wc_admin_connect_page(
array(
'id' => 'woocommerce-products',
'screen_id' => 'edit-product',
'title' => __( 'Products', 'woocommerce-admin' ),
'path' => add_query_arg( 'post_type', 'product', $posttype_list_base ),
)
);
// WooCommerce > Products > Add New.
wc_admin_connect_page(
array(
'id' => 'woocommerce-add-product',
'parent' => 'woocommerce-products',
'screen_id' => 'product-add',
'title' => __( 'Add New', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Edit Order.
wc_admin_connect_page(
array(
'id' => 'woocommerce-edit-product',
'parent' => 'woocommerce-products',
'screen_id' => 'product',
'title' => __( 'Edit Product', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Import Products.
wc_admin_connect_page(
array(
'id' => 'woocommerce-import-products',
'parent' => 'woocommerce-products',
'screen_id' => 'product_page_product_importer',
'title' => __( 'Import Products', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Export Products.
wc_admin_connect_page(
array(
'id' => 'woocommerce-export-products',
'parent' => 'woocommerce-products',
'screen_id' => 'product_page_product_exporter',
'title' => __( 'Export Products', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Product categories.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-categories',
'parent' => 'woocommerce-products',
'screen_id' => 'edit-product_cat',
'title' => __( 'Product categories', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Edit category.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-edit-category',
'parent' => 'woocommerce-products',
'screen_id' => 'product_cat',
'title' => __( 'Edit category', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Product tags.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-tags',
'parent' => 'woocommerce-products',
'screen_id' => 'edit-product_tag',
'title' => __( 'Product tags', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Edit tag.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-edit-tag',
'parent' => 'woocommerce-products',
'screen_id' => 'product_tag',
'title' => __( 'Edit tag', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Attributes.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-attributes',
'parent' => 'woocommerce-products',
'screen_id' => 'product_page_product_attributes',
'title' => __( 'Attributes', 'woocommerce-admin' ),
)
);
// WooCommerce > Products > Edit attribute.
wc_admin_connect_page(
array(
'id' => 'woocommerce-product-edit-attribute',
'parent' => 'woocommerce-products',
'screen_id' => 'product_page_product_attribute-edit',
'title' => __( 'Edit attribute', 'woocommerce-admin' ),
)
);

View File

@@ -0,0 +1,66 @@
<?php
/**
* Core Functions
*
* Holds core functions for wc-admin.
*
* @package WC_Admin\Functions
*/
use \Automattic\WooCommerce\Admin\Loader;
/**
* Format a number using the decimal and thousands separator settings in WooCommerce.
*
* @param mixed $number Number to be formatted.
* @return string
*/
function wc_admin_number_format( $number ) {
$currency_settings = Loader::get_currency_settings();
return number_format(
$number,
0,
$currency_settings['decimalSeparator'],
$currency_settings['thousandSeparator']
);
}
/**
* Retrieves a URL to relative path inside WooCommerce admin with
* the provided query parameters.
*
* @param string $path Relative path of the desired page.
* @param array $query Query parameters to append to the path.
*
* @return string Fully qualified URL pointing to the desired path.
*/
function wc_admin_url( $path = null, $query = array() ) {
if ( ! empty( $query ) ) {
$query_string = http_build_query( $query );
$path = $path ? '&path=' . $path . '&' . $query_string : '';
}
return admin_url( 'admin.php?page=wc-admin' . $path, dirname( __FILE__ ) );
}
/**
* Record an event using Tracks.
*
* @internal WooCommerce core only includes Tracks in admin, not the REST API, so we need to include it.
* @param string $event_name Event name for tracks.
* @param array $properties Properties to pass along with event.
*/
function wc_admin_record_tracks_event( $event_name, $properties = array() ) {
if ( ! class_exists( 'WC_Tracks' ) ) {
if ( ! defined( 'WC_ABSPATH' ) || ! file_exists( WC_ABSPATH . 'includes/tracks/class-wc-tracks.php' ) ) {
return;
}
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php';
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php';
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php';
include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-footer-pixel.php';
include_once WC_ABSPATH . 'includes/tracks/class-wc-site-tracking.php';
}
WC_Tracks::record_event( $event_name, $properties );
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Admin report export download
*
* @package WooCommerce/Admin/Templates/Emails/HTML
*/
defined( 'ABSPATH' ) || exit;
/*
* @hooked WC_Emails::email_header() Output the email header
*/
do_action( 'woocommerce_email_header', $email_heading, $email );
?>
<a href="<?php echo esc_url( $download_url ); ?>">
<?php echo esc_html( sprintf( __( 'Download your %s Report', 'woocommerce-admin' ), $report_name ) ); ?>
</a>
<?php
/*
* @hooked WC_Emails::email_footer() Output the email footer
*/
do_action( 'woocommerce_email_footer', $email );

View File

@@ -0,0 +1,19 @@
<?php
/**
* Admin report export download email (plain text)
*
* @package WooCommerce/Admin/Templates/Emails/HTML
*/
defined( 'ABSPATH' ) || exit;
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
echo esc_html( wp_strip_all_tags( $email_heading ) );
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
/* translators: %1$s: report name, %2$s: download URL */
echo sprintf( __( 'Download your %1$s Report: %2$s', 'woocommerce-admin' ), $report_name, $download_url );
echo "\n\n----------------------------------------\n\n";
echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );

View File

@@ -0,0 +1,16 @@
<?php
// WARNING: Do not directly edit this file.
// This file is auto-generated as part of the build process and things may break.
if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
function wc_admin_get_feature_config() {
return array(
'activity-panels' => true,
'analytics' => true,
'analytics-dashboard' => true,
'analytics-dashboard/customizable' => true,
'devdocs' => false,
'onboarding' => true,
'store-alerts' => true,
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Convenience functions for PageController.
*
* @package Woocommerce Admin
*/
use Automattic\WooCommerce\Admin\PageController;
/**
* Connect an existing page to WooCommerce Admin.
* Passthrough to PageController::connect_page().
*
* @param array $options Options for PageController::connect_page().
*/
function wc_admin_connect_page( $options ) {
$controller = PageController::get_instance();
$controller->connect_page( $options );
}
/**
* Register JS-powered WooCommerce Admin Page.
* Passthrough to PageController::register_page().
*
* @param array $options Options for PageController::register_page().
*/
function wc_admin_register_page( $options ) {
$controller = PageController::get_instance();
$controller->register_page( $options );
}
/**
* Is this page connected to WooCommerce Admin?
* Passthrough to PageController::is_connected_page().
*
* @return boolean True if the page is connected to WooCommerce Admin.
*/
function wc_admin_is_connected_page() {
$controller = PageController::get_instance();
return $controller->is_connected_page();
}
/**
* Is this a WooCommerce Admin Page?
* Passthrough to PageController::is_registered_page().
*
* @return boolean True if the page is a WooCommerce Admin page.
*/
function wc_admin_is_registered_page() {
$controller = PageController::get_instance();
return $controller->is_registered_page();
}
/**
* Get breadcrumbs for WooCommerce Admin Page navigation.
* Passthrough to PageController::get_breadcrumbs().
*
* @return array Navigation pieces (breadcrumbs).
*/
function wc_admin_get_breadcrumbs() {
$controller = PageController::get_instance();
return $controller->get_breadcrumbs();
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* WooCommerce Admin Updates
*
* Functions for updating data, used by the background updater.
*
* @package WooCommerce/Admin
*/
use \Automattic\WooCommerce\Admin\Install as Installer;
/**
* Update order stats `status` index length.
* See: https://github.com/woocommerce/woocommerce-admin/issues/2969.
*/
function wc_admin_update_0201_order_status_index() {
global $wpdb;
// Max DB index length. See wp_get_db_schema().
$max_index_length = 191;
$index = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}wc_order_stats WHERE key_name = 'status'" );
if ( property_exists( $index, 'Sub_part' ) ) {
// The index was created with the right length. Time to bail.
if ( $max_index_length === $index->Sub_part ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName
return;
}
// We need to drop the index so it can be recreated.
$wpdb->query( "DROP INDEX `status` ON {$wpdb->prefix}wc_order_stats" );
}
// Recreate the status index with a max length.
$wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}wc_order_stats ADD INDEX status (status(%d))", $max_index_length ) );
}
/**
* Update DB Version.
*/
function wc_admin_update_0201_db_version() {
Installer::update_db_version( '0.20.1' );
}
/**
* Rename "gross_total" to "total_sales".
* See: https://github.com/woocommerce/woocommerce-admin/issues/3175
*/
function wc_admin_update_0230_rename_gross_total() {
global $wpdb;
// We first need to drop the new `total_sales` column, since dbDelta() will have created it.
$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_order_stats DROP COLUMN `total_sales`" );
// Then we can rename the existing `gross_total` column.
$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_order_stats CHANGE COLUMN `gross_total` `total_sales` double DEFAULT 0 NOT NULL" );
}
/**
* Update DB Version.
*/
function wc_admin_update_0230_db_version() {
Installer::update_db_version( '0.23.0' );
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,613 @@
=== WooCommerce Admin ===
Contributors: automattic
Tags: ecommerce, e-commerce, store, sales, reports, analytics, dashboard, activity, notices, insights, stats, woo commerce, woocommerce
Requires at least: 5.3.0
Tested up to: 5.3.2
Requires PHP: 5.6.20
Stable tag: 1.0.0
License: GPLv3
License URI: https://github.com/woocommerce/woocommerce-admin/blob/master/license.txt
== Description ==
WooCommerce Admin is a new JavaScript-driven interface for managing your store. The plugin includes new and improved reports and a dashboard to monitor key metrics of your site.
**New Reports for WooCommerce**
WooCommerce Admin has a host of new reports that are optimized for speed and have advanced filters that allow you to dive into data about your store:
- Revenue Report
- Orders Report
- Products Report
- Categories Report
- Coupons Report
- Taxes Report
- Downloads Report
- Stock Report
- Customers Report
**Customizable Dashboard**
WooCommerce Admin also allows store owners to customize a new dashboard screen with “performance indicators” that correspond in importance to their stores operation.
== Getting Started ==
= Minimum Requirements =
* WordPress 5.3
* WooCommerce 3.6.0 or greater
* PHP version 5.6.20 or greater. PHP 7.2 or greater is recommended
* MySQL version 5.0 or greater. MySQL 5.6 or greater is recommended
Visit the [WooCommerce server requirements documentation](https://docs.woocommerce.com/document/server-requirements/) for a detailed list of server requirements.
= Automatic installation =
Automatic installation is the easiest option, as WordPress handles the file transfers and you don't need to leave your web browser. To perform an automatic install:
1. Log in to your WordPress dashboard.
2. Go to: Plugins > Add New.
3. Enter “WooCommerce Admin” in the Search field, and view details about its point release, the rating and description.
4. Select “Install Now” when youre ready.
= Manual installation =
The manual installation method involves downloading the plugin and uploading it to your web server via your favorite FTP application. The WordPress Codex contains instructions at [Manual Plugin Installation](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation).
= Where can I report bugs or contribute to the project? =
Bugs should be reported in the [WooCommerce Admin GitHub repository](https://github.com/woocommerce/woocommerce-admin/).
= This is awesome! Can I contribute? =
Yes, you can! Join our [GitHub repository](https://github.com/woocommerce/woocommerce-admin/)
Release and roadmap notes are available on the [WooCommerce Developers Blog](https://woocommerce.wordpress.com/category/wc-admin/)
== Screenshots ==
1. WooCommerce Admin Dashboard
2. Activity Panels
3. Analytics
== Changelog ==
= 0.25.0 2020-01-29 =
- Fix: Onboarding: Mark profiler complete on WC update #3590 (On Merge to WC Core, Onboarding)
- Dev: fix strict standards in Scheduler classes #3583
- Task: Avoid redundant/unnecessary cleanup. #3580 (On Merge to WC Core)
- Fix: Fix styling of search control in report table header and filters. #3603 (Analytics, Components, Packages)
- Fix: report table search component. #3618 (Analytics, Components, Packages)
- Performance: Remove unnecessary use of lodash get #3598 (Onboarding)
- Tweak: don't modify page titles for existing WooCommerce pages. #3585 (On Merge to WC Core)
- Dev: Handle custom currency formats #3349 (Components, Packages)
- Fix: Update broken note action hooks #3535 (Activity Panel, Inbox)
- Dev: Onboarding: Only Show Retry Button on Plugin install failure #3545
- Dev: SelectControl: Add an option to show all options on refocus #3551 (Components, Packages)
- Fix: SQL error in category table sort. #3521 (Analytics)
- Dev: Onboarding: Add option to track appearance completion #3503 (Onboarding)
- Enhancement: add customer privacy erasure support. #3511 (Analytics)
- Fix: handle uninstallation when in feature plugin mode. #3546 (On Merge to WC Core)
- Fix: Exception on dashboard once Onboarding tasks are complete #3513
- Tweak: Onboarding - Add Skip Step link on Themes Step. #3490 (Onboarding)
- Bug: Add SelectControl debouncing and keyboard fixes #3507 (Components, Packages)
- Tweak: Included RegExp for JS files in examples.config.js #3510 (Build, Extensibility) 👏 @yash-webkul
- Bug: Onboarding: Don't show skip link if no plugins exist to skip #3485 (Onboarding)
- Fix: Make package-based load inert when plugin is active. #3555 (On Merge to WC Core)
- Dev: Onboarding: Add purchase products task list item #3472 (Onboarding)
- Fix: Properly style theme selection button in Site Profiler #3489
- Dev: Onboarding: Add tracks to extension purchase task and modal #3493 (Onboarding)
- Tweak: Change Refunds to Returns on the dashboard. #3514
- Dev: Update translation scripts and instructions for creating `.json` files. #3433 (Build)
- Bug: Onboarding: Add in tracks for failed plugin installation #3483 (Onboarding)
- Dev: Onboarding: Add theme install and activation endpoints #3482 (Onboarding, REST API)
- Fix: component styles for WordPress 5.3. #3357 (Build, Components, Design, Packages)
- Dev: Onboarding: Add another option to platforms. #3471 (Onboarding)
- Fix: `CompareFilter` functionality regression. #3421 (Analytics, Components, Packages)
- Fix: don't run database migrations on new installs. #3473
- Fix: WC-Admin header on settings and status pages. #3389 (Activity Panel)
- Enhancement: allow filtering of hidden WP notices. #3391 (Activity Panel, Extensibility)
- Fix: show pending product reviews when comment moderation is disabled. #3459 (Activity Panel)
- Dev: AssetDataRegistry: update SQL example #3418
- Tweak: Scroll to notices when displayed while the notice area is scrolled out of view. #3390 (Activity Panel)
- Dev: DataStores: normalize contexts to plural #3419
- Enhancement: allow report cache layer to be turned off. #3434
- Bug: Fix user data fields filter name. #3428 (Dashboard)
- Fix: error when trying to download report data. #3429 (Analytics)
- Dev: Update to latest PHPUnit 7 #3571 (Build)
- Fix: invalidate Reports cache when changing Analytics settings. #3465 (Analytics, REST API, Settings)
- Bug: Onboarding: Skip Shipping connect step if Jetpack is already connected #3486 (Onboarding)
- Fix: Time zone offset calculation on customer last active date. #3388 (Analytics)
- Dev: Default the Calypso environment to production #3347 (Onboarding)
- Bug: Onboarding: Update shipping task button text #3400 (Onboarding)
- Bug: Onboarding: Update check for TOS accepted in tax step #3245 (Onboarding)
- Fix: remove the header when user doesn't have required permissions #3386 (Activity Panel)
- Dev: Add autocomplete props to address fields in onboarding and adjust country/state matching #3338 (Onboarding)
- Dev: Add purchase modal at the end of the profile wizard #3444 (Onboarding)
- Dev: Standardize hook and filter prefixes. #3339 (On Merge to WC Core)
- Fix: make report filter date props optional #3359 (Components, Extensibility, Packages)
- Tweak: Add/disable plugin filter #3361
- Dev: Onboarding - Add button to continue setup after importing products #3402 (Build, Onboarding)
= 0.24.0 2020-01-06 =
- Bug: Add SelectControl debouncing and keyboard fixes #3507 (Components, Packages)
- Fix: Properly style theme selection button in Site Profiler #3489
- Dev: Update translation scripts and instructions for creating `.json` files. #3433 (Build)
- Dev: Add initial documentation for the historical data import. #3506 (Documentation)
- Dev: Onboarding: Add purchase products task list item #3472 (Onboarding)
- Dev: Onboarding: Add another option to platforms. #3471 (Onboarding)
- Dev: Add singular/plural distinction to onboarding plugins installation #3517 (Onboarding)
- Dev: Onboarding: Add theme install and activation endpoints #3482 (Onboarding, REST API)
- Tweak: Onboarding - Add Skip Step link on Themes Step. #3490 (Onboarding)
- Bug: Onboarding: Skip Shipping connect step if Jetpack is already connected #3486 (Onboarding)
- Bug: CI: use PR repo instead of main repo when checking out branches. #3512 (Build)
- Tweak: Included RegExp for JS files in examples.config.js #3510 (Build, Extensibility) 👏 @yash-webkul
- Dev: Onboarding: Add tracks to extension purchase task and modal #3493 (Onboarding)
- Tweak: Change Refunds to Returns on the dashboard. #3514
- Fix: invalidate Reports cache when changing Analytics settings. #3465 (Analytics, REST API, Settings)
- Dev: DataStores: normalize contexts to plural #3419
- Dev: AssetDataRegistry: update SQL example #3418
- Fix: show pending product reviews when comment moderation is disabled. #3459 (Activity Panel)
- Enhancement: allow report cache layer to be turned off. #3434
- Tweak: Scroll to notices when displayed while the notice area is scrolled out of view. #3390 (Activity Panel)
- Dev: Add autocomplete props to address fields in onboarding and adjust country/state matching #3338 (Onboarding)
- Dev: Update instructions for documenting new components. #3443 (Components, Documentation)
- Dev: Add purchase modal at the end of the profile wizard #3444 (Onboarding)
- Fix: component styles for WordPress 5.3. #3357 (Build, Components, Design, Packages)
- Bug: Onboarding: Don't show skip link if no plugins exist to skip #3485 (Onboarding)
- Bug: Onboarding: Add in tracks for failed plugin installation #3483 (Onboarding)
- Dev: Fix the "is this card useful" prompt display #3427 (Onboarding)
- Tweak: Add/disable plugin filter #3361
- Bug: Onboarding: Update shipping task button text #3400 (Onboarding)
- Dev: Default the Calypso environment to production #3347 (Onboarding)
- Dev: Standardize hook and filter prefixes. #3339 (On Merge to WC Core)
- Fix: WC-Admin header on settings and status pages. #3389 (Activity Panel)
- Dev: Fix revenue question translation with currency conversion #3358 (Onboarding, Packages)
- Fix: make report filter date props optional #3359 (Components, Extensibility, Packages)
- Dev: Onboarding - Add button to continue setup after importing products #3402 (Build, Onboarding)
- Bug: Onboarding: Update check for TOS accepted in tax step #3245 (Onboarding)
= 0.23.3 2019-12-26 =
- Fix: don't run database migrations on new installs. #3473
= 0.23.2 2019-12-19 =
- Enhancement: allow filtering of hidden WP notices. #3391 (Activity Panel, Extensibility)
- Fix: error when trying to download report data. #3429 (Analytics)
- Bug: Fix user data fields filter name. #3428 (Dashboard)
- Fix: `CompareFilter` functionality regression. #3421 (Analytics, Components, Packages)
- Fix: Time zone offset calculation on customer last active date. #3388 (Analytics)
- Fix: remove the header when user doesn't have required permissions #3386 (Activity Panel)
= 0.23.1 2019-12-08 =
- Fix: undefined function error.
= 0.23.0 2019-12-06 =
- Dev: Add currency extension #3328 (Packages)
- Fix: errant moment locale data loading. #3362 (Components, Packages)
- Dev: Packages: Prep for release. #3325 (Build, Components, Packages)
- Bug: Settings: Persist date range selection #3293
- Dev: Remove Newspack dependency. #3302
- Dev: DB Updates: invalidate cache after update #3299
- Fix: beginning of next day calculation on daylight saving time on the days the time changes. #3333 (Analytics)
- Tweak: Add a filter that allows disabling WooCommerce Admin. #3350 (On Merge to WC Core)
- Fix: Retrieve week last year data by calendar date instead of week alignment. #3271 (Analytics, Packages)
- Bug: Check if extended_info is set for order report items #3315 (REST API)
- Tweak: remove global settings dependency from Navigation package. #3294 (Components, Packages)
- Fix: Fix checkout of master branch in CI environment. #3296 (Build)
- Fix: decouple Date package from global wcSettings object. #3278 (Components, Packages)
- Fix: Make the order count between customer and table total consistent. #3290 (Analytics)
- Fix: decouple Currency and Number packages from global wcSettings object. #3277 (Components, Packages)
- Bug: Filter invalid statuses from saved statuses #3268
- Dev: use a filter to set default locale #3273
- Dev: Allow core inclusion via Package.php #3196 (Build)
- Dev: Fix filter gap on analytics select controls #3259 (Components, Packages)
- Bug: Correcting and clarifying analytics terms and calculations #3104 (Analytics, Components, Packages)
- Dev: rename data store get_* to add_* functions where function does not return values #3275
- Tweak: Search component: remove dependency on settings global from countries autocompleter. #3262 (Components, Packages)
- Dev: PHPCS fixes for onboarding files #3269 (Documentation)
- Dev: Add a tracks queue to delay event recording & log embed page views #3250
- Dev: Make WooCommerce brand localizable #3247
- Fix: Add single customer REST API end point. #3174 (REST API)
- Dev: fix typos in JS docs #3248
- Bug: Select control: fix misalignment and hide scroll bar #3215 (Components, Packages)
- Dev: capitalize WooCommerce Services #3249
= 0.22.0 2019-11-13 =
- Fix: Incorrect calculation of tax summary on Taxes screen. #3158 (Analytics)
- Fix: Correct product and coupon count on edited orders. #3103 (Analytics)
- Bug: Update filter picker to use key instead of id #3214 (Analytics, Components, Packages)
- Enhancement: prompt stores to add their first product(s). #3119 (Inbox)
- Dev: Move Analytics REST endpoints to `wc-analytics` namespace. #3204 (Components, Packages)
- Tweak: add usage tracking inbox notice. #3112 (Activity Panel, Inbox)
- Task: Fix PHP linter errors. #3188
- Enhancement: Add query filters to data stores.
- Dev: Refactor data store classes. #2961 (REST API)
- Dev: WooCommerce Admin has been marked as compatible with WooCommerce 3.8.0. #3201
- Bug: Fix ellipsis menu style #3141 (Analytics, Components, Packages)
- Tweak: Field misalignment in product edit screen. #3145
- Dev: update tested to version for WC and WP #3162
- none needed. #3138 (Documentation)
= 0.21.0 2019-10-30 =
- Fix: report export format when generated server-side. #2987 (Analytics, Packages)
- Tweak: automatically update DB version during plugin updates. #3113
- Tweak: add visual feedback for Inbox Note Actions #3039 (Activity Panel, Build, Inbox)
- Fix: Address discrepancies in Revenue totals between Analytics screens. #3095 (Analytics)
- Tweak: track inbox note views. #3096 (Activity Panel, Build, Inbox)
- Dev: Fix asset image URL and business extension images #3062
- Dev: Fix broken plugin builds by adding missing feature flag check #3053 (Build)
- Enhancement: add management link to Reviews panel. #3011 (Activity Panel)
- Fix: Error in category comparison chart. #3027 (Analytics, REST API)
- Dev: Add the ability to create custom plugin builds #3044 (Build)
- Fix: create table error during import. #3022 (Analytics, Build)
- Tweak: Moved WC-Admin specific actions from TableCard to ReportTable. #2900 (Build, Components, Packages)
= 0.20.1 2019-09-24 =
- Fix: use category lookup id instead of term taxonomy id (#3027)
- Fix: Update order stats table status index length. (#3022)
= 0.20.0 2019-09-24 =
- Dev: Fix issue #2992 (order number in orders panel) #2994
- Dev: Replace lodash isNaN() with native Number.isNaN() #2998 (Build, Packages)
- Enhancement: add option to email a download link when exporting reports. #2899 (Analytics, REST API)
- Dev: Ignore filename rules for PSR-4 classes. #2967 (Build)
- Fix: Catch notices that are hooked earlier than priority 10. #2981
- Dev: Category Lookup Table - Fix Category Segments #2253
- Performance: add caching layer to analytics. #2970 (Analytics)
- Dev: Replace lodash isFinite() with native Number.isFinite() #2977 (Build, Components, Packages)
- Bug: Fix conflict with Blocks 2.4 #2846
- Dev: Add filter to notes datastore where clauses. #2819
- Dev: Copy component READMEs to docs folder before GH page deploy. #2952 (Build)
- Dev: Add GitHub pages deploy step to Travis config. #2940 (Build, Documentation)
- Dev: Fix plugin installation when working from a push instead of a pull request. #2950 (Build)
- Dev: Fix pull request branch detection on CI (try #2) #2944 (Build)
- Dev: Only build pushes to master branch. #2941 (Build)
- Performance: reduce JS bundle size. #2933 (Build)
- Dev: Fix pull request branch detection on CI #2942 (Build)
- Dev: refresh component documentation #2872 (Build, Components, Documentation, Packages)
- Dev: Simplified intervals_missing method of DataStore class #2920 👏 @codemascot
- Dev: Simplified intervals_missing method of TimeInterval class #2920 👏 @codemascot
= 0.19.0 2019-09-24 =
- Dev: Use upstream webpack-rtl-plugin #2870 (Build)
- Dev: Fix variable name typo #2922
- Fix: persist date_last_active for customer reports #2881 (Analytics) 👏 @cojennin
- Tweak: consistent naming for report columns. #2845 (Design)
- Tweak: schedule customer lookup table updates instead of running during checkout. #2832
- Tweak: have Import note action scroll to import section of settings screen. #2799 (Activity Panel, Components, Inbox, Packages)
- Bug: Fix chart type buttons misalignment #2871 (Components, Packages)
- Updated to color-studio v2 and refreshed the color scheme #2837 (Build, Components, Packages)
- Tweak: change report charts filter name. #2843 (Components, Documentation, Packages)
- Dev: Update no data message in table #2854 (Components, Packages)
= 0.18.0 2019-08-28 =
- Fix: Product in dropdown clickable in FF/Safari #2839 (Components, Packages) 👏 @cojennin
- Fix: gross order total calculation. #2817 (Analytics)
- Fix: Date calculation on notes being double adjusted to UTC. #2818 (Inbox)
- Bug: Orders report now shows coupons in Coupon(s) column #2812 (Analytics) 👏 @cojennin
- Fix: Bug navigating from DevDoc component pages to WP dashboard pages. #2827 (Documentation)
- Enhancement: add Facebook extension inbox note. #2798 (Activity Panel, Inbox)
- Enhancement: handle simple to variable product changes in reports. #2814 (Analytics)
- Dev: Release Scripts: Update to reflect refactor #2796 (Build)
- Fix: Import all used Gutenberg component styles. #2679
- Fix: report column visibility preference bug. #2806 (Analytics, Components, Packages, Settings)
- Dev: Components: prepare changelogs for release #2802 (Build, Components, Packages)
- Fix: remove date picker from Customers Report. #2805 (Analytics)
- Tweak: add empty dataset treatment for report tables. #2801 (Analytics, Components, Packages)
- Dev: Search List Control: fix long count values cut-off in IE11 #2783 (Components, Packages)
- Dev: Update List actionable items to be wrapped with Link #2779 (Components, Packages)
= 0.17.0 2019-08-15 =
- Fix: chart data fetch/render over long time periods #2785 (Analytics)
- Task: update the class filename exclusion to /src/ for PSR-4 #2794
- Tweak: Increase zIndex on popover elements. #2777
- Fix: chart display when comparing categories. #2710 (Analytics)
- Fix: Allow WooCommerce Admin to be deleted through the plugin screen when WooCommerce is not active. #2762
- Fix: Charts being partially rendered on long time periods. #2776 (Analytics)
- Task: update WC tested to version to 3.7.0 #2782
- Fix: Customer last active date showing `Invalid date`. #2764 (Analytics)
- Task: Update report endpoints to be PSR-4 autoloaded #2755 (Build)
- Bug: Fix daily cron event (PSR-4) #2754 (Build)
- Fix: issue where product category update button was not always clickable #2753
- Fix: Add version parameter to `_doing_it_wrong` on `current_screen`. #2733
- Task: Update feature classes to be PSR-4 autoloaded. #2736 (Build)
- Fix: Short circuit admin title filter when applied by third parties too early. #2744
- Fix: chart display when comparing categories. #2708 (Analytics)
- Bug: Only apply current submenu CSS reset on non-embed pages. #2687
- Dev: Add `wc_admin_get_feature_config` filter to feature config array. #2689
= 0.16.0 2019-07-24 =
- Tweak: Change verbiage of feedback notification. #2677
- Dev: Update unit tests to work with PHPUnit 7+. #2678
- Fix: Bug that was preventing confirmation dialog from showing after saving settings. #2660
- Fix: completed orders lingering in activity panel. #2659 (Activity Panel, REST API)
- Dev: Webpack: Use wp.url instead of bundling package #2663 (Build)
- Fix: Location of unread indicators in activity panel. #2662
- Bug: Fix missing nav on connected admin pages. #2676
- Task: Add priority 2 Tracks events #2633 (Components, Packages)
- Dev: Replace 'stringifyQuery' with 'addQueryArgs' #2655 (Build, Components, Packages)
- Task: Add priority 3 Tracks events #2638 (Components, Packages)
- Fix: Z-index issue in empty message on chart #2646 (Components, Packages)
- Dev: Tests setup: bring in Woo Core dependencies #2640 (Build)
- Fix: Disable placeholder animation when prefers-reduced-motion is set #2645 (Accessibility)
- Task: Add instructions for translating to contributing docs. #2618 (Documentation)
- Dev: show example extension list as list in readme #2619 (Documentation)
- Fix: react-spring breaking IE11 #2698 (Build)
= 0.15.0 2019-07-11 =
- Fix: Compare checkboxes in report tables #2571
- Dev: Introduce a new Products by Tag(s) block #554
- Dev: Tracks: Fix Link onclick #2594 (Components, Packages)
- Fix: Use correct links in DevDocs. #2602 (Documentation)
- Fix: Prevent error when no actionable order statuses are set #2577
- Fix: exclude old WC auto-draft orders from import #2612 (Analytics)
- Dev: Fix: propType validation warnings on embedded pages #2607
- Fix: Only calculate one order row when the order has multiple coupons. #2484 (Analytics)
- Fix: Cast is_snoozable as int prior to database save #2573
- Performance: check for scheduled snooze action once per hour instead of every admin request. #2593
- Fix: Update layout of Settings Page and notify users when settings are not saved. #2544
- Dev: Update color-studio to 1.0.5 #2575 (Build, Packages)
- Task: Add tracks event to Store Alert and Inbox Notification action clicks #2559
- Fix: Change size of Header to 56px - fixes bug in product edit page toolbar being hidden.
- Dev: Add countLabel prop to SearchListItem #2569 (Components, Packages)
- Task: Send tracks event for snoozing store alerts. #2560
- Fix: Stock notifs: fix low_stock_amount for variations #2546
- Enhancement: Add Report CSV Export Endpoint #2526 (Analytics, REST API)
- Tweak: remove placeholder link. #2561
- Fix: WordPress Notifications Activity Panel falsely saying there are notices to be seen. #2552
- Dev: Route handling: Remove hash in favor of path parameter #2444 (Components, Packages)
- Dev: Customizable dashboard: handle turned off feature flag #2548
- Dev: Dashboard: Add Tracks events #2550 (Dashboard)
- Task: Tracks: Add chart and table events #2557
- Fix: Fixes issue with alerts on mobile #2537
- Bug: Fix batch queue range bug. #2521
- Dev: Advanced Filters: Add Tracks events #2525 (Components, Packages)
- Tweak: Open external note action links in a new tab #2462 (Activity Panel, Inbox)
- Tweak: fix some report endpoint default params. #2496 (REST API)
- Enhancement: Remove updated stock products from Activity Panel #2442 (Activity Panel)
- Fix: Track name for activity panel open event #2532
- Enhancement: Add Reports CSV Exporter Class #2502 (Analytics)
- Fix: margins on non-update notices. #2487 👏 @benignant
- Enhancement: add state/region to Customers Report. #2463 (Analytics) 👏 @KZeni
= 0.14.0 2019-06-24 =
- Dev: Action Scheduler: fix potential endless sync #2425
- Dev: Fix Activity Panel being overlapped by editor toolbar #2446 (Activity Panel)
- Bug: add match=any support for advanced orders table filter #2228
- Dev: update tested to version in plugin repo readme.txt #2443
- Fix: PHP errors on missing refund orders during import. #2464 (Analytics)
- Dev: Allow keyboard interaction in Stock Activity Panel form #2447 (Accessibility, Activity Panel)
- Dev: Activity Panels: Add track event when panels are opened. #2498 (Build)
- Dev: Add wp-api-fetch script to dependencies #2460
- Dev: Remove the "wpClosedMenu" page setting #2441 (Build)
- Enhancement: Report Tables: Add example extension #2440 (Build, Components, Extensibility, Packages)
- Fix: fix type in readme changelog for 0.13.0 #2457 (Documentation)
- Tweak: Add test to catch missing version updates. #2424 (Build)
- Dev: Fix revenue and orders charts using the wrong endpoint in the Dashboard #2417 (Dashboard)
- Fix: handle internal `rest_do_request()` error conditions. #2423 (Dashboard)
- Dev: Fix conflicts with WordPress.com toolbar #2431 (Activity Panel)
- Task: Remove test menu from Orders panel #2438 (Activity Panel)
- Dev: Table queries: remove befor/after from no interval endpoints #2422
- Tweak: Reduce style dependencies on WP core, avoid errantly including WP core's Google Fonts. #2432 (Components)
- Dev: Update Dashboard Charts naming and order #2429 (Dashboard)
- Bug: Fix search bar in Orders page not being tapable on mobile #2384
= 0.13.2 2019-06-13 =
- Fix: Bump plugin version for database update.
= 0.13.1 2019-06-12 =
- Fix: Exit deactivate early if WooCommerce not active. #2410
= 0.13.0 2019-06-12 =
- Fix: Notes: update sales record link #2397
- Enhancement: Settings: Add default date settings #2292 (Components, Dashboard, Packages)
- Dev: Add tracks to import jobs #2193
- Dev: Notes: Add filters to disable milestone and sales record notes. #2227
- Enhancement: Trigger an action server side when admin note actions are clicked #2325
- Dev: Fix Activity Panel layout on mobile #2405 (Activity Panel)
- Dev: Add uninstall file to release ZIP #2402 (Build)
- Bug: Fix wrong average numbers in chart legends #2352 (Analytics, Components, Packages)
- Dev: Add date_created_gmt property to orders endpoint #2086 (Activity Panel)
- Enhancement: Add progress bars to Historical Data Import screen #2312
- Bug: Fix some data not to being imported when 'skip_existing' option is enabled #2385
- Fix: Double space at 191 row #2369 👏 @shoheitanaka
- Enhancement: Only show unactioned notes in the Inbox panel. #2327 (Inbox)
- Bug: Don't create Notices tab in Activity Panel if notices HTML element was removed by a plugin #2378
- Tweak: Allow paragraph tags in admin notes. #2344 (Inbox)
- Tweak: Allow note action URLs to be empty. #2324 (Inbox)
- Bug: Prevent inactive tabs from making requests #2377
- Task: Remove second beta warning from readme #2362
- Fix: Misspelling at update config.js #2368 👏 @shoheitanaka
- Dev: Transpile newspack-components package #2348 (Build)
- Dev: Low stock handling inconsistencies with WooCommerce 3.6+ #2191
- Dev: Remove d3-array dependency #2286 (Build)
- Dev: Dashboard Extentions: Add a section #2280 (Build, Components, Extensibility, Packages)
- Fix: Move StoreAlerts below screen meta links on embed pages. #2291
- Fix: Extensions: fix registering a page #2301 (Extensibility)
- Dev: Fix `box-shadow-8dp` error. #2290 (Components, Packages)
- Fix: Dashboard Extension Example: Make namespace unique #2302 (Extensibility)
- Dev: Persisted Queries: Avoid adding to non time related screens #2225 (Build)
- Dev: Remove customizable dashboard heading #2321 (Dashboard)
- Bug: Add missing space in Orders Activity Panel card #2306 (Activity Panel)
- Task: Remove beta warning from readme. #2340
- Dev: Dashboard: Extend charts data #2258 (Dashboard, Extensibility)
- Bug: Handle `WC_Admin_Notices`. #2245 (Activity Panel, On Merge to WC Core)
- Bug: Exclude auto-draft and trashed orders from imports. #2265
- Fix: wc-api: Remove reference to import update functions #2277 (Dashboard)
- Enhancement: Add support for “primary” admin notice actions. #2269
- Enhancement: Allow links in admin notice content. #2272
- Fix: Remove errant import update operation from `wp-api` spec. #2271
- Dev: Hook up import/totals endpoint to Historical Data Import screen #2208
- Fix: Refactor main file into singleton class structure #2226
= 0.12.0 2019-05-14 =
- Fix: dashboard issues #2194
- Fix: Dashboard: re-arrange section dropdown order #2216
- Fix: Refactor page handler API #2209
- Fix: EllipsisMenu misalignment in wide containers #2207
- Dev: Handle “all guests” when getting customers from orders. #2038
- Dev: Order milestones: prevent order count queries from being issues on every request. #2224
- Dev: Replace Rebuild reports settings with Import Historical Data #2198
- Dev: add a defaultValue parameter to the ReportFilters component #2150 👏 @rrennick
- Dev: Record refunds separate from order entries #2083
- Dev: Add refund filtering to order reports #2179
- Dev: Hook up API Historical Data screen to API endpoints #2177
- Fix: Show Y grid ticks/lines in charts where all values are lower than 1 #2147
- Fix: Dashboard: Rename class/feature to remove conflict with legacy dashboard widget #2138
- Fix: add defaultOrder field to tablecard header #2139 👏 @rrennick
- Fix: Travis: Exit scripts with error when one occurs #2184
- Dev: Save section/block preferences in user data #2148
- Fix: Vertically center Dashboard 'Add more' button #2171
- Fix: Don't make an API call when there are no performance indicators #2157
- Dev: Add Historical Data Import Screen #2073
- Fix: Fix customer type filter param in orders report #2169
- Dev: EllipsisMenu: Refactor children to renderContent #2154
- Dev: Dashboard: Section add, remove, move up, and move down #2134
- Fix: Persist chart interval in Dashboard #2156
- Dev: Allow renaming Dashboard sections #2131
- Dev: Add import endpoint and controller #2034
- Fix: Prevent chart type buttons breaking in two lines #2135
- Dev: Support Sequential Order Numbers in Downloads Report order number filter #1907
- Dev: Scroll to top of the table when navigating table pages #2051
- Dev: Add empty state for the Reviews panels #2124
= 0.11.0 2019-04-17 =
- Dev: Extend report submenu items #2033
- Dev: Extension Examples #2018
- Dev: Update admin notes readme #2043
- Dev: Add profile wizard base page #1933
- Dev: Method to publish core ready features #1863
- Dev: Add PHP autofix to pre-commit hook #2022
- Dev: PHP Lint fixes #2023
- Dev: Update Link in readme to Proper Location #2026
- Dev: Split D3Chart utils/axis file #2000
- Dev: Remove coupon_total from order stats table #1975
- Fix: Transpile acorn-jsx package #2064
- Fix: Inbox: Giving Feedback Notice #2019 👏 @Mr-AjayM
- Fix: Check if welcome message notice exists before creating it #2063
- Fix: Respect manual offsets in default before/after params #2035
- Fix: Fix wrong dates in Reviews and Orders panels of the Activity Panel #2013
- Fix: Make customer name extendable #1976
- Fix: center mobile icon vertically #1990 👏 @rrennick
- Fix: search filter focus #1994
- Fix: Fix date filter layout on Android #1996
- Fix: Order Status filter: <select> is not vertically aligned #1971 👏 @dinhtungdu
- Fix: add filter length condition to enable filter check #1983 👏 @rrennick
- Fix: lookup product title in order meta if product has been deleted #1977 👏 @rrennick
- Fix: Cast order totals as floats when calculating net total #1945
- Enhancement: Add empty state in Stock panel #2049
- Enhancement: Add empty state in Orders panel #2037
- Enhancement: Hook up Stock panel #1998
- Enhancement: Add order milestone notifications. #1872
- Enhancement: Add leaderboard controller and endpoint #1995
- Enhancement: Hook up leaderboards endpoint to dashboard #2004
- Enhancement: Activity Panel: Add a mobile app note #2008
- Enhancement: Scroll to top only when URL changes #1989
- Enhancement: Allow negative values in charts #1979
= 0.10.0 2019-04-02 =
- Dev: Properly namespaced methods in wc-admin.php. props @ronakganatra9 #1804
- Dev: Changed text-domain to `woocommerce-admin` #1795
- Dev: Updates to plugin header #1764
- Dev: Clear scheduled actions on plugin deactivate #1715
- Dev: Refactor getFilterQuery #1873
- Dev: Update npm dependencies #1913
- Fix: ESLint error in leaderboards #1914
- Fix: REST API Requests failing with isUnboundedRequest #1954
- Fix: Bugs in Reviews panel #1947
- Fix: Scroll to top of page when changing reports #1958
- Fix: Update variations controller filter names #1949
- Fix: Sku sorting in variations report #1952 #1801 props @ronakganatra9
- Fix: REST API customers exact match filters
- Fix: EmptyContent on narrow viewports #1703
- Fix: Empty chart and summary numbers in Categories Report #1698
- Fix: Display chart and summary numbers placeholders when loading search terms #1694
- Fix: Display order number on reports #1706
- Fix: Missing Date Picker Calendar in RTL #1810
- Fix: Circles on chart hover #1816
- Fix: Date Range dropdown for RTL locales #1803
- Fix: Sorting by tax_code in Taxes Report #1811
- Fix: Charts in RTL locales #1794
- Fix: RTL css #1781
- Fix: CSS Lint issues in store-alerts #1793
- Fix: Products stats segmentation when filtering categories #1779
- Fix: Remove timezone from appendTimestamp() output #1778
- Fix: REST API Make variations and coupons endpoints return zero-value items #1765
- Fix: REST API make categories return zero-value items #1755
- Fix: REST API Make products and rates endpoints return zero-value items #1722
- Fix: Taxes report, sort taxes numerically #1766
- Fix: Chart comparison mode single category #1762
- Fix: Simplify options in filters #1748
- Fix: Show compared keys in chart legends even if their values are 0 #1754
- Fix: IE11 Leaderboard issue
- Fix: Use IDs instead of labels to identify chart legend items #1730
- Fix: Update table columns visibility when loading finishes #1729
- Fix: Update chart colors #1731
- Fix: Date Picker css tabs #1717
- Fix: Infinite loop in customer population #1936
- Fix: PHP notices on orders data store #1870
- Fix: Table links to internal reports #1824
- Accessibility: Add button focus style to report chart interval selector #1740
- Enhancement: Add "Cancel All" method to ActionScheduler Store #1813
- Enhancement: Add store notice for processing historical data #1763
- Enhancement: Ensure other scheduled actions are claimed before WC Admin actions #1759
- Enhancement: Load on hold reviews in the Activity Panel: #1871
- Enhancement: Add overlay to Product Placeholder image #1880
- Enhancement: Added "Remind me later" ability to StoreAlert component #1848
- Enhancement: Set reviews Activity Panel unread indicator based off REST API data #1824
- Enhancement: Set orders Activity Panel unread indicator based off REST API data #1860
- Enhancement: Hide Reviews Activity Panel if reviews are disabled #1852
- Enhancement: Lookup product title in order meta if product has been deleted #1977
- Performance: Avoid un-needed chart re-renders #1780
- Performance: Don't run lookup table historical population on activation of plugin #1741
- Performance: Don't dispatch REST API requests when window/tab is hidden #1732
- Performance: Only check for unsnooze note on admin_init #1960
= 0.8.0 - 2019-02-28 =
- Table Component: Reset search on compare
- MenuItem Component: Added isCheckBox prop
- a11y: Enhancements to “Rows per Table” on the dashboard
- Taxes Report: Fix comparison mode and segmentation labels
- Fix css linter errors
- Test Framework: Require WordPress 5.0 or greater to run phpunit
- Table Component: Fix search positioning in small viewports
- Chart Component: Preserve chart colors bug fix
- Navigation: Close wp-admin menu when navigating between reports in narrow viewports
- Analytics: Dont show variation table when in an active search
- Dashboard: Fix for style of summary number placeholders
- Downloads/Customers Report: Fix for filters
- Product Variation name format now matches Woo core
- phpcs fixes in unit tests
- Chart: Mouse pointer now displayed in entire clickable area
- Analytics: Sort tabular data when selecting a Summary Number
- Chart: Fixes for the legend totals
- Build: Move IE11 css to specific css file
- Downloads Report: Fix for sorting bug
- Stock Report: fix the product link
- Chart: Refactor of d3 logic
- Autocompleter: Increase debounce time to reduce API requests
- Segmented Selection Component: fix for missing borders
- Chart: Add messaging when no data is available for selected filters
- Setup: Improvements to install flow

View File

@@ -0,0 +1,94 @@
<?php
/**
* REST API Coupons Controller
*
* Handles requests to /coupons/*
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Coupons controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Coupons_Controller
*/
class Coupons extends \WC_REST_Coupons_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['search'] = array(
'description' => __( 'Limit results to coupons with codes matching a given string.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Add coupon code searching to the WC API.
*
* @param WP_REST_Request $request Request data.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
if ( ! empty( $request['search'] ) ) {
$args['search'] = $request['search'];
$args['s'] = false;
}
return $args;
}
/**
* Get a collection of posts and add the code search option to WP_Query.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_search_code_filter' ), 10, 2 );
$response = parent::get_items( $request );
remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_search_code_filter' ), 10 );
return $response;
}
/**
* Add code searching to the WP Query
*
* @param string $where Where clause used to search posts.
* @param object $wp_query WP_Query object.
* @return string
*/
public static function add_wp_query_search_code_filter( $where, $wp_query ) {
global $wpdb;
$search = $wp_query->get( 'search' );
if ( $search ) {
$search = $wpdb->esc_like( $search );
$search = "'%" . $search . "%'";
$where .= ' AND ' . $wpdb->posts . '.post_title LIKE ' . $search;
}
return $where;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* REST API Customers Controller
*
* Handles requests to /customers/*
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Customers controller.
*
* @package WooCommerce Admin/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller
*/
class Customers extends \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'customers';
/**
* Register the routes for customers.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique ID for the resource.', 'woocommerce-admin' ),
'type' => 'integer',
),
),
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = parent::prepare_reports_query( $request );
$args['customers'] = $request['include'];
return $args;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['include'] = $params['customers'];
unset( $params['customers'] );
return $params;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* REST API Data Controller
*
* Handles requests to /data
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Data controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class Data extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Return the list of data resources.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$response = parent::get_items( $request );
$response->data[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response(
(object) array(
'slug' => 'download-ips',
'description' => __( 'An endpoint used for searching download logs for a specific IP address.', 'woocommerce-admin' ),
),
$request
)
);
return $response;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* REST API Data countries controller.
*
* Handles requests to the /data/countries endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* REST API Data countries controller class.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Countries_Controller
*/
class DataCountries extends \WC_REST_Data_Countries_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* REST API Data Download IP Controller
*
* Handles requests to /data/download-ips
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Data Download IP controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class DataDownloadIPs extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data/download-ips';
/**
* Register routes.
*
* @since 3.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Return the download IPs matching the passed parameters.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
global $wpdb;
if ( isset( $request['match'] ) ) {
$downloads = $wpdb->get_results(
$wpdb->prepare(
"SELECT DISTINCT( user_ip_address ) FROM {$wpdb->prefix}wc_download_log
WHERE user_ip_address LIKE %s
LIMIT 10",
$request['match'] . '%'
)
);
} else {
return new \WP_Error( 'woocommerce_rest_data_download_ips_invalid_request', __( 'Invalid request. Please pass the match parameter.', 'woocommerce-admin' ), array( 'status' => 400 ) );
}
$data = array();
if ( ! empty( $downloads ) ) {
foreach ( $downloads as $download ) {
$response = $this->prepare_item_for_response( $download, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
}
return rest_ensure_response( $data );
}
/**
* Prepare the data object for response.
*
* @since 3.5.0
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter the list returned from the API.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original item.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_data_download_ip', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given object.
*/
protected function prepare_links( $item ) {
$links = array(
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['match'] = array(
'description' => __( 'A partial IP address can be passed and matching results will be returned.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_download_ips',
'type' => 'object',
'properties' => array(
'user_ip_address' => array(
'type' => 'string',
'description' => __( 'IP address.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* REST API bootstrap.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\Loader;
/**
* Init class.
*/
class Init {
/**
* Boostrap REST API.
*/
public function __construct() {
// Hook in data stores.
add_filter( 'woocommerce_data_stores', array( __CLASS__, 'add_data_stores' ) );
// REST API extensions init.
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
// Add currency symbol to orders endpoint response.
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
}
/**
* Init REST API.
*/
public function rest_api_init() {
$controllers = array(
'Automattic\WooCommerce\Admin\API\Notes',
'Automattic\WooCommerce\Admin\API\NoteActions',
'Automattic\WooCommerce\Admin\API\Coupons',
'Automattic\WooCommerce\Admin\API\Customers',
'Automattic\WooCommerce\Admin\API\Data',
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Leaderboards',
'Automattic\WooCommerce\Admin\API\Options',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductCategories',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductReviews',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\Reports\Controller',
'Automattic\WooCommerce\Admin\API\SettingOptions',
'Automattic\WooCommerce\Admin\API\Reports\Import\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Export\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Categories\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Taxes',
'Automattic\WooCommerce\Admin\API\Themes',
);
if ( Loader::is_onboarding_enabled() ) {
$controllers = array_merge(
$controllers,
array(
'Automattic\WooCommerce\Admin\API\OnboardingProfile',
'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
'Automattic\WooCommerce\Admin\API\OnboardingTasks',
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
)
);
}
// The performance indicators controller must be registered last, after other /stats endpoints have been registered.
$controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
$controllers = apply_filters( 'woocommerce_admin_rest_controllers', $controllers );
foreach ( $controllers as $controller ) {
$this->$controller = new $controller();
$this->$controller->register_routes();
}
}
/**
* Adds data stores.
*
* @param array $data_stores List of data stores.
* @return array
*/
public static function add_data_stores( $data_stores ) {
return array_merge(
$data_stores,
array(
'report-revenue-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
'report-orders' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore',
'report-orders-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
'report-products' => 'Automattic\WooCommerce\Admin\API\Reports\Products\DataStore',
'report-variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore',
'report-products-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\DataStore',
'report-categories' => 'Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore',
'report-taxes' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore',
'report-taxes-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore',
'report-coupons' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore',
'report-coupons-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore',
'report-downloads' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore',
'report-downloads-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\DataStore',
'admin-note' => 'Automattic\WooCommerce\Admin\Notes\DataStore',
'report-customers' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore',
'report-customers-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore',
'report-stock-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore',
)
);
}
/**
* Add the currency symbol (in addition to currency code) to each Order
* object in REST API responses. For use in formatCurrency().
*
* @param {WP_REST_Response} $response REST response object.
* @returns {WP_REST_Response}
*/
public static function add_currency_symbol_to_order_response( $response ) {
$response_data = $response->get_data();
$currency_code = $response_data['currency'];
$currency_symbol = get_woocommerce_currency_symbol( $currency_code );
$response_data['currency_symbol'] = html_entity_decode( $currency_symbol );
$response->set_data( $response_data );
return $response;
}
}

View File

@@ -0,0 +1,553 @@
<?php
/**
* REST API Leaderboards Controller
*
* Handles requests to /leaderboards
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore as CategoriesDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
/**
* Leaderboards controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class Leaderboards extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'leaderboards';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/allowed',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_allowed_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_allowed_item_schema' ),
)
);
}
/**
* Get the data for the coupons leaderboard.
*
* @param int $per_page Number of rows.
* @param string $after Items after date.
* @param string $before Items before date.
* @param string $persisted_query URL query string.
*/
public function get_coupons_leaderboard( $per_page, $after, $before, $persisted_query ) {
$coupons_data_store = new CouponsDataStore();
$coupons_data = $per_page > 0 ? $coupons_data_store->get_data(
array(
'orderby' => 'orders_count',
'order' => 'desc',
'after' => $after,
'before' => $before,
'per_page' => $per_page,
'extended_info' => true,
)
)->data : array();
$rows = array();
foreach ( $coupons_data as $coupon ) {
$url_query = wp_parse_args(
array(
'filter' => 'single_coupon',
'coupons' => $coupon['coupon_id'],
),
$persisted_query
);
$coupon_url = wc_admin_url( '/analytics/coupons', $url_query );
$coupon_code = isset( $coupon['extended_info'] ) && isset( $coupon['extended_info']['code'] ) ? $coupon['extended_info']['code'] : '';
$rows[] = array(
array(
'display' => "<a href='{$coupon_url}'>{$coupon_code}</a>",
'value' => $coupon_code,
),
array(
'display' => wc_admin_number_format( $coupon['orders_count'] ),
'value' => $coupon['orders_count'],
),
array(
'display' => wc_price( $coupon['amount'] ),
'value' => $coupon['amount'],
),
);
}
return array(
'id' => 'coupons',
'label' => __( 'Top Coupons - Number of Orders', 'woocommerce-admin' ),
'headers' => array(
array(
'label' => __( 'Coupon Code', 'woocommerce-admin' ),
),
array(
'label' => __( 'Orders', 'woocommerce-admin' ),
),
array(
'label' => __( 'Amount Discounted', 'woocommerce-admin' ),
),
),
'rows' => $rows,
);
}
/**
* Get the data for the categories leaderboard.
*
* @param int $per_page Number of rows.
* @param string $after Items after date.
* @param string $before Items before date.
* @param string $persisted_query URL query string.
*/
public function get_categories_leaderboard( $per_page, $after, $before, $persisted_query ) {
$categories_data_store = new CategoriesDataStore();
$categories_data = $per_page > 0 ? $categories_data_store->get_data(
array(
'orderby' => 'items_sold',
'order' => 'desc',
'after' => $after,
'before' => $before,
'per_page' => $per_page,
'extended_info' => true,
)
)->data : array();
$rows = array();
foreach ( $categories_data as $category ) {
$url_query = wp_parse_args(
array(
'filter' => 'single_category',
'categories' => $category['category_id'],
),
$persisted_query
);
$category_url = wc_admin_url( '/analytics/categories', $url_query );
$category_name = isset( $category['extended_info'] ) && isset( $category['extended_info']['name'] ) ? $category['extended_info']['name'] : '';
$rows[] = array(
array(
'display' => "<a href='{$category_url}'>{$category_name}</a>",
'value' => $category_name,
),
array(
'display' => wc_admin_number_format( $category['items_sold'] ),
'value' => $category['items_sold'],
),
array(
'display' => wc_price( $category['net_revenue'] ),
'value' => $category['net_revenue'],
),
);
}
return array(
'id' => 'categories',
'label' => __( 'Top Categories - Items Sold', 'woocommerce-admin' ),
'headers' => array(
array(
'label' => __( 'Category', 'woocommerce-admin' ),
),
array(
'label' => __( 'Items Sold', 'woocommerce-admin' ),
),
array(
'label' => __( 'Net Sales', 'woocommerce-admin' ),
),
),
'rows' => $rows,
);
}
/**
* Get the data for the customers leaderboard.
*
* @param int $per_page Number of rows.
* @param string $after Items after date.
* @param string $before Items before date.
* @param string $persisted_query URL query string.
*/
public function get_customers_leaderboard( $per_page, $after, $before, $persisted_query ) {
$customers_data_store = new CustomersDataStore();
$customers_data = $per_page > 0 ? $customers_data_store->get_data(
array(
'orderby' => 'total_spend',
'order' => 'desc',
'order_after' => $after,
'order_before' => $before,
'per_page' => $per_page,
)
)->data : array();
$rows = array();
foreach ( $customers_data as $customer ) {
$url_query = wp_parse_args(
array(
'filter' => 'single_customer',
'customers' => $customer['id'],
),
$persisted_query
);
$customer_url = wc_admin_url( '/analytics/customers', $url_query );
$rows[] = array(
array(
'display' => "<a href='{$customer_url}'>{$customer['name']}</a>",
'value' => $customer['name'],
),
array(
'display' => wc_admin_number_format( $customer['orders_count'] ),
'value' => $customer['orders_count'],
),
array(
'display' => wc_price( $customer['total_spend'] ),
'value' => $customer['total_spend'],
),
);
}
return array(
'id' => 'customers',
'label' => __( 'Top Customers - Total Spend', 'woocommerce-admin' ),
'headers' => array(
array(
'label' => __( 'Customer Name', 'woocommerce-admin' ),
),
array(
'label' => __( 'Orders', 'woocommerce-admin' ),
),
array(
'label' => __( 'Total Spend', 'woocommerce-admin' ),
),
),
'rows' => $rows,
);
}
/**
* Get the data for the products leaderboard.
*
* @param int $per_page Number of rows.
* @param string $after Items after date.
* @param string $before Items before date.
* @param string $persisted_query URL query string.
*/
public function get_products_leaderboard( $per_page, $after, $before, $persisted_query ) {
$products_data_store = new ProductsDataStore();
$products_data = $per_page > 0 ? $products_data_store->get_data(
array(
'orderby' => 'items_sold',
'order' => 'desc',
'after' => $after,
'before' => $before,
'per_page' => $per_page,
'extended_info' => true,
)
)->data : array();
$rows = array();
foreach ( $products_data as $product ) {
$url_query = wp_parse_args(
array(
'filter' => 'single_product',
'products' => $product['product_id'],
),
$persisted_query
);
$product_url = wc_admin_url( '/analytics/products', $url_query );
$product_name = isset( $product['extended_info'] ) && isset( $product['extended_info']['name'] ) ? $product['extended_info']['name'] : '';
$rows[] = array(
array(
'display' => "<a href='{$product_url}'>{$product_name}</a>",
'value' => $product_name,
),
array(
'display' => wc_admin_number_format( $product['items_sold'] ),
'value' => $product['items_sold'],
),
array(
'display' => wc_price( $product['net_revenue'] ),
'value' => $product['net_revenue'],
),
);
}
return array(
'id' => 'products',
'label' => __( 'Top Products - Items Sold', 'woocommerce-admin' ),
'headers' => array(
array(
'label' => __( 'Product', 'woocommerce-admin' ),
),
array(
'label' => __( 'Items Sold', 'woocommerce-admin' ),
),
array(
'label' => __( 'Net Sales', 'woocommerce-admin' ),
),
),
'rows' => $rows,
);
}
/**
* Get an array of all leaderboards.
*
* @param int $per_page Number of rows.
* @param string $after Items after date.
* @param string $before Items before date.
* @param string $persisted_query URL query string.
* @return array
*/
public function get_leaderboards( $per_page, $after, $before, $persisted_query ) {
$leaderboards = array(
$this->get_customers_leaderboard( $per_page, $after, $before, $persisted_query ),
$this->get_coupons_leaderboard( $per_page, $after, $before, $persisted_query ),
$this->get_categories_leaderboard( $per_page, $after, $before, $persisted_query ),
$this->get_products_leaderboard( $per_page, $after, $before, $persisted_query ),
);
return apply_filters( 'woocommerce_leaderboards', $leaderboards, $per_page, $after, $before, $persisted_query );
}
/**
* Return all leaderboards.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$persisted_query = json_decode( $request['persisted_query'], true );
$leaderboards = $this->get_leaderboards( $request['per_page'], $request['after'], $request['before'], $persisted_query );
$data = array();
if ( ! empty( $leaderboards ) ) {
foreach ( $leaderboards as $leaderboard ) {
$response = $this->prepare_item_for_response( $leaderboard, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
}
return rest_ensure_response( $data );
}
/**
* Returns a list of allowed leaderboards.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_allowed_items( $request ) {
$leaderboards = $this->get_leaderboards( 0, null, null, null );
$data = array();
foreach ( $leaderboards as $leaderboard ) {
$data[] = (object) array(
'id' => $leaderboard['id'],
'label' => $leaderboard['label'],
'headers' => $leaderboard['headers'],
);
}
$objects = array();
foreach ( $data as $item ) {
$prepared = $this->prepare_item_for_response( $item, $request );
$objects[] = $this->prepare_response_for_collection( $prepared );
}
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', count( $data ) );
$response->header( 'X-WP-TotalPages', 1 );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
return $response;
}
/**
* Prepare the data object for response.
*
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
/**
* Filter the list returned from the API.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original item.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_leaderboard', $response, $item, $request );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 5,
'minimum' => 1,
'maximum' => 20,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['persisted_query'] = array(
'description' => __( 'URL query to persist across links.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'leaderboard',
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'string',
'description' => __( 'Leaderboard ID.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
),
'label' => array(
'type' => 'string',
'description' => __( 'Displayed title for the leaderboard.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
),
'headers' => array(
'type' => 'array',
'description' => __( 'Table headers.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'array',
'properties' => array(
'label' => array(
'description' => __( 'Table column header.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'rows' => array(
'type' => 'array',
'description' => __( 'Table rows.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'array',
'properties' => array(
'display' => array(
'description' => __( 'Table cell display.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Table cell value.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get schema for the list of allowed leaderboards.
*
* @return array $schema
*/
public function get_public_allowed_item_schema() {
$schema = $this->get_public_item_schema();
unset( $schema['properties']['rows'] );
return $schema;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* REST API Admin Note Action controller
*
* Handles requests to the admin note action endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\Notes\WC_Admin_Notes;
/**
* REST API Admin Note Action controller class.
*
* @package WooCommerce/API
* @extends WC_REST_CRUD_Controller
*/
class NoteActions extends Notes {
/**
* Register the routes for admin notes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<note_id>[\d-]+)/action/(?P<action_id>[\d-]+)',
array(
'args' => array(
'note_id' => array(
'description' => __( 'Unique ID for the Note.', 'woocommerce-admin' ),
'type' => 'integer',
),
'action_id' => array(
'description' => __( 'Unique ID for the Note Action.', 'woocommerce-admin' ),
'type' => 'integer',
),
),
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'trigger_note_action' ),
// @todo - double check these permissions for taking note actions.
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Trigger a note action.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function trigger_note_action( $request ) {
$note = WC_Admin_Notes::get_note( $request->get_param( 'note_id' ) );
if ( ! $note ) {
return new \WP_Error(
'woocommerce_note_invalid_id',
__( 'Sorry, there is no resource with that ID.', 'woocommerce-admin' ),
array( 'status' => 404 )
);
}
// Find note action by ID.
$action_id = $request->get_param( 'action_id' );
$actions = $note->get_actions( 'edit' );
$triggered_action = false;
foreach ( $actions as $action ) {
if ( $action->id === $action_id ) {
$triggered_action = $action;
}
}
if ( ! $triggered_action ) {
return new \WP_Error(
'woocommerce_note_action_invalid_id',
__( 'Sorry, there is no resource with that ID.', 'woocommerce-admin' ),
array( 'status' => 404 )
);
}
/**
* Fires when an admin note action is taken.
*
* @param string $name The triggered action name.
* @param WC_Admin_Note $note The corresponding Note.
*/
do_action( 'woocommerce_note_action', $triggered_action->name, $note );
/**
* Fires when an admin note action is taken.
* For more specific targeting of note actions.
*
* @param WC_Admin_Note $note The corresponding Note.
*/
do_action( 'woocommerce_note_action_' . $triggered_action->name, $note );
// Update the note with the status for this action.
if ( ! empty( $triggered_action->status ) ) {
$note->set_status( $triggered_action->status );
}
$note->save();
if ( in_array( $note->get_type(), array( 'error', 'update' ) ) ) {
$tracks_event = 'wcadmin_store_alert_action';
} else {
$tracks_event = 'wcadmin_inbox_action_click';
}
wc_admin_record_tracks_event(
$tracks_event,
array(
'note_name' => $note->get_name(),
'note_type' => $note->get_type(),
'note_title' => $note->get_title(),
'note_content' => $note->get_content(),
'note_icon' => $note->get_icon(),
'action_name' => $triggered_action->name,
'action_label' => $triggered_action->label,
)
);
$data = $note->get_data();
$data = $this->prepare_item_for_response( $data, $request );
$data = $this->prepare_response_for_collection( $data );
return rest_ensure_response( $data );
}
}

View File

@@ -0,0 +1,492 @@
<?php
/**
* REST API Admin Notes controller
*
* Handles requests to the admin notes endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Notes\WC_Admin_Note;
use Automattic\WooCommerce\Admin\Notes\WC_Admin_Notes;
/**
* REST API Admin Notes controller class.
*
* @package WooCommerce/API
* @extends WC_REST_CRUD_Controller
*/
class Notes extends \WC_REST_CRUD_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'admin/notes';
/**
* Register the routes for admin notes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique ID for the resource.', 'woocommerce-admin' ),
'type' => 'integer',
),
),
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get a single note.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$note = WC_Admin_Notes::get_note( $request->get_param( 'id' ) );
if ( ! $note ) {
return new \WP_Error(
'woocommerce_note_invalid_id',
__( 'Sorry, there is no resource with that ID.', 'woocommerce-admin' ),
array( 'status' => 404 )
);
}
if ( is_wp_error( $note ) ) {
return $note;
}
$data = $note->get_data();
$data = $this->prepare_item_for_response( $data, $request );
$data = $this->prepare_response_for_collection( $data );
return rest_ensure_response( $data );
}
/**
* Get all notes.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response
*/
public function get_items( $request ) {
$query_args = $this->prepare_objects_query( $request );
$notes = WC_Admin_Notes::get_notes( 'edit', $query_args );
$data = array();
foreach ( (array) $notes as $note_obj ) {
$note = $this->prepare_item_for_response( $note_obj, $request );
$note = $this->prepare_response_for_collection( $note );
$data[] = $note;
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', WC_Admin_Notes::get_notes_count( $query_args['type'], $query_args['status'] ) );
return $response;
}
/**
* Prepare objects query.
*
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = array();
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['per_page'] = $request['per_page'];
$args['page'] = $request['page'];
$args['type'] = isset( $request['type'] ) ? $request['type'] : array();
$args['status'] = isset( $request['status'] ) ? $request['status'] : array();
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date_created';
}
/**
* Filter the query arguments for a request.
*
* Enables adding extra arguments or setting defaults for a post
* collection request.
*
* @param array $args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$args = apply_filters( 'woocommerce_rest_notes_object_query', $args, $request );
return $args;
}
/**
* Check whether a given request has permission to read a single note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to read notes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Update a single note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function update_item( $request ) {
$note = WC_Admin_Notes::get_note( $request->get_param( 'id' ) );
if ( ! $note ) {
return new \WP_Error(
'woocommerce_note_invalid_id',
__( 'Sorry, there is no resource with that ID.', 'woocommerce-admin' ),
array( 'status' => 404 )
);
}
// @todo Status is the only field that can be updated at the moment. We should also implement the "date reminder" setting.
$note_changed = false;
if ( ! is_null( $request->get_param( 'status' ) ) ) {
$note->set_status( $request->get_param( 'status' ) );
$note_changed = true;
}
if ( ! is_null( $request->get_param( 'date_reminder' ) ) ) {
$note->set_date_reminder( $request->get_param( 'date_reminder' ) );
$note_changed = true;
}
if ( $note_changed ) {
$note->save();
}
return $this->get_item( $request );
}
/**
* Makes sure the current user has access to WRITE the settings APIs.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Prepare a path or query for serialization to the client.
*
* @param string $query The query, path, or URL to transform.
* @return string A fully formed URL.
*/
public function prepare_query_for_response( $query ) {
if ( empty( $query ) ) {
return $query;
}
if ( 'https://' === substr( $query, 0, 8 ) ) {
return $query;
}
if ( 'http://' === substr( $query, 0, 7 ) ) {
return $query;
}
if ( '?' === substr( $query, 0, 1 ) ) {
return admin_url( 'admin.php' . $query );
}
return admin_url( $query );
}
/**
* Prepare a note object for serialization.
*
* @param array $data Note data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $data, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data['date_created_gmt'] = wc_rest_prepare_date_response( $data['date_created'] );
$data['date_created'] = wc_rest_prepare_date_response( $data['date_created'], false );
$data['date_reminder_gmt'] = wc_rest_prepare_date_response( $data['date_reminder'] );
$data['date_reminder'] = wc_rest_prepare_date_response( $data['date_reminder'], false );
$data['title'] = stripslashes( $data['title'] );
$data['content'] = stripslashes( $data['content'] );
$data['is_snoozable'] = (bool) $data['is_snoozable'];
foreach ( (array) $data['actions'] as $key => $value ) {
$data['actions'][ $key ]->label = stripslashes( $data['actions'][ $key ]->label );
$data['actions'][ $key ]->url = $this->prepare_query_for_response( $data['actions'][ $key ]->query );
$data['actions'][ $key ]->status = stripslashes( $data['actions'][ $key ]->status );
}
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links(
array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $data['id'] ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
)
);
/**
* Filter a note returned from the API.
*
* Allows modification of the note data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param array $data The original note.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_note', $response, $data, $request );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'note_id',
'date',
'type',
'title',
'status',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['type'] = array(
'description' => __( 'Type of note.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => WC_Admin_Note::get_allowed_types(),
'type' => 'string',
),
);
$params['status'] = array(
'description' => __( 'Status of note.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => WC_Admin_Note::get_allowed_statuses(),
'type' => 'string',
),
);
return $params;
}
/**
* Get the note's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'note',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID of the note record.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Name of the note.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'The type of the note (e.g. error, warning, etc.).', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'locale' => array(
'description' => __( 'Locale used for the note title and content.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Title of the note.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'content' => array(
'description' => __( 'Content of the note.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'icon' => array(
'description' => __( 'Icon (gridicon) for the note.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'content_data' => array(
'description' => __( 'Content data for the note. JSON string. Available for re-localization.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'The status of the note (e.g. unactioned, actioned).', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'source' => array(
'description' => __( 'Source of the note.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( 'Date the note was created.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'Date the note was created (GMT).', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_reminder' => array(
'description' => __( 'Date after which the user should be reminded of the note, if any.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true, // @todo Allow date_reminder to be updated.
),
'date_reminder_gmt' => array(
'description' => __( 'Date after which the user should be reminded of the note, if any (GMT).', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'is_snoozable' => array(
'description' => __( 'Whether or a user can request to be reminded about the note.', 'woocommerce-admin' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'actions' => array(
'description' => __( 'An array of actions, if any, for the note.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@@ -0,0 +1,574 @@
<?php
/**
* REST API Onboarding Plugins Controller
*
* Handles requests to install and activate depedent plugins.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\Features\Onboarding;
defined( 'ABSPATH' ) || exit;
/**
* Onboarding Plugins Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class OnboardingPlugins extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/plugins';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'install_plugin' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/active',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'active_plugins' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'activate_plugins' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/connect-jetpack',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'connect_jetpack' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/request-wccom-connect',
array(
array(
'methods' => 'POST',
'callback' => array( $this, 'request_wccom_connect' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/finish-wccom-connect',
array(
array(
'methods' => 'POST',
'callback' => array( $this, 'finish_wccom_connect' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/connect-paypal',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'connect_paypal' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/connect-square',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'connect_square' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
}
/**
* Check if a given request has access to manage plugins.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Installs the requested plugin.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Plugin Status
*/
public function install_plugin( $request ) {
$allowed_plugins = Onboarding::get_allowed_plugins();
$plugin = sanitize_title_with_dashes( $request['plugin'] );
if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$path = $allowed_plugins[ $plugin ];
$slug = sanitize_key( $plugin );
$installed_plugins = get_plugins();
if ( in_array( $path, array_keys( $installed_plugins ), true ) ) {
return( array(
'slug' => $slug,
'name' => $installed_plugins[ $path ]['Name'],
'status' => 'success',
) );
}
include_once ABSPATH . '/wp-admin/includes/admin.php';
include_once ABSPATH . '/wp-admin/includes/plugin-install.php';
include_once ABSPATH . '/wp-admin/includes/plugin.php';
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
$api = plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( is_wp_error( $api ) ) {
$properties = array(
/* translators: %s: plugin slug (example: woocommerce-services) */
'error_message' => __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce-admin' ),
'api' => $api,
'slug' => $slug,
);
wc_admin_record_tracks_event( 'install_plugin_error', $properties );
return new \WP_Error(
'woocommerce_rest_plugin_install',
sprintf(
/* translators: %s: plugin slug (example: woocommerce-services) */
__( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce-admin' ),
$slug
),
500
);
}
$upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() );
$result = $upgrader->install( $api->download_link );
if ( is_wp_error( $result ) || is_null( $result ) ) {
$properties = array(
/* translators: %s: plugin slug (example: woocommerce-services) */
'error_message' => __( 'The requested plugin `%s` could not be installed.', 'woocommerce-admin' ),
'slug' => $slug,
'api' => $api,
'upgrader' => $upgrader,
'result' => $result,
);
wc_admin_record_tracks_event( 'install_plugin_error', $properties );
return new \WP_Error(
'woocommerce_rest_plugin_install',
sprintf(
/* translators: %s: plugin slug (example: woocommerce-services) */
__( 'The requested plugin `%s` could not be installed.', 'woocommerce-admin' ),
$slug
),
500
);
}
return array(
'slug' => $slug,
'name' => $api->name,
'status' => 'success',
);
}
/**
* Returns a list of active plugins.
*
* @return array Active plugins
*/
public function active_plugins() {
$plugins = Onboarding::get_active_plugins();
return( array(
'plugins' => array_values( $plugins ),
) );
}
/**
* Activate the requested plugin.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Plugin Status
*/
public function activate_plugins( $request ) {
$allowed_plugins = Onboarding::get_allowed_plugins();
$_plugins = explode( ',', $request['plugins'] );
$plugins = array_intersect( array_keys( $allowed_plugins ), $_plugins );
if ( empty( $plugins ) || ! is_array( $plugins ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Invalid plugins.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
foreach ( $plugins as $plugin ) {
$slug = $plugin;
$path = $allowed_plugins[ $slug ];
$installed_plugins = get_plugins();
if ( ! in_array( $path, array_keys( $installed_plugins ), true ) ) {
/* translators: %s: plugin slug (example: woocommerce-services) */
return new \WP_Error( 'woocommerce_rest_invalid_plugin', sprintf( __( 'Invalid plugin %s.', 'woocommerce-admin' ), $slug ), 404 );
}
$result = activate_plugin( $path );
if ( ! is_null( $result ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', sprintf( __( 'The requested plugins could not be activated.', 'woocommerce-admin' ), $slug ), 500 );
}
}
return( array(
'activatedPlugins' => array_values( $plugins ),
'active' => Onboarding::get_active_plugins(),
'status' => 'success',
) );
}
/**
* Generates a Jetpack Connect URL.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Connection URL for Jetpack
*/
public function connect_jetpack( $request ) {
if ( ! class_exists( '\Jetpack' ) ) {
return new \WP_Error( 'woocommerce_rest_jetpack_not_active', __( 'Jetpack is not installed or active.', 'woocommerce-admin' ), 404 );
}
$redirect_url = apply_filters( 'woocommerce_admin_onboarding_jetpack_connect_redirect_url', esc_url_raw( $request['redirect_url'] ) );
$connect_url = \Jetpack::init()->build_connect_url( true, $redirect_url, 'woocommerce-onboarding' );
$calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ) ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production';
$connect_url = add_query_arg( array( 'calypso_env' => $calypso_env ), $connect_url );
return( array(
'slug' => 'jetpack',
'name' => __( 'Jetpack', 'woocommerce-admin' ),
'connectAction' => $connect_url,
) );
}
/**
* Kicks off the WCCOM Connect process.
*
* @return WP_Error|array Connection URL for WooCommerce.com
*/
public function request_wccom_connect() {
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-api.php';
if ( ! class_exists( 'WC_Helper_API' ) ) {
return new WP_Error( 'woocommerce_rest_helper_not_active', __( 'There was an error loading the WooCommerce.com Helper API.', 'woocommerce-admin' ), 404 );
}
$redirect_uri = wc_admin_url( '&task=connect&wccom-connected=1' );
$request = \WC_Helper_API::post(
'oauth/request_token',
array(
'body' => array(
'home_url' => home_url(),
'redirect_uri' => $redirect_uri,
),
)
);
$code = wp_remote_retrieve_response_code( $request );
if ( 200 !== $code ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce-admin' ), 500 );
}
$secret = json_decode( wp_remote_retrieve_body( $request ) );
if ( empty( $secret ) ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce-admin' ), 500 );
}
do_action( 'woocommerce_helper_connect_start' );
$connect_url = add_query_arg(
array(
'home_url' => rawurlencode( home_url() ),
'redirect_uri' => rawurlencode( $redirect_uri ),
'secret' => rawurlencode( $secret ),
'wccom-from' => 'onboarding',
),
\WC_Helper_API::url( 'oauth/authorize' )
);
if ( defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ) ) ) {
$connect_url = add_query_arg(
array(
'calypso_env' => WOOCOMMERCE_CALYPSO_ENVIRONMENT,
),
$connect_url
);
}
return( array(
'connectAction' => $connect_url,
) );
}
/**
* Finishes connecting to WooCommerce.com.
*
* @param object $rest_request Request details.
* @return WP_Error|array Contains success status.
*/
public function finish_wccom_connect( $rest_request ) {
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper.php';
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-api.php';
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-updater.php';
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
if ( ! class_exists( 'WC_Helper_API' ) ) {
return new WP_Error( 'woocommerce_rest_helper_not_active', __( 'There was an error loading the WooCommerce.com Helper API.', 'woocommerce-admin' ), 404 );
}
// Obtain an access token.
$request = \WC_Helper_API::post(
'oauth/access_token',
array(
'body' => array(
'request_token' => wp_unslash( $rest_request['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
'home_url' => home_url(),
),
)
);
$code = wp_remote_retrieve_response_code( $request );
if ( 200 !== $code ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce-admin' ), 500 );
}
$access_token = json_decode( wp_remote_retrieve_body( $request ), true );
if ( ! $access_token ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce-admin' ), 500 );
}
\WC_Helper_Options::update(
'auth',
array(
'access_token' => $access_token['access_token'],
'access_token_secret' => $access_token['access_token_secret'],
'site_id' => $access_token['site_id'],
'user_id' => get_current_user_id(),
'updated' => time(),
)
);
if ( ! \WC_Helper::_flush_authentication_cache() ) {
\WC_Helper_Options::update( 'auth', array() );
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to WooCommerce.com. Please try again.', 'woocommerce-admin' ), 500 );
}
delete_transient( '_woocommerce_helper_subscriptions' );
\WC_Helper_Updater::flush_updates_cache();
do_action( 'woocommerce_helper_connected' );
return array(
'success' => true,
);
}
/**
* Returns a URL that can be used to connect to PayPal.
*
* @return WP_Error|array Connect URL.
*/
public function connect_paypal() {
if ( ! function_exists( 'wc_gateway_ppec' ) ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to PayPal.', 'woocommerce-admin' ), 500 );
}
$redirect_url = add_query_arg(
array(
'env' => 'live',
'wc_ppec_ips_admin_nonce' => wp_create_nonce( 'wc_ppec_ips' ),
),
wc_admin_url( '&task=payments&paypal-connect-finish=1' )
);
// https://github.com/woocommerce/woocommerce-gateway-paypal-express-checkout/blob/b6df13ba035038aac5024d501e8099a37e13d6cf/includes/class-wc-gateway-ppec-ips-handler.php#L79-L93.
$query_args = array(
'redirect' => urlencode( $redirect_url ),
'countryCode' => WC()->countries->get_base_country(),
'merchantId' => md5( site_url( '/' ) . time() ),
);
$connect_url = add_query_arg( $query_args, wc_gateway_ppec()->ips->get_middleware_login_url( 'live' ) );
return( array(
'connectUrl' => $connect_url,
) );
}
/**
* Returns a URL that can be used to connect to Square.
*
* @return WP_Error|array Connect URL.
*/
public function connect_square() {
if ( ! class_exists( '\WooCommerce\Square\Handlers\Connection' ) ) {
return new WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to Square.', 'woocommerce-admin' ), 500 );
}
$url = \WooCommerce\Square\Handlers\Connection::CONNECT_URL_PRODUCTION;
$redirect_url = wp_nonce_url( wc_admin_url( '&task=payments&square-connect-finish=1' ), 'wc_square_connected' );
$args = array(
'redirect' => urlencode( urlencode( $redirect_url ) ),
'scopes' => implode(
',',
array(
'MERCHANT_PROFILE_READ',
'PAYMENTS_READ',
'PAYMENTS_WRITE',
'ORDERS_READ',
'ORDERS_WRITE',
'CUSTOMERS_READ',
'CUSTOMERS_WRITE',
'SETTLEMENTS_READ',
'ITEMS_READ',
'ITEMS_WRITE',
'INVENTORY_READ',
'INVENTORY_WRITE',
)
),
);
$connect_url = add_query_arg( $args, $url );
return( array(
'connectUrl' => $connect_url,
) );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'onboarding_plugin',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Plugin slug.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Plugin name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Plugin status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_connect_schema() {
$schema = $this->get_item_schema();
unset( $schema['properties']['status'] );
$schema['properties']['connectAction'] = array(
'description' => __( 'Action that should be completed to connect Jetpack.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
);
return $schema;
}
}

View File

@@ -0,0 +1,407 @@
<?php
/**
* REST API Onboarding Profile Controller
*
* Handles requests to /onboarding/profile
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Features\Onboarding;
/**
* Onboarding Profile controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class OnboardingProfile extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/profile';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_items' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to read onboarding profile data.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to edit onboarding profile data.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot edit this resource.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Return all onboarding profile data.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
include_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
$onboarding_data = get_option( Onboarding::PROFILE_DATA_OPTION, array() );
$item_schema = $this->get_item_schema();
$items = array();
foreach ( $item_schema['properties'] as $key => $property_schema ) {
$items[ $key ] = isset( $onboarding_data[ $key ] ) ? $onboarding_data[ $key ] : null;
}
$wccom_auth = \WC_Helper_Options::get( 'auth' );
$items['wccom_connected'] = empty( $wccom_auth['access_token'] ) ? false : true;
$item = $this->prepare_item_for_response( $items, $request );
$data = $this->prepare_response_for_collection( $item );
return rest_ensure_response( $data );
}
/**
* Update onboarding profile data.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function update_items( $request ) {
$params = $request->get_json_params();
$query_args = $this->prepare_objects_query( $params );
$onboarding_data = get_option( Onboarding::PROFILE_DATA_OPTION, array() );
update_option( Onboarding::PROFILE_DATA_OPTION, array_merge( $onboarding_data, $query_args ) );
$result = array(
'status' => 'success',
'message' => __( 'Onboarding profile data has been updated.', 'woocommerce-admin' ),
);
$response = $this->prepare_item_for_response( $result, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Prepare objects query.
*
* @param array $params The params sent in the request.
* @return array
*/
protected function prepare_objects_query( $params ) {
$args = array();
$properties = self::get_profile_properties();
foreach ( $properties as $key => $property ) {
if ( isset( $params[ $key ] ) ) {
$args[ $key ] = $params[ $key ];
}
}
/**
* Filter the query arguments for a request.
*
* Enables adding extra arguments or setting defaults for a post
* collection request.
*
* @param array $args Key value array of query var to query value.
* @param array $params The params sent in the request.
*/
$args = apply_filters( 'woocommerce_rest_onboarding_profile_object_query', $args, $params );
return $args;
}
/**
* Prepare the data object for response.
*
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
/**
* Filter the list returned from the API.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original item.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_onboarding_prepare_profile', $response, $item, $request );
}
/**
* Get onboarding profile properties.
*
* @return array
*/
public static function get_profile_properties() {
$properties = array(
'completed' => array(
'type' => 'boolean',
'description' => __( 'Whether or not the profile was completed.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
),
'plugins' => array(
'type' => 'string',
'description' => __( 'How the Jetpack/WooCommerce Services step was handled.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'skipped',
'skipped-wcs',
'already-installed',
'installed-wcs',
'installed',
),
),
'account_type' => array(
'type' => 'string',
'description' => __( 'Account type used for Jetpack.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'new',
'existing',
'google',
),
),
'industry' => array(
'type' => 'array',
'description' => __( 'Industry.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => array_keys( Onboarding::get_allowed_industries() ),
'type' => 'string',
),
),
'product_types' => array(
'type' => 'array',
'description' => __( 'Types of products sold.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => array_keys( Onboarding::get_allowed_product_types() ),
'type' => 'string',
),
),
'product_count' => array(
'type' => 'string',
'description' => __( 'Number of products to be added.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'0',
'1-10',
'11-100',
'101-1000',
'1000+',
),
),
'selling_venues' => array(
'type' => 'string',
'description' => __( 'Other places the store is selling products.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'no',
'other',
'brick-mortar',
'brick-mortar-other',
'other-woocommerce',
),
),
'revenue' => array(
'type' => 'string',
'description' => __( 'Current annual revenue of the store.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'none',
'up-to-2500',
'2500-10000',
'10000-50000',
'50000-250000',
'more-than-250000',
),
),
'other_platform' => array(
'type' => 'string',
'description' => __( 'Name of other platform used to sell.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
'enum' => array(
'shopify',
'bigcommerce',
'magento',
'wix',
'other',
),
),
'business_extensions' => array(
'type' => 'array',
'description' => __( 'Extra business extensions to install.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => array( 'mailchimp-for-woocommerce', 'facebook-for-woocommerce' ),
'type' => 'string',
),
),
'theme' => array(
'type' => 'string',
'description' => __( 'Selected store theme.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'sanitize_callback' => 'sanitize_title_with_dashes',
'validate_callback' => 'rest_validate_request_arg',
),
'items_purchased' => array(
'type' => 'boolean',
'description' => __( 'Whether or not the user opted to purchase items now or later.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
),
'setup_client' => array(
'type' => 'boolean',
'description' => __( 'Whether or not this store was setup for a client.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
),
'wccom_connected' => array(
'type' => 'boolean',
'description' => __( 'Whether or not the store was connected to WooCommerce.com during the extension flow.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
'validate_callback' => 'rest_validate_request_arg',
),
);
return apply_filters( 'woocommerce_rest_onboarding_profile_properties', $properties );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
// Unset properties used for collection params.
$properties = self::get_profile_properties();
foreach ( $properties as $key => $property ) {
unset( $properties[ $key ]['default'] );
unset( $properties[ $key ]['items'] );
unset( $properties[ $key ]['validate_callback'] );
unset( $properties[ $key ]['sanitize_callback'] );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'onboarding_profile',
'type' => 'object',
'properties' => $properties,
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
// Unset properties used for item schema.
$params = self::get_profile_properties();
foreach ( $params as $key => $param ) {
unset( $params[ $key ]['context'] );
unset( $params[ $key ]['readonly'] );
}
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
return apply_filters( 'woocommerce_rest_onboarding_profile_collection_params', $params );
}
}

View File

@@ -0,0 +1,431 @@
<?php
/**
* REST API Onboarding Tasks Controller
*
* Handles requests to complete various onboarding tasks.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\Features\Onboarding;
defined( 'ABSPATH' ) || exit;
/**
* Onboarding Tasks Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class OnboardingTasks extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/tasks';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/import_sample_products',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'import_sample_products' ),
'permission_callback' => array( $this, 'import_products_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/create_store_pages',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_store_pages' ),
'permission_callback' => array( $this, 'create_pages_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/create_homepage',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_homepage' ),
'permission_callback' => array( $this, 'create_pages_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check if a given request has access to create a product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function import_products_permission_check( $request ) {
if ( ! wc_rest_check_post_permissions( 'product', 'create' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create a product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_pages_permission_check( $request ) {
if ( ! wc_rest_check_post_permissions( 'page', 'create' ) || ! current_user_can( 'manage_options' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create new pages.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Import sample products from WooCommerce sample CSV.
*
* @return WP_Error|WP_REST_Response
*/
public static function import_sample_products() {
include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';
$file = WC_ABSPATH . 'sample-data/sample_products.csv';
if ( file_exists( $file ) && class_exists( 'WC_Product_CSV_Importer' ) ) {
// Override locale so we can return mappings from WooCommerce in English language stores.
add_filter( 'locale', '__return_false', 9999 );
$importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
$args = array(
'parse' => true,
'mapping' => self::get_header_mappings( $file ),
);
$args = apply_filters( 'woocommerce_product_csv_importer_args', $args, $importer_class );
$importer = new $importer_class( $file, $args );
$import = $importer->import();
return rest_ensure_response( $import );
} else {
return new \WP_Error( 'woocommerce_rest_import_error', __( 'Sorry, the sample products data file was not found.', 'woocommerce-admin' ) );
}
}
/**
* Get header mappings from CSV columns.
*
* @param string $file File path.
* @return array Mapped headers.
*/
public static function get_header_mappings( $file ) {
include_once WC_ABSPATH . 'includes/admin/importers/mappings/mappings.php';
$importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
$importer = new $importer_class( $file, array() );
$raw_headers = $importer->get_raw_keys();
$default_columns = wc_importer_default_english_mappings( array() );
$special_columns = wc_importer_default_special_english_mappings( array() );
$headers = array();
foreach ( $raw_headers as $key => $field ) {
$index = $field;
$headers[ $index ] = $field;
if ( isset( $default_columns[ $field ] ) ) {
$headers[ $index ] = $default_columns[ $field ];
} else {
foreach ( $special_columns as $regex => $special_key ) {
if ( preg_match( self::sanitize_special_column_name_regex( $regex ), $field, $matches ) ) {
$headers[ $index ] = $special_key . $matches[1];
break;
}
}
}
}
return $headers;
}
/**
* Sanitize special column name regex.
*
* @param string $value Raw special column name.
* @return string
*/
public static function sanitize_special_column_name_regex( $value ) {
return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/';
}
/**
* Returns a valid cover block with an image, if one exists, or background as a fallback.
*
* @param array $image Image to use for the cover block. Should contain a media ID and image URL.
* @return string Block content.
*/
private static function get_homepage_cover_block( $image ) {
if ( ! empty( $image['url'] ) && ! empty( $image['id'] ) ) {
return '<!-- wp:cover {"url":"' . esc_url( $image['url'] ) . '","id":' . intval( $image['id'] ) . '} -->
<div class="wp-block-cover has-background-dim" style="background-image:url(' . esc_url( $image['url'] ) . ')"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
<p class="has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:button {"align":"center"} -->
<div class="wp-block-button aligncenter"><a class="wp-block-button__link">' . __( 'Go shopping', 'woocommerce-admin' ) . '</a></div>
<!-- /wp:button --></div></div>
<!-- /wp:cover -->';
}
return '<!-- wp:cover {"overlayColor":"very-dark-gray"} -->
<div class="wp-block-cover has-very-dark-gray-background-color has-background-dim"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
<p class="has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:button {"align":"center"} -->
<div class="wp-block-button aligncenter"><a class="wp-block-button__link">' . __( 'Go shopping', 'woocommerce-admin' ) . '</a></div>
<!-- /wp:button --></div></div>
<!-- /wp:cover -->';
}
/**
* Returns a valid media block with an image, if one exists, or a uninitialized media block the user can set.
*
* @param array $image Image to use for the cover block. Should contain a media ID and image URL.
* @param string $align If the image should be aligned to the left or right.
* @return string Block content.
*/
private static function get_homepage_media_block( $image, $align = 'left' ) {
$media_position = 'right' === $align ? '"mediaPosition":"right",' : '';
$css_class = 'right' === $align ? ' has-media-on-the-right' : '';
if ( ! empty( $image['url'] ) && ! empty( $image['id'] ) ) {
return '<!-- wp:media-text {' . $media_position . '"mediaId":' . intval( $image['id'] ) . ',"mediaType":"image"} -->
<div class="wp-block-media-text alignwide' . $css_class . '""><figure class="wp-block-media-text__media"><img src="' . esc_url( $image['url'] ) . '" alt="" class="wp-image-' . intval( $image['id'] ) . '"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"' . __( 'Content…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
<p class="has-large-font-size"></p>
<!-- /wp:paragraph --></div></div>
<!-- /wp:media-text -->';
}
return '<!-- wp:media-text {' . $media_position . '} -->
<div class="wp-block-media-text alignwide' . $css_class . '"><figure class="wp-block-media-text__media"></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"' . __( 'Content…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
<p class="has-large-font-size"></p>
<!-- /wp:paragraph --></div></div>
<!-- /wp:media-text -->';
}
/**
* Returns a homepage template to be inserted into a post. A different template will be used depending on the number of products.
*
* @param int $post_id ID of the homepage template.
* @return string Template contents.
*/
private static function get_homepage_template( $post_id ) {
$products = wp_count_posts( 'product' );
if ( $products->publish >= 4 ) {
$images = self::sideload_homepage_images( $post_id, 1 );
$image_1 = ! empty( $images[0] ) ? $images[0] : '';
$cover = self::get_homepage_cover_block( $image_1 );
return $cover . '
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'Shop by Category', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:shortcode -->
[product_categories limit="3" columns="3" orderby="menu_order"]
<!-- /wp:shortcode -->
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'New In', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-new {"columns":4} -->
<div class="wp-block-woocommerce-product-new">[products limit="4" columns="4" orderby="date" order="DESC"]</div>
<!-- /wp:woocommerce/product-new -->
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'Fan Favorites', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-top-rated {"columns":4} -->
<div class="wp-block-woocommerce-product-top-rated">[products limit="4" columns="4" orderby="rating"]</div>
<!-- /wp:woocommerce/product-top-rated -->
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'On Sale', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-on-sale {"columns":4} -->
<div class="wp-block-woocommerce-product-on-sale">[products limit="4" columns="4" orderby="date" order="DESC" on_sale="1"]</div>
<!-- /wp:woocommerce/product-on-sale -->
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'Best Sellers', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-best-sellers {"columns":4} -->
<div class="wp-block-woocommerce-product-best-sellers">[products limit="4" columns="4" best_selling="1"]</div>
<!-- /wp:woocommerce/product-best-sellers -->
';
}
$images = self::sideload_homepage_images( $post_id, 3 );
$image_1 = ! empty( $images[0] ) ? $images[0] : '';
$image_2 = ! empty( $images[1] ) ? $images[1] : '';
$image_3 = ! empty( $images[2] ) ? $images[2] : '';
$cover = self::get_homepage_cover_block( $image_1 );
return $cover . '
<!-- wp:heading {"align":"center"} -->
<h2 style="text-align:center">' . __( 'New Products', 'woocommerce-admin' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-new /--> ' .
self::get_homepage_media_block( $image_1, 'right' ) .
self::get_homepage_media_block( $image_2, 'left' ) .
self::get_homepage_media_block( $image_3, 'right' ) . '
<!-- wp:woocommerce/featured-product /-->';
}
/**
* Gets the possible industry images from the plugin folder for sideloading. If an image doesn't exist, other.jpg is used a fallback.
*
* @return array An array of images by industry.
*/
private static function get_available_homepage_images() {
$industry_images = array();
$industries = Onboarding::get_allowed_industries();
foreach ( $industries as $industry_slug => $label ) {
$file_path = WC_ADMIN_ABSPATH . 'images/onboarding/' . $industry_slug . '.jpg';
if ( 'other' === $industry_slug || ! file_exists( $file_path ) ) {
$industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', plugins_url( 'images/onboarding/other.jpg', WC_ADMIN_PLUGIN_FILE ), $industry_slug );
continue;
}
$industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', plugins_url( 'images/onboarding/' . $industry_slug . '.jpg', WC_ADMIN_PLUGIN_FILE ), $industry_slug );
}
return $industry_images;
}
/**
* Uploads a number of images to a homepage template, depending on the selected industry from the profile wizard.
*
* @param int $post_id ID of the homepage template.
* @param int $number_of_images The number of images that should be sideloaded (depending on how many media slots are in the template).
* @return array An array of images that have been attached to the post.
*/
private static function sideload_homepage_images( $post_id, $number_of_images ) {
$profile = get_option( Onboarding::PROFILE_DATA_OPTION, array() );
$images_to_sideload = array();
$available_images = self::get_available_homepage_images();
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
if ( ! empty( $profile['industry'] ) ) {
foreach ( $profile['industry'] as $selected_industry ) {
$images_to_sideload[] = ! empty( $available_images[ $selected_industry ] ) ? $available_images[ $selected_industry ] : $available_images['other'];
}
}
// Make sure we have at least {$number_of_images} images.
if ( count( $images_to_sideload ) < $number_of_images ) {
for ( $i = count( $images_to_sideload ); $i < $number_of_images; $i++ ) {
$images_to_sideload[] = ! empty( $profile['industry'] ) && ! empty( $available_images[ $profile['industry'][0] ] ) ? $available_images[ $profile['industry'][0] ] : $available_images['other'];
}
}
$already_sideloaded = array();
$images_for_post = array();
foreach ( $images_to_sideload as $image ) {
// Avoid uploading two of the same image, if an image is repeated.
if ( ! empty( $already_sideloaded[ $image ] ) ) {
$images_for_post[] = $already_sideloaded[ $image ];
continue;
}
$sideload_id = \media_sideload_image( $image, $post_id, null, 'id' );
if ( ! is_wp_error( $sideload_id ) ) {
$sideload_url = wp_get_attachment_url( $sideload_id );
$already_sideloaded[ $image ] = array(
'id' => $sideload_id,
'url' => $sideload_url,
);
$images_for_post[] = $already_sideloaded[ $image ];
}
}
return $images_for_post;
}
/**
* Creates base store starter pages like my account and checkout.
* Note that WC_Install::create_pages already checks if pages exist before creating them again.
*
* @return bool
*/
public static function create_store_pages() {
\WC_Install::create_pages();
return true;
}
/**
* Create a homepage from a template.
*
* @return WP_Error|array
*/
public static function create_homepage() {
$post_id = wp_insert_post(
array(
'post_title' => __( 'Homepage', 'woocommerce-admin' ),
'post_type' => 'page',
'post_status' => 'draft',
'post_content' => '', // Template content is updated below, so images can be attached to the post.
)
);
if ( ! is_wp_error( $post_id ) ) {
$template = self::get_homepage_template( $post_id );
wp_update_post(
array(
'ID' => $post_id,
'post_content' => $template,
)
);
update_option( 'woocommerce_onboarding_homepage_post_id', $post_id );
return array(
'status' => 'success',
'message' => __( 'Homepage created successfully.', 'woocommerce-admin' ),
'post_id' => $post_id,
'edit_post_link' => htmlspecialchars_decode( get_edit_post_link( $post_id ) ),
);
} else {
return $post_id;
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* REST API Onboarding Themes Controller
*
* Handles requests to install and activate themes.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\Features\Onboarding;
defined( 'ABSPATH' ) || exit;
/**
* Onboarding Themes Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class OnboardingThemes extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/themes';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'install_theme' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'activate_theme' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}
/**
* Check if a given request has access to manage themes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_themes' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage themes.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Installs the requested theme.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Theme installation status.
*/
public function install_theme( $request ) {
$allowed_themes = Onboarding::get_allowed_themes();
$theme = sanitize_title_with_dashes( $request['theme'] );
if ( ! in_array( $theme, $allowed_themes, true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce-admin' ), 404 );
}
$slug = sanitize_key( $theme );
$installed_themes = wp_get_themes();
if ( in_array( $slug, array_keys( $installed_themes ), true ) ) {
return( array(
'slug' => $slug,
'name' => $installed_themes[ $slug ]->get( 'Name' ),
'status' => 'success',
) );
}
include_once ABSPATH . '/wp-admin/includes/admin.php';
include_once ABSPATH . '/wp-admin/includes/theme-install.php';
include_once ABSPATH . '/wp-admin/includes/theme.php';
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-theme-upgrader.php';
$api = themes_api(
'theme_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( is_wp_error( $api ) ) {
return new \WP_Error(
'woocommerce_rest_theme_install',
sprintf(
/* translators: %s: theme slug (example: woocommerce-services) */
__( 'The requested theme `%s` could not be installed. Theme API call failed.', 'woocommerce-admin' ),
$slug
),
500
);
}
$upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() );
$result = $upgrader->install( $api->download_link );
if ( is_wp_error( $result ) || is_null( $result ) ) {
return new \WP_Error(
'woocommerce_rest_theme_install',
sprintf(
/* translators: %s: theme slug (example: woocommerce-services) */
__( 'The requested theme `%s` could not be installed.', 'woocommerce-admin' ),
$slug
),
500
);
}
return array(
'slug' => $slug,
'name' => $api->name,
'status' => 'success',
);
}
/**
* Activate the requested theme.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Theme activation status.
*/
public function activate_theme( $request ) {
$allowed_themes = Onboarding::get_allowed_themes();
$theme = sanitize_title_with_dashes( $request['theme'] );
if ( ! in_array( $theme, $allowed_themes, true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/theme.php';
$installed_themes = wp_get_themes();
if ( ! in_array( $theme, array_keys( $installed_themes ), true ) ) {
/* translators: %s: theme slug (example: woocommerce-services) */
return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'Invalid theme %s.', 'woocommerce-admin' ), $slug ), 404 );
}
$result = switch_theme( $theme );
if ( ! is_null( $result ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'The requested theme could not be activated.', 'woocommerce-admin' ), $slug ), 500 );
}
return( array(
'slug' => $theme,
'name' => $installed_themes[ $theme ]->get( 'Name' ),
'status' => 'success',
) );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'onboarding_theme',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Theme slug.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Theme name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Theme status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@@ -0,0 +1,211 @@
<?php
/**
* REST API Options Controller
*
* Handles requests to get and update options in the wp_options table.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Options Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class Options extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'options';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_options' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_options' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}
/**
* Check if a given request has access to get options.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$params = explode( ',', $request['options'] );
if ( ! isset( $request['options'] ) || ! is_array( $params ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'You must supply an array of options.', 'woocommerce-admin' ), 500 );
}
foreach ( $params as $option ) {
if ( ! $this->user_has_permission( $option, $request ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view these options.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
}
return true;
}
/**
* Check if the user has permission given an option name.
*
* @param string $option Option name.
* @param WP_REST_Request $request Full details about the request.
* @return boolean
*/
public function user_has_permission( $option, $request ) {
$permissions = $this->get_option_permissions( $request );
if ( isset( $permissions[ $option ] ) ) {
return $permissions[ $option ];
}
return current_user_can( 'manage_options' );
}
/**
* Check if a given request has access to update options.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$params = $request->get_json_params();
if ( ! is_array( $params ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You must supply an array of options and values.', 'woocommerce-admin' ), 500 );
}
foreach ( $params as $option_name => $option_value ) {
if ( ! $this->user_has_permission( $option_name, $request ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage these options.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
}
return true;
}
/**
* Get an array of options and respective permissions for the current user.
*
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
public function get_option_permissions( $request ) {
$permissions = array(
'theme_mods_' . get_stylesheet() => current_user_can( 'edit_theme_options' ),
'woocommerce_setup_jetpack_opted_in' => current_user_can( 'manage_woocommerce' ),
'woocommerce_stripe_settings' => current_user_can( 'manage_woocommerce' ),
'woocommerce_ppec_paypal_settings' => current_user_can( 'manage_woocommerce' ),
'woocommerce_task_list_payments' => current_user_can( 'manage_woocommerce' ),
'woocommerce_demo_store' => current_user_can( 'manage_woocommerce' ),
'woocommerce_demo_store_notice' => current_user_can( 'manage_woocommerce' ),
);
return apply_filters( 'woocommerce_rest_api_option_permissions', $permissions, $request );
}
/**
* Gets an array of options and respective values.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Options object with option values.
*/
public function get_options( $request ) {
$params = explode( ',', $request['options'] );
$options = array();
if ( ! is_array( $params ) ) {
return array();
}
foreach ( $params as $option ) {
$options[ $option ] = get_option( $option );
}
return $options;
}
/**
* Updates an array of objects.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Options object with a boolean if the option was updated.
*/
public function update_options( $request ) {
$params = $request->get_json_params();
$updated = array();
if ( ! is_array( $params ) ) {
return array();
}
foreach ( $params as $key => $value ) {
$updated[ $key ] = update_option( $key, $value );
}
return $updated;
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'options',
'type' => 'object',
'properties' => array(
'options' => array(
'type' => 'array',
'description' => __( 'Array of options with associated values.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* REST API Orders Controller
*
* Handles requests to /orders/*
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Orders controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Orders_Controller
*/
class Orders extends \WC_REST_Orders_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
// This needs to remain a string to support extensions that filter Order Number.
$params['number'] = array(
'description' => __( 'Limit result set to orders matching part of an order number.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
// Fix the default 'status' value until it can be patched in core.
$params['status']['default'] = array( 'any' );
return $params;
}
/**
* Prepare objects query.
*
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
global $wpdb;
$args = parent::prepare_objects_query( $request );
// Search by partial order number.
if ( ! empty( $request['number'] ) ) {
$partial_number = trim( $request['number'] );
$limit = intval( $args['posts_per_page'] );
$order_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID
FROM {$wpdb->prefix}posts
WHERE post_type = 'shop_order'
AND ID LIKE %s
LIMIT %d",
$wpdb->esc_like( absint( $partial_number ) ) . '%',
$limit
)
);
// Force WP_Query return empty if don't found any order.
$order_ids = empty( $order_ids ) ? array( 0 ) : $order_ids;
$args['post__in'] = $order_ids;
}
return $args;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* REST API Product Categories Controller
*
* Handles requests to /products/categories.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Product categories controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Product_Categories_Controller
*/
class ProductCategories extends \WC_REST_Product_Categories_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* REST API Product Reviews Controller
*
* Handles requests to /products/reviews.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Product reviews controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Product_Reviews_Controller
*/
class ProductReviews extends \WC_REST_Product_Reviews_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Prepare links for the request.
*
* @param WP_Comment $review Product review object.
* @return array Links for the given product review.
*/
protected function prepare_links( $review ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( 0 !== (int) $review->comment_post_ID ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ),
'embeddable' => true,
);
}
if ( 0 !== (int) $review->user_id ) {
$links['reviewer'] = array(
'href' => rest_url( 'wp/v2/users/' . $review->user_id ),
'embeddable' => true,
);
}
return $links;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* REST API Product Variations Controller
*
* Handles requests to /products/variations.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Product variations controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Product_Variations_Controller
*/
class ProductVariations extends \WC_REST_Product_Variations_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['search'] = array(
'description' => __( 'Search by similar product name or sku.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Add product name and sku filtering to the WC API.
*
* @param WP_REST_Request $request Request data.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
if ( ! empty( $request['search'] ) ) {
$args['search'] = $request['search'];
unset( $args['s'] );
}
return $args;
}
/**
* Get a collection of posts and add the post title filter option to WP_Query.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
add_filter( 'posts_where', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_filter' ), 10, 2 );
add_filter( 'posts_join', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_join' ), 10, 2 );
add_filter( 'posts_groupby', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_group_by' ), 10, 2 );
$response = parent::get_items( $request );
remove_filter( 'posts_where', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_filter' ), 10 );
remove_filter( 'posts_join', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_join' ), 10 );
remove_filter( 'posts_groupby', array( 'Automattic\WooCommerce\Admin\API\Products', 'add_wp_query_group_by' ), 10 );
return $response;
}
/**
* Get the Product's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['name'] = array(
'description' => __( 'Product parent name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
);
$schema['properties']['type'] = array(
'description' => __( 'Product type.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'variation',
'enum' => array( 'variation' ),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['parent_id'] = array(
'description' => __( 'Product parent ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
return $schema;
}
/**
* Prepare a single variation output for response.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$response = parent::prepare_object_for_response( $object, $request );
$data = $response->get_data();
$data['name'] = $object->get_name( $context );
$data['type'] = $object->get_type();
$data['parent_id'] = $object->get_parent_id( $context );
$response->set_data( $data );
return $response;
}
}

View File

@@ -0,0 +1,244 @@
<?php
/**
* REST API Products Controller
*
* Handles requests to /products/*
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Products controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Products_Controller
*/
class Products extends \WC_REST_Products_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Adds properties that can be embed via ?_embed=1.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$properties_to_embed = array(
'id',
'name',
'slug',
'permalink',
'images',
'description',
'short_description',
);
foreach ( $properties_to_embed as $property ) {
$schema['properties'][ $property ]['context'][] = 'embed';
}
return $schema;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['low_in_stock'] = array(
'description' => __( 'Limit result set to products that are low or out of stock.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
);
$params['search'] = array(
'description' => __( 'Search by similar product name or sku.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Add product name and sku filtering to the WC API.
*
* @param WP_REST_Request $request Request data.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
if ( ! empty( $request['search'] ) ) {
$args['search'] = trim( $request['search'] );
unset( $args['s'] );
}
if ( ! empty( $request['low_in_stock'] ) ) {
$args['low_in_stock'] = $request['low_in_stock'];
$args['post_type'] = array( 'product', 'product_variation' );
}
return $args;
}
/**
* Get a collection of posts and add the post title filter option to WP_Query.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
add_filter( 'posts_fields', array( __CLASS__, 'add_wp_query_fields' ), 10, 2 );
add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 );
add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 );
add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10, 2 );
$response = parent::get_items( $request );
remove_filter( 'posts_fields', array( __CLASS__, 'add_wp_query_fields' ), 10 );
remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 );
remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 );
remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10 );
return $response;
}
/**
* Add `low_stock_amount` property to product data
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$data = parent::prepare_object_for_response( $object, $request );
$object_data = $object->get_data();
if ( $request->get_param( 'low_in_stock' ) && is_numeric( $object_data['low_stock_amount'] ) ) {
$data->data['low_stock_amount'] = $object_data['low_stock_amount'];
}
return $data;
}
/**
* Add in conditional select fields to the query.
*
* @param string $select Select clause used to select fields from the query.
* @param object $wp_query WP_Query object.
* @return string
*/
public static function add_wp_query_fields( $select, $wp_query ) {
if ( $wp_query->get( 'low_in_stock' ) ) {
$select .= ', low_stock_amount_meta.meta_value AS low_stock_amount';
}
return $select;
}
/**
* Add in conditional search filters for products.
*
* @param string $where Where clause used to search posts.
* @param object $wp_query WP_Query object.
* @return string
*/
public static function add_wp_query_filter( $where, $wp_query ) {
global $wpdb;
$search = $wp_query->get( 'search' );
if ( $search ) {
$search = $wpdb->esc_like( $search );
$search = "'%" . $search . "%'";
$where .= " AND ({$wpdb->posts}.post_title LIKE {$search}";
$where .= wc_product_sku_enabled() ? ' OR wc_product_meta_lookup.sku LIKE ' . $search . ')' : ')';
}
if ( $wp_query->get( 'low_in_stock' ) ) {
$low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
$where .= "
AND wc_product_meta_lookup.stock_quantity IS NOT NULL
AND wc_product_meta_lookup.stock_status IN('instock','outofstock')
AND (
(
low_stock_amount_meta.meta_value > ''
AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED)
)
OR (
(
low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= ''
)
AND wc_product_meta_lookup.stock_quantity <= {$low_stock_amount}
)
)";
}
return $where;
}
/**
* Join posts meta tables when product search or low stock query is present.
*
* @param string $join Join clause used to search posts.
* @param object $wp_query WP_Query object.
* @return string
*/
public static function add_wp_query_join( $join, $wp_query ) {
global $wpdb;
$search = $wp_query->get( 'search' );
if ( $search && wc_product_sku_enabled() ) {
$join = self::append_product_sorting_table_join( $join );
}
if ( $wp_query->get( 'low_in_stock' ) ) {
$join = self::append_product_sorting_table_join( $join );
$join .= " LEFT JOIN {$wpdb->postmeta} AS low_stock_amount_meta ON {$wpdb->posts}.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount' ";
}
return $join;
}
/**
* Join wc_product_meta_lookup to posts if not already joined.
*
* @param string $sql SQL join.
* @return string
*/
protected static function append_product_sorting_table_join( $sql ) {
global $wpdb;
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
}
return $sql;
}
/**
* Group by post ID to prevent duplicates.
*
* @param string $groupby Group by clause used to organize posts.
* @param object $wp_query WP_Query object.
* @return string
*/
public static function add_wp_query_group_by( $groupby, $wp_query ) {
global $wpdb;
$search = $wp_query->get( 'search' );
$low_in_stock = $wp_query->get( 'low_in_stock' );
if ( empty( $groupby ) && ( $search || $low_in_stock ) ) {
$groupby = $wpdb->posts . '.ID';
}
return $groupby;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* REST API Reports Cache.
*
* Handles report data object caching.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Cache class.
*
* @package WooCommerce Admin/API
*/
class Cache {
/**
* Cache version. Used to invalidate all cached values.
*/
const VERSION_OPTION = 'woocommerce_reports';
/**
* Invalidate cache.
*/
public static function invalidate() {
\WC_Cache_Helper::get_transient_version( self::VERSION_OPTION, true );
}
/**
* Get cache version number.
*
* @return string
*/
public static function get_version() {
$version = \WC_Cache_Helper::get_transient_version( self::VERSION_OPTION );
return $version;
}
/**
* Get cached value.
*
* @param string $key Cache key.
* @return mixed
*/
public static function get( $key ) {
$transient_version = self::get_version();
$transient_value = get_transient( $key );
if (
isset( $transient_value['value'], $transient_value['version'] ) &&
$transient_value['version'] === $transient_version
) {
return $transient_value['value'];
}
return false;
}
/**
* Update cached value.
*
* @param string $key Cache key.
* @param mixed $value New value.
* @return bool
*/
public static function set( $key, $value ) {
$transient_version = self::get_version();
$transient_value = array(
'version' => $transient_version,
'value' => $value,
);
$result = set_transient( $key, $transient_value, WEEK_IN_SECONDS );
return $result;
}
}

View File

@@ -0,0 +1,355 @@
<?php
/**
* REST API Reports categories controller
*
* Handles requests to the /reports/categories endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports categories controller class.
*
* @package WooCommerce/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/categories';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['extended_info'] = $request['extended_info'];
$args['categories'] = (array) $request['categories'];
$args['status_is'] = (array) $request['status_is'];
$args['status_is_not'] = (array) $request['status_is_not'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$categories_query = new Query( $query_args );
$report_data = $categories_query->get_data();
if ( is_wp_error( $report_data ) ) {
return $report_data;
}
if ( ! isset( $report_data->data ) || ! isset( $report_data->page_no ) || ! isset( $report_data->pages ) ) {
return new \WP_Error( 'woocommerce_rest_reports_categories_invalid_response', __( 'Invalid response from data store.', 'woocommerce-admin' ), array( 'status' => 500 ) );
}
$out_data = array();
foreach ( $report_data->data as $datum ) {
$item = $this->prepare_item_for_response( $datum, $request );
$out_data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_categories', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param \Automattic\WooCommerce\Admin\API\Reports\Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'category' => array(
'href' => rest_url( sprintf( '/%s/products/categories/%d', $this->namespace, $object['category_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_categories',
'type' => 'object',
'properties' => array(
'category_id' => array(
'description' => __( 'Category ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'items_sold' => array(
'description' => __( 'Amount of items sold.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'net_revenue' => array(
'description' => __( 'Total Sales.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Number of orders.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'products_count' => array(
'description' => __( 'Amount of products.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'extended_info' => array(
'name' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Category name.', 'woocommerce-admin' ),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'category_id',
'enum' => array(
'category_id',
'items_sold',
'net_revenue',
'orders_count',
'products_count',
'category',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['status_is'] = array(
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['status_is_not'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['categories'] = array(
'description' => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each category to the report.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'category' => __( 'Category', 'woocommerce-admin' ),
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'net_revenue' => __( 'Net Revenue', 'woocommerce-admin' ),
'products_count' => __( 'Products', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'category' => $item['extended_info']['name'],
'items_sold' => $item['items_sold'],
'net_revenue' => $item['net_revenue'],
'products_count' => $item['products_count'],
'orders_count' => $item['orders_count'],
);
}
}

View File

@@ -0,0 +1,312 @@
<?php
/**
* API\Reports\Categories\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Categories\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'categories';
/**
* Order by setting used for sorting categories data.
*
* @var string
*/
private $order_by = '';
/**
* Order setting used for sorting categories data.
*
* @var string
*/
private $order = '';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'category_id' => 'intval',
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
'products_count' => 'intval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'categories';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
'products_count' => "COUNT(DISTINCT {$table_name}.product_id) as products_count",
);
}
/**
* Return the database query with parameters used for Categories report: time span and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = self::get_db_table_name();
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
// join wp_order_product_lookup_table with relationships and taxonomies
// @todo How to handle custom product tables?
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$wpdb->term_relationships} ON {$order_product_lookup_table}.product_id = {$wpdb->term_relationships}.object_id" );
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->wc_category_lookup}.category_id" );
$included_categories = $this->get_included_categories( $query_args );
if ( $included_categories ) {
$this->subquery->add_sql_clause( 'where', "AND {$wpdb->wc_category_lookup}.category_tree_id IN ({$included_categories})" );
// Limit is left out here so that the grouping in code by PHP can be applied correctly.
// This also needs to be put after the term_taxonomy JOIN so that we can match the correct term name.
$this->add_order_by_params( $query_args, 'outer', 'default_results.category_id' );
} else {
$this->add_order_by_params( $query_args, 'inner', "{$wpdb->wc_category_lookup}.category_tree_id" );
}
// @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 ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
}
$this->add_order_status_clause( $query_args, $order_product_lookup_table, $this->subquery );
$this->subquery->add_sql_clause( 'where', "AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL" );
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $from_arg Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
*/
protected function add_order_by_params( $query_args, $from_arg, $id_cell ) {
global $wpdb;
$lookup_table = self::get_db_table_name();
$order_by_clause = $this->add_order_by_clause( $query_args, $this );
$this->add_orderby_order_clause( $query_args, $this );
if ( false !== strpos( $order_by_clause, '_terms' ) ) {
$join = "JOIN {$wpdb->terms} AS _terms ON {$id_cell} = _terms.term_id";
if ( 'inner' === $from_arg ) {
// Even though this is an (inner) JOIN, we're adding it as a `left_join` to
// affect its order in the query statement. The SqlQuery::$sql_filters variable
// determines the order in which joins are concatenated.
// See: https://github.com/woocommerce/woocommerce-admin/blob/1f261998e7287b77bc13c3d4ee2e84b717da7957/src/API/Reports/SqlQuery.php#L46-L50.
$this->subquery->add_sql_clause( 'left_join', $join );
} else {
$this->add_sql_clause( 'join', $join );
}
}
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'time_interval';
}
if ( 'category' === $order_by ) {
return '_terms.name';
}
return $order_by;
}
/**
* Returns an array of ids of included categories, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_categories_array( $query_args ) {
if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) {
return $query_args['categories'];
}
return array();
}
/**
* Returns the page of data according to page number and items per page.
*
* @param array $data Data to paginate.
* @param integer $page_no Page number.
* @param integer $items_per_page Number of items per page.
* @return array
*/
protected function page_records( $data, $page_no, $items_per_page ) {
$offset = ( $page_no - 1 ) * $items_per_page;
return array_slice( $data, $offset, $items_per_page );
}
/**
* Enriches the category data.
*
* @param array $categories_data Categories data.
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$categories_data, $query_args ) {
foreach ( $categories_data as $key => $category_data ) {
$extended_info = new \ArrayObject();
if ( $query_args['extended_info'] ) {
$extended_info['name'] = get_the_category_by_ID( $category_data['category_id'] );
}
$categories_data[ $key ]['extended_info'] = $extended_info;
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'categories' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$included_categories = $this->get_included_categories_array( $query_args );
$this->add_sql_query_params( $query_args );
if ( count( $included_categories ) > 0 ) {
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$this->add_sql_clause( 'select', $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) ) );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.category_id = {$table_name}.category_id"
);
$categories_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$categories_query = $this->subquery->get_query_statement();
}
$categories_data = $wpdb->get_results(
$categories_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $categories_data ) {
return new \WP_Error( 'woocommerce_analytics_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ), array( 'status' => 500 ) );
}
$record_count = count( $categories_data );
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$categories_data = $this->page_records( $categories_data, $query_args['page'], $query_args['per_page'] );
$this->include_extended_info( $categories_data, $query_args );
$categories_data = array_map( array( $this, 'cast_numbers' ), $categories_data );
$data = (object) array(
'data' => $categories_data,
'total' => $record_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
global $wpdb;
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', "{$wpdb->wc_category_lookup}.category_tree_id as category_id," );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', "{$wpdb->wc_category_lookup}.category_tree_id" );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class for parameter-based Categories Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'order' => 'desc',
* 'orderby' => 'items_sold',
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Query
*/
class Query extends ReportsQuery {
const REPORT_NAME = 'report-categories';
/**
* Valid fields for Categories report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get categories data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_categories_query_args', $this->get_query_vars() );
$results = \WC_Data_Store::load( self::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_analytics_categories_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,315 @@
<?php
/**
* REST API Reports controller extended by WC Admin plugin.
*
* Handles requests to the reports endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports controller class.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports';
/**
* Register the routes for reports.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to read reports.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
$reports = array(
array(
'slug' => 'performance-indicators',
'description' => __( 'Batch endpoint for getting specific performance indicators from `stats` endpoints.', 'woocommerce-admin' ),
),
array(
'slug' => 'revenue/stats',
'description' => __( 'Stats about revenue.', 'woocommerce-admin' ),
),
array(
'slug' => 'orders/stats',
'description' => __( 'Stats about orders.', 'woocommerce-admin' ),
),
array(
'slug' => 'products',
'description' => __( 'Products detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'products/stats',
'description' => __( 'Stats about products.', 'woocommerce-admin' ),
),
array(
'slug' => 'categories',
'description' => __( 'Product categories detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'categories/stats',
'description' => __( 'Stats about product categories.', 'woocommerce-admin' ),
),
array(
'slug' => 'coupons',
'description' => __( 'Coupons detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'coupons/stats',
'description' => __( 'Stats about coupons.', 'woocommerce-admin' ),
),
array(
'slug' => 'taxes',
'description' => __( 'Taxes detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'taxes/stats',
'description' => __( 'Stats about taxes.', 'woocommerce-admin' ),
),
array(
'slug' => 'downloads',
'description' => __( 'Product downloads detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'downloads/files',
'description' => __( 'Product download files detailed reports.', 'woocommerce-admin' ),
),
array(
'slug' => 'downloads/stats',
'description' => __( 'Stats about product downloads.', 'woocommerce-admin' ),
),
array(
'slug' => 'customers',
'description' => __( 'Customers detailed reports.', 'woocommerce-admin' ),
),
);
/**
* Filter the list of allowed reports, so that data can be loaded from third party extensions in addition to WooCommerce core.
* Array items should be in format of array( 'slug' => 'downloads/stats', 'description' => '',
* 'url' => '', and 'path' => '/wc-ext/v1/...'.
*
* @param array $endpoints The list of allowed reports..
*/
$reports = apply_filters( 'woocommerce_admin_reports', $reports );
foreach ( $reports as $report ) {
if ( empty( $report['slug'] ) ) {
continue;
}
if ( empty( $report['path'] ) ) {
$report['path'] = '/' . $this->namespace . '/reports/' . $report['slug'];
}
// Allows a different admin page to be loaded here,
// or allows an empty url if no report exists for a set of performance indicators.
if ( ! isset( $report['url'] ) ) {
if ( '/stats' === substr( $report['slug'], -6 ) ) {
$url_slug = substr( $report['slug'], 0, -6 );
} else {
$url_slug = $report['slug'];
}
$report['url'] = '/analytics/' . $url_slug;
}
$item = $this->prepare_item_for_response( (object) $report, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Get the order number for an order. If no filter is present for `woocommerce_order_number`, we can just return the ID.
* Returns the parent order number if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string
*/
public function get_order_number( $order_id ) {
$order = wc_get_order( $order_id );
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
}
if ( ! has_filter( 'woocommerce_order_number' ) ) {
return $order->get_id();
}
return $order->get_order_number();
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'description' => $report->description,
'path' => $report->path,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links(
array(
'self' => array(
'href' => rest_url( $report->path ),
),
'report' => array(
'href' => $report->url,
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
)
);
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the resource.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'path' => array(
'description' => __( 'API path.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Get order statuses without prefixes.
*
* @return array
*/
public function get_order_statuses() {
return array_keys( $this->get_order_status_labels() );
}
/**
* Get order statuses (and labels) without prefixes.
*
* @return array
*/
public function get_order_status_labels() {
$order_statuses = array();
foreach ( wc_get_order_statuses() as $key => $label ) {
$new_key = str_replace( 'wc-', '', $key );
$order_statuses[ $new_key ] = $label;
}
return $order_statuses;
}
}

View File

@@ -0,0 +1,331 @@
<?php
/**
* REST API Reports coupons controller
*
* Handles requests to the /reports/coupons endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports coupons controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/coupons';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['before'] = $request['before'];
$args['after'] = $request['after'];
$args['page'] = $request['page'];
$args['per_page'] = $request['per_page'];
$args['orderby'] = $request['orderby'];
$args['order'] = $request['order'];
$args['coupons'] = (array) $request['coupons'];
$args['extended_info'] = $request['extended_info'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new Query( $query_args );
$report_data = $coupons_query->get_data();
$data = array();
foreach ( $report_data->data as $coupons_data ) {
$item = $this->prepare_item_for_response( $coupons_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_coupons', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'coupon' => array(
'href' => rest_url( sprintf( '/%s/coupons/%d', $this->namespace, $object['coupon_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_coupons',
'type' => 'object',
'properties' => array(
'coupon_id' => array(
'description' => __( 'Coupon ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'amount' => array(
'description' => __( 'Net discount amount.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Number of orders.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'extended_info' => array(
'code' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Coupon code.', 'woocommerce-admin' ),
),
'date_created' => array(
'type' => 'date-time',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Coupon creation date.', 'woocommerce-admin' ),
),
'date_created_gmt' => array(
'type' => 'date-time',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Coupon creation date in GMT.', 'woocommerce-admin' ),
),
'date_expires' => array(
'type' => 'date-time',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Coupon expiration date.', 'woocommerce-admin' ),
),
'date_expires_gmt' => array(
'type' => 'date-time',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Coupon expiration date in GMT.', 'woocommerce-admin' ),
),
'discount_type' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'enum' => array_keys( wc_get_coupon_types() ),
'description' => __( 'Coupon discount type.', 'woocommerce-admin' ),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'coupon_id',
'enum' => array(
'coupon_id',
'code',
'amount',
'orders_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupons'] = array(
'description' => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'code' => __( 'Coupon Code', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'amount' => __( 'Amount Discounted', 'woocommerce-admin' ),
'created' => __( 'Created', 'woocommerce-admin' ),
'expires' => __( 'Expires', 'woocommerce-admin' ),
'type' => __( 'Type', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$date_expires = empty( $item['extended_info']['date_expires'] )
? __( 'N/A', 'woocommerce-admin' )
: $item['extended_info']['date_expires'];
return array(
'code' => $item['extended_info']['code'],
'orders_count' => $item['orders_count'],
'amount' => $item['amount'],
'created' => $item['extended_info']['date_created'],
'expires' => $date_expires,
'type' => $item['extended_info']['discount_type'],
);
}
}

View File

@@ -0,0 +1,474 @@
<?php
/**
* API\Reports\Coupons\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
* API\Reports\Coupons\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_coupon_lookup';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'coupons';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'coupon_id' => 'intval',
'amount' => 'floatval',
'orders_count' => 'intval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'coupons';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'coupon_id' => 'coupon_id',
'amount' => 'SUM(discount_amount) as amount',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 5 );
add_action( 'delete_post', array( __CLASS__, 'delete_coupon' ) );
}
/**
* Returns an array of ids of included coupons, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_included_coupons_array( $query_args ) {
if ( isset( $query_args['coupons'] ) && is_array( $query_args['coupons'] ) && count( $query_args['coupons'] ) > 0 ) {
return $query_args['coupons'];
}
return array();
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$order_coupon_lookup_table = self::get_db_table_name();
$this->add_time_period_sql_params( $query_args, $order_coupon_lookup_table );
$this->get_limit_sql_params( $query_args );
$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
if ( $included_coupons ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})" );
$this->add_order_by_params( $query_args, 'outer', 'default_results.coupon_id' );
} else {
$this->add_order_by_params( $query_args, 'inner', "{$order_coupon_lookup_table}.coupon_id" );
}
$this->add_order_status_clause( $query_args, $order_coupon_lookup_table, $this->subquery );
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $from_arg Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
*/
protected function add_order_by_params( $query_args, $from_arg, $id_cell ) {
global $wpdb;
$lookup_table = self::get_db_table_name();
$order_by_clause = $this->add_order_by_clause( $query_args, $this );
$join = "JOIN {$wpdb->posts} AS _coupons ON {$id_cell} = _coupons.ID";
$this->add_orderby_order_clause( $query_args, $this );
if ( 'inner' === $from_arg ) {
$this->subquery->clear_sql_clause( 'join' );
if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
$this->subquery->add_sql_clause( 'join', $join );
}
} else {
$this->clear_sql_clause( 'join' );
if ( false !== strpos( $order_by_clause, '_coupons' ) ) {
$this->add_sql_clause( 'join', $join );
}
}
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'time_interval';
}
if ( 'code' === $order_by ) {
return '_coupons.post_title';
}
return $order_by;
}
/**
* Enriches the coupon data with extra attributes.
*
* @param array $coupon_data Coupon data.
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$coupon_data, $query_args ) {
foreach ( $coupon_data as $idx => $coupon_datum ) {
$extended_info = new \ArrayObject();
if ( $query_args['extended_info'] ) {
$coupon_id = $coupon_datum['coupon_id'];
$coupon = new \WC_Coupon( $coupon_id );
$gmt_timzone = new \DateTimeZone( 'UTC' );
$date_expires = $coupon->get_date_expires();
if ( null === $date_expires ) {
$date_expires = '';
$date_expires_gmt = '';
} else {
$date_expires = $date_expires->format( TimeInterval::$iso_datetime_format );
$date_expires_gmt = new \DateTime( $date_expires );
$date_expires_gmt->setTimezone( $gmt_timzone );
$date_expires_gmt = $date_expires_gmt->format( TimeInterval::$iso_datetime_format );
}
$date_created = $coupon->get_date_created();
if ( null === $date_created ) {
$date_created = '';
$date_created_gmt = '';
} else {
$date_created = $date_created->format( TimeInterval::$iso_datetime_format );
$date_created_gmt = new \DateTime( $date_created );
$date_created_gmt->setTimezone( $gmt_timzone );
$date_created_gmt = $date_created_gmt->format( TimeInterval::$iso_datetime_format );
}
$extended_info = array(
'code' => $coupon->get_code(),
'date_created' => $date_created,
'date_created_gmt' => $date_created_gmt,
'date_expires' => $date_expires,
'date_expires_gmt' => $date_expires_gmt,
'discount_type' => $coupon->get_discount_type(),
);
}
$coupon_data[ $idx ]['extended_info'] = $extended_info;
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'coupon_id',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'coupons' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_coupons = $this->get_included_coupons_array( $query_args );
$limit_params = $this->get_limit_params( $query_args );
$this->subquery->add_sql_clause( 'select', $selections );
$this->add_sql_query_params( $query_args );
if ( count( $included_coupons ) > 0 ) {
$total_results = count( $included_coupons );
$total_pages = (int) ceil( $total_results / $limit_params['per_page'] );
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );
$this->add_sql_clause( 'select', $this->format_join_selections( $fields, array( 'coupon_id' ) ) );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.coupon_id = {$table_name}.coupon_id"
);
$coupons_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$coupons_query = $this->subquery->get_query_statement();
$this->subquery->clear_sql_clause( array( 'select', 'order_by' ) );
$this->subquery->add_sql_clause( 'select', 'coupon_id' );
$coupon_subquery = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$coupon_subquery // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
}
$coupon_data = $wpdb->get_results(
$coupons_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $coupon_data ) {
return $data;
}
$this->include_extended_info( $coupon_data, $query_args );
$coupon_data = array_map( array( $this, 'cast_numbers' ), $coupon_data );
$data = (object) array(
'data' => $coupon_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Create or update an an entry in the wc_order_coupon_lookup table for an order.
*
* @since 3.5.0
* @param int $order_id Order ID.
* @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 -1;
}
// Refunds don't affect coupon stats so return successfully if one is called here.
if ( 'shop_order_refund' === $order->get_type() ) {
return true;
}
$table_name = self::get_db_table_name();
$existing_items = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT coupon_id FROM {$table_name} WHERE order_id = %d",
$order_id
)
);
$existing_items = array_flip( $existing_items );
$coupon_items = $order->get_items( 'coupon' );
$coupon_items_count = count( $coupon_items );
$num_updated = 0;
foreach ( $coupon_items as $coupon_item ) {
$coupon_id = wc_get_coupon_id_by_code( $coupon_item->get_code() );
unset( $existing_items[ $coupon_id ] );
if ( ! $coupon_id ) {
$coupon_items_count--;
continue;
}
$result = $wpdb->replace(
self::get_db_table_name(),
array(
'order_id' => $order_id,
'coupon_id' => $coupon_id,
'discount_amount' => $coupon_item->get_discount(),
'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
),
array(
'%d',
'%d',
'%f',
'%s',
)
);
/**
* Fires when coupon's reports are updated.
*
* @param int $coupon_id Coupon ID.
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_update_coupon', $coupon_id, $order_id );
// Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists.
$num_updated += 2 === intval( $result ) ? 1 : intval( $result );
}
if ( ! empty( $existing_items ) ) {
$existing_items = array_flip( $existing_items );
$format = array_fill( 0, count( $existing_items ), '%d' );
$format = implode( ',', $format );
array_unshift( $existing_items, $order_id );
$wpdb->query(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"DELETE FROM {$table_name} WHERE order_id = %d AND coupon_id in ({$format})",
$existing_items
)
);
}
return ( $coupon_items_count === $num_updated );
}
/**
* Clean coupons data when an order is deleted.
*
* @param int $order_id Order ID.
*/
public static function sync_on_order_delete( $order_id ) {
global $wpdb;
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when coupon's reports are removed from database.
*
* @param int $coupon_id Coupon ID.
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_delete_coupon', 0, $order_id );
ReportsCache::invalidate();
}
/**
* Deletes the coupon lookup information when a coupon is deleted.
* This keeps data consistent if it gets resynced at any point.
*
* @param int $post_id Post ID.
*/
public static function delete_coupon( $post_id ) {
global $wpdb;
if ( 'shop_coupon' !== get_post_type( $post_id ) ) {
return;
}
$wpdb->delete(
self::get_db_table_name(),
array( 'coupon_id' => $post_id )
);
ReportsCache::invalidate();
}
/**
* Gets coupons based on the provided arguments.
*
* @todo Upon core merge, including this in core's `class-wc-coupon-data-store-cpt.php` might make more sense.
* @param array $args Array of args to filter the query by. Supports `include`.
* @return array Array of results.
*/
public function get_coupons( $args ) {
global $wpdb;
$query = "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type='shop_coupon'";
$included_coupons = $this->get_included_coupons( $args, 'include' );
if ( ! empty( $included_coupons ) ) {
$query .= " AND ID IN ({$included_coupons})";
}
return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'coupon_id' );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class for parameter-based Coupons Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'coupons' => array(5, 120),
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Coupons\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Coupons\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_coupons_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-coupons' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_coupons_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,358 @@
<?php
/**
* REST API Reports coupons stats controller
*
* Handles requests to the /reports/coupons/stats endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
* REST API Reports coupons stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/coupons/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['segmentby'] = $request['segmentby'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new Query( $query_args );
try {
$report_data = $coupons_query->get_data();
} catch ( ParameterException $e ) {
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( (object) $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_coupons_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$data_values = array(
'amount' => array(
'description' => __( 'Net discount amount.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
'format' => 'currency',
),
'coupons_count' => array(
'description' => __( 'Number of coupons.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'title' => __( 'Discounted Orders', 'woocommerce-admin' ),
'description' => __( 'Number of discounted orders.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
),
);
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce-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',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'amount',
'coupons_count',
'orders_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupons'] = array(
'description' => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce-admin' ),
'type' => 'string',
'enum' => array(
'product',
'variation',
'category',
'coupon',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@@ -0,0 +1,251 @@
<?php
/**
* API\Reports\Coupons\Stats\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Coupons\Stats\DataStore.
*/
class DataStore extends CouponsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'date_start_gmt' => 'strval',
'date_end_gmt' => 'strval',
'amount' => 'floatval',
'coupons_count' => 'intval',
'orders_count' => 'intval',
);
/**
* SQL columns to select in the db query.
*
* @var array
*/
protected $report_columns;
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'coupons_stats';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'coupons_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'amount' => 'SUM(discount_amount) as amount',
'coupons_count' => 'COUNT(DISTINCT coupon_id) as coupons_count',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
* Updates the database query with parameters used for Products Stats report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function update_sql_query_params( $query_args ) {
global $wpdb;
$clauses = array(
'where' => '',
'join' => '',
);
$order_coupon_lookup_table = self::get_db_table_name();
$included_coupons = $this->get_included_coupons( $query_args, 'coupons' );
if ( $included_coupons ) {
$clauses['where'] .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$clauses['join'] .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_coupon_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$clauses['where'] .= " AND ( {$order_status_filter} )";
}
$this->add_time_period_sql_params( $query_args, $order_coupon_lookup_table );
$this->add_intervals_sql_params( $query_args, $order_coupon_lookup_table );
$clauses['where_time'] = $this->get_sql_clause( 'where_time' );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) );
$this->interval_query->add_sql_clause( 'select', 'AS time_interval' );
foreach ( array( 'join', 'where_time', 'where' ) as $clause ) {
$this->interval_query->add_sql_clause( $clause, $clauses[ $clause ] );
$this->total_query->add_sql_clause( $clause, $clauses[ $clause ] );
}
}
/**
* 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.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'interval' => 'week',
'coupons' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$limit_params = $this->get_limit_sql_params( $query_args );
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->total_query->add_sql_clause( 'select', $selections );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return $data;
}
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$segmenter = new Segmenter( $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( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return $data;
}
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $limit_params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $limit_params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} 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 );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class for parameter-based Products Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'coupons' => array(5, 120),
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Coupons\Stats\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_coupons_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-coupons-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_coupons_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,331 @@
<?php
/**
* Class for adding segmenting support to coupons/stats without cluttering the data store.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class Segmenter extends ReportsSegmenter {
/**
* Returns column => query mapping 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 array Column => SELECT query mapping.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'amount' => "SUM($products_table.coupon_amount) as amount",
);
return $columns_mapping;
}
/**
* Returns column => query mapping 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 array Column => SELECT query mapping.
*/
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 $columns_mapping;
}
/**
* Returns column => query mapping 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 array Column => SELECT query mapping.
*/
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 $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 \Automattic\WooCommerce\Admin\API\Reports\ParameterException 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'] ) {
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $table_name );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$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 ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce-admin' ) );
}
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $table_name );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$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'] ) {
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $table_name );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$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
LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->wc_category_lookup}.category_id
";
$segmenting_where = " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
$segmenting_groupby = "{$wpdb->wc_category_lookup}.category_tree_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'] ) {
$coupon_level_columns = $this->segment_selections_orders( $table_name );
$segmenting_selections = $this->prepare_selections( $coupon_level_columns );
$this->report_columns = $coupon_level_columns;
$segmenting_from = '';
$segmenting_groupby = "$table_name.coupon_id";
$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
return $segments;
}
}

View File

@@ -0,0 +1,611 @@
<?php
/**
* REST API Reports customers controller
*
* Handles requests to the /reports/customers endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/**
* REST API Reports customers controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/customers';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['registered_before'] = $request['registered_before'];
$args['registered_after'] = $request['registered_after'];
$args['order_before'] = $request['before'];
$args['order_after'] = $request['after'];
$args['page'] = $request['page'];
$args['per_page'] = $request['per_page'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['match'] = $request['match'];
$args['search'] = $request['search'];
$args['searchby'] = $request['searchby'];
$args['name_includes'] = $request['name_includes'];
$args['name_excludes'] = $request['name_excludes'];
$args['username_includes'] = $request['username_includes'];
$args['username_excludes'] = $request['username_excludes'];
$args['email_includes'] = $request['email_includes'];
$args['email_excludes'] = $request['email_excludes'];
$args['country_includes'] = $request['country_includes'];
$args['country_excludes'] = $request['country_excludes'];
$args['last_active_before'] = $request['last_active_before'];
$args['last_active_after'] = $request['last_active_after'];
$args['orders_count_min'] = $request['orders_count_min'];
$args['orders_count_max'] = $request['orders_count_max'];
$args['total_spend_min'] = $request['total_spend_min'];
$args['total_spend_max'] = $request['total_spend_max'];
$args['avg_order_value_min'] = $request['avg_order_value_min'];
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = TimeInterval::normalize_between_params( $request, $between_params_numeric, false );
$between_params_date = array( 'last_active', 'registered' );
$normalized_params_date = TimeInterval::normalize_between_params( $request, $between_params_date, true );
$args = array_merge( $args, $normalized_params_numeric, $normalized_params_date );
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$customers_query = new Query( $query_args );
$report_data = $customers_query->get_data();
$data = array();
foreach ( $report_data->data as $customer_data ) {
$item = $this->prepare_item_for_response( $customer_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Get one report.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_item( $request ) {
$query_args = $this->prepare_reports_query( $request );
$query_args['customers'] = array( $request->get_param( 'id' ) );
$customers_query = new Query( $query_args );
$report_data = $customers_query->get_data();
$data = array();
foreach ( $report_data->data as $customer_data ) {
$item = $this->prepare_item_for_response( $customer_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $report, $request );
// Registered date is UTC.
$data['date_registered_gmt'] = wc_rest_prepare_date_response( $data['date_registered'] );
$data['date_registered'] = wc_rest_prepare_date_response( $data['date_registered'], false );
// Last active date is local time.
$data['date_last_active_gmt'] = wc_rest_prepare_date_response( $data['date_last_active'], false );
$data['date_last_active'] = wc_rest_prepare_date_response( $data['date_last_active'] );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_customers', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param array $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
if ( empty( $object['user_id'] ) ) {
return array();
}
return array(
'customer' => array(
'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object['id'] ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/customers', $this->namespace ) ),
),
);
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customers',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Customer ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'user_id' => array(
'description' => __( 'User ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'username' => array(
'description' => __( 'Username.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'country' => array(
'description' => __( 'Country.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'city' => array(
'description' => __( 'City.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'state' => array(
'description' => __( 'Region.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_registered' => array(
'description' => __( 'Date registered.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_registered_gmt' => array(
'description' => __( 'Date registered GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_last_active' => array(
'description' => __( 'Date last active.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_last_active_gmt' => array(
'description' => __( 'Date last active GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Order count.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_spend' => array(
'description' => __( 'Total spend.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_order_value' => array(
'description' => __( 'Avg order value.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources with orders published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources with orders published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date_registered',
'enum' => array(
'username',
'name',
'country',
'city',
'state',
'postcode',
'date_registered',
'date_last_active',
'orders_count',
'total_spend',
'avg_order_value',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['search'] = array(
'description' => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['searchby'] = array(
'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
'type' => 'string',
'default' => 'name',
'enum' => array(
'name',
'username',
'email',
),
);
$params['name_includes'] = array(
'description' => __( 'Limit response to objects with specfic names.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['name_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic names.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username_includes'] = array(
'description' => __( 'Limit response to objects with specfic usernames.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic usernames.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email_includes'] = array(
'description' => __( 'Limit response to objects including emails.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email_excludes'] = array(
'description' => __( 'Limit response to objects excluding emails.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country_includes'] = array(
'description' => __( 'Limit response to objects with specfic countries.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic countries.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_before'] = array(
'description' => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_after'] = array(
'description' => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
);
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
);
$params['orders_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'woocommerce-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_max'] = array(
'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'woocommerce-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_between'] = array(
'description' => __( 'Limit response to objects with an order count between two given integers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_max'] = array(
'description' => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_between'] = array(
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['avg_order_value_min'] = array(
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_max'] = array(
'description' => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_between'] = array(
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['last_order_before'] = array(
'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_order_after'] = array(
'description' => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customers'] = array(
'description' => __( 'Limit result to items with specified customer ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'name' => __( 'Name', 'woocommerce-admin' ),
'username' => __( 'Username', 'woocommerce-admin' ),
'last_active' => __( 'Last Active', 'woocommerce-admin' ),
'registered' => __( 'Sign Up', 'woocommerce-admin' ),
'email' => __( 'Email', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'total_spend' => __( 'Total Spend', 'woocommerce-admin' ),
'avg_order_value' => __( 'AOV', 'woocommerce-admin' ),
'country' => __( 'Country', 'woocommerce-admin' ),
'city' => __( 'City', 'woocommerce-admin' ),
'region' => __( 'Region', 'woocommerce-admin' ),
'postcode' => __( 'Postal Code', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'name' => $item['name'],
'username' => $item['username'],
'last_active' => $item['date_last_active'],
'registered' => $item['date_registered'],
'email' => $item['email'],
'orders_count' => $item['orders_count'],
'total_spend' => self::csv_number_format( $item['total_spend'] ),
'avg_order_value' => self::csv_number_format( $item['avg_order_value'] ),
'country' => $item['country'],
'city' => $item['city'],
'region' => $item['state'],
'postcode' => $item['postcode'],
);
}
}

View File

@@ -0,0 +1,703 @@
<?php
/**
* Admin\API\Reports\Customers\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
* Admin\API\Reports\Customers\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_customer_lookup';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'customers';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'id' => 'intval',
'user_id' => 'intval',
'orders_count' => 'intval',
'total_spend' => 'floatval',
'avg_order_value' => 'floatval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'customers';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
global $wpdb;
$table_name = self::get_db_table_name();
$orders_count = 'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END )';
$total_spend = 'SUM( total_sales )';
$this->report_columns = array(
'id' => "{$table_name}.customer_id as id",
'user_id' => 'user_id',
'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
'email' => 'email',
'country' => 'country',
'city' => 'city',
'state' => 'state',
'postcode' => 'postcode',
'date_registered' => 'date_registered',
'date_last_active' => 'IF( date_last_active <= "0000-00-00 00:00:00", NULL, date_last_active ) AS date_last_active',
'date_last_order' => "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order",
'orders_count' => "{$orders_count} as orders_count",
'total_spend' => "{$total_spend} as total_spend",
'avg_order_value' => "CASE WHEN {$orders_count} = 0 THEN NULL ELSE {$total_spend} / {$orders_count} END AS avg_order_value",
);
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'edit_user_profile_update', array( __CLASS__, 'update_registered_customer' ) );
add_action( 'updated_user_meta', array( __CLASS__, 'update_registered_customer_via_last_active' ), 10, 3 );
}
/**
* Trigger a customer update if their "last active" meta value was changed.
* Function expects to be hooked into the `updated_user_meta` action.
*
* @param int $meta_id ID of updated metadata entry.
* @param int $user_id ID of the user being updated.
* @param string $meta_key Meta key being updated.
*/
public static function update_registered_customer_via_last_active( $meta_id, $user_id, $meta_key ) {
if ( 'wc_last_active' === $meta_key ) {
self::update_registered_customer( $user_id );
}
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'name' === $order_by ) {
return "CONCAT_WS( ' ', first_name, last_name )";
}
return $order_by;
}
/**
* Fills WHERE clause of SQL request with date-related constraints.
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
*/
protected function add_time_period_sql_params( $query_args, $table_name ) {
global $wpdb;
$this->clear_sql_clause( array( 'where', 'where_time', 'having' ) );
$date_param_mapping = array(
'registered' => array(
'clause' => 'where',
'column' => $table_name . '.date_registered',
),
'order' => array(
'clause' => 'where',
'column' => $wpdb->prefix . 'wc_order_stats.date_created',
),
'last_active' => array(
'clause' => 'where',
'column' => $table_name . '.date_last_active',
),
'last_order' => array(
'clause' => 'having',
'column' => "MAX( {$wpdb->prefix}wc_order_stats.date_created )",
),
);
$match_operator = $this->get_match_operator( $query_args );
$where_time_clauses = array();
$having_time_clauses = array();
foreach ( $date_param_mapping as $query_param => $param_info ) {
$subclauses = array();
$before_arg = $query_param . '_before';
$after_arg = $query_param . '_after';
$column_name = $param_info['column'];
if ( ! empty( $query_args[ $before_arg ] ) ) {
$datetime = new \DateTime( $query_args[ $before_arg ] );
$datetime_str = $datetime->format( TimeInterval::$sql_datetime_format );
$subclauses[] = "{$column_name} <= '$datetime_str'";
}
if ( ! empty( $query_args[ $after_arg ] ) ) {
$datetime = new \DateTime( $query_args[ $after_arg ] );
$datetime_str = $datetime->format( TimeInterval::$sql_datetime_format );
$subclauses[] = "{$column_name} >= '$datetime_str'";
}
if ( $subclauses && ( 'where' === $param_info['clause'] ) ) {
$where_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
}
if ( $subclauses && ( 'having' === $param_info['clause'] ) ) {
$having_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
}
}
if ( $where_time_clauses ) {
$this->subquery->add_sql_clause( 'where_time', 'AND ' . implode( " {$match_operator} ", $where_time_clauses ) );
}
if ( $having_time_clauses ) {
$this->subquery->add_sql_clause( 'having', 'AND ' . implode( " {$match_operator} ", $having_time_clauses ) );
}
}
/**
* Updates the database query with parameters used for Customers report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$customer_lookup_table = self::get_db_table_name();
$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';
$this->add_time_period_sql_params( $query_args, $customer_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->add_order_by_sql_params( $query_args );
$this->subquery->add_sql_clause( 'left_join', "LEFT JOIN {$order_stats_table_name} ON {$customer_lookup_table}.customer_id = {$order_stats_table_name}.customer_id" );
$match_operator = $this->get_match_operator( $query_args );
$where_clauses = array();
$having_clauses = array();
$exact_match_params = array(
'name',
'username',
'email',
'country',
);
foreach ( $exact_match_params as $exact_match_param ) {
if ( ! empty( $query_args[ $exact_match_param . '_includes' ] ) ) {
$exact_match_arguments = $query_args[ $exact_match_param . '_includes' ];
$exact_match_arguments_escaped = array_map( 'esc_sql', explode( ',', $exact_match_arguments ) );
$included = implode( "','", $exact_match_arguments_escaped );
// 'country_includes' is a list of country codes, the others will be a list of customer ids.
$table_column = 'country' === $exact_match_param ? $exact_match_param : 'customer_id';
$where_clauses[] = "{$customer_lookup_table}.{$table_column} IN ('{$included}')";
}
if ( ! empty( $query_args[ $exact_match_param . '_excludes' ] ) ) {
$exact_match_arguments = $query_args[ $exact_match_param . '_excludes' ];
$exact_match_arguments_escaped = array_map( 'esc_sql', explode( ',', $exact_match_arguments ) );
$excluded = implode( "','", $exact_match_arguments_escaped );
// 'country_includes' is a list of country codes, the others will be a list of customer ids.
$table_column = 'country' === $exact_match_param ? $exact_match_param : 'customer_id';
$where_clauses[] = "{$customer_lookup_table}.{$table_column} NOT IN ('{$excluded}')";
}
}
$search_params = array(
'name',
'username',
'email',
);
if ( ! empty( $query_args['search'] ) ) {
$name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';
if ( empty( $query_args['searchby'] ) || 'name' === $query_args['searchby'] || ! in_array( $query_args['searchby'], $search_params, true ) ) {
$searchby = "CONCAT_WS( ' ', first_name, last_name )";
} else {
$searchby = $query_args['searchby'];
}
$where_clauses[] = $wpdb->prepare( "{$searchby} LIKE %s", $name_like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
// Allow a list of customer IDs to be specified.
if ( ! empty( $query_args['customers'] ) ) {
$included_customers = $this->get_filtered_ids( $query_args, 'customers' );
$where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
}
$numeric_params = array(
'orders_count' => array(
'column' => 'COUNT( order_id )',
'format' => '%d',
),
'total_spend' => array(
'column' => 'SUM( total_sales )',
'format' => '%f',
),
'avg_order_value' => array(
'column' => '( SUM( total_sales ) / COUNT( order_id ) )',
'format' => '%f',
),
);
foreach ( $numeric_params as $numeric_param => $param_info ) {
$subclauses = array();
$min_param = $numeric_param . '_min';
$max_param = $numeric_param . '_max';
$or_equal = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';
if ( isset( $query_args[ $min_param ] ) ) {
$subclauses[] = $wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
"{$param_info['column']} >{$or_equal} {$param_info['format']}",
$query_args[ $min_param ]
);
}
if ( isset( $query_args[ $max_param ] ) ) {
$subclauses[] = $wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
"{$param_info['column']} <{$or_equal} {$param_info['format']}",
$query_args[ $max_param ]
);
}
if ( $subclauses ) {
$having_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')';
}
}
if ( $where_clauses ) {
$preceding_match = empty( $this->get_sql_clause( 'where_time' ) ) ? ' AND ' : " {$match_operator} ";
$this->subquery->add_sql_clause( 'where', $preceding_match . implode( " {$match_operator} ", $where_clauses ) );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$this->subquery->add_sql_clause( 'left_join', "AND ( {$order_status_filter} )" );
}
if ( $having_clauses ) {
$preceding_match = empty( $this->get_sql_clause( 'having' ) ) ? ' AND ' : " {$match_operator} ";
$this->subquery->add_sql_clause( 'having', $preceding_match . implode( " {$match_operator} ", $having_clauses ) );
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$customers_table_name = self::get_db_table_name();
$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'order_before' => TimeInterval::default_before(),
'order_after' => TimeInterval::default_after(),
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->add_sql_query_params( $query_args );
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) as tt
";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$customer_data = $wpdb->get_results(
$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $customer_data ) {
return $data;
}
$customer_data = array_map( array( $this, 'cast_numbers' ), $customer_data );
$data = (object) array(
'data' => $customer_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Returns an existing customer ID for an order if one exists.
*
* @param object $order WC Order.
* @return int|bool
*/
public static function get_existing_customer_id_from_order( $order ) {
global $wpdb;
if ( ! is_a( $order, 'WC_Order' ) ) {
return false;
}
$user_id = $order->get_customer_id();
if ( 0 === $user_id ) {
$customer_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d",
$order->get_id()
)
);
if ( $customer_id ) {
return $customer_id;
}
$email = $order->get_billing_email( 'edit' );
if ( $email ) {
return self::get_guest_id_by_email( $email );
} else {
return false;
}
} else {
return self::get_customer_id_by_user_id( $user_id );
}
}
/**
* Get or create a customer from a given order.
*
* @param object $order WC Order.
* @return int|bool
*/
public static function get_or_create_customer_from_order( $order ) {
if ( ! $order ) {
return false;
}
global $wpdb;
if ( ! is_a( $order, 'WC_Order' ) ) {
return false;
}
$returning_customer_id = self::get_existing_customer_id_from_order( $order );
if ( $returning_customer_id ) {
return $returning_customer_id;
}
$data = array(
'first_name' => $order->get_customer_first_name(),
'last_name' => $order->get_customer_last_name(),
'email' => $order->get_billing_email( 'edit' ),
'city' => $order->get_billing_city( 'edit' ),
'state' => $order->get_billing_state( 'edit' ),
'postcode' => $order->get_billing_postcode( 'edit' ),
'country' => $order->get_billing_country( 'edit' ),
'date_last_active' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
);
$format = array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
);
// Add registered customer data.
if ( 0 !== $order->get_user_id() ) {
$user_id = $order->get_user_id();
$customer = new \WC_Customer( $user_id );
$data['user_id'] = $user_id;
$data['username'] = $customer->get_username( 'edit' );
$data['date_registered'] = $customer->get_date_created( 'edit' ) ? $customer->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null;
$format[] = '%d';
$format[] = '%s';
$format[] = '%s';
}
$result = $wpdb->insert( self::get_db_table_name(), $data, $format );
$customer_id = $wpdb->insert_id;
/**
* Fires when a new report customer is created.
*
* @param int $customer_id Customer ID.
*/
do_action( 'woocommerce_analytics_new_customer', $customer_id );
return $result ? $customer_id : false;
}
/**
* Retrieve a guest ID (when user_id is null) by email.
*
* @param string $email Email address.
* @return false|array Customer array if found, boolean false if not.
*/
public static function get_guest_id_by_email( $email ) {
global $wpdb;
$table_name = self::get_db_table_name();
$customer_id = $wpdb->get_var(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT customer_id FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1",
$email
)
);
return $customer_id ? (int) $customer_id : false;
}
/**
* Retrieve a registered customer row id by user_id.
*
* @param string|int $user_id User ID.
* @return false|int Customer ID if found, boolean false if not.
*/
public static function get_customer_id_by_user_id( $user_id ) {
global $wpdb;
$table_name = self::get_db_table_name();
$customer_id = $wpdb->get_var(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT customer_id FROM {$table_name} WHERE user_id = %d LIMIT 1",
$user_id
)
);
return $customer_id ? (int) $customer_id : false;
}
/**
* Retrieve the oldest orders made by a customer.
*
* @param int $customer_id Customer ID.
* @return array Orders.
*/
public static function get_oldest_orders( $customer_id ) {
global $wpdb;
$orders_table = $wpdb->prefix . 'wc_order_stats';
$excluded_statuses = array_map( array( __CLASS__, 'normalize_order_status' ), self::get_excluded_report_order_statuses() );
$excluded_statuses_condition = '';
if ( ! empty( $excluded_statuses ) ) {
$excluded_statuses_str = implode( "','", $excluded_statuses );
$excluded_statuses_condition = "AND status NOT IN ('{$excluded_statuses_str}')";
}
return $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d {$excluded_statuses_condition} ORDER BY date_created, order_id ASC LIMIT 2",
$customer_id
)
);
}
/**
* Update the database with customer data.
*
* @param int $user_id WP User ID to update customer data for.
* @return int|bool|null Number or rows modified or false on failure.
*/
public static function update_registered_customer( $user_id ) {
global $wpdb;
$customer = new \WC_Customer( $user_id );
if ( ! self::is_valid_customer( $user_id ) ) {
return false;
}
$last_order = $customer->get_last_order();
if ( ! $last_order ) {
$first_name = get_user_meta( $user_id, 'first_name', true );
$last_name = get_user_meta( $user_id, 'last_name', true );
} else {
$first_name = $last_order->get_customer_first_name();
$last_name = $last_order->get_customer_last_name();
}
$last_active = $customer->get_meta( 'wc_last_active', true, 'edit' );
$data = array(
'user_id' => $user_id,
'username' => $customer->get_username( 'edit' ),
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $customer->get_email( 'edit' ),
'city' => $customer->get_billing_city( 'edit' ),
'state' => $customer->get_billing_state( 'edit' ),
'postcode' => $customer->get_billing_postcode( 'edit' ),
'country' => $customer->get_billing_country( 'edit' ),
'date_registered' => $customer->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
'date_last_active' => $last_active ? gmdate( 'Y-m-d H:i:s', $last_active ) : null,
);
$format = array(
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
);
$customer_id = self::get_customer_id_by_user_id( $user_id );
if ( $customer_id ) {
// Preserve customer_id for existing user_id.
$data['customer_id'] = $customer_id;
$format[] = '%d';
}
$results = $wpdb->replace( self::get_db_table_name(), $data, $format );
/**
* Fires when customser's reports are updated.
*
* @param int $customer_id Customer ID.
*/
do_action( 'woocommerce_analytics_update_customer', $customer_id );
ReportsCache::invalidate();
return $results;
}
/**
* Check if a user ID is a valid customer or other user role with past orders.
*
* @param int $user_id User ID.
* @return bool
*/
protected static function is_valid_customer( $user_id ) {
$customer = new \WC_Customer( $user_id );
if ( absint( $customer->get_id() ) !== absint( $user_id ) ) {
return false;
}
$customer_roles = (array) apply_filters( 'woocommerce_analytics_customer_roles', array( 'customer' ) );
if ( $customer->get_order_count() < 1 && ! in_array( $customer->get_role(), $customer_roles, true ) ) {
return false;
}
return true;
}
/**
* Delete a customer lookup row.
*
* @param int $customer_id Customer ID.
*/
public static function delete_customer( $customer_id ) {
global $wpdb;
$customer_id = (int) $customer_id;
$wpdb->delete( self::get_db_table_name(), array( 'customer_id' => $customer_id ) );
/**
* Fires when a customer is deleted.
*
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_delete_customer', $customer_id );
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$table_name = self::get_db_table_name();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', $table_name );
$this->subquery->add_sql_clause( 'select', "{$table_name}.customer_id" );
$this->subquery->add_sql_clause( 'group_by', "{$table_name}.customer_id" );
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class for parameter-based Customers Report querying
*
* Example usage:
* $args = array(
* 'registered_before' => '2018-07-19 00:00:00',
* 'registered_after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'avg_order_value_min' => 100,
* 'country' => 'GB',
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Customers\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Customers report.
*
* @return array
*/
protected function get_default_query_vars() {
return array(
'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'fields' => '*',
);
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_customers_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-customers' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_customers_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,368 @@
<?php
/**
* REST API Reports customers stats controller
*
* Handles requests to the /reports/customers/stats endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/**
* REST API Reports customers stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/customers/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['registered_before'] = $request['registered_before'];
$args['registered_after'] = $request['registered_after'];
$args['match'] = $request['match'];
$args['search'] = $request['search'];
$args['name_includes'] = $request['name_includes'];
$args['name_excludes'] = $request['name_excludes'];
$args['username_includes'] = $request['username_includes'];
$args['username_excludes'] = $request['username_excludes'];
$args['email_includes'] = $request['email_includes'];
$args['email_excludes'] = $request['email_excludes'];
$args['country_includes'] = $request['country_includes'];
$args['country_excludes'] = $request['country_excludes'];
$args['last_active_before'] = $request['last_active_before'];
$args['last_active_after'] = $request['last_active_after'];
$args['orders_count_min'] = $request['orders_count_min'];
$args['orders_count_max'] = $request['orders_count_max'];
$args['total_spend_min'] = $request['total_spend_min'];
$args['total_spend_max'] = $request['total_spend_max'];
$args['avg_order_value_min'] = $request['avg_order_value_min'];
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = TimeInterval::normalize_between_params( $request, $between_params_numeric, false );
$between_params_date = array( 'last_active', 'registered' );
$normalized_params_date = TimeInterval::normalize_between_params( $request, $between_params_date, true );
$args = array_merge( $args, $normalized_params_numeric, $normalized_params_date );
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$customers_query = new Query( $query_args );
$report_data = $customers_query->get_data();
$out_data = array(
'totals' => $report_data,
);
return rest_ensure_response( $out_data );
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_customers_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
// @todo Should any of these be 'indicator's?
$totals = array(
'customers_count' => array(
'description' => __( 'Number of customers.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_orders_count' => array(
'description' => __( 'Average number of orders.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_total_spend' => array(
'description' => __( 'Average total spend per customer.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
'avg_avg_order_value' => array(
'description' => __( 'Average AOV per customer.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customers_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['search'] = array(
'description' => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['searchby'] = array(
'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
'type' => 'string',
'default' => 'name',
'enum' => array(
'name',
'username',
'email',
),
);
$params['name_includes'] = array(
'description' => __( 'Limit response to objects with specfic names.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['name_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic names.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username_includes'] = array(
'description' => __( 'Limit response to objects with specfic usernames.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic usernames.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email_includes'] = array(
'description' => __( 'Limit response to objects including emails.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email_excludes'] = array(
'description' => __( 'Limit response to objects excluding emails.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country_includes'] = array(
'description' => __( 'Limit response to objects with specfic countries.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country_excludes'] = array(
'description' => __( 'Limit response to objects excluding specfic countries.', 'woocommerce-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_before'] = array(
'description' => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_after'] = array(
'description' => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
);
$params['registered_before'] = array(
'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_after'] = array(
'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['registered_between'] = array(
'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_date_arg' ),
);
$params['orders_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'woocommerce-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_max'] = array(
'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'woocommerce-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orders_count_between'] = array(
'description' => __( 'Limit response to objects with an order count between two given integers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_max'] = array(
'description' => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_between'] = array(
'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['avg_order_value_min'] = array(
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_max'] = array(
'description' => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'woocommerce-admin' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_between'] = array(
'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => array( '\Automattic\WooCommerce\Admin\API\Reports\TimeInterval', 'rest_validate_between_numeric_arg' ),
);
$params['last_order_before'] = array(
'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_order_after'] = array(
'description' => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customers'] = array(
'description' => __( 'Limit result to items with specified customer ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* API\Reports\Customers\Stats\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
/**
* API\Reports\Customers\Stats\DataStore.
*/
class DataStore extends CustomersDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'customers_count' => 'intval',
'avg_orders_count' => 'floatval',
'avg_total_spend' => 'floatval',
'avg_avg_order_value' => 'floatval',
);
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'customers_stats';
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'customers_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$this->report_columns = array(
'customers_count' => 'COUNT( * ) as customers_count',
'avg_orders_count' => 'AVG( orders_count ) as avg_orders_count',
'avg_total_spend' => 'AVG( total_spend ) as avg_total_spend',
'avg_avg_order_value' => 'AVG( avg_order_value ) as avg_avg_order_value',
);
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$customers_table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'customers_count' => 0,
'avg_orders_count' => 0,
'avg_total_spend' => 0.0,
'avg_avg_order_value' => 0.0,
);
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
// Clear SQL clauses set for parent class queries that are different here.
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', 'SUM( total_sales ) AS total_spend,' );
$this->subquery->add_sql_clause(
'select',
'SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count,'
);
$this->subquery->add_sql_clause(
'select',
'CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value'
);
$this->clear_sql_clause( array( 'order_by', 'limit' ) );
$this->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'from', "({$this->subquery->get_query_statement()}) AS tt" );
$report_data = $wpdb->get_results(
$this->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $report_data ) {
return $data;
}
$data = (object) $this->cast_numbers( $report_data[0] );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class for parameter-based Customers Report Stats querying
*
* Example usage:
* $args = array(
* 'registered_before' => '2018-07-19 00:00:00',
* 'registered_after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'avg_order_value_min' => 100,
* 'country' => 'GB',
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Customers\Stats\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Customers report.
*
* @return array
*/
protected function get_default_query_vars() {
return array(
'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'fields' => '*', // @todo Needed?
);
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_customers_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-customers-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_customers_stats_select_query', $results, $args );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<?php
/**
* Reports Data Store Interface
*
* @package WooCommerce Admin/Interface
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WooCommerce Reports data store interface.
*
* @since 3.5.0
*/
interface DataStoreInterface {
/**
* Get the data based on args.
*
* @param array $args Query parameters.
* @return stdClass|WP_Error
*/
public function get_data( $args );
}

View File

@@ -0,0 +1,417 @@
<?php
/**
* REST API Reports downloads controller
*
* Handles requests to the /reports/downloads endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports downloads controller class.
*
* @package WooCommerce/API
* @extends Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads';
/**
* Get items.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$args = array();
$registered = array_keys( $this->get_collection_params() );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
$args[ $param_name ] = $request[ $param_name ];
}
}
$reports = new Query( $args );
$downloads_data = $reports->get_data();
$data = array();
foreach ( $downloads_data->data as $download_data ) {
$item = $this->prepare_item_for_response( $download_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $downloads_data->total );
$response->header( 'X-WP-TotalPages', (int) $downloads_data->pages );
$page = $downloads_data->page_no;
$max_pages = $downloads_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
$response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' );
// Figure out file name.
// Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197.
$product_id = intval( $data['product_id'] );
$_product = wc_get_product( $product_id );
$file_path = $_product->get_file_download_path( $data['download_id'] );
$filename = basename( $file_path );
$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$response->data['file_path'] = $file_path;
$customer = new \WC_Customer( $data['user_id'] );
$response->data['username'] = $customer->get_username();
$response->data['order_number'] = $this->get_order_number( $data['order_id'] );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_downloads', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param Array $object Object data.
* @return array Links for the given post.
*/
protected function prepare_links( $object ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
'embeddable' => true,
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_downloads',
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'ID.', 'woocommerce-admin' ),
),
'product_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce-admin' ),
),
'date' => array(
'description' => __( "The date of the download, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_gmt' => array(
'description' => __( 'The date of the download, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'download_id' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Download ID.', 'woocommerce-admin' ),
),
'file_name' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'File name.', 'woocommerce-admin' ),
),
'file_path' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'File URL.', 'woocommerce-admin' ),
),
'product_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce-admin' ),
),
'order_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order ID.', 'woocommerce-admin' ),
),
'order_number' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order Number.', 'woocommerce-admin' ),
),
'user_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'User ID for the downloader.', 'woocommerce-admin' ),
),
'username' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'User name of the downloader.', 'woocommerce-admin' ),
),
'ip_address' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'IP address for the downloader.', 'woocommerce-admin' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'product',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['order_includes'] = array(
'description' => __( 'Limit result set to items that have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['order_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['customer_includes'] = array(
'description' => __( 'Limit response to objects that have the specified user ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['customer_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['ip_address_includes'] = array(
'description' => __( 'Limit response to objects that have a specified ip address.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
$params['ip_address_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have a specified ip address.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
return $params;
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'date' => __( 'Date', 'woocommerce-admin' ),
'product' => __( 'Product Title', 'woocommerce-admin' ),
'file_name' => __( 'File Name', 'woocommerce-admin' ),
'order_number' => __( 'Order #', 'woocommerce-admin' ),
'user_id' => __( 'User Name', 'woocommerce-admin' ),
'ip_address' => __( 'IP', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'date' => $item['date'],
'product' => $item['_embedded']['product'][0]['name'],
'file_name' => $item['file_name'],
'order_number' => $item['order_number'],
'user_id' => $item['username'],
'ip_address' => $item['ip_address'],
);
}
}

View File

@@ -0,0 +1,391 @@
<?php
/**
* API\Reports\Downloads\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Downloads\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_download_log';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'downloads';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'id' => 'intval',
'date' => 'strval',
'date_gmt' => 'strval',
'download_id' => 'strval', // String because this can sometimes be a hash.
'file_name' => 'strval',
'product_id' => 'intval',
'order_id' => 'intval',
'user_id' => 'intval',
'ip_address' => 'strval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'downloads';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$this->report_columns = array(
'id' => 'download_log_id as id',
'date' => 'timestamp as date_gmt',
'download_id' => 'product_permissions.download_id',
'product_id' => 'product_permissions.product_id',
'order_id' => 'product_permissions.order_id',
'user_id' => 'product_permissions.user_id',
'ip_address' => 'user_ip_address as ip_address',
);
}
/**
* Updates the database query with parameters used for downloads report.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$lookup_table = self::get_db_table_name();
$permission_table = $wpdb->prefix . 'woocommerce_downloadable_product_permissions';
$operator = $this->get_match_operator( $query_args );
$where_filters = array();
$join = "JOIN {$permission_table} as product_permissions ON {$lookup_table}.permission_id = product_permissions.permission_id";
$where_time = $this->add_time_period_sql_params( $query_args, $lookup_table );
if ( $where_time ) {
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where_time', $where_time );
} else {
$this->interval_query->add_sql_clause( 'where_time', $where_time );
}
}
$this->get_limit_sql_params( $query_args );
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'product_id',
'IN',
$this->get_included_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'product_id',
'NOT IN',
$this->get_excluded_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'order_id',
'IN',
$this->get_included_orders( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'order_id',
'NOT IN',
$this->get_excluded_orders( $query_args )
);
$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
$customer_lookup = "SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN (%s)";
$included_customers = $this->get_included_customers( $query_args );
$excluded_customers = $this->get_excluded_customers( $query_args );
if ( $included_customers ) {
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'user_id',
'IN',
sprintf( $customer_lookup, $included_customers )
);
}
if ( $excluded_customers ) {
$where_filters[] = $this->get_object_where_filter(
$lookup_table,
'permission_id',
$permission_table,
'user_id',
'NOT IN',
sprintf( $customer_lookup, $excluded_customers )
);
}
$included_ip_addresses = $this->get_included_ip_addresses( $query_args );
$excluded_ip_addresses = $this->get_excluded_ip_addresses( $query_args );
if ( $included_ip_addresses ) {
$where_filters[] = "{$lookup_table}.user_ip_address IN ('{$included_ip_addresses}')";
}
if ( $excluded_ip_addresses ) {
$where_filters[] = "{$lookup_table}.user_ip_address NOT IN ('{$excluded_ip_addresses}')";
}
$where_filters = array_filter( $where_filters );
$where_subclause = implode( " $operator ", $where_filters );
if ( $where_subclause ) {
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'where', "AND ( $where_subclause )" );
} else {
$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
}
}
if ( isset( $this->subquery ) ) {
$this->subquery->add_sql_clause( 'join', $join );
} else {
$this->interval_query->add_sql_clause( 'join', $join );
}
$this->add_order_by( $query_args );
}
/**
* Returns comma separated ids of included ip address, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_ip_addresses( $query_args ) {
if ( isset( $query_args['ip_address_includes'] ) && is_array( $query_args['ip_address_includes'] ) && count( $query_args['ip_address_includes'] ) > 0 ) {
$query_args['ip_address_includes'] = array_map( 'esc_sql', $query_args['ip_address_includes'] );
}
return self::get_filtered_ids( $query_args, 'ip_address_includes', "','" );
}
/**
* Returns comma separated ids of excluded ip address, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_excluded_ip_addresses( $query_args ) {
if ( isset( $query_args['ip_address_excludes'] ) && is_array( $query_args['ip_address_excludes'] ) && count( $query_args['ip_address_excludes'] ) > 0 ) {
$query_args['ip_address_excludes'] = array_map( 'esc_sql', $query_args['ip_address_excludes'] );
}
return self::get_filtered_ids( $query_args, 'ip_address_excludes', "','" );
}
/**
* Returns comma separated ids of included customers, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_customers( $query_args ) {
return self::get_filtered_ids( $query_args, 'customer_includes' );
}
/**
* Returns comma separated ids of excluded customers, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_excluded_customers( $query_args ) {
return self::get_filtered_ids( $query_args, 'customer_excludes' );
}
/**
* Gets WHERE time clause of SQL request with date-related constraints.
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @return string
*/
protected function add_time_period_sql_params( $query_args, $table_name ) {
$where_time = '';
if ( $query_args['before'] ) {
$datetime_str = $query_args['before']->format( TimeInterval::$sql_datetime_format );
$where_time .= " AND {$table_name}.timestamp <= '$datetime_str'";
}
if ( $query_args['after'] ) {
$datetime_str = $query_args['after']->format( TimeInterval::$sql_datetime_format );
$where_time .= " AND {$table_name}.timestamp >= '$datetime_str'";
}
return $where_time;
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
*/
protected function add_order_by( $query_args ) {
global $wpdb;
$this->clear_sql_clause( 'order_by' );
$order_by = '';
if ( isset( $query_args['orderby'] ) ) {
$order_by = $this->normalize_order_by( $query_args['orderby'] );
$this->add_sql_clause( 'order_by', $order_by );
}
if ( false !== strpos( $order_by, '_products' ) ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->posts} AS _products ON product_permissions.product_id = _products.ID" );
}
$this->add_orderby_order_clause( $query_args, $this );
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'timestamp',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$download_data = $wpdb->get_results(
$this->subquery->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $download_data ) {
return $data;
}
$download_data = array_map( array( $this, 'cast_numbers' ), $download_data );
$data = (object) array(
'data' => $download_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
global $wpdb;
if ( 'date' === $order_by ) {
return $wpdb->prefix . 'wc_download_log.timestamp';
}
if ( 'product' === $order_by ) {
return '_products.post_title';
}
return $order_by;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$table_name = self::get_db_table_name();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'from', $table_name );
$this->subquery->add_sql_clause( 'select', "{$table_name}.download_log_id" );
$this->subquery->add_sql_clause( 'group_by', "{$table_name}.download_log_id" );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* REST API Reports downloads files controller
*
* Handles requests to the /reports/downloads/files endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Files;
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports downloads files controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads/files';
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Class for parameter-based downloads report querying.
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'products' => array(1,2,3)
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Downloads\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Downloads\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for downloads report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get downloads data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_downloads_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-downloads' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_downloads_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,375 @@
<?php
/**
* REST API Reports downloads stats controller
*
* Handles requests to the /reports/downloads/stats endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports downloads stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['match'] = $request['match'];
$args['product_includes'] = (array) $request['product_includes'];
$args['product_excludes'] = (array) $request['product_excludes'];
$args['customer_includes'] = (array) $request['customer_includes'];
$args['customer_excludes'] = (array) $request['customer_excludes'];
$args['order_includes'] = (array) $request['order_includes'];
$args['order_excludes'] = (array) $request['order_excludes'];
$args['ip_address_includes'] = (array) $request['ip_address_includes'];
$args['ip_address_excludes'] = (array) $request['ip_address_excludes'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$downloads_query = new Query( $query_args );
$report_data = $downloads_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_downloads_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'download_count' => array(
'title' => __( 'Downloads', 'woocommerce-admin' ),
'description' => __( 'Number of downloads.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_orders_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'download_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['order_includes'] = array(
'description' => __( 'Limit result set to items that have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['order_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['customer_includes'] = array(
'description' => __( 'Limit response to objects that have the specified customer ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['customer_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have the specified customer ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['ip_address_includes'] = array(
'description' => __( 'Limit response to objects that have a specified ip address.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
$params['ip_address_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have a specified ip address.', 'woocommerce-admin' ),
'type' => 'array',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
return $params;
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* API\Reports\Downloads\Stats\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore as DownloadsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Downloads\Stats\DataStore.
*/
class DataStore extends DownloadsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'download_count' => 'intval',
);
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'downloads_stats';
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'downloads_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$this->report_columns = array(
'download_count' => 'COUNT(DISTINCT download_log_id) as download_count',
);
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'fields' => '*',
'interval' => 'week',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
$this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$db_records_count = count( $db_intervals );
$params = $this->get_limit_params( $query_args );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where', $this->interval_query->get_sql_clause( 'where' ) );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce-admin' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ', MAX(timestamp) AS datetime_anchor' );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce-admin' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
$this->create_interval_subtotals( $data->intervals );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'time_interval';
}
return $order_by;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Class for parameter-based downloads Reports querying
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Downloads\Stats\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Orders report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get revenue data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_downloads_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-downloads-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_downloads_stats_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,244 @@
<?php
/**
* REST API Reports Export Controller
*
* Handles requests to:
* - /reports/[report]/export
* - /reports/[report]/export/[id]/status
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Export;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\ReportExporter;
/**
* Reports Export controller.
*
* @package WooCommerce Admin/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/(?P<type>[a-z]+)/export';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'export_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_export_collection_params(),
),
'schema' => array( $this, 'get_export_public_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<export_id>[a-z0-9]+)/status',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'export_status' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_export_status_public_schema' ),
)
);
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_export_collection_params() {
$params = array();
$params['report_args'] = array(
'description' => __( 'Parameters to pass on to the exported report.', 'woocommerce-admin' ),
'type' => 'object',
'validate_callback' => 'rest_validate_request_arg', // @todo: use each controller's schema?
);
$params['email'] = array(
'description' => __( 'When true, email a link to download the export to the requesting user.', 'woocommerce-admin' ),
'type' => 'boolean',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the Report Export's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_export_public_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_export',
'type' => 'object',
'properties' => array(
'status' => array(
'description' => __( 'Regeneration status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'message' => array(
'description' => __( 'Regenerate data message.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the Export status schema, conforming to JSON Schema.
*
* @return array
*/
public function get_export_status_public_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_export_status',
'type' => 'object',
'properties' => array(
'percent_complete' => array(
'description' => __( 'Percentage complete.', 'woocommerce-admin' ),
'type' => 'int',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'download_url' => array(
'description' => __( 'Export download URL.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Export data based on user request params.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function export_items( $request ) {
$report_type = $request['type'];
$report_args = empty( $request['report_args'] ) ? array() : $request['report_args'];
$send_email = isset( $request['email'] ) ? $request['email'] : false;
$export_id = str_replace( '.', '', microtime( true ) );
$total_rows = ReportExporter::queue_report_export( $export_id, $report_type, $report_args, $send_email );
if ( 0 === $total_rows ) {
$response = rest_ensure_response(
array(
'status' => 'error',
'message' => __( 'There is no data to export for the given request.', 'woocommerce-admin' ),
)
);
} else {
$response = rest_ensure_response(
array(
'status' => 'success',
'message' => __( 'Your report file is being generated.', 'woocommerce-admin' ),
)
);
// Include a link to the export status endpoint.
$response->add_links(
array(
'status' => array(
'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
),
)
);
ReportExporter::update_export_percentage_complete( $report_type, $export_id, 0 );
}
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Export status based on user request params.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function export_status( $request ) {
$report_type = $request['type'];
$export_id = $request['export_id'];
$percentage = ReportExporter::get_export_percentage_complete( $report_type, $export_id );
if ( false === $percentage ) {
return new \WP_Error(
'woocommerce_admin_reports_export_invalid_id',
__( 'Sorry, there is no export with that ID.', 'woocommerce-admin' ),
array( 'status' => 404 )
);
}
$result = array(
'percent_complete' => $percentage,
);
// @todo - add thing in the links below instead?
if ( 100 === $percentage ) {
$query_args = array(
'action' => ReportExporter::DOWNLOAD_EXPORT_ACTION,
'filename' => "wc-{$report_type}-report-export-{$export_id}",
);
$result['download_url'] = add_query_arg( $query_args, admin_url() );
}
// Wrap the data in a response object.
$response = rest_ensure_response( $result );
// Include a link to the export status endpoint.
$response->add_links(
array(
'self' => array(
'href' => rest_url( sprintf( '%s/reports/%s/export/%s/status', $this->namespace, $report_type, $export_id ) ),
),
)
);
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Reports Exportable Controller Interface
*
* @package WooCommerce Admin/Interface
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WooCommerce Reports exportable controller interface.
*
* @since 3.5.0
*/
interface ExportableInterface {
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns();
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Value.
*/
public function prepare_item_for_export( $item );
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* REST API Reports exportable traits
*
* Collection of utility methods for exportable reports.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
/**
* ExportableTraits class.
*/
trait ExportableTraits {
/**
* Format numbers for CSV using store precision setting.
*
* @param string|float $value Numeric value.
* @return string Formatted value.
*/
public static function csv_number_format( $value ) {
$decimals = wc_get_price_decimals();
// See: @woocommerce/currency: getCurrencyFormatDecimal().
return number_format( $value, $decimals, '.', '' );
}
}

View File

@@ -0,0 +1,311 @@
<?php
/**
* REST API Reports Import Controller
*
* Handles requests to /reports/import
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Import;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\ReportsSync;
/**
* Reports Imports controller.
*
* @package WooCommerce Admin/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/import';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'import_items' ),
'permission_callback' => array( $this, 'import_permissions_check' ),
'args' => $this->get_import_collection_params(),
),
'schema' => array( $this, 'get_import_public_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/cancel',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'cancel_import' ),
'permission_callback' => array( $this, 'import_permissions_check' ),
),
'schema' => array( $this, 'get_import_public_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/delete',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'delete_imported_items' ),
'permission_callback' => array( $this, 'import_permissions_check' ),
),
'schema' => array( $this, 'get_import_public_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/status',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_import_status' ),
'permission_callback' => array( $this, 'import_permissions_check' ),
),
'schema' => array( $this, 'get_import_public_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/totals',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_import_totals' ),
'permission_callback' => array( $this, 'import_permissions_check' ),
'args' => $this->get_import_collection_params(),
),
'schema' => array( $this, 'get_import_public_schema' ),
)
);
}
/**
* Makes sure the current user has access to WRITE the settings APIs.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function import_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Import data based on user request params.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function import_items( $request ) {
$query_args = $this->prepare_objects_query( $request );
$import = ReportsSync::regenerate_report_data( $query_args['days'], $query_args['skip_existing'] );
if ( is_wp_error( $import ) ) {
$result = array(
'status' => 'error',
'message' => $import->get_error_message(),
);
} else {
$result = array(
'status' => 'success',
'message' => $import,
);
}
$response = $this->prepare_item_for_response( $result, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Prepare request object as query args.
*
* @param WP_REST_Request $request Request data.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = array();
$args['skip_existing'] = $request['skip_existing'];
$args['days'] = $request['days'];
return $args;
}
/**
* Prepare the data object for response.
*
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
/**
* Filter the list returned from the API.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original item.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_reports_import', $response, $item, $request );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_import_collection_params() {
$params = array();
$params['days'] = array(
'description' => __( 'Number of days to import.', 'woocommerce-admin' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 0,
);
$params['skip_existing'] = array(
'description' => __( 'Skip importing existing order data.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_import_public_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_import',
'type' => 'object',
'properties' => array(
'status' => array(
'description' => __( 'Regeneration status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'message' => array(
'description' => __( 'Regenerate data message.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Cancel all queued import actions.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function cancel_import( $request ) {
ReportsSync::clear_queued_actions();
$result = array(
'status' => 'success',
'message' => __( 'All pending and in-progress import actions have been cancelled.', 'woocommerce-admin' ),
);
$response = $this->prepare_item_for_response( $result, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Delete all imported items.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function delete_imported_items( $request ) {
$delete = ReportsSync::delete_report_data();
if ( is_wp_error( $delete ) ) {
$result = array(
'status' => 'error',
'message' => $delete->get_error_message(),
);
} else {
$result = array(
'status' => 'success',
'message' => $delete,
);
}
$response = $this->prepare_item_for_response( $result, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Get the status of the current import.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_import_status( $request ) {
$result = ReportsSync::get_import_stats();
$response = $this->prepare_item_for_response( $result, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
/**
* Get the total orders and customers based on user supplied params.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_import_totals( $request ) {
$query_args = $this->prepare_objects_query( $request );
$totals = ReportsSync::get_import_totals( $query_args['days'], $query_args['skip_existing'] );
$response = $this->prepare_item_for_response( $totals, $request );
$data = $this->prepare_response_for_collection( $response );
return rest_ensure_response( $data );
}
}

View File

@@ -0,0 +1,506 @@
<?php
/**
* REST API Reports orders controller
*
* Handles requests to the /reports/orders endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports orders controller class.
*
* @package WooCommerce/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends ReportsController implements ExportableInterface {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/orders';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['before'] = $request['before'];
$args['after'] = $request['after'];
$args['page'] = $request['page'];
$args['per_page'] = $request['per_page'];
$args['orderby'] = $request['orderby'];
$args['order'] = $request['order'];
$args['product_includes'] = (array) $request['product_includes'];
$args['product_excludes'] = (array) $request['product_excludes'];
$args['coupon_includes'] = (array) $request['coupon_includes'];
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
$args['status_is'] = (array) $request['status_is'];
$args['status_is_not'] = (array) $request['status_is_not'];
$args['customer_type'] = $request['customer_type'];
$args['extended_info'] = $request['extended_info'];
$args['refunds'] = $request['refunds'];
$args['match'] = $request['match'];
$args['order_includes'] = $request['order_includes'];
$args['order_excludes'] = $request['order_excludes'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$orders_query = new Query( $query_args );
$report_data = $orders_query->get_data();
$data = array();
foreach ( $report_data->data as $orders_data ) {
$orders_data['order_number'] = $this->get_order_number( $orders_data['order_id'] );
$item = $this->prepare_item_for_response( $orders_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_orders', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'order' => array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object['order_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_orders',
'type' => 'object',
'properties' => array(
'order_id' => array(
'description' => __( 'Order ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order_number' => array(
'description' => __( 'Order Number.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "Date the order was created, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'Date the order was created, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Order status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_id' => array(
'description' => __( 'Customer ID.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_items_sold' => array(
'description' => __( 'Number of items sold.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'net_total' => array(
'description' => __( 'Net total revenue.', 'woocommerce-admin' ),
'type' => 'float',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_type' => array(
'description' => __( 'Returning or new customer.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'extended_info' => array(
'products' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'List of order product IDs, names, quantities.', 'woocommerce-admin' ),
),
'coupons' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'List of order coupons.', 'woocommerce-admin' ),
),
'customer' => array(
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order customer information.', 'woocommerce-admin' ),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 0,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'num_items_sold',
'net_total',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['coupon_includes'] = array(
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupon_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['tax_rate_includes'] = array(
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tax_rate_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['status_is'] = array(
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['status_is_not'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['customer_type'] = array(
'description' => __( 'Limit result set to returning or new customers.', 'woocommerce-admin' ),
'type' => 'string',
'default' => '',
'enum' => array(
'',
'returning',
'new',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce-admin' ),
'type' => 'string',
'default' => '',
'enum' => array(
'',
'all',
'partial',
'full',
'none',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order_includes'] = array(
'description' => __( 'Limit result set to items that have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['order_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
/**
* Get products column export value.
*
* @param array $products Products from report row.
* @return string
*/
protected function _get_products( $products ) {
$products_list = array();
foreach ( $products as $product ) {
$products_list[] = sprintf(
/* translators: 1: numeric product quantity, 2: name of product */
__( '%1$s× %2$s', 'woocommerce-admin' ),
$product['quantity'],
$product['name']
);
}
return implode( ', ', $products_list );
}
/**
* Get coupons column export value.
*
* @param array $coupons Coupons from report row.
* @return string
*/
protected function _get_coupons( $coupons ) {
return implode( ', ', wp_list_pluck( $coupons, 'code' ) );
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
return array(
'date_created' => __( 'Date', 'woocommerce-admin' ),
'order_number' => __( 'Order #', 'woocommerce-admin' ),
'status' => __( 'Status', 'woocommerce-admin' ),
'customer_type' => __( 'Customer', 'woocommerce-admin' ),
'products' => __( 'Product(s)', 'woocommerce-admin' ),
'num_items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'coupons' => __( 'Coupon(s)', 'woocommerce-admin' ),
'net_total' => __( 'N. Revenue', 'woocommerce-admin' ),
);
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
return array(
'date_created' => $item['date_created'],
'order_number' => $item['order_number'],
'status' => $item['status'],
'customer_type' => $item['customer_type'],
'products' => isset( $item['extended_info']['products'] ) ? $this->_get_products( $item['extended_info']['products'] ) : null,
'num_items_sold' => $item['num_items_sold'],
'coupons' => isset( $item['extended_info']['coupons'] ) ? $this->_get_coupons( $item['extended_info']['coupons'] ) : null,
'net_total' => $item['net_total'],
);
}
}

View File

@@ -0,0 +1,444 @@
<?php
/**
* API\Reports\Orders\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Orders\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_stats';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'orders';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'order_id' => 'intval',
'parent_id' => 'intval',
'date_created' => 'strval',
'date_created_gmt' => 'strval',
'status' => 'strval',
'customer_id' => 'intval',
'net_total' => 'floatval',
'total_sales' => 'floatval',
'num_items_sold' => 'intval',
'customer_type' => 'strval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'orders';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
// Avoid ambigious columns in SQL query.
$this->report_columns = array(
'order_id' => "{$table_name}.order_id",
'parent_id' => "{$table_name}.parent_id",
'date_created' => "{$table_name}.date_created",
'date_created_gmt' => "{$table_name}.date_created_gmt",
'status' => "REPLACE({$table_name}.status, 'wc-', '') as status",
'customer_id' => "{$table_name}.customer_id",
'net_total' => "{$table_name}.net_total",
'total_sales' => "{$table_name}.total_sales",
'num_items_sold' => "{$table_name}.num_items_sold",
'customer_type' => "(CASE WHEN {$table_name}.returning_customer = 1 THEN 'returning' WHEN {$table_name}.returning_customer = 0 THEN 'new' ELSE '' END) as customer_type",
);
}
/**
* Updates the database query with parameters used for orders report: coupons and products filters.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$order_stats_lookup_table = self::get_db_table_name();
$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
$order_tax_lookup_table = $wpdb->prefix . 'wc_order_tax_lookup';
$operator = $this->get_match_operator( $query_args );
$where_subquery = array();
$this->add_time_period_sql_params( $query_args, $order_stats_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->add_order_by_sql_params( $query_args );
$status_subquery = $this->get_status_subquery( $query_args );
if ( $status_subquery ) {
if ( empty( $query_args['status_is'] ) && empty( $query_args['status_is_not'] ) ) {
$this->subquery->add_sql_clause( 'where', "AND {$status_subquery}" );
} else {
$where_subquery[] = $status_subquery;
}
}
$included_orders = $this->get_included_orders( $query_args );
if ( $included_orders ) {
$where_subquery[] = "{$order_stats_lookup_table}.order_id IN ({$included_orders})";
}
$excluded_orders = $this->get_excluded_orders( $query_args );
if ( $excluded_orders ) {
$where_subquery[] = "{$order_stats_lookup_table}.order_id NOT IN ({$excluded_orders})";
}
if ( $query_args['customer_type'] ) {
$returning_customer = 'returning' === $query_args['customer_type'] ? 1 : 0;
$where_subquery[] = "{$order_stats_lookup_table}.returning_customer = ${returning_customer}";
}
$refund_subquery = $this->get_refund_subquery( $query_args );
$this->subquery->add_sql_clause( 'from', $refund_subquery['from_clause'] );
if ( $refund_subquery['where_clause'] ) {
$where_subquery[] = $refund_subquery['where_clause'];
}
$included_coupons = $this->get_included_coupons( $query_args );
$excluded_coupons = $this->get_excluded_coupons( $query_args );
if ( $included_coupons || $excluded_coupons ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$order_coupon_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_coupon_lookup_table}.order_id" );
}
if ( $included_coupons ) {
$where_subquery[] = "{$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})";
}
if ( $excluded_coupons ) {
$where_subquery[] = "{$order_coupon_lookup_table}.coupon_id NOT IN ({$excluded_coupons})";
}
$included_products = $this->get_included_products( $query_args );
$excluded_products = $this->get_excluded_products( $query_args );
if ( $included_products || $excluded_products ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
}
if ( $included_products ) {
$where_subquery[] = "{$order_product_lookup_table}.product_id IN ({$included_products})";
}
if ( $excluded_products ) {
$where_subquery[] = "{$order_product_lookup_table}.product_id NOT IN ({$excluded_products})";
}
$included_tax_rates = ! empty( $query_args['tax_rate_includes'] ) ? implode( ',', $query_args['tax_rate_includes'] ) : false;
$excluded_tax_rates = ! empty( $query_args['tax_rate_excludes'] ) ? implode( ',', $query_args['tax_rate_excludes'] ) : false;
if ( $included_tax_rates || $excluded_tax_rates ) {
$this->subquery->add_sql_clause( 'join', "LEFT JOIN {$order_tax_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_tax_lookup_table}.order_id" );
}
if ( $included_tax_rates ) {
$where_subquery[] = "{$order_tax_lookup_table}.tax_rate_id IN ({$included_tax_rates})";
}
if ( $excluded_tax_rates ) {
$where_subquery[] = "{$order_tax_lookup_table}.tax_rate_id NOT IN ({$excluded_tax_rates}) OR {$order_tax_lookup_table}.tax_rate_id IS NULL";
}
if ( 0 < count( $where_subquery ) ) {
$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_created',
'before' => '',
'after' => '',
'fields' => '*',
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer_type' => null,
'status_is' => array(),
'extended_info' => false,
'refunds' => null,
'order_includes' => array(),
'order_excludes' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( 0 === $params['per_page'] ) {
$total_pages = 0;
} else {
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
}
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
$data = (object) array(
'data' => array(),
'total' => $db_records_count,
'pages' => 0,
'page_no' => 0,
);
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$orders_data = $wpdb->get_results(
$this->subquery->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $orders_data ) {
return $data;
}
if ( $query_args['extended_info'] ) {
$this->include_extended_info( $orders_data, $query_args );
}
$orders_data = array_map( array( $this, 'cast_numbers' ), $orders_data );
$data = (object) array(
'data' => $orders_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'date_created';
}
return $order_by;
}
/**
* Enriches the order data.
*
* @param array $orders_data Orders data.
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$orders_data, $query_args ) {
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$products = $this->get_products_by_order_ids( array_keys( $mapped_orders ) );
$mapped_products = $this->map_array_by_key( $products, 'product_id' );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );
$mapped_data = array();
foreach ( $products as $product ) {
if ( ! isset( $mapped_data[ $product['order_id'] ] ) ) {
$mapped_data[ $product['order_id'] ]['products'] = array();
}
$mapped_data[ $product['order_id'] ]['products'][] = array(
'id' => $product['product_id'],
'name' => $product['product_name'],
'quantity' => $product['product_quantity'],
);
}
foreach ( $coupons as $coupon ) {
if ( ! isset( $mapped_data[ $coupon['order_id'] ] ) ) {
$mapped_data[ $product['order_id'] ]['coupons'] = array();
}
$mapped_data[ $coupon['order_id'] ]['coupons'][] = array(
'id' => $coupon['coupon_id'],
'code' => wc_format_coupon_code( $coupon['coupon_code'] ),
);
}
foreach ( $orders_data as $key => $order_data ) {
$defaults = array(
'products' => array(),
'coupons' => array(),
'customer' => array(),
);
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
if ( $order_data['customer_id'] && isset( $mapped_customers[ $order_data['customer_id'] ] ) ) {
$orders_data[ $key ]['extended_info']['customer'] = $mapped_customers[ $order_data['customer_id'] ];
}
}
}
/**
* Returns the same array index by a given key
*
* @param array $array Array to be looped over.
* @param string $key Key of values used for new array.
* @return array
*/
protected function map_array_by_key( $array, $key ) {
$mapped = array();
foreach ( $array as $item ) {
$mapped[ $item[ $key ] ] = $item;
}
return $mapped;
}
/**
* Get product IDs, names, and quantity from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
*/
protected function get_products_by_order_ids( $order_ids ) {
global $wpdb;
$order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup';
$included_order_ids = implode( ',', $order_ids );
$products = $wpdb->get_results(
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
FROM {$wpdb->posts}
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->posts}.ID
WHERE
order_id IN ({$included_order_ids})
",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
return $products;
}
/**
* Get customer data from Order data.
*
* @param array $orders Array of orders data.
* @return array
*/
protected function get_customers_by_orders( $orders ) {
global $wpdb;
$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
$customer_ids = array();
foreach ( $orders as $order ) {
if ( $order['customer_id'] ) {
$customer_ids[] = intval( $order['customer_id'] );
}
}
if ( empty( $customer_ids ) ) {
return array();
}
$customer_ids = implode( ',', $customer_ids );
$customers = $wpdb->get_results(
"SELECT * FROM {$customer_lookup_table} WHERE customer_id IN ({$customer_ids})",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
return $customers;
}
/**
* Get coupon information from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
*/
protected function get_coupons_by_order_ids( $order_ids ) {
global $wpdb;
$order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup';
$included_order_ids = implode( ',', $order_ids );
$coupons = $wpdb->get_results(
"SELECT order_id, coupon_id, post_title as coupon_code
FROM {$wpdb->posts}
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->posts}.ID
WHERE
order_id IN ({$included_order_ids})
",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
return $coupons;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', self::get_db_table_name() . '.order_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Class for parameter-based Orders Reports querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'interval' => 'week',
* 'products' => array(15, 18),
* 'coupons' => array(138),
* 'status_is' => array('completed'),
* 'status_is_not' => array('failed'),
* 'new_customers' => false,
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Orders\Query
*/
class Query extends ReportsQuery {
/**
* Get order data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_orders_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-orders' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_orders_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,519 @@
<?php
/**
* REST API Reports orders stats controller
*
* Handles requests to the /reports/orders/stats endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
* REST API Reports orders stats controller class.
*
* @package WooCommerce/API
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
*/
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/orders/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['match'] = $request['match'];
$args['status_is'] = (array) $request['status_is'];
$args['status_is_not'] = (array) $request['status_is_not'];
$args['product_includes'] = (array) $request['product_includes'];
$args['product_excludes'] = (array) $request['product_excludes'];
$args['coupon_includes'] = (array) $request['coupon_includes'];
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
$args['customer'] = $request['customer'];
$args['refunds'] = $request['refunds'];
$args['categories'] = (array) $request['categories'];
$args['segmentby'] = $request['segmentby'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$orders_query = new Query( $query_args );
try {
$report_data = $orders_query->get_data();
} catch ( ParameterException $e ) {
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_orders_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$data_values = array(
'net_revenue' => array(
'description' => __( 'Net Sales.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
'orders_count' => array(
'title' => __( 'Orders', 'woocommerce-admin' ),
'description' => __( 'Number of orders', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
),
'avg_order_value' => array(
'description' => __( 'Average order value.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
'format' => 'currency',
),
'avg_items_per_order' => array(
'description' => __( 'Average items per order', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_items_sold' => array(
'description' => __( 'Number of items sold', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'coupons' => array(
'description' => __( 'Amount discounted by coupons.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'coupons_count' => array(
'description' => __( 'Unique coupons count.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_returning_customers' => array(
'description' => __( 'Number of orders done by returning customers', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'num_new_customers' => array(
'description' => __( 'Number of orders done by new customers', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'products' => array(
'description' => __( 'Number of distinct products sold.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'woocommerce-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce-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',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $intervals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'net_revenue',
'orders_count',
'avg_order_value',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['status_is'] = array(
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'default' => null,
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['status_is_not'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['coupon_includes'] = array(
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['coupon_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['tax_rate_includes'] = array(
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tax_rate_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['customer'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
'type' => 'string',
'enum' => array(
'new',
'returning',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce-admin' ),
'type' => 'string',
'default' => '',
'enum' => array(
'',
'all',
'partial',
'full',
'none',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce-admin' ),
'type' => 'string',
'enum' => array(
'product',
'category',
'variation',
'coupon',
'customer_type', // new vs returning.
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@@ -0,0 +1,656 @@
<?php
/**
* API\Reports\Orders\Stats\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
* API\Reports\Orders\Stats\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_stats';
/**
* Cron event name.
*/
const CRON_EVENT = 'wc_order_stats_update';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'orders_stats';
/**
* Type for each column to cast values correctly later.
*
* @var array
*/
protected $column_types = array(
'orders_count' => 'intval',
'num_items_sold' => 'intval',
'gross_sales' => 'floatval',
'total_sales' => 'floatval',
'coupons' => 'floatval',
'coupons_count' => 'intval',
'refunds' => 'floatval',
'taxes' => 'floatval',
'shipping' => 'floatval',
'net_revenue' => 'floatval',
'avg_items_per_order' => 'floatval',
'avg_order_value' => 'floatval',
'num_returning_customers' => 'intval',
'num_new_customers' => 'intval',
'products' => 'intval',
'segment_id' => 'intval',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'orders_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
// Avoid ambigious columns in SQL query.
$refunds = "ABS( SUM( CASE WHEN {$table_name}.net_total < 0 THEN {$table_name}.net_total ELSE 0 END ) )";
$gross_sales =
"( SUM({$table_name}.total_sales)" .
' + COALESCE( SUM(discount_amount), 0 )' . // SUM() all nulls gives null.
" - SUM({$table_name}.tax_total)" .
" - SUM({$table_name}.shipping_total)" .
" + {$refunds}" .
' ) as gross_sales';
$this->report_columns = array(
'orders_count' => "SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) as orders_count",
'num_items_sold' => "SUM({$table_name}.num_items_sold) as num_items_sold",
'gross_sales' => $gross_sales,
'total_sales' => "SUM({$table_name}.total_sales) AS total_sales",
'coupons' => 'COALESCE( SUM(discount_amount), 0 ) AS coupons', // SUM() all nulls gives null.
'coupons_count' => 'COALESCE( coupons_count, 0 ) as coupons_count',
'refunds' => "{$refunds} AS refunds",
'taxes' => "SUM({$table_name}.tax_total) AS taxes",
'shipping' => "SUM({$table_name}.shipping_total) AS shipping",
'net_revenue' => "SUM({$table_name}.net_total) AS net_revenue",
'avg_items_per_order' => "SUM( {$table_name}.num_items_sold ) / SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) AS avg_items_per_order",
'avg_order_value' => "SUM( {$table_name}.net_total ) / SUM( CASE WHEN {$table_name}.parent_id = 0 THEN 1 ELSE 0 END ) AS avg_order_value",
// Count returning customers as ( total_customers - new_customers ) to get an accurate number and count customers in with both new and old statuses as new.
'num_returning_customers' => "( COUNT( DISTINCT( {$table_name}.customer_id ) ) - COUNT( DISTINCT( CASE WHEN {$table_name}.returning_customer = 0 THEN {$table_name}.customer_id END ) ) ) AS num_returning_customers",
'num_new_customers' => "COUNT( DISTINCT( CASE WHEN {$table_name}.returning_customer = 0 THEN {$table_name}.customer_id END ) ) AS num_new_customers",
);
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'delete_post', array( __CLASS__, 'delete_order' ) );
}
/**
* Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function orders_stats_sql_filter( $query_args ) {
// @todo Performance of all of this?
global $wpdb;
$from_clause = '';
$orders_stats_table = self::get_db_table_name();
$product_lookup = $wpdb->prefix . 'wc_order_product_lookup';
$coupon_lookup = $wpdb->prefix . 'wc_order_coupon_lookup';
$tax_rate_lookup = $wpdb->prefix . 'wc_order_tax_lookup';
$operator = $this->get_match_operator( $query_args );
$where_filters = array();
// Products filters.
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$product_lookup,
'product_id',
'IN',
$this->get_included_products( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$product_lookup,
'product_id',
'NOT IN',
$this->get_excluded_products( $query_args )
);
// Coupons filters.
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$coupon_lookup,
'coupon_id',
'IN',
$this->get_included_coupons( $query_args )
);
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$coupon_lookup,
'coupon_id',
'NOT IN',
$this->get_excluded_coupons( $query_args )
);
// Tax rate filters.
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$tax_rate_lookup,
'tax_rate_id',
'IN',
implode( ',', $query_args['tax_rate_includes'] )
);
$where_filters[] = $this->get_object_where_filter(
$orders_stats_table,
'order_id',
$tax_rate_lookup,
'tax_rate_id',
'NOT IN',
implode( ',', $query_args['tax_rate_excludes'] )
);
$where_filters[] = $this->get_customer_subquery( $query_args );
$refund_subquery = $this->get_refund_subquery( $query_args );
$from_clause .= $refund_subquery['from_clause'];
if ( $refund_subquery['where_clause'] ) {
$where_filters[] = $refund_subquery['where_clause'];
}
$where_filters = array_filter( $where_filters );
$where_subclause = implode( " $operator ", $where_filters );
// Append status filter after to avoid matching ANY on default statuses.
$order_status_filter = $this->get_status_subquery( $query_args, $operator );
if ( $order_status_filter ) {
if ( empty( $query_args['status_is'] ) && empty( $query_args['status_is_not'] ) ) {
$operator = 'AND';
}
$where_subclause = implode( " $operator ", array_filter( array( $where_subclause, $order_status_filter ) ) );
}
// To avoid requesting the subqueries twice, the result is applied to all queries passed to the method.
if ( $where_subclause ) {
$this->total_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
$this->total_query->add_sql_clause( 'join', $from_clause );
$this->interval_query->add_sql_clause( 'where', "AND ( $where_subclause )" );
$this->interval_query->add_sql_clause( 'join', $from_clause );
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'interval' => 'week',
'fields' => '*',
'segmentby' => '',
'match' => 'all',
'status_is' => array(),
'status_is_not' => array(),
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer' => '',
'categories' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'totals' => (object) array(),
'intervals' => (object) array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->add_order_by_sql_params( $query_args );
$where_time = $this->get_sql_clause( 'where_time' );
$params = $this->get_limit_sql_params( $query_args );
$coupon_join = "LEFT JOIN (
SELECT
order_id,
SUM(discount_amount) AS discount_amount,
COUNT(DISTINCT coupon_id) AS coupons_count
FROM
{$wpdb->prefix}wc_order_coupon_lookup
GROUP BY
order_id
) order_coupon_lookup
ON order_coupon_lookup.order_id = {$wpdb->prefix}wc_order_stats.order_id";
// Additional filtering for Orders report.
$this->orders_stats_sql_filter( $query_args );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'left_join', $coupon_join );
$this->total_query->add_sql_clause( 'where_time', $where_time );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
}
// @todo Remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $where_time,
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $where_time,
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$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;
$segmenter = new Segmenter( $query_args, $this->report_columns );
$unique_coupons = $this->get_unique_coupon_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['coupons_count'] = $unique_coupons;
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->add_sql_clause( 'left_join', $coupon_join );
$this->interval_query->add_sql_clause( 'where_time', $where_time );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // phpcs:ignore cache ok, DB call ok, , unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
}
if ( isset( $intervals[0] ) ) {
$unique_coupons = $this->get_unique_coupon_count( $intervals_query['from_clause'], $intervals_query['where_time_clause'], $intervals_query['where_clause'], true );
$intervals[0]['coupons_count'] = $unique_coupons;
}
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} 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 );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Get unique products based on user time query
*
* @param string $from_clause From clause with date query.
* @param string $where_time_clause Where clause with date query.
* @param string $where_clause Where clause with date query.
* @return integer Unique product count.
*/
public function get_unique_product_count( $from_clause, $where_time_clause, $where_clause ) {
global $wpdb;
$table_name = self::get_db_table_name();
return $wpdb->get_var(
"SELECT
COUNT( DISTINCT {$wpdb->prefix}wc_order_product_lookup.product_id )
FROM
{$wpdb->prefix}wc_order_product_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_product_lookup.order_id = {$table_name}.order_id
{$from_clause}
WHERE
1=1
{$where_time_clause}
{$where_clause}"
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
}
/**
* Get unique coupons based on user time query
*
* @param string $from_clause From clause with date query.
* @param string $where_time_clause Where clause with date query.
* @param string $where_clause Where clause with date query.
* @return integer Unique product count.
*/
public function get_unique_coupon_count( $from_clause, $where_time_clause, $where_clause ) {
global $wpdb;
$table_name = self::get_db_table_name();
return $wpdb->get_var(
"SELECT
COUNT(DISTINCT coupon_id)
FROM
{$wpdb->prefix}wc_order_coupon_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_coupon_lookup.order_id = {$table_name}.order_id
{$from_clause}
WHERE
1=1
{$where_time_clause}
{$where_clause}"
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
}
/**
* 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 ) && 'shop_order_refund' !== get_post_type( $post_id ) ) {
return -1;
}
$order = wc_get_order( $post_id );
if ( ! $order ) {
return -1;
}
return self::update( $order );
}
/**
* Update the database with stats data.
*
* @param WC_Order|WC_Order_Refund $order Order or refund to update row for.
* @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 = self::get_db_table_name();
if ( ! $order->get_id() || ! $order->get_date_created() ) {
return -1;
}
$data = array(
'order_id' => $order->get_id(),
'parent_id' => $order->get_parent_id(),
'date_created' => $order->get_date_created()->date( 'Y-m-d H:i:s' ),
'date_created_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
'num_items_sold' => self::get_num_items_sold( $order ),
'total_sales' => $order->get_total(),
'tax_total' => $order->get_total_tax(),
'shipping_total' => $order->get_shipping_total(),
'net_total' => self::get_net_total( $order ),
'status' => self::normalize_order_status( $order->get_status() ),
'customer_id' => $order->get_report_customer_id(),
'returning_customer' => $order->is_returning_customer(),
);
$format = array(
'%d',
'%d',
'%s',
'%s',
'%d',
'%f',
'%f',
'%f',
'%f',
'%s',
'%d',
'%d',
);
if ( 'shop_order_refund' === $order->get_type() ) {
$parent_order = wc_get_order( $order->get_parent_id() );
if ( $parent_order ) {
$data['parent_id'] = $parent_order->get_id();
$format[] = '%d';
}
} else {
$data['returning_customer'] = self::is_returning_customer( $order );
}
// Update or add the information to the DB.
$result = $wpdb->replace( $table_name, $data, $format );
/**
* Fires when order's stats reports are updated.
*
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_update_order_stats', $order->get_id() );
// Check the rows affected for success. Using REPLACE can affect 2 rows if the row already exists.
return ( 1 === $result || 2 === $result );
}
/**
* Deletes the order stats when an order is deleted.
*
* @param int $post_id Post ID.
*/
public static function delete_order( $post_id ) {
global $wpdb;
$order_id = (int) $post_id;
if ( 'shop_order' !== get_post_type( $order_id ) && 'shop_order_refund' !== get_post_type( $order_id ) ) {
return;
}
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when orders stats are deleted.
*
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_delete_order_stats', $order_id );
ReportsCache::invalidate();
}
/**
* Calculation methods.
*/
/**
* Get number of items sold among all orders.
*
* @param array $order WC_Order object.
* @return int
*/
protected static function get_num_items_sold( $order ) {
$num_items = 0;
$line_items = $order->get_items( 'line_item' );
foreach ( $line_items as $line_item ) {
$num_items += $line_item->get_quantity();
}
return $num_items;
}
/**
* Get the net amount from an order without shipping, tax, or refunds.
*
* @param array $order WC_Order object.
* @return float
*/
protected static function get_net_total( $order ) {
$net_total = floatval( $order->get_total() ) - floatval( $order->get_total_tax() ) - floatval( $order->get_shipping_total() );
return (float) $net_total;
}
/**
* Check to see if an order's customer has made previous orders or not
*
* @param array $order WC_Order object.
* @return bool
*/
public static function is_returning_customer( $order ) {
$customer_id = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_existing_customer_id_from_order( $order );
if ( ! $customer_id ) {
return false;
}
$oldest_orders = \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_oldest_orders( $customer_id );
if ( empty( $oldest_orders ) ) {
return false;
}
$first_order = $oldest_orders[0];
$second_order = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
$excluded_statuses = self::get_excluded_report_order_statuses();
// Order is older than previous first order.
if ( $order->get_date_created() < wc_string_to_datetime( $first_order->date_created ) &&
! in_array( $order->get_status(), $excluded_statuses, true )
) {
self::set_customer_first_order( $customer_id, $order->get_id() );
return false;
}
// The current order is the oldest known order.
$is_first_order = (int) $order->get_id() === (int) $first_order->order_id;
// Order date has changed and next oldest is now the first order.
$date_change = $second_order &&
$order->get_date_created() > wc_string_to_datetime( $first_order->date_created ) &&
wc_string_to_datetime( $second_order->date_created ) < $order->get_date_created();
// Status has changed to an excluded status and next oldest order is now the first order.
$status_change = $second_order &&
in_array( $order->get_status(), $excluded_statuses, true );
if ( $is_first_order && ( $date_change || $status_change ) ) {
self::set_customer_first_order( $customer_id, $second_order->order_id );
return true;
}
return (int) $order->get_id() !== (int) $first_order->order_id;
}
/**
* Set a customer's first order and all others to returning.
*
* @param int $customer_id Customer ID.
* @param int $order_id Order ID.
*/
protected static function set_customer_first_order( $customer_id, $order_id ) {
global $wpdb;
$orders_stats_table = self::get_db_table_name();
$wpdb->query(
$wpdb->prepare(
"UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
$order_id,
$customer_id
)
);
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Class for parameter-based Order Stats Reports querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'interval' => 'week',
* 'categories' => array(15, 18),
* 'coupons' => array(138),
* 'status_in' => array('completed'),
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Orders\Stats\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Orders report.
*
* @return array
*/
protected function get_default_query_vars() {
return array(
'fields' => array(
'net_revenue',
'avg_order_value',
'orders_count',
'avg_items_per_order',
'num_items_sold',
'coupons',
'coupons_count',
'num_returning_customers',
'num_new_customers',
),
);
}
/**
* Get revenue data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_orders_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-orders-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_orders_stats_select_query', $results, $args );
}
}

View File

@@ -0,0 +1,439 @@
<?php
/**
* Class for adding segmenting support without cluttering the data stores.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class Segmenter extends ReportsSegmenter {
/**
* Returns column => query mapping 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 array Column => SELECT query mapping.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'num_items_sold' => "SUM($products_table.product_qty) as num_items_sold",
'total_sales' => "SUM($products_table.product_gross_revenue) AS total_sales",
'coupons' => 'SUM( coupon_lookup_left_join.discount_amount ) AS coupons',
'coupons_count' => 'COUNT( DISTINCT( coupon_lookup_left_join.coupon_id ) ) AS coupons_count',
'refunds' => "SUM( CASE WHEN $products_table.product_gross_revenue < 0 THEN $products_table.product_gross_revenue ELSE 0 END ) AS refunds",
'taxes' => "SUM($products_table.tax_amount) AS taxes",
'shipping' => "SUM($products_table.shipping_amount) AS shipping",
'net_revenue' => "SUM($products_table.product_net_revenue) AS net_revenue",
);
return $columns_mapping;
}
/**
* Returns column => query mapping 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 array Column => SELECT query mapping.
*/
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) / 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 $columns_mapping;
}
/**
* Returns column => query mapping to be used for order-level segmenting query
* (e.g. avg items per order or Net Sales 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 array Column => SELECT query mapping.
*/
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",
'total_sales' => "SUM($order_stats_table.total_sales) AS total_sales",
'coupons' => "SUM($order_stats_table.discount_amount) AS coupons",
'coupons_count' => 'COUNT( DISTINCT(coupon_lookup_left_join.coupon_id) ) AS coupons_count',
'refunds' => "SUM( CASE WHEN $order_stats_table.parent_id != 0 THEN $order_stats_table.total_sales END ) 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) 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) / 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 $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( 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( 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 \Automattic\WooCommerce\Admin\API\Reports\ParameterException 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_from = "LEFT JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup_left_join ON ($table_name.order_id = coupon_lookup_left_join.order_id) ";
$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?
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $unique_orders_table );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$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 ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce-admin' ) );
}
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $unique_orders_table );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$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'] ) {
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$order_level_columns = $this->get_segment_selections_order_level( $unique_orders_table );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
'order_level' => $this->prepare_selections( $order_level_columns ),
);
$this->report_columns = array_merge( $product_level_columns, $order_level_columns );
$segmenting_from .= "
INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)
LEFT JOIN {$wpdb->term_relationships} ON {$product_segmenting_table}.product_id = {$wpdb->term_relationships}.object_id
LEFT JOIN {$wpdb->wc_category_lookup} ON {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->wc_category_lookup}.category_id
";
$segmenting_where = " AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL";
$segmenting_groupby = "{$wpdb->wc_category_lookup}.category_tree_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',
);
$coupon_level_columns = $this->segment_selections_orders( $table_name, $coupon_override );
$segmenting_selections = $this->prepare_selections( $coupon_level_columns );
$this->report_columns = $coupon_level_columns;
$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'] ) {
$customer_level_columns = $this->segment_selections_orders( $table_name );
$segmenting_selections = $this->prepare_selections( $customer_level_columns );
$this->report_columns = $customer_level_columns;
$segmenting_groupby = "$table_name.returning_customer";
$segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params );
}
return $segments;
}
}

View File

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

View File

@@ -0,0 +1,525 @@
<?php
/**
* REST API Performance indicators controller
*
* Handles requests to the /reports/store-performance endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators;
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Performance indicators controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/performance-indicators';
/**
* Contains a list of endpoints by report slug.
*
* @var array
*/
protected $endpoints = array();
/**
* Contains a list of allowed stats.
*
* @var array
*/
protected $allowed_stats = array();
/**
* Contains a list of stat labels.
*
* @var array
*/
protected $labels = array();
/**
* Contains a list of endpoints by url.
*
* @var array
*/
protected $urls = array();
/**
* Register the routes for reports.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/allowed',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_allowed_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_allowed_item_schema' ),
)
);
}
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['before'] = $request['before'];
$args['after'] = $request['after'];
$args['stats'] = $request['stats'];
return $args;
}
/**
* Get information such as allowed stats, stat labels, and endpoint data from stats reports.
*
* @return WP_Error|True
*/
private function get_indicator_data() {
// Data already retrieved.
if ( ! empty( $this->endpoints ) && ! empty( $this->labels ) && ! empty( $this->allowed_stats ) ) {
return true;
}
$request = new \WP_REST_Request( 'GET', '/wc-analytics/reports' );
$response = rest_do_request( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( 200 !== $response->get_status() ) {
return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce-admin' ) );
}
$endpoints = $response->get_data();
$allowed_stats = array();
foreach ( $endpoints as $endpoint ) {
if ( '/stats' === substr( $endpoint['slug'], -6 ) ) {
$request = new \WP_REST_Request( 'OPTIONS', $endpoint['path'] );
$response = rest_do_request( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$data = $response->get_data();
$prefix = substr( $endpoint['slug'], 0, -6 );
if ( empty( $data['schema']['properties']['totals']['properties'] ) ) {
continue;
}
foreach ( $data['schema']['properties']['totals']['properties'] as $property_key => $schema_info ) {
if ( empty( $schema_info['indicator'] ) || ! $schema_info['indicator'] ) {
continue;
}
$stat = $prefix . '/' . $property_key;
$allowed_stats[] = $stat;
$stat_label = empty( $schema_info['title'] ) ? $schema_info['description'] : $schema_info['title'];
$this->labels[ $stat ] = trim( preg_replace( '/\W+/', ' ', $stat_label ) );
$this->formats[ $stat ] = isset( $schema_info['format'] ) ? $schema_info['format'] : 'number';
}
$this->endpoints[ $prefix ] = $endpoint['path'];
$this->urls[ $prefix ] = $endpoint['_links']['report'][0]['href'];
}
}
$this->allowed_stats = $allowed_stats;
return true;
}
/**
* Returns a list of allowed performance indicators.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_allowed_items( $request ) {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
return $indicator_data;
}
$data = array();
foreach ( $this->allowed_stats as $stat ) {
$pieces = $this->get_stats_parts( $stat );
$report = $pieces[0];
$chart = $pieces[1];
$data[] = (object) array(
'stat' => $stat,
'chart' => $chart,
'label' => $this->labels[ $stat ],
);
}
usort( $data, array( $this, 'sort' ) );
$objects = array();
foreach ( $data as $item ) {
$prepared = $this->prepare_item_for_response( $item, $request );
$objects[] = $this->prepare_response_for_collection( $prepared );
}
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', count( $data ) );
$response->header( 'X-WP-TotalPages', 1 );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
return $response;
}
/**
* Sorts the list of stats. Sorted by custom arrangement.
*
* @see https://github.com/woocommerce/woocommerce-admin/issues/1282
* @param object $a First item.
* @param object $b Second item.
* @return order
*/
public function sort( $a, $b ) {
/**
* Custom ordering for store performance indicators.
*
* @see https://github.com/woocommerce/woocommerce-admin/issues/1282
* @param array $indicators A list of ordered indicators.
*/
$stat_order = apply_filters(
'woocommerce_rest_report_sort_performance_indicators',
array(
'revenue/total_sales',
'revenue/net_revenue',
'orders/orders_count',
'orders/avg_order_value',
'products/items_sold',
'revenue/refunds',
'coupons/orders_count',
'coupons/amount',
'taxes/total_tax',
'taxes/order_tax',
'taxes/shipping_tax',
'revenue/shipping',
'downloads/download_count',
)
);
$a = array_search( $a->stat, $stat_order );
$b = array_search( $b->stat, $stat_order );
if ( false === $a && false === $b ) {
return 0;
} elseif ( false === $a ) {
return 1;
} elseif ( false === $b ) {
return -1;
} else {
return $a - $b;
}
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
return $indicator_data;
}
$query_args = $this->prepare_reports_query( $request );
if ( empty( $query_args['stats'] ) ) {
return new \WP_Error( 'woocommerce_analytics_performance_indicators_empty_query', __( 'A list of stats to query must be provided.', 'woocommerce-admin' ), 400 );
}
$stats = array();
foreach ( $query_args['stats'] as $stat ) {
$is_error = false;
$pieces = $this->get_stats_parts( $stat );
$report = $pieces[0];
$chart = $pieces[1];
if ( ! in_array( $stat, $this->allowed_stats ) ) {
continue;
}
$request_url = $this->endpoints[ $report ];
$request = new \WP_REST_Request( 'GET', $request_url );
$request->set_param( 'before', $query_args['before'] );
$request->set_param( 'after', $query_args['after'] );
$response = rest_do_request( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$data = $response->get_data();
$format = $this->formats[ $stat ];
$label = $this->labels[ $stat ];
if ( 200 !== $response->get_status() || ! isset( $data['totals'][ $chart ] ) ) {
$stats[] = (object) array(
'stat' => $stat,
'chart' => $chart,
'label' => $label,
'format' => $format,
'value' => null,
);
continue;
}
$stats[] = (object) array(
'stat' => $stat,
'chart' => $chart,
'label' => $label,
'format' => $format,
'value' => $data['totals'][ $chart ],
);
}
usort( $stats, array( $this, 'sort' ) );
$objects = array();
foreach ( $stats as $stat ) {
$data = $this->prepare_item_for_response( $stat, $request );
$objects[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', count( $stats ) );
$response->header( 'X-WP-TotalPages', 1 );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $stat_data Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $stat_data, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $stat_data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $data ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_performance_indicators', $response, $stat_data, $request );
}
/**
* Prepare links for the request.
*
* @param \Automattic\WooCommerce\Admin\API\Reports\Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$pieces = $this->get_stats_parts( $object->stat );
$endpoint = $pieces[0];
$stat = $pieces[1];
$url = $this->urls[ $endpoint ];
$links = array(
'api' => array(
'href' => rest_url( $this->endpoints[ $endpoint ] ),
),
'report' => array(
'href' => ! empty( $url ) ? $url : '',
),
);
return $links;
}
/**
* Returns the endpoint part of a stat request (prefix) and the actual stat total we want.
* To allow extensions to namespace (example: fue/emails/sent), we break on the last forward slash.
*
* @param string $full_stat A stat request string like orders/avg_order_value or fue/emails/sent.
* @return array Containing the prefix (endpoint) and suffix (stat).
*/
private function get_stats_parts( $full_stat ) {
$endpoint = substr( $full_stat, 0, strrpos( $full_stat, '/' ) );
$stat = substr( $full_stat, ( strrpos( $full_stat, '/' ) + 1 ) );
return array(
$endpoint,
$stat,
);
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
$allowed_stats = array();
} else {
$allowed_stats = $this->allowed_stats;
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_performance_indicator',
'type' => 'object',
'properties' => array(
'stat' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => $allowed_stats,
),
'chart' => array(
'description' => __( 'The specific chart this stat referrers to.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Human readable label for the stat.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'format' => array(
'description' => __( 'Format of the stat.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'number', 'currency' ),
),
'value' => array(
'description' => __( 'Value of the stat. Returns null if the stat does not exist or cannot be loaded.', 'woocommerce-admin' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get schema for the list of allowed performance indicators.
*
* @return array $schema
*/
public function get_public_allowed_item_schema() {
$schema = $this->get_public_item_schema();
unset( $schema['properties']['value'] );
unset( $schema['properties']['format'] );
return $schema;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$indicator_data = $this->get_indicator_data();
if ( is_wp_error( $indicator_data ) ) {
$allowed_stats = __( 'There was an issue loading the report endpoints', 'woocommerce-admin' );
} else {
$allowed_stats = implode( ', ', $this->allowed_stats );
}
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['stats'] = array(
'description' => sprintf(
/* translators: Allowed values is a list of stat endpoints. */
__( 'Limit response to specific report stats. Allowed values: %s.', 'woocommerce-admin' ),
$allowed_stats
),
'type' => 'array',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
'enum' => $this->allowed_stats,
),
'default' => $this->allowed_stats,
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@@ -0,0 +1,432 @@
<?php
/**
* REST API Reports products controller
*
* Handles requests to the /reports/products endpoint.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
/**
* REST API Reports products controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-analytics';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/products';
/**
* Mapping between external parameter name and name used in query class.
*
* @var array
*/
protected $param_mapping = array(
'products' => 'product_includes',
);
/**
* Get items.
*
* @param WP_REST_Request $request Request data.
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$args = array();
$registered = array_keys( $this->get_collection_params() );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
if ( isset( $this->param_mapping[ $param_name ] ) ) {
$args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
} else {
$args[ $param_name ] = $request[ $param_name ];
}
}
}
$reports = new Query( $args );
$products_data = $reports->get_data();
$data = array();
foreach ( $products_data->data as $product_data ) {
$item = $this->prepare_item_for_response( $product_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $products_data->total );
$response->header( 'X-WP-TotalPages', (int) $products_data->pages );
$page = $products_data->page_no;
$max_pages = $products_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_products', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param Array $object Object data.
* @return array Links for the given post.
*/
protected function prepare_links( $object ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_products',
'type' => 'object',
'properties' => array(
'product_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce-admin' ),
),
'items_sold' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of items sold.', 'woocommerce-admin' ),
),
'net_revenue' => array(
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Total Net Sales of all items sold.', 'woocommerce-admin' ),
),
'orders_count' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of orders product appeared in.', 'woocommerce-admin' ),
),
'extended_info' => array(
'name' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product name.', 'woocommerce-admin' ),
),
'price' => array(
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product price.', 'woocommerce-admin' ),
),
'image' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product image.', 'woocommerce-admin' ),
),
'permalink' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product link.', 'woocommerce-admin' ),
),
'category_ids' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product category IDs.', 'woocommerce-admin' ),
),
'stock_status' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory status.', 'woocommerce-admin' ),
),
'stock_quantity' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory quantity.', 'woocommerce-admin' ),
),
'low_stock_amount' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory threshold for low stock.', 'woocommerce-admin' ),
),
'variations' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product variations IDs.', 'woocommerce-admin' ),
),
'sku' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product SKU.', 'woocommerce-admin' ),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'net_revenue',
'orders_count',
'items_sold',
'product_name',
'variations',
'sku',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['categories'] = array(
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each product to the report.', 'woocommerce-admin' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get stock status column export value.
*
* @param array $status Stock status from report row.
* @return string
*/
protected function _get_stock_status( $status ) {
$statuses = wc_get_product_stock_status_options();
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
}
/**
* Get categories column export value.
*
* @param array $category_ids Category IDs from report row.
* @return string
*/
protected function _get_categories( $category_ids ) {
$category_names = get_terms(
array(
'taxonomy' => 'product_cat',
'include' => $category_ids,
'fields' => 'names',
)
);
return implode( ', ', $category_names );
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
$export_columns = array(
'product_name' => __( 'Product Title', 'woocommerce-admin' ),
'sku' => __( 'SKU', 'woocommerce-admin' ),
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
'net_revenue' => __( 'N. Revenue', 'woocommerce-admin' ),
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
'product_cat' => __( 'Category', 'woocommerce-admin' ),
'variations' => __( 'Variations', 'woocommerce-admin' ),
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_columns['stock_status'] = __( 'Status', 'woocommerce-admin' );
$export_columns['stock'] = __( 'Stock', 'woocommerce-admin' );
}
return $export_columns;
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
'product_name' => $item['extended_info']['name'],
'sku' => $item['extended_info']['sku'],
'items_sold' => $item['items_sold'],
'net_revenue' => $item['net_revenue'],
'orders_count' => $item['orders_count'],
'product_cat' => $this->_get_categories( $item['extended_info']['category_ids'] ),
'variations' => isset( $item['extended_info']['variations'] ) ? count( $item['extended_info']['variations'] ) : 0,
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
if ( $item['extended_info']['manage_stock'] ) {
$export_item['stock_status'] = $this->_get_stock_status( $item['extended_info']['stock_status'] );
$export_item['stock'] = $item['extended_info']['stock_quantity'];
} else {
$export_item['stock_status'] = __( 'N/A', 'woocommerce-admin' );
$export_item['stock'] = __( 'N/A', 'woocommerce-admin' );
}
}
return $export_item;
}
}

View File

@@ -0,0 +1,531 @@
<?php
/**
* API\Reports\Products\DataStore class file.
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
/**
* API\Reports\Products\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'products';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
// Extended attributes.
'name' => 'strval',
'price' => 'floatval',
'image' => 'strval',
'permalink' => 'strval',
'stock_status' => 'strval',
'stock_quantity' => 'intval',
'low_stock_amount' => 'intval',
'category_ids' => 'array_values',
'variations' => 'array_values',
'sku' => 'strval',
);
/**
* Extended product attributes to include in the data.
*
* @var array
*/
protected $extended_attributes = array(
'name',
'price',
'image',
'permalink',
'stock_status',
'stock_quantity',
'manage_stock',
'low_stock_amount',
'category_ids',
'variations',
'sku',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'products';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'product_id' => 'product_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 10 );
}
/**
* Fills FROM clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $arg_name Target of the JOIN sql param.
* @param string $id_cell ID cell identifier, like `table_name.id_column_name`.
*/
protected function add_from_sql_params( $query_args, $arg_name, $id_cell ) {
global $wpdb;
$type = 'join';
// Order by product name requires extra JOIN.
switch ( $query_args['orderby'] ) {
case 'product_name':
$join = " JOIN {$wpdb->posts} AS _products ON {$id_cell} = _products.ID";
break;
case 'sku':
$join = " JOIN {$wpdb->postmeta} AS postmeta ON {$id_cell} = postmeta.post_id AND postmeta.meta_key = '_sku'";
break;
case 'variations':
$type = 'left_join';
$join = "LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->posts} WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$id_cell} = _variations.post_parent";
break;
default:
$join = '';
break;
}
if ( $join ) {
if ( 'inner' === $arg_name ) {
$this->subquery->add_sql_clause( $type, $join );
} else {
$this->add_sql_clause( $type, $join );
}
}
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = self::get_db_table_name();
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->add_order_by_sql_params( $query_args );
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$this->add_from_sql_params( $query_args, 'outer', 'default_results.product_id' );
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
} else {
$this->add_from_sql_params( $query_args, 'inner', "{$order_product_lookup_table}.product_id" );
}
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return self::get_db_table_name() . '.date_created';
}
if ( 'product_name' === $order_by ) {
return 'post_title';
}
if ( 'sku' === $order_by ) {
return 'meta_value';
}
return $order_by;
}
/**
* Enriches the product data with attributes specified by the extended_attributes.
*
* @param array $products_data Product data.
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$products_data, $query_args ) {
global $wpdb;
$product_names = array();
foreach ( $products_data as $key => $product_data ) {
$extended_info = new \ArrayObject();
if ( $query_args['extended_info'] ) {
$product_id = $product_data['product_id'];
$product = wc_get_product( $product_id );
// Product was deleted.
if ( ! $product ) {
if ( ! isset( $product_names[ $product_id ] ) ) {
$product_names[ $product_id ] = $wpdb->get_var(
$wpdb->prepare(
"SELECT i.order_item_name
FROM {$wpdb->prefix}woocommerce_order_items i, {$wpdb->prefix}woocommerce_order_itemmeta m
WHERE i.order_item_id = m.order_item_id
AND m.meta_key = '_product_id'
AND m.meta_value = %s
ORDER BY i.order_item_id DESC
LIMIT 1",
$product_id
)
);
}
/* translators: %s is product name */
$products_data[ $key ]['extended_info']['name'] = $product_names[ $product_id ] ? sprintf( __( '%s (Deleted)', 'woocommerce-admin' ), $product_names[ $product_id ] ) : __( '(Deleted)', 'woocommerce-admin' );
continue;
}
$extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data );
foreach ( $extended_attributes as $extended_attribute ) {
if ( 'variations' === $extended_attribute ) {
if ( ! $product->is_type( 'variable' ) ) {
continue;
}
$function = 'get_children';
} else {
$function = 'get_' . $extended_attribute;
}
if ( is_callable( array( $product, $function ) ) ) {
$value = $product->{$function}();
$extended_info[ $extended_attribute ] = $value;
}
}
// If there is no set low_stock_amount, use the one in user settings.
if ( '' === $extended_info['low_stock_amount'] ) {
$extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
}
$extended_info = $this->cast_numbers( $extended_info );
}
$products_data[ $key ]['extended_info'] = $extended_info;
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'categories' => array(),
'product_includes' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_products = $this->get_included_products_array( $query_args );
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( count( $included_products ) > 0 ) {
$total_results = count( $included_products );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
}
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'product_id' ) );
$ids_table = $this->get_ids_table( $included_products, 'product_id' );
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.product_id = {$table_name}.product_id"
);
$products_query = $this->get_query_statement();
} else {
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$products_query = $this->subquery->get_query_statement();
}
$product_data = $wpdb->get_results(
$products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $product_data ) {
return $data;
}
$this->include_extended_info( $product_data, $query_args );
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Create or update an entry in the wc_admin_order_product_lookup table for an order.
*
* @since 3.5.0
* @param int $order_id Order ID.
* @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;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return -1;
}
$table_name = self::get_db_table_name();
$existing_items = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT order_item_id FROM {$table_name} WHERE order_id = %d",
$order_id
)
);
$existing_items = array_flip( $existing_items );
$order_items = $order->get_items();
$num_updated = 0;
$decimals = wc_get_price_decimals();
$round_tax = 'no' === get_option( 'woocommerce_tax_round_at_subtotal' );
foreach ( $order_items as $order_item ) {
$order_item_id = $order_item->get_id();
unset( $existing_items[ $order_item_id ] );
$product_qty = $order_item->get_quantity( 'edit' );
$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 );
// Skip line items without changes to product quantity.
if ( ! $product_qty ) {
$num_updated++;
continue;
}
// Tax amount.
$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 ] ) ? (float) $tax_data['total'][ $tax_item_id ] : 0;
}
$net_revenue = round( $order_item->get_total( 'edit' ), $decimals );
if ( $round_tax ) {
$tax_amount = round( $tax_amount, $decimals );
}
$result = $wpdb->replace(
self::get_db_table_name(),
array(
'order_item_id' => $order_item_id,
'order_id' => $order->get_id(),
'product_id' => wc_get_order_item_meta( $order_item_id, '_product_id' ),
'variation_id' => wc_get_order_item_meta( $order_item_id, '_variation_id' ),
'customer_id' => $order->get_report_customer_id(),
'product_qty' => $product_qty,
'product_net_revenue' => $net_revenue,
'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ),
'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,
),
array(
'%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.
)
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
/**
* Fires when product's reports are updated.
*
* @param int $order_item_id Order Item ID.
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_update_product', $order_item_id, $order->get_id() );
// Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists.
$num_updated += 2 === intval( $result ) ? 1 : intval( $result );
}
if ( ! empty( $existing_items ) ) {
$existing_items = array_flip( $existing_items );
$format = array_fill( 0, count( $existing_items ), '%d' );
$format = implode( ',', $format );
array_unshift( $existing_items, $order_id );
$wpdb->query(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"DELETE FROM {$table_name} WHERE order_id = %d AND order_item_id in ({$format})",
$existing_items
)
);
}
return ( count( $order_items ) === $num_updated );
}
/**
* Clean products data when an order is deleted.
*
* @param int $order_id Order ID.
*/
public static function sync_on_order_delete( $order_id ) {
global $wpdb;
$wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
/**
* Fires when product's reports are removed from database.
*
* @param int $product_id Product ID.
* @param int $order_id Order ID.
*/
do_action( 'woocommerce_analytics_delete_product', 0, $order_id );
ReportsCache::invalidate();
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', 'product_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'product_id' );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class for parameter-based Products Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'categories' => array(15, 18),
* 'products' => array(1,2,3)
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Products\Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit;
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Products\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_products_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-products' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_products_select_query', $results, $args );
}
}

Some files were not shown because too many files have changed in this diff Show More