This commit is contained in:
Prospress Inc
2016-11-23 15:34:50 +01:00
committed by Remco Tolsma
parent 82336a2bd8
commit 153a2cd7ed
151 changed files with 12316 additions and 3635 deletions

View File

@@ -1,6 +1,12 @@
/*------------------------------------------------------------------------------
Subscriptions 2.0 About Page CSS
Subscriptions About Page CSS
------------------------------------------------------------------------------*/
ul {
list-style-type: disc;
padding-left: 1.6em;
margin-top: 0;
margin-bottom: 1.6em;
}
.wcs-badge:before {
font-family: WooCommerce !important;
content: "\e03d";
@@ -34,8 +40,26 @@
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,.2);
box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
.about-wrap h2 {
border-bottom: 1px solid #DDD;
margin: 0;
padding: 1em 0;
}
.about-wrap .changelog {
margin-bottom: 0;
}
.about-wrap .feature-section {
padding-bottom: 2em;
padding-top: 1.6em;
padding-bottom: 0.4em;
border-bottom: 1px solid #DDD;
}
.about-wrap .still-more .feature-section {
padding-top: 0;
padding-bottom: 0.4em;
border-bottom: 1px solid #DDD;
}
.about-wrap .under-the-hood {
padding-top: 0;
}
.about-wrap .wcs-badge {
position: absolute;
@@ -75,10 +99,11 @@
.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary,
.woocommerce-message a.button-primary:focus, p.woocommerce-actions a.button-primary:focus,
.woocommerce-message a.button-primary:active, p.woocommerce-actions a.button-primary:active {
background: #cc99c2;
background: #b366a4;
border-color: #b366a4;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
text-shadow: 0 -1px 1px #b366a4, 1px 0 1px #b366a4, 0 1px 1px #b366a4, -1px 0 1px #b366a4;
color: #fff;
text-decoration: none;
}
@@ -95,7 +120,8 @@
border-color: #aa559a;
}
.woocommerce-message a.docs,.woocommerce-message a.skip,p.woocommerce-actions a.docs,p.woocommerce-actions a.skip {
.woocommerce-message a.skip,
p.woocommerce-actions a.skip {
opacity: .7;
}
@@ -120,6 +146,9 @@ p.woocommerce-actions {
.about-wrap .feature-section.two-col .col {
float: left;
}
.about-wrap .feature-section .col {
margin-top: 1em;
}
.about-wrap .feature-section .col.last-feature {
margin-right: 0;
}

View File

@@ -90,54 +90,47 @@ a.close-subscriptions-search {
}
/* Edit Product/Subscriptions Page */
.woocommerce_options_panel .subscription_pricing {
float: left;
clear: both;
height: 100%;
width: 100%;
.woocommerce_options_panel ._subscription_price_fields .wrap,
.woocommerce_options_panel ._subscription_trial_length_field .wrap,
.woocommerce_options_panel ._subscription_payment_sync_date_day_field .wrap {
display: block;
width: 50%;
}
.woocommerce_options_panel .subscription_pricing .description {
font-style: normal;
@media only screen and (max-width:1280px){
.woocommerce_options_panel ._subscription_price_fields .wrap,
.woocommerce_options_panel ._subscription_trial_length_field .wrap,
.woocommerce_options_panel ._subscription_payment_sync_date_day_field .wrap {
width:80%;
}
.subscription_pricing .form-field,
.subscription_pricing .form-field *,
.variable_subscription_pricing .form-field,
.variable_subscription_pricing .form-field *,
.variable_subscription_trial .form-field,
.variable_subscription_trial .form-field * {
float:left;
}
p._subscription_price_field,
p._subscription_period_field,
p._subscription_length_field {
display: inline-block;
.woocommerce_options_panel ._subscription_price_fields .wrap input,
.woocommerce_options_panel ._subscription_price_fields .wrap select {
width:30.75%;
margin-right:3.8%;
}
.woocommerce_options_panel input[type=text].wc_input_subscription_price,
.woocommerce_options_panel input[type=text].wc_input_subscription_intial_price,
.woocommerce_options_panel input[type=text].wc_input_subscription_trial_length,
.woocommerce_options_panel input[type=text].wc_input_subscription_payment_sync {
width: 5.5em;
.woocommerce_options_panel ._subscription_trial_length_field .wrap input,
.woocommerce_options_panel ._subscription_trial_length_field .wrap select,
.woocommerce_options_panel ._subscription_payment_sync_date_day_field .wrap input,
.woocommerce_options_panel ._subscription_payment_sync_date_day_field .wrap select {
width:48%;
margin-right:3.8%;
}
p._subscription_period_field label,
p._subscription_length_field label,
p._subscription_period_interval_field label,
p._subscription_trial_period_field label,
.woocommerce_options_panel ._subscription_price_fields .wrap .last,
.woocommerce_options_panel ._subscription_trial_length_field .wrap .last,
.woocommerce_options_panel ._subscription_payment_sync_date_day_field .wrap .last {
margin-right:0 !important;
}
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_interval_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field label {
display: none;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_price_field {
padding-right: 0 !important;
}
p._subscription_price_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field input,
p._subscription_sign_up_fee_field input,
p._subscription_trial_length_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field input {
width: 5em;
@@ -145,32 +138,20 @@ p._subscription_trial_length_field input,
.woocommerce_options_panel #sale-price-period {
margin-right: 1em;
}
.wc_input_subscription_length {
min-width: 90px;
}
.wc-metaboxes-wrapper .wc-metabox table td .wc_input_subscription_length {
width: 90px;
}
._subscription_trial_period {
min-width: 7.1em;
}
.wc-metaboxes-wrapper .wc-metabox table td ._subscription_trial_period {
width: 7.1em;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_period_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_length_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_period_interval_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_trial_period_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_payment_sync_date_month_field {
padding: 5px 0 5px 5px !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field {
width: 70px !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_sign_up_fee_field {
width: 100% !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_sign_up_fee_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field {
@@ -204,24 +185,6 @@ p._subscription_trial_length_field input,
margin-left: 5px;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_trial_length_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_sign_up_fee_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_payment_sync_date_day_field {
clear: left;
padding-right: 0px !important;
}
.woocommerce_options_panel p._subscription_trial_length_field,
.woocommerce_options_panel p._subscription_trial_period_field,
.woocommerce_options_panel p._subscription_sign_up_fee_field {
margin-top: 0;
}
.woocommerce_options_panel p._subscription_trial_period_field img,
.woocommerce_options_panel p._subscription_sign_up_fee_field img,
.woocommerce_options_panel p._subscription_payment_sync_date_field img,
.woocommerce_options_panel p._subscription_payment_sync_date_month_field img {
padding-top: 0.7em;
}
.subscription_sync_week_month select.wc_input_subscription_payment_sync {
min-width: 180px;
}
@@ -230,21 +193,19 @@ p._subscription_trial_length_field input,
width: auto !important;
}
._subscription_payment_sync_field img.help_tip {
padding-top: 8px;
}
.variable_subscription_sync p._subscription_payment_sync_field {
padding-left: 0 !important;
}
/* Variation Pricing Fields in WooCommerce 2.3 */
.variable_subscription_pricing_2_3 .wcs_hidden_label {
display: none;
.wcs_hidden_label {
display: none !important;
}
/* Variation Pricing Fields in WooCommerce 2.3 */
.variable_subscription_pricing_2_3 .wc_input_subscription_price {
max-width: 24%;
clear: left;
float: left;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_period_interval {
max-width: 41%;
@@ -264,12 +225,10 @@ p._subscription_trial_length_field input,
.variable_subscription_pricing_2_3 .wc_input_subscription_trial_length {
max-width: 48%;
}
.variable_subscription_pricing_2_3 .variable_subscription_length,
.variable_subscription_pricing_2_3 .variable_subscription_trial_sign_up {
clear: both;
}
.variable_subscription_pricing_2_3 p._subscription_length_field label {
display: inline-block;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_payment_sync_day {
max-width: 13%;
}
@@ -294,6 +253,11 @@ p._subscription_trial_length_field input,
float: right;
}
}
._subscription_limit_field .description {
display: block;
clear: both;
margin-left: 0px;
}
/* Users Administration Screen */
.woocommerce_active_subscriber .active-subscriber:before {
@@ -413,7 +377,8 @@ p._subscription_trial_length_field input,
color: #AA0000;
}
/* Related Orders Metabox on Edit Subscription/Order Admin Screen */
/* Related Orders and Failed Payment Retries Metabox on Edit Subscription/Order Admin Screen */
#renewal_payment_retries .inside,
#subscription_renewal_orders .inside {
margin: 0;
padding: 0;
@@ -480,6 +445,82 @@ p._subscription_trial_length_field input,
border-bottom: none;
}
/* WooCommerce Orders admin table */
table.wp-list-table .column-subscription_relationship {
width: 48px;
text-align: center;
}
table.wp-list-table span.normal_order {
color: #999;
}
table.wp-list-table .subscription_head,
table.wp-list-table .subscription_parent_order,
table.wp-list-table .subscription_resubscribe_order,
table.wp-list-table .subscription_renewal_order {
display: block;
text-indent: -9999px;
position: relative;
height: 1em;
width: 1em;
margin: 0 auto;
}
table.wp-list-table .subscription_head:after,
table.wp-list-table .subscription_parent_order:after {
font-family: WooCommerce;
content: "\e014";
}
table.wp-list-table .subscription_resubscribe_order:after {
font-family: WooCommerce;
content: "\e014";
color: #999;
}
table.wp-list-table .subscription_renewal_order:after {
font-family: WooCommerce;
content: "\e031";
}
table.wp-list-table .payment_retry:after {
font-family: WooCommerce;
content: "\e012";
}
table.wp-list-table .subscription_head:after,
table.wp-list-table .subscription_parent_order:after,
table.wp-list-table .subscription_resubscribe_order:after,
table.wp-list-table .subscription_renewal_order:after {
font-weight: 400;
margin: 0;
text-indent: 0;
position: absolute;
width: 100%;
height: 100%;
text-align: center;
line-height: 16px;
top: 0;
speak: none;
font-variant: normal;
text-transform: none;
-webkit-font-smoothing: antialiased;
left: 0;
}
@media only screen and (max-width: 782px) {
table.wp-list-table .subscription_parent_order,
table.wp-list-table .subscription_resubscribe_order,
table.wp-list-table .subscription_renewal_order{
margin: 0;
}
}
@media only screen and (max-width: 782px) {
table.wp-list-table .column-subscription_relationship {
text-align: inherit;
}
}
/* Hide irrelevant sections on Edit Subscription screen */
body.post-type-shop_subscription .order_actions #actions optgroup[label='Resend order emails'],
body.post-type-shop_subscription .add-items .description.tips,
@@ -513,3 +554,6 @@ body.post-type-shop_subscription .add-items .button.refund-items {
}
}
span.product-type.variable-subscription:before {
content: "\e003" !important;
}

16
assets/css/dashboard.css Normal file
View File

@@ -0,0 +1,16 @@
/*------------------------------------------------------------------------------
Subscriptions 2.1 Dashboard Stats
------------------------------------------------------------------------------*/
#woocommerce_dashboard_status .wc_status_list li.signup-count a:before {
content: "\e02c";
color: #5da5da;
}
#woocommerce_dashboard_status .wc_status_list li.renewal-count a:before {
content: "\e02c";
color: #f29ec4;
}
#woocommerce_dashboard_status .wc_status_list li.signup-count {
border-right: 1px solid #ececec;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1,466 @@
/*
Axis Labels Plugin for flot.
http://github.com/markrcote/flot-axislabels
Original code is Copyright (c) 2010 Xuan Luo.
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
(function ($) {
var options = {
axisLabels: {
show: true
}
};
function canvasSupported() {
return !!document.createElement('canvas').getContext;
}
function canvasTextSupported() {
if (!canvasSupported()) {
return false;
}
var dummy_canvas = document.createElement('canvas');
var context = dummy_canvas.getContext('2d');
return typeof context.fillText == 'function';
}
function css3TransitionSupported() {
var div = document.createElement('div');
return typeof div.style.MozTransition != 'undefined' // Gecko
|| typeof div.style.OTransition != 'undefined' // Opera
|| typeof div.style.webkitTransition != 'undefined' // WebKit
|| typeof div.style.transition != 'undefined';
}
function AxisLabel(axisName, position, padding, plot, opts) {
this.axisName = axisName;
this.position = position;
this.padding = padding;
this.plot = plot;
this.opts = opts;
this.width = 0;
this.height = 0;
}
AxisLabel.prototype.cleanup = function() {
};
CanvasAxisLabel.prototype = new AxisLabel();
CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
function CanvasAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position, padding,
plot, opts);
}
CanvasAxisLabel.prototype.calculateSize = function() {
if (!this.opts.axisLabelFontSizePixels)
this.opts.axisLabelFontSizePixels = 14;
if (!this.opts.axisLabelFontFamily)
this.opts.axisLabelFontFamily = 'sans-serif';
var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
if (this.position == 'left' || this.position == 'right') {
this.width = this.opts.axisLabelFontSizePixels + this.padding;
this.height = 0;
} else {
this.width = 0;
this.height = this.opts.axisLabelFontSizePixels + this.padding;
}
};
CanvasAxisLabel.prototype.draw = function(box) {
if (!this.opts.axisLabelColour)
this.opts.axisLabelColour = 'black';
var ctx = this.plot.getCanvas().getContext('2d');
ctx.save();
ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
this.opts.axisLabelFontFamily;
ctx.fillStyle = this.opts.axisLabelColour;
var width = ctx.measureText(this.opts.axisLabel).width;
var height = this.opts.axisLabelFontSizePixels;
var x, y, angle = 0;
if (this.position == 'top') {
x = box.left + box.width/2 - width/2;
y = box.top + height*0.72;
} else if (this.position == 'bottom') {
x = box.left + box.width/2 - width/2;
y = box.top + box.height - height*0.72;
} else if (this.position == 'left') {
x = box.left + height*0.72;
y = box.height/2 + box.top + width/2;
angle = -Math.PI/2;
} else if (this.position == 'right') {
x = box.left + box.width - height*0.72;
y = box.height/2 + box.top - width/2;
angle = Math.PI/2;
}
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(this.opts.axisLabel, 0, 0);
ctx.restore();
};
HtmlAxisLabel.prototype = new AxisLabel();
HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
function HtmlAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
this.elem = null;
}
HtmlAxisLabel.prototype.calculateSize = function() {
var elem = $('<div class="axisLabels" style="position:absolute;">' +
this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(elem);
// store height and width of label itself, for use in draw()
this.labelWidth = elem.outerWidth(true);
this.labelHeight = elem.outerHeight(true);
elem.remove();
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelWidth + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
HtmlAxisLabel.prototype.cleanup = function() {
if (this.elem) {
this.elem.remove();
}
};
HtmlAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
this.elem = $('<div id="' + this.axisName +
'Label" " class="axisLabels" style="position:absolute;">'
+ this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
if (this.position == 'top') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + 'px');
} else if (this.position == 'bottom') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + box.height - this.labelHeight +
'px');
} else if (this.position == 'left') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + 'px');
} else if (this.position == 'right') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + box.width - this.labelWidth +
'px');
}
};
CssTransformAxisLabel.prototype = new HtmlAxisLabel();
CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
}
CssTransformAxisLabel.prototype.calculateSize = function() {
HtmlAxisLabel.prototype.calculateSize.call(this);
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelHeight + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
var stransforms = {
'-moz-transform': '',
'-webkit-transform': '',
'-o-transform': '',
'-ms-transform': ''
};
if (x != 0 || y != 0) {
var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
stransforms['-moz-transform'] += stdTranslate;
stransforms['-webkit-transform'] += stdTranslate;
stransforms['-o-transform'] += stdTranslate;
stransforms['-ms-transform'] += stdTranslate;
}
if (degrees != 0) {
var rotation = degrees / 90;
var stdRotate = ' rotate(' + degrees + 'deg)';
stransforms['-moz-transform'] += stdRotate;
stransforms['-webkit-transform'] += stdRotate;
stransforms['-o-transform'] += stdRotate;
stransforms['-ms-transform'] += stdRotate;
}
var s = 'top: 0; left: 0; ';
for (var prop in stransforms) {
if (stransforms[prop]) {
s += prop + ':' + stransforms[prop] + ';';
}
}
s += ';';
return s;
};
CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = { x: 0, y: 0, degrees: 0 };
if (this.position == 'bottom') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top + box.height - this.labelHeight;
} else if (this.position == 'top') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top;
} else if (this.position == 'left') {
offsets.degrees = -90;
offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
offsets.y = box.height/2 + box.top;
} else if (this.position == 'right') {
offsets.degrees = 90;
offsets.x = box.left + box.width - this.labelWidth/2
- this.labelHeight/2;
offsets.y = box.height/2 + box.top;
}
offsets.x = Math.round(offsets.x);
offsets.y = Math.round(offsets.y);
return offsets;
};
CssTransformAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
var offsets = this.calculateOffsets(box);
this.elem = $('<div class="axisLabels ' + this.axisName +
'Label" style="position:absolute; ' +
this.transforms(offsets.degrees, offsets.x, offsets.y) +
'">' + this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
};
IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
CssTransformAxisLabel.prototype.constructor.call(this, axisName,
position, padding,
plot, opts);
this.requiresResize = false;
}
IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
// I didn't feel like learning the crazy Matrix stuff, so this uses
// a combination of the rotation transform and CSS positioning.
var s = '';
if (degrees != 0) {
var rotation = degrees/90;
while (rotation < 0) {
rotation += 4;
}
s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
// see below
this.requiresResize = (this.position == 'right');
}
if (x != 0) {
s += 'left: ' + x + 'px; ';
}
if (y != 0) {
s += 'top: ' + y + 'px; ';
}
return s;
};
IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
this, box);
// adjust some values to take into account differences between
// CSS and IE rotations.
if (this.position == 'top') {
// FIXME: not sure why, but placing this exactly at the top causes
// the top axis label to flip to the bottom...
offsets.y = box.top + 1;
} else if (this.position == 'left') {
offsets.x = box.left;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
} else if (this.position == 'right') {
offsets.x = box.left + box.width - this.labelHeight;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
}
return offsets;
};
IeTransformAxisLabel.prototype.draw = function(box) {
CssTransformAxisLabel.prototype.draw.call(this, box);
if (this.requiresResize) {
this.elem = this.plot.getPlaceholder().find("." + this.axisName +
"Label");
// Since we used CSS positioning instead of transforms for
// translating the element, and since the positioning is done
// before any rotations, we have to reset the width and height
// in case the browser wrapped the text (specifically for the
// y2axis).
this.elem.css('width', this.labelWidth);
this.elem.css('height', this.labelHeight);
}
};
function init(plot) {
plot.hooks.processOptions.push(function (plot, options) {
if (!options.axisLabels.show)
return;
// This is kind of a hack. There are no hooks in Flot between
// the creation and measuring of the ticks (setTicks, measureTickLabels
// in setupGrid() ) and the drawing of the ticks and plot box
// (insertAxisLabels in setupGrid() ).
//
// Therefore, we use a trick where we run the draw routine twice:
// the first time to get the tick measurements, so that we can change
// them, and then have it draw it again.
var secondPass = false;
var axisLabels = {};
var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
var defaultPadding = 2; // padding between axis and tick labels
plot.hooks.draw.push(function (plot, ctx) {
var hasAxisLabels = false;
if (!secondPass) {
// MEASURE AND SET OPTIONS
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
// Handle redraws initiated outside of this plug-in.
if (axisName in axisLabels) {
axis.labelHeight = axis.labelHeight -
axisLabels[axisName].height;
axis.labelWidth = axis.labelWidth -
axisLabels[axisName].width;
opts.labelHeight = axis.labelHeight;
opts.labelWidth = axis.labelWidth;
axisLabels[axisName].cleanup();
delete axisLabels[axisName];
}
if (!opts || !opts.axisLabel || !axis.show)
return;
hasAxisLabels = true;
var renderer = null;
if (!opts.axisLabelUseHtml &&
navigator.appName == 'Microsoft Internet Explorer') {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null) {
rv = parseFloat(RegExp.$1);
}
if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = CssTransformAxisLabel;
} else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = IeTransformAxisLabel;
} else if (opts.axisLabelUseCanvas) {
renderer = CanvasAxisLabel;
} else {
renderer = HtmlAxisLabel;
}
} else {
if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
renderer = HtmlAxisLabel;
} else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
renderer = CanvasAxisLabel;
} else {
renderer = CssTransformAxisLabel;
}
}
var padding = opts.axisLabelPadding === undefined ?
defaultPadding : opts.axisLabelPadding;
axisLabels[axisName] = new renderer(axisName,
axis.position, padding,
plot, opts);
// flot interprets axis.labelHeight and .labelWidth as
// the height and width of the tick labels. We increase
// these values to make room for the axis label and
// padding.
axisLabels[axisName].calculateSize();
// AxisLabel.height and .width are the size of the
// axis label and padding.
// Just set opts here because axis will be sorted out on
// the redraw.
opts.labelHeight = axis.labelHeight +
axisLabels[axisName].height;
opts.labelWidth = axis.labelWidth +
axisLabels[axisName].width;
});
// If there are axis labels, re-draw with new label widths and
// heights.
if (hasAxisLabels) {
secondPass = true;
plot.setupGrid();
plot.draw();
}
} else {
secondPass = false;
// DRAW
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
if (!opts || !opts.axisLabel || !axis.show)
return;
axisLabels[axisName].draw(axis.box);
});
}
});
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'axisLabels',
version: '2.0'
});
})(jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,201 @@
/*
* Flot plugin to order bars side by side.
*
* Released under the MIT license by Benjamin BUFFET, 20-Sep-2010.
* Modifications made by Steven Hall <github.com/emmerich>, 01-May-2013.
*
* This plugin is an alpha version.
*
* To activate the plugin you must specify the parameter "order" for the specific serie :
*
* $.plot($("#placeholder"), [{ data: [ ... ], bars :{ order = null or integer }])
*
* If 2 series have the same order param, they are ordered by the position in the array;
*
* The plugin adjust the point by adding a value depanding of the barwidth
* Exemple for 3 series (barwidth : 0.1) :
*
* first bar décalage : -0.15
* second bar décalage : -0.05
* third bar décalage : 0.05
*
*/
// INFO: decalage/decallage is French for gap. It's used to denote the spacing applied to each
// bar.
(function($){
function init(plot){
var orderedBarSeries;
var nbOfBarsToOrder;
var borderWidth;
var borderWidthInXabsWidth;
var pixelInXWidthEquivalent = 1;
var isHorizontal = false;
// A mapping of order integers to decallage.
var decallageByOrder = {};
/*
* This method add shift to x values
*/
function reOrderBars(plot, serie, datapoints){
var shiftedPoints = null;
if(serieNeedToBeReordered(serie)){
checkIfGraphIsHorizontal(serie);
calculPixel2XWidthConvert(plot);
retrieveBarSeries(plot);
calculBorderAndBarWidth(serie);
if(nbOfBarsToOrder >= 2){
var position = findPosition(serie);
var decallage = 0;
var centerBarShift = calculCenterBarShift();
// If we haven't already calculated the decallage for this order value, do it.
if(typeof decallageByOrder[serie.bars.order] === 'undefined') {
if (isBarAtLeftOfCenter(position)){
decallageByOrder[serie.bars.order] = -1*(sumWidth(orderedBarSeries,position-1,Math.floor(nbOfBarsToOrder / 2)-1)) - centerBarShift;
}else{
decallageByOrder[serie.bars.order] = sumWidth(orderedBarSeries,Math.ceil(nbOfBarsToOrder / 2),position-2) + centerBarShift + borderWidthInXabsWidth*2;
}
}
// Lookup the decallage based on the series' order value.
decallage = decallageByOrder[serie.bars.order];
shiftedPoints = shiftPoints(datapoints,serie,decallage);
datapoints.points = shiftedPoints;
}
}
return shiftedPoints;
}
function serieNeedToBeReordered(serie){
return serie.bars != null
&& serie.bars.show
&& serie.bars.order != null;
}
function calculPixel2XWidthConvert(plot){
var gridDimSize = isHorizontal ? plot.getPlaceholder().innerHeight() : plot.getPlaceholder().innerWidth();
var minMaxValues = isHorizontal ? getAxeMinMaxValues(plot.getData(),1) : getAxeMinMaxValues(plot.getData(),0);
var AxeSize = minMaxValues[1] - minMaxValues[0];
pixelInXWidthEquivalent = AxeSize / gridDimSize;
}
function getAxeMinMaxValues(series,AxeIdx){
var minMaxValues = new Array();
for(var i = 0; i < series.length; i++){
minMaxValues[0] = series[i].data[0][AxeIdx];
minMaxValues[1] = series[i].data[series[i].data.length - 1][AxeIdx];
}
return minMaxValues;
}
function retrieveBarSeries(plot){
orderedBarSeries = findOthersBarsToReOrders(plot.getData());
nbOfBarsToOrder = orderedBarSeries.length;
}
function findOthersBarsToReOrders(series){
var retSeries = new Array();
var orderValuesSeen = [];
for(var i = 0; i < series.length; i++){
if(series[i].bars.order != null && series[i].bars.show &&
orderValuesSeen.indexOf(series[i].bars.order) < 0){
orderValuesSeen.push(series[i].bars.order);
retSeries.push(series[i]);
}
}
return retSeries.sort(sortByOrder);
}
function sortByOrder(serie1,serie2){
var x = serie1.bars.order;
var y = serie2.bars.order;
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
}
function calculBorderAndBarWidth(serie){
borderWidth = typeof serie.bars.lineWidth !== 'undefined' ? serie.bars.lineWidth : 2;
borderWidthInXabsWidth = borderWidth * pixelInXWidthEquivalent;
}
function checkIfGraphIsHorizontal(serie){
if(serie.bars.horizontal){
isHorizontal = true;
}
}
function findPosition(serie){
var pos = 0
for (var i = 0; i < orderedBarSeries.length; ++i) {
if (serie == orderedBarSeries[i]){
pos = i;
break;
}
}
return pos+1;
}
function calculCenterBarShift(){
var width = 0;
if(nbOfBarsToOrder%2 != 0)
width = (orderedBarSeries[Math.ceil(nbOfBarsToOrder / 2)].bars.barWidth)/2;
return width;
}
function isBarAtLeftOfCenter(position){
return position <= Math.ceil(nbOfBarsToOrder / 2);
}
function sumWidth(series,start,end){
var totalWidth = 0;
for(var i = start; i <= end; i++){
totalWidth += series[i].bars.barWidth+borderWidthInXabsWidth*2;
}
return totalWidth;
}
function shiftPoints(datapoints,serie,dx){
var ps = datapoints.pointsize;
var points = datapoints.points;
var j = 0;
for(var i = isHorizontal ? 1 : 0;i < points.length; i += ps){
points[i] += dx;
//Adding the new x value in the serie to be abble to display the right tooltip value,
//using the index 3 to not overide the third index.
serie.data[j][3] = points[i];
j++;
}
return points;
}
plot.hooks.processDatapoints.push(reOrderBars);
}
var options = {
series : {
bars: {order: null} // or number/string
}
};
$.plot.plugins.push({
init: init,
options: options,
name: "orderBars",
version: "0.2"
});
})(jQuery);

View File

@@ -0,0 +1,23 @@
/*
* Flot plugin to order bars side by side.
*
* Released under the MIT license by Benjamin BUFFET, 20-Sep-2010.
* Modifications made by Steven Hall <github.com/emmerich>, 01-May-2013.
*
* This plugin is an alpha version.
*
* To activate the plugin you must specify the parameter "order" for the specific serie :
*
* $.plot($("#placeholder"), [{ data: [ ... ], bars :{ order = null or integer }])
*
* If 2 series have the same order param, they are ordered by the position in the array;
*
* The plugin adjust the point by adding a value depanding of the barwidth
* Exemple for 3 series (barwidth : 0.1) :
*
* first bar décalage : -0.15
* second bar décalage : -0.05
* third bar décalage : 0.05
*
*/
!function(r){function n(r){function n(r,n,a){var i=null;if(t(n)&&(f(n),e(r),o(r),u(n),g>=2)){var s=l(n),v=0,W=d();"undefined"==typeof D[n.bars.order]&&(h(s)?D[n.bars.order]=-1*b(p,s-1,Math.floor(g/2)-1)-W:D[n.bars.order]=b(p,Math.ceil(g/2),s-2)+W+2*y),v=D[n.bars.order],i=c(a,n,v),a.points=i}return i}function t(r){return null!=r.bars&&r.bars.show&&null!=r.bars.order}function e(r){var n=w?r.getPlaceholder().innerHeight():r.getPlaceholder().innerWidth(),t=w?a(r.getData(),1):a(r.getData(),0),e=t[1]-t[0];W=e/n}function a(r,n){for(var t=new Array,e=0;e<r.length;e++)t[0]=r[e].data[0][n],t[1]=r[e].data[r[e].data.length-1][n];return t}function o(r){p=i(r.getData()),g=p.length}function i(r){for(var n=new Array,t=[],e=0;e<r.length;e++)null!=r[e].bars.order&&r[e].bars.show&&t.indexOf(r[e].bars.order)<0&&(t.push(r[e].bars.order),n.push(r[e]));return n.sort(s)}function s(r,n){var t=r.bars.order,e=n.bars.order;return e>t?-1:t>e?1:0}function u(r){v="undefined"!=typeof r.bars.lineWidth?r.bars.lineWidth:2,y=v*W}function f(r){r.bars.horizontal&&(w=!0)}function l(r){for(var n=0,t=0;t<p.length;++t)if(r==p[t]){n=t;break}return n+1}function d(){var r=0;return g%2!=0&&(r=p[Math.ceil(g/2)].bars.barWidth/2),r}function h(r){return r<=Math.ceil(g/2)}function b(r,n,t){for(var e=0,a=n;t>=a;a++)e+=r[a].bars.barWidth+2*y;return e}function c(r,n,t){for(var e=r.pointsize,a=r.points,o=0,i=w?1:0;i<a.length;i+=e)a[i]+=t,n.data[o][3]=a[i],o++;return a}var p,g,v,y,W=1,w=!1,D={};r.hooks.processDatapoints.push(n)}var t={series:{bars:{order:null}}};r.plot.plugins.push({init:n,options:t,name:"orderBars",version:"0.2"})}(jQuery);

View File

@@ -0,0 +1,65 @@
jQuery(function($) {
$.extend({
wcs_format_money: function(value,decimal_precision) {
return window.accounting.formatMoney(
value,
{
symbol: wcs_reports.currency_format_symbol,
format: wcs_reports.currency_format,
decimal: wcs_reports.currency_format_decimal_sep,
thousand: wcs_reports.currency_format_thousand_sep,
precision: decimal_precision,
}
);
},
});
// We're on the Subscriptions Upcoming Revenue Report page, change datepicker to future dates.
if ( $( '#woocommerce_subscriptions_upcoming_recurring_revenue_chart' ).length > 0 ) {
$( '.range_datepicker' ).datepicker( 'destroy' );
var dates = $( '.range_datepicker' ).datepicker({
changeMonth: true,
changeYear: true,
defaultDate: "",
dateFormat: "yy-mm-dd",
numberOfMonths: 1,
minDate: "+0D",
showButtonPanel: true,
showOn: "focus",
buttonImageOnly: true,
onSelect: function( selectedDate ) {
var option = $(this).is('.from') ? "minDate" : "maxDate",
instance = $( this ).data( "datepicker" ),
date = $.datepicker.parseDate( instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings );
dates.not( this ).datepicker( 'option', option, date );
}
});
}
// We're on the Payment Retry page, change datepicker to allow both future dates and historical dates
if ( $( '#woocommerce_subscriptions_payment_retry_chart' ).length > 0 ) {
$( '.range_datepicker' ).datepicker( 'destroy' );
var dates = $( '.range_datepicker' ).datepicker({
changeMonth: true,
changeYear: true,
defaultDate: "",
dateFormat: "yy-mm-dd",
numberOfMonths: 1,
showButtonPanel: true,
showOn: "focus",
buttonImageOnly: true,
onSelect: function( selectedDate ) {
var option = $(this).is('.from') ? "minDate" : "maxDate",
instance = $( this ).data( "datepicker" ),
date = $.datepicker.parseDate( instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings );
dates.not( this ).datepicker( 'option', option, date );
}
});
}
});

View File

@@ -0,0 +1,8 @@
jQuery(document).ready(function($){
$('body.post-type-shop_order #post').submit(function(){
if('wcs_retry_renewal_payment' == $( "body.post-type-shop_order select[name='wc_order_action']" ).val()) {
return confirm(wcs_admin_order_meta_boxes.retry_renewal_payment_action_warning);
}
});
});

View File

@@ -1,5 +1,37 @@
*** WooCommerce Subscriptions Changelog ***
2016.11.12 - version 2.1.0
* New: Subscription Reports to get insights into the performance of your subscription business: https://docs.woocommerce.com/document/subscriptions/reports/
* New: Failed Recurring Payment Retry System to help recover revenue that would otherwise be lost: https://docs.woocommerce.com/document/subscriptions/failed-payment-retry/
* New: More subscription emails to help you keep on top of important events, like customer suspension and expiration: https://docs.woocommerce.com/document/subscriptions/version-2-1/#section-4
* New: Cancellation date to keep track of when customers cancel subscriptions, not just when they end after being cancelled: https://docs.woocommerce.com/document/subscriptions/version-2-1/#section-5
* New: Allow resubscribing to subscriptions Pending Cancellation: https://docs.woocommerce.com/document/subscriptions/version-2-1/#resubscribe-to-subscriptions-pending-cancellation
* New: REST API Endpoints for Subscriptions built on WordPress REST API infrastructure: https://docs.woocommerce.com/document/subscriptions/version-2-1/#wp-rest-api-endpoints
* New: Additional order type filters on the WooCommerce > Orders administration screen to filter orders to show only subscription parent, renewal, resubscribe or switch orders: https://docs.woocommerce.com/document/subscriptions/version-2-1/#additional-order-type-filters
* Tweak: Always use Renewal Order totals and data for renewal payments, making it possible to easily add one-time fees or discounts to renewals: https://docs.woocommerce.com/document/subscriptions/version-2-1/#renewal-orders-always-used-for-renewal-data
* Tweak: Improved responsiveness and layout of the subscription pricing fields on the Edit Product screen: https://docs.woocommerce.com/document/subscriptions/version-2-1/#edit-product-interface-improvements
* Tweak: Use renewal order/subscription addresses as the default address fields loaded on checkout for renewal, resubscribe and switch checkouts. https://docs.woocommerce.com/document/subscriptions/version-2-1/#use-renewal-order-or-subscription-address-on-checkout
* Tweak: Process switches (upgrades and downgrades) on order status change instead of when the customer completes checkout: https://docs.woocommerce.com/document/subscriptions/version-2-1/#process-upgrades-and-downgrades-on-order-status-change
* Tweak: Small performance improvements by no longer calling deprecated hooks and caching some queries: https://docs.woocommerce.com/document/subscriptions/version-2-1/#performance-improvements
* Tweak: Apply add-to-cart validation to renewals and resubscribes (using the 'woocommerce_add_to_cart_validation' hook): https://docs.woocommerce.com/document/subscriptions/version-2-1/#add-to-cart-validation-applied-to-renewal-and-resubscribe-process
* Tweak: Link to the My Account > Subscriptions page on the Order Received/Thank you page when using WooCommerce 2.6 so that the customer can see all their subscriptions if they purchased multiple subscriptions in the transaction.
* Tweak: Use the 'pay_for_order' user capability check to determine whether a user can pay for a renewal and resubscribe rather than checking their ID against the customer ID on the subscription. (PR#1692 / PR#1716)
* Tweak: Increase next payment date threshold for end date to safeguard against payments being processed on the last day of a subscription. (PR#1689)
* Tweak: Only check for PayPal Reference Transaction support once per week instead of daily.
* Tweak: Consistently use the same tooltips in the store admin area as WooCommerce.
* Tweak: Initiate auto-switch process when loading the Grouped product page. (PR#1641)
* Tweak: New 'woocommerce_subscriptions_after_recurring_shipping_rates' hook.
* Tweak: Update scheduling system (Action Scheduler) to version 1.5.
* Fix: Do not create new orders when processing a renewal and resubscribe payment with different details to the original order by ensuring the cart hash used by WooCommerce is updated when creating the order. (PR#1687)
* Fix: Prevent My Account > Subscriptions endpoint page title overriding custom menus and other items on the page calling 'the_title' filter. (PR#1737)
* Fix: Do not change the status of subscriptions using PayPal Standard when the request to update the status at PayPal.com fails because of incorrect API credentials. (PR#1743)
* Fix: Display incorrect PayPal API credentials notice after a status change request or reference transaction check returns incorrect credentials error. (PR#1743)
* Fix: Calculate switching next payment date calculations for a free product to a non-free product correctly. (PR#1661)
* Fix: Process renewals with PayPal Reference Transactions correctly when the ALTERNATE_WP_CRON constant is defined. (PR#1733)
* Fix: Prevent grouped subscription products being switched to non-subscription product in the same grouped product. (PR#1666)
* Fix: Treat empty subscription suspension count option value as 0, meaning no suspensions are allowed. (PR#1728)
* Fix: Only attempt to get paid renewal orders if there are renewal orders. (PR#1718)
2016.09.23 - version 2.0.20
* Tweak: add new 'woocommerce_subscription_before_actions' and 'woocommerce_subscription_after_actions' hooks to the view-subscription.php template. (PR#1608)
* Tweak: use WC_Subscriptions_Product::is_subscription() when checking if a product is sync'd instead of checking the product type directly so that the 'woocommerce_is_subscription' filter is applied and additional product types can add support for synchronisation. (PR#1635)
@@ -302,7 +334,7 @@
* Fix: download links in the "download your files" email
2015.10.27 - version 2.0.3
* New: One Time Shipping feature: https://docs.woothemes.com/document/subscriptions/store-manager-guide/#one-time-shipping
* New: One Time Shipping feature: https://docs.woocommerce.com/document/subscriptions/store-manager-guide/#one-time-shipping
* Tweak: redirect to View Subscription page after changing payment method for payment methods redirecting back to My Account page
* Fix: reactivation of subscriptions using PayPal Standard as the payment method after a subscription renewal payment IPN message is sent (introduced with 2.0.2)
* Fix: use of start date in based on current GMT/UTC offset instead of GMT/UTC offset at the time the subscription was created to handle daylight savings time and changes to a site's timezone
@@ -340,16 +372,16 @@
* Fix: upgrade error when upgrading a sync'd subscription that has been trashed for a product that has been permanently deleted
2015.10.05 - version 2.0.0
* New: purchase different subscription products in the same transaction: https://docs.woothemes.com/document/subscriptions/version-2/#section-2
* New: administration interface for Adding or Editing a subscription: https://docs.woothemes.com/document/subscriptions/version-2/#section-3
* New: downloadable content dripping: https://docs.woothemes.com/document/subscriptions/version-2/#section-4
* New: customer facing View Subscription page: https://docs.woothemes.com/document/subscriptions/version-2/#section-5
* New: support for PayPal Reference Transactions: https://docs.woothemes.com/document/subscriptions/version-2/#section-8
* New: Pending Cancellation status applied to a subscription after it has been cancelled but the customer or store manager until the prepaid term ends: https://docs.woothemes.com/document/subscriptions/version-2/#pending-cancellation
* Tweak: Subscriptions administration list table now includes recurring total, payment method and all search/sorting features for stores with a large number of subscriptions: https://docs.woothemes.com/document/subscriptions/version-2/#list-table
* Tweak: Improved flow on renewal - create renewal orders before processing the payment: https://docs.woothemes.com/document/subscriptions/version-2/#section-7
* Tweak: one end date is now used to refer to the date on which a subscription did or will expire or was cancelled: https://docs.woothemes.com/document/subscriptions/version-2/#one-end-date
* Tweak: the renewal of a cancelled or expired subscription is now called "Resubscribe" to avoid confusion with normal renewal process: https://docs.woothemes.com/document/subscriptions/version-2/#resubscribe-not-renew
* New: purchase different subscription products in the same transaction: https://docs.woocommerce.com/document/subscriptions/version-2/#section-2
* New: administration interface for Adding or Editing a subscription: https://docs.woocommerce.com/document/subscriptions/version-2/#section-3
* New: downloadable content dripping: https://docs.woocommerce.com/document/subscriptions/version-2/#section-4
* New: customer facing View Subscription page: https://docs.woocommerce.com/document/subscriptions/version-2/#section-5
* New: support for PayPal Reference Transactions: https://docs.woocommerce.com/document/subscriptions/version-2/#section-8
* New: Pending Cancellation status applied to a subscription after it has been cancelled but the customer or store manager until the prepaid term ends: https://docs.woocommerce.com/document/subscriptions/version-2/#pending-cancellation
* Tweak: Subscriptions administration list table now includes recurring total, payment method and all search/sorting features for stores with a large number of subscriptions: https://docs.woocommerce.com/document/subscriptions/version-2/#list-table
* Tweak: Improved flow on renewal - create renewal orders before processing the payment: https://docs.woocommerce.com/document/subscriptions/version-2/#section-7
* Tweak: one end date is now used to refer to the date on which a subscription did or will expire or was cancelled: https://docs.woocommerce.com/document/subscriptions/version-2/#one-end-date
* Tweak: the renewal of a cancelled or expired subscription is now called "Resubscribe" to avoid confusion with normal renewal process: https://docs.woocommerce.com/document/subscriptions/version-2/#resubscribe-not-renew
2015.09.29 - version 1.5.31
* Tweak: introduce a new transient lock at the start of PayPal IPN handling to prevent duplicate IPN handling on sites taking more than a minute to process an IPN message and set the permanent lock
@@ -637,7 +669,7 @@
* Tweak: move sign-up fee field to own row on the Edit Product screen to improve display on small screens
* Tweak: when asked what type of product they are, variable subscriptions and subscription variations will now identify as both the subscription and standard equivalent (e.g. if a variable subscription is asked if it is a variable product, it will say yes, if a subscription variation is asked if it is a standard variation, it will say yes).
* Tweak: in API responses, include variable subscriptions' children & subscription variations' parent objects (due to tweak in variable/variation self-identification above)
* Tweak: limit subscription regardless of status to prevent the same customer account effectively being able to access more than one free trial period. More: http://docs.woothemes.com/document/subscriptions/store-manager-guide/#limit-subscription
* Tweak: limit subscription regardless of status to prevent the same customer account effectively being able to access more than one free trial period. More: http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#limit-subscription
* Tweak: only allow customers to purchase subscriptions with PayPal once the store's API credentials are entered
* Tweak: prefill "Change" next payment date fields with date in site's timezone instead of next payment date in GMT/UTC timezone
* Fix: fix stock management for variable subscriptions (due to the tweak above making variable subscriptions identify as variable products)
@@ -653,7 +685,7 @@
2014.04.09 - version 1.4.10
* Tweak: Do not add the "Payment received" order note when a subscription has a free trial and no sign-up fee, instead display "Payment authorized" for subscriptions using automatic recurring payments and "Free trial commenced" for those using manual renewals
* Tweak: On the Manage Subscriptions screen, do not display the subscription sign-up date in the "Last Payment" column if the subscription has a free trial and no sign-up fee (as there was no actual payment made)
* Tweak: Reinstate enctype='multipart/form-data' on the subscription add to cart form to fix compatibilty with file uploads via Product Addons extension (related to http://docs.woothemes.com/document/subscriptions/faq/#section-45)
* Tweak: Reinstate enctype='multipart/form-data' on the subscription add to cart form to fix compatibilty with file uploads via Product Addons extension (related to http://docs.woocommerce.com/document/subscriptions/faq/#section-45)
* Tweak: Only output P tags for product meta on My Subscriptions table when product has meta
* Tweak: add enable/disable field for the Customer Renewal Invoice email
* Fix: set correct order date and ID in renewal order emails' subject & heading when sending more than one email in the same request (workaround until woothemes/woocommerce#5168 is implemented in WC 2.2)
@@ -757,7 +789,7 @@
* Tweak: use a string literal text domain for translation plugins which don't support variable text domains, like the latest version of WPML
* Tweak: try to align next payment dates with PayPal's schedule by update the next payment date time whenever PayPal processes a payment
* Tweak: reduce hidden meta data included on the Manage Subscriptions screen to improve load time and provide cleaner search/filter URLs
* Tweak: new 'woocommerce_subscription_lengths' filter for custom subscription lengths: http://docs.woothemes.com/document/subscriptions/faq/#section-57
* Tweak: new 'woocommerce_subscription_lengths' filter for custom subscription lengths: http://docs.woocommerce.com/document/subscriptions/faq/#section-57
* Tweak: add related order links to the Edit Order screen for renewal orders of cancelled/expired subscriptions
* Fix: only load email classes once in case something is destroying and then reinitiating the Woocommerce::woocommerce_mailer property
* Fix: only set recurring payment method meta data when an order is marked as completed for those orders that contain a subscription
@@ -802,8 +834,8 @@
* Fix: error message displayed when no payment gateways are available to change the payment method.
2013.09.20 - version 1.4
* New: "Switch" subscriptions feature to allow customers to upgrade/downgrade their subscription: http://docs.woothemes.com/document/subscriptions/switching-guide/
* New: The payment method used for automatic recurring payments can now be changed by subscribers: http://docs.woothemes.com/document/subscriptions/customers-view/#section-5
* New: "Switch" subscriptions feature to allow customers to upgrade/downgrade their subscription: http://docs.woocommerce.com/document/subscriptions/switching-guide/
* New: The payment method used for automatic recurring payments can now be changed by subscribers: http://docs.woocommerce.com/document/subscriptions/customers-view/#section-5
* New: Database structure for subscriptions to improve performance on all sites and fix memory exhaustion issues on sites with tens of thousands of subscriptions. Subscription meta data is now stored in order meta not user meta.
* New: The recurring shipping method, title, tax and total used for renewals can now be changed by store managers from the 'Edit Order' page of the original order used to purchase the subscription (for those payment gateways which support changing recurring amounts).
* New: Renewal order emails: using the WooCommerce email system for renewal orders so they can be enabled/disabled, and the content/templates can be customised.
@@ -832,7 +864,7 @@
2013.08.15 - version 1.3.11
* When a subscription payment is due on the 29th or 30th day of February (i.e. it is normally charged on the 29th or 30th day of the month, and the next billing month is February) charge on the last day of February instead. More details: http://docs.woothemes.com/document/subscriptions/faq/#section-16
* When a subscription payment is due on the 29th or 30th day of February (i.e. it is normally charged on the 29th or 30th day of the month, and the next billing month is February) charge on the last day of February instead. More details: http://docs.woocommerce.com/document/subscriptions/faq/#section-16
* Trial and subscription expiration dates now match payment dates by using the last day of the month instead of PHP's strtotime() month addition
* Fix Variable Subscription prices when not all variations have a sale price or sign-up fee
* Fix duplicate payment bug where a 2nd payment was charged (n - 1) hours after the first for the payment immediately following a free trial when the next payment date was changed for a subscription with a free trial that was manually added to the site on sites using a timezone of UTC -n
@@ -884,8 +916,8 @@
* Fix bug applying discount coupons twice to manual renewals and renewal of a failed automatic payment
2013.06.24 - version 1.3.5
* Coupon Behaviour Change: due to popular demand, WooCommerce's Product and Cart coupons now discount only the first payment. This also improves compatibility with the Point & Rewards and Gift Certificate extensions. More details: http://docs.woothemes.com/document/faq/#section-3
* Add new 'WCS_DEBUG' mode to make it easier to test renewal payments. More details: http://docs.woothemes.com/document/subscriptions/faq/#section-29
* Coupon Behaviour Change: due to popular demand, WooCommerce's Product and Cart coupons now discount only the first payment. This also improves compatibility with the Point & Rewards and Gift Certificate extensions. More details: http://docs.woocommerce.com/document/faq/#section-4
* Add new 'WCS_DEBUG' mode to make it easier to test renewal payments. More details: https://docs.woocommerce.com/document/testing-subscription-renewal-payments/
* Add new option to limit a subscription product to one active subscription per customer
* Update translation files (pot file)
* Hide redundant "Sold Individually" checkbox on "Edit Product" screen (subscriptions can only be purchased individually)

View File

@@ -0,0 +1,107 @@
<?php
/**
* An interface for creating a store for retry details.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Store
* @category Class
* @author Prospress
* @since 2.1
*/
abstract class WCS_Retry_Store {
/** @var ActionScheduler_Store */
private static $store = null;
/**
* Save the details of a retry to the database
*
* @param WCS_Retry $retry
* @return int the retry's ID
*/
abstract public function save( WCS_Retry $retry );
/**
* Get the details of a retry from the database
*
* @param int $retry_id
* @return WCS_Retry
*/
abstract public function get_retry( $retry_id );
/**
* Get a set of retries from the database
*
* @param array $args A set of filters:
* 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries.
* 'date_query': array of dates to filter retries those that occur 'after' or 'before' a certain (or inbetween those two dates). Should be a MySQL formated date/time string.
* @return array An array of WCS_Retry objects
*/
abstract public function get_retries( $args );
/**
* Get the IDs of all retries from the database for a given order
*
* @param int $order_id
* @return array
*/
abstract protected function get_retry_ids_for_order( $order_id );
/**
* Setup the class, if required
*
* @return null
*/
abstract public function init();
/**
* Get the details of all retries (if any) for a given order
*
* @param int $order_id
* @return array
*/
public function get_retries_for_order( $order_id ) {
$retries = array();
foreach ( $this->get_retry_ids_for_order( $order_id ) as $retry_id ) {
$retries[ $retry_id ] = $this->get_retry( $retry_id );
}
return $retries;
}
/**
* Get the details of the last retry (if any) recorded for a given order
*
* @param int $order_id
* @return WCS_Retry | null
*/
public function get_last_retry_for_order( $order_id ) {
$retry_ids = $this->get_retry_ids_for_order( $order_id );
if ( ! empty( $retry_ids ) ) {
$last_retry_id = array_pop( $retry_ids );
$last_retry = $this->get_retry( $last_retry_id );
} else {
$last_retry = null;
}
return $last_retry;
}
/**
* Get the number of retries stored in the database for a given order
*
* @param int $order_id
* @return int
*/
public function get_retry_count_for_order( $order_id ) {
$retry_post_ids = $this->get_retry_ids_for_order( $order_id );
return count( $retry_post_ids );
}
}

View File

@@ -35,7 +35,7 @@ abstract class WCS_Scheduler {
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
* @param string $datetime A MySQL formatted date/time string in the GMT/UTC timezone.
*/
abstract public function update_date( $subscription, $date_type, $datetime );
@@ -51,8 +51,8 @@ abstract class WCS_Scheduler {
* When a subscription's status is updated, maybe schedule an event
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
* @param string $new_status A valid subscription status
* @param string $old_status A valid subscription status
*/
abstract public function update_status( $subscription, $new_status, $old_status );
}

View File

@@ -57,9 +57,6 @@ class WC_Subscriptions_Admin {
// Add subscription shipping options on edit product page
add_action( 'woocommerce_product_options_shipping', __CLASS__ . '::subscription_shipping_fields' );
// Add advanced subscription options on edit product page
add_action( 'woocommerce_product_options_reviews', __CLASS__ . '::subscription_advanced_fields' );
// And also on the variations section
add_action( 'woocommerce_product_after_variable_attributes', __CLASS__ . '::variable_subscription_pricing_fields', 10, 3 );
@@ -111,6 +108,8 @@ class WC_Subscriptions_Admin {
// Do not display formatted order total on the Edit Order administration screen
add_filter( 'woocommerce_get_formatted_order_total', __CLASS__ . '::maybe_remove_formatted_order_total_filter', 0, 2 );
add_action( 'woocommerce_payment_gateways_settings', __CLASS__ . '::add_recurring_payment_gateway_information', 10 , 1 );
}
/**
@@ -122,8 +121,8 @@ class WC_Subscriptions_Admin {
*/
public static function add_subscription_products_to_select( $product_types ) {
$product_types['subscription'] = __( 'Simple Subscription', 'woocommerce-subscriptions' );
$product_types['variable-subscription'] = __( 'Variable Subscription', 'woocommerce-subscriptions' );
$product_types['subscription'] = __( 'Simple subscription', 'woocommerce-subscriptions' );
$product_types['variable-subscription'] = __( 'Variable subscription', 'woocommerce-subscriptions' );
return $product_types;
}
@@ -136,62 +135,60 @@ class WC_Subscriptions_Admin {
public static function subscription_pricing_fields() {
global $post;
$chosen_price = get_post_meta( $post->ID, '_subscription_price', true );
$chosen_interval = get_post_meta( $post->ID, '_subscription_period_interval', true );
$chosen_trial_length = WC_Subscriptions_Product::get_trial_length( $post->ID );
$chosen_trial_period = WC_Subscriptions_Product::get_trial_period( $post->ID );
$price_tooltip = __( 'Choose the subscription price, billing interval and period.', 'woocommerce-subscriptions' );
// translators: placeholder is trial period validation message if passed an invalid value (e.g. "Trial period can not exceed 4 weeks")
$trial_tooltip = sprintf( _x( 'An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. %s', 'Trial period field tooltip on Edit Product administration screen', 'woocommerce-subscriptions' ), self::get_trial_period_validation_message() );
// Set month as the default billing period
if ( ! $subscription_period = get_post_meta( $post->ID, '_subscription_period', true ) ) {
$subscription_period = 'month';
if ( ! $chosen_period = get_post_meta( $post->ID, '_subscription_period', true ) ) {
$chosen_period = 'month';
}
echo '<div class="options_group subscription_pricing show_if_subscription">';
// Subscription Price
woocommerce_wp_text_input( array(
'id' => '_subscription_price',
'class' => 'wc_input_subscription_price',
// translators: placeholder is a currency symbol / code
'label' => sprintf( __( 'Subscription Price (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ),
'type' => 'text',
'custom_attributes' => array(
'step' => 'any',
'min' => '0',
),
) );
// Subscription Period Interval
woocommerce_wp_select( array(
'id' => '_subscription_period_interval',
'class' => 'wc_input_subscription_period_interval',
'label' => __( 'Subscription Periods', 'woocommerce-subscriptions' ),
'options' => wcs_get_subscription_period_interval_strings(),
)
);
// Billing Period
woocommerce_wp_select( array(
'id' => '_subscription_period',
'class' => 'wc_input_subscription_period',
'label' => __( 'Billing Period', 'woocommerce-subscriptions' ),
'value' => $subscription_period,
'description' => _x( 'for', 'for in "Every month _for_ 12 months"', 'woocommerce-subscriptions' ),
'options' => wcs_get_subscription_period_strings(),
)
);
// Subscription Price, Interval and Period
?><p class="form-field _subscription_price_fields _subscription_price_field">
<label for="_subscription_price"><?php printf( esc_html__( 'Subscription price (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); ?></label>
<span class="wrap">
<input type="text" id="_subscription_price" name="_subscription_price" class="wc_input_subscription_price" placeholder="<?php echo esc_attr_x( 'e.g. 5.90', 'example price', 'woocommerce-subscriptions' ); ?>" step="any" min="0" value="<?php echo esc_attr( $chosen_price ); ?>" />
<label for="_subscription_period_interval" class="wcs_hidden_label"><?php esc_html_e( 'Subscription interval', 'woocommerce-subscriptions' ); ?></label>
<select id="_subscription_period_interval" name="_subscription_period_interval" class="wc_input_subscription_period_interval">
<?php foreach ( wcs_get_subscription_period_interval_strings() as $value => $label ) { ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $chosen_interval, true ) ?>><?php echo esc_html( $label ); ?></option>
<?php } ?>
</select>
<label for="_subscription_period" class="wcs_hidden_label"><?php esc_html_e( 'Subscription period', 'woocommerce-subscriptions' ); ?></label>
<select id="_subscription_period" name="_subscription_period" class="wc_input_subscription_period last" >
<?php foreach ( wcs_get_subscription_period_strings() as $value => $label ) { ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $chosen_period, true ) ?>><?php echo esc_html( $label ); ?></option>
<?php } ?>
</select>
</span>
<?php echo wcs_help_tip( $price_tooltip ); ?>
</p><?php
// Subscription Length
woocommerce_wp_select( array(
'id' => '_subscription_length',
'class' => 'wc_input_subscription_length',
'label' => __( 'Subscription Length', 'woocommerce-subscriptions' ),
'options' => wcs_get_subscription_ranges( $subscription_period ),
'class' => 'wc_input_subscription_length select short',
'label' => __( 'Subscription length', 'woocommerce-subscriptions' ),
'options' => wcs_get_subscription_ranges( $chosen_period ),
'desc_tip' => true,
'description' => __( 'Automatically expire the subscription after this length of time. This length is in addition to any free trial or amount of time provided before a synchronised first renewal date.', 'woocommerce-subscriptions' ),
)
);
// Sign-up Fee
woocommerce_wp_text_input( array(
'id' => '_subscription_sign_up_fee',
'class' => 'wc_input_subscription_intial_price',
'class' => 'wc_input_subscription_intial_price short',
// translators: %s is a currency symbol / code
'label' => sprintf( __( 'Sign-up Fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ),
'description' => __( 'Optionally include an amount to be charged at the outset of the subscription. The sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced.', 'woocommerce-subscriptions' ),
'desc_tip' => true,
@@ -203,23 +200,19 @@ class WC_Subscriptions_Admin {
) );
// Trial Length
woocommerce_wp_text_input( array(
'id' => '_subscription_trial_length',
'class' => 'wc_input_subscription_trial_length',
'label' => __( 'Free Trial', 'woocommerce-subscriptions' ),
) );
// Trial Period
woocommerce_wp_select( array(
'id' => '_subscription_trial_period',
'class' => 'wc_input_subscription_trial_period',
'label' => __( 'Subscription Trial Period', 'woocommerce-subscriptions' ),
'options' => wcs_get_available_time_periods(),
// translators: placeholder is trial period validation message if passed an invalid value (e.g. "Trial period can not exceed 4 weeks")
'description' => sprintf( _x( 'An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. %s', 'Trial period dropdown\'s description in pricing fields', 'woocommerce-subscriptions' ), self::get_trial_period_validation_message() ),
'desc_tip' => true,
'value' => WC_Subscriptions_Product::get_trial_period( $post->ID ), // Explicitly set value in to ensure backward compatibility
) );
?><p class="form-field _subscription_trial_length_field">
<label for="_subscription_trial_length"><?php esc_html_e( 'Free trial', 'woocommerce-subscriptions' ); ?></label>
<span class="wrap">
<input type="text" id="_subscription_trial_length" name="_subscription_trial_length" class="wc_input_subscription_trial_length" value="<?php echo esc_attr( $chosen_trial_length ); ?>" />
<label for="_subscription_trial_period" class="wcs_hidden_label"><?php esc_html_e( 'Subscription Trial Period', 'woocommerce-subscriptions' ); ?></label>
<select id="_subscription_trial_period" name="_subscription_trial_period" class="wc_input_subscription_trial_period last" >
<?php foreach ( wcs_get_available_time_periods() as $value => $label ) { ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $chosen_trial_period, true ) ?>><?php echo esc_html( $label ); ?></option>
<?php } ?>
</select>
</span>
<?php echo wcs_help_tip( $trial_tooltip ); ?>
</p><?php
do_action( 'woocommerce_subscriptions_product_options_pricing' );
@@ -243,7 +236,7 @@ class WC_Subscriptions_Admin {
// Only one Subscription per customer
woocommerce_wp_checkbox( array(
'id' => '_subscription_one_time_shipping',
'label' => __( 'One Time Shipping', 'woocommerce-subscriptions' ),
'label' => __( 'One time shipping', 'woocommerce-subscriptions' ),
'description' => __( 'Shipping for subscription products is normally charged on the initial order and all renewal orders. Enable this to only charge shipping once on the initial order. Note: for this setting to be enabled the subscription must not have a free trial or a synced renewal date.', 'woocommerce-subscriptions' ),
'desc_tip' => true,
) );
@@ -254,30 +247,11 @@ class WC_Subscriptions_Admin {
/**
* Output advanced subscription options on the "Edit Product" admin screen
*
* @deprecated 2.1
* @since 1.3.5
*/
public static function subscription_advanced_fields() {
global $post;
echo '</div>';
echo '<div class="options_group limit_subscription show_if_subscription show_if_variable-subscription">';
// Only one Subscription per customer
woocommerce_wp_select( array(
'id' => '_subscription_limit',
'label' => __( 'Limit Subscription', 'woocommerce-subscriptions' ),
// translators: placeholders are opening and closing link tags
'description' => sprintf( __( 'Only allow a customer to have one subscription to this product. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#limit-subscription">', '</a>' ),
'options' => array(
'no' => __( 'Do not limit', 'woocommerce-subscriptions' ),
'active' => __( 'Limit to one active subscription', 'woocommerce-subscriptions' ),
'any' => __( 'Limit to one of any status', 'woocommerce-subscriptions' ),
),
) );
do_action( 'woocommerce_subscriptions_product_options_advanced' );
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::admin_edit_product_fields()' );
}
/**
@@ -318,13 +292,13 @@ class WC_Subscriptions_Admin {
global $post;
if ( WC_Subscriptions_Product::is_subscription( $post->ID ) ) : ?>
<optgroup label="<?php esc_attr_e( 'Subscription Pricing', 'woocommerce-subscriptions' ); ?>">
<option value="variable_subscription_sign_up_fee"><?php esc_html_e( 'Subscription Sign-up Fee', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_period_interval"><?php esc_html_e( 'Subscription Billing Interval', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_period"><?php esc_html_e( 'Subscription Period', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_length"><?php esc_html_e( 'Subscription Length', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_trial_length"><?php esc_html_e( 'Free Trial Length', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_trial_period"><?php esc_html_e( 'Free Trial Period', 'woocommerce-subscriptions' ); ?></option>
<optgroup label="<?php esc_attr_e( 'Subscription pricing', 'woocommerce-subscriptions' ); ?>">
<option value="variable_subscription_sign_up_fee"><?php esc_html_e( 'Subscription sign-up fee', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_period_interval"><?php esc_html_e( 'Subscription billing interval', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_period"><?php esc_html_e( 'Subscription period', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_length"><?php esc_html_e( 'Subscription length', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_trial_length"><?php esc_html_e( 'Free trial length', 'woocommerce-subscriptions' ); ?></option>
<option value="variable_subscription_trial_period"><?php esc_html_e( 'Free trial period', 'woocommerce-subscriptions' ); ?></option>
</optgroup>
<?php endif;
}
@@ -351,8 +325,8 @@ class WC_Subscriptions_Admin {
update_post_meta( $post_id, '_regular_price', $subscription_price );
update_post_meta( $post_id, '_sale_price', $sale_price );
$date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? strtotime( $_POST['_sale_price_dates_from'] ) : '';
$date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? strtotime( $_POST['_sale_price_dates_to'] ) : '';
$date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_from'] ) : '';
$date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_to'] ) : '';
$now = gmdate( 'U' );
@@ -777,7 +751,7 @@ class WC_Subscriptions_Admin {
delete_transient( WC_Subscriptions::$activation_transient );
}
if ( $is_woocommerce_screen || $is_activation_screen ) {
if ( $is_woocommerce_screen || $is_activation_screen || 'edit-product' == $screen->id ) {
wp_enqueue_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), WC_Subscriptions::$version );
wp_enqueue_style( 'woocommerce_subscriptions_admin', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/css/admin.css', array( 'woocommerce_admin_styles' ), WC_Subscriptions::$version );
}
@@ -793,7 +767,7 @@ class WC_Subscriptions_Admin {
// Move Active Subscriber before Orders for aesthetics
$last_column = array_slice( $columns, -1, 1, true );
array_pop( $columns );
$columns['woocommerce_active_subscriber'] = __( 'Active Subscriber?', 'woocommerce-subscriptions' );
$columns['woocommerce_active_subscriber'] = __( 'Active subscriber?', 'woocommerce-subscriptions' );
$columns += $last_column;
}
@@ -993,25 +967,6 @@ class WC_Subscriptions_Admin {
$roles_options[ $role ] = translate_user_role( $details['name'] );
}
$available_gateways = array();
foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) {
if ( $gateway->supports( 'subscriptions' ) ) {
$available_gateways[] = sprintf( '%s [<code>%s</code>]', $gateway->title, $gateway->id );
}
}
if ( count( $available_gateways ) == 0 ) {
// translators: $1-2: opening and closing tags of a link that takes to PayPal settings, $3-4: opening and closing tags of a link that takes to Woo marketplace / Stripe product page
$available_gateways_description = sprintf( __( 'No payment gateways capable of processing automatic subscription payments are enabled. Please enable the %1$sPayPal Standard%2$s gateway or get the %3$sfree Stripe extension%4$s if you want to process automatic payments.', 'woocommerce-subscriptions' ), '<strong><a href="' . admin_url( 'admin.php?page=wc-settings&tab=checkout&section=wc_gateway_paypal' ) . '">', '</a></strong>', '<strong><a href="https://www.woothemes.com/products/stripe/">', '</a></strong>' );
} elseif ( count( $available_gateways ) == 1 ) {
// translators: placeholder is name of a gateway
$available_gateways_description = sprintf( __( 'The %s gateway can process automatic subscription payments.', 'woocommerce-subscriptions' ), '<strong>' . $available_gateways[0] . '</strong>' );
} elseif ( count( $available_gateways ) > 1 ) {
// translators: %1$s - a comma separated list of gateway names (e.g. "stripe, paypal, worldpay"), %2$s - one name of gateway (e.g. "authorize.net")
$available_gateways_description = sprintf( __( 'The %1$s & %2$s gateways can process automatic subscription payments.', 'woocommerce-subscriptions' ), '<strong>' . implode( '</strong>, <strong>', array_slice( $available_gateways, 0, count( $available_gateways ) - 1 ) ) . '</strong>', '<strong>' . array_pop( $available_gateways ) . '</strong>' );
}
return apply_filters( 'woocommerce_subscription_settings', array(
array(
@@ -1093,7 +1048,7 @@ class WC_Subscriptions_Admin {
'default' => 'no',
'type' => 'checkbox',
// translators: placeholders are opening and closing link tags
'desc_tip' => sprintf( __( 'With manual renewals, a customer\'s subscription is put on-hold until they login and pay to renew it. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#accept-manual-renewals">', '</a>' ),
'desc_tip' => sprintf( __( 'With manual renewals, a customer\'s subscription is put on-hold until they login and pay to renew it. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#accept-manual-renewals">', '</a>' ),
'checkboxgroup' => 'start',
'show_if_checked' => 'option',
),
@@ -1104,7 +1059,7 @@ class WC_Subscriptions_Admin {
'default' => 'no',
'type' => 'checkbox',
// translators: placeholders are opening and closing link tags
'desc_tip' => sprintf( __( 'If you never want a customer to be automatically charged for a subscription renewal payment, you can turn off automatic payments completely. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#turn-off-automatic-payments">', '</a>' ),
'desc_tip' => sprintf( __( 'If you never want a customer to be automatically charged for a subscription renewal payment, you can turn off automatic payments completely. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#turn-off-automatic-payments">', '</a>' ),
'checkboxgroup' => 'end',
'show_if_checked' => 'yes',
),
@@ -1148,28 +1103,6 @@ class WC_Subscriptions_Admin {
),
array( 'type' => 'sectionend', 'id' => self::$option_prefix . '_miscellaneous' ),
array(
'name' => __( 'Payment Gateways', 'woocommerce-subscriptions' ),
'desc' => $available_gateways_description,
'id' => self::$option_prefix . '_payment_gateways_available',
'type' => 'informational',
),
array(
// translators: placeholders are opening and closing link tags
'desc' => sprintf( __( 'Other payment gateways can be used to process %smanual subscription renewal payments%s only.', 'woocommerce-subscriptions' ), '<a href="http://docs.woothemes.com/document/subscriptions/renewal-process/">', '</a>' ),
'id' => self::$option_prefix . '_payment_gateways_additional',
'type' => 'informational',
),
array(
// translators: $1-$2: opening and closing tags. Link to documents->payment gateways, 3$-4$: opening and closing tags. Link to woothemes extensions shop page
'desc' => sprintf( __( 'Find new gateways that %1$ssupport automatic subscription payments%2$s in the official %3$sWooCommerce Marketplace%4$s.', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woothemes.com/document/subscriptions/payment-gateways/' ) . '">', '</a>', '<a href="' . esc_url( 'http://www.woothemes.com/product-category/woocommerce-extensions/' ) . '">', '</a>' ),
'id' => self::$option_prefix . '_payment_gateways_additional',
'type' => 'informational',
),
) );
}
@@ -1209,7 +1142,7 @@ class WC_Subscriptions_Admin {
<p class="submit">
<a href="<?php echo esc_url( self::add_subscription_url() ); ?>" class="button button-primary"><?php esc_html_e( 'Add a Subscription Product', 'woocommerce-subscriptions' ); ?></a>
<a href="<?php echo esc_url( self::settings_tab_url() ); ?>" class="docs button button-primary"><?php esc_html_e( 'Settings', 'woocommerce-subscriptions' ); ?></a>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://www.woothemes.com/products/woocommerce-subscriptions/" data-text="Woot! I can sell subscriptions with #WooCommerce" data-via="WooThemes" data-size="large">Tweet</a>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://www.woocommerce.com/products/woocommerce-subscriptions/" data-text="Woot! I can sell subscriptions with #WooCommerce" data-via="WooThemes" data-size="large">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
</p>
</div>
@@ -1511,13 +1444,64 @@ class WC_Subscriptions_Admin {
}
/**
* Deprecated due to new meta boxes required for WC 2.2.
* Add recurring payment gateway information after the Settings->Checkout->Payment Gateways table.
* This includes links to find additional gateways, information about manual renewals
* and a warning if no payment gateway which supports automatic recurring payments is enabled/setup correctly.
*
* @deprecated 1.5.10
* @since 2.1
*/
public static function add_related_orders_meta_box() {
_deprecated_function( __METHOD__, '1.5.10', __CLASS__ . '::add_meta_boxes()' );
self::add_meta_boxes();
public static function add_recurring_payment_gateway_information( $settings ) {
$available_gateways_description = '';
if ( ! WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscriptions' ) ) {
// translators: $1-2: opening and closing tags of a link that takes to Woo marketplace / Stripe product page
$available_gateways_description = sprintf( __( 'No payment gateways capable of processing automatic subscription payments are enabled. If you would like to process automatic payments, we recommend the %1$sfree Stripe extension%2$s.', 'woocommerce-subscriptions' ), '<strong><a href="https://www.woocommerce.com/products/stripe/">', '</a></strong>' );
}
$recurring_payment_settings = array(
array(
'name' => __( 'Recurring Payments', 'woocommerce-subscriptions' ),
'desc' => $available_gateways_description,
'id' => WC_Subscriptions_Admin::$option_prefix . '_payment_gateways_available',
'type' => 'informational',
),
array(
// translators: placeholders are opening and closing link tags
'desc' => sprintf( __( 'Payment gateways which don\'t support automatic recurring payments can be used to process %smanual subscription renewal payments%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woocommerce.com/document/subscriptions/renewal-process/">', '</a>' ),
'id' => WC_Subscriptions_Admin::$option_prefix . '_payment_gateways_additional',
'type' => 'informational',
),
array(
// translators: $1-$2: opening and closing tags. Link to documents->payment gateways, 3$-4$: opening and closing tags. Link to woothemes extensions shop page
'desc' => sprintf( __( 'Find new gateways that %1$ssupport automatic subscription payments%2$s in the official %3$sWooCommerce Marketplace%4$s.', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woocommerce.com/document/subscriptions/payment-gateways/' ) . '">', '</a>', '<a href="' . esc_url( 'http://www.woocommerce.com/product-category/woocommerce-extensions/' ) . '">', '</a>' ),
'id' => WC_Subscriptions_Admin::$option_prefix . '_payment_gateways_additional',
'type' => 'informational',
),
);
$insert_index = array_search( array(
'type' => 'sectionend',
'id' => 'payment_gateways_options',
), $settings
);
// reconstruct the settings array, inserting the new settings after the payment gatways table
$checkout_settings = array();
foreach ( $settings as $key => $value ) {
$checkout_settings[ $key ] = $value;
unset( $settings[ $key ] );
if ( $key == $insert_index ) {
$checkout_settings = array_merge( $checkout_settings, $recurring_payment_settings, $settings );
break;
}
}
return $checkout_settings;
}
/**
@@ -1557,23 +1541,6 @@ class WC_Subscriptions_Admin {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Removes anything that's not a digit or a dot from a string. Sadly it assumes that the decimal separator is a dot.
* That however can be changed in WooCommerce settings, surfacing bugs such as 9,90 becoming 990, a hundred fold
* increase. Use wc_format_decimal instead.
*
* Left in for backward compatibility reasons.
*
* @deprecated 1.5.24
*/
private static function clean_number( $number ) {
_deprecated_function( __METHOD__, '1.5.23', 'wc_format_decimal()' );
$number = preg_replace( '/[^0-9\.]/', '', $number );
return $number;
}
/**
* Filter the "Orders" list to show only renewal orders associated with a specific parent order.
*

View File

@@ -43,6 +43,8 @@ class WCS_Admin_Meta_Boxes {
add_action( 'woocommerce_order_action_wcs_create_pending_renewal', __CLASS__ . '::create_pending_renewal_action_request', 10, 1 );
add_filter( 'woocommerce_resend_order_emails_available', __CLASS__ . '::remove_order_email_actions', 0, 1 );
add_action( 'woocommerce_order_action_wcs_retry_renewal_payment', __CLASS__ . '::process_retry_renewal_payment_action_request', 10, 1 );
}
/**
@@ -111,6 +113,14 @@ class WCS_Admin_Meta_Boxes {
'payment_method' => wcs_get_subscription( $post )->payment_method,
'search_customers_nonce' => wp_create_nonce( 'search-customers' ),
) ) );
} else if ( 'shop_order' == $screen->id ) {
wp_enqueue_script( 'wcs-admin-meta-boxes-order', plugin_dir_url( WC_Subscriptions::$plugin_file ) . '/assets/js/admin/wcs-meta-boxes-order.js' );
wp_localize_script( 'wcs-admin-meta-boxes-order', 'wcs_admin_order_meta_boxes', array(
'retry_renewal_payment_action_warning' => __( "Are you sure you want to retry payment for this renewal order?\n\nThis will attempt to charge the customer and send renewal order emails (if emails are enabled).", 'woocommerce-subscriptions' ),
)
);
}
}
@@ -131,6 +141,9 @@ class WCS_Admin_Meta_Boxes {
}
$actions['wcs_create_pending_renewal'] = esc_html__( 'Create pending renewal order', 'woocommerce-subscriptions' );
} else if ( self::can_renewal_order_be_retried( $theorder ) ) {
$actions['wcs_retry_renewal_payment'] = esc_html__( 'Retry Renewal Payment', 'woocommerce-subscriptions' );
}
return $actions;
@@ -181,6 +194,57 @@ class WCS_Admin_Meta_Boxes {
return $email_actions;
}
/**
* Process the action request to retry renewal payment for failed renewal orders.
*
* @param WC_Order $order
* @since 2.1
*/
public static function process_retry_renewal_payment_action_request( $order ) {
if ( self::can_renewal_order_be_retried( $order ) ) {
// init payment gateways
WC()->payment_gateways();
do_action( 'woocommerce_scheduled_subscription_payment_' . $order->payment_method, $order->get_total(), $order );
}
}
/**
* Determines if a renewal order payment can be retried. A renewal order payment can only be retried when:
* - Order is a renewal order
* - Order status is failed
* - Order payment method isn't empty
* - Order total > 0
* - Subscription/s aren't manual
* - Subscription payment method supports date changes
* - Order payment method has_action('woocommerce_scheduled_subscription_payment_..')
*
* @param WC_Order $order
* @return bool
* @since 2.1
*/
private static function can_renewal_order_be_retried( $order ) {
$can_be_retried = false;
if ( wcs_order_contains_renewal( $order ) && $order->has_status( 'failed' ) && ! empty( $order->payment_method ) && $order->get_total() > 0 ) {
$order_payment_gateway = wc_get_payment_gateway_by_order( $order );
$order_payment_gateway_supports = ( isset( $order_payment_gateway->id ) ) ? has_action( 'woocommerce_scheduled_subscription_payment_' . $order_payment_gateway->id ) : false;
foreach ( wcs_get_subscriptions_for_renewal_order( $order ) as $subscription ) {
$supports_date_changes = $subscription->payment_method_supports( 'subscription_date_changes' );
$is_automatic = ! $subscription->is_manual();
break;
}
$can_be_retried = $order_payment_gateway_supports && $supports_date_changes && $is_automatic;
}
return $can_be_retried;
}
}
new WCS_Admin_Meta_Boxes();

View File

@@ -73,19 +73,98 @@ class WCS_Admin_Post_Types {
return $pieces;
}
// we need to name ID again due to name conflict if we don't
$pieces['fields'] .= ", {$wpdb->posts}.ID AS original_id, {$wpdb->posts}.post_parent AS original_parent, CASE (SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_subscription_renewal' AND meta_value = original_id)
WHEN 0 THEN CASE (SELECT COUNT(*) FROM {$wpdb->posts} WHERE ID = original_parent)
WHEN 0 THEN 0
ELSE (SELECT post_date_gmt FROM {$wpdb->posts} WHERE ID = original_parent)
END
ELSE (SELECT p.post_date_gmt FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_subscription_renewal' AND meta_value = original_id ORDER BY p.post_date_gmt DESC LIMIT 1)
END
AS last_payment";
// Let's check whether we even have the privileges to do the things we want to do
if ( $this->is_db_user_privileged() ) {
$pieces = self::posts_clauses_high_performance( $pieces );
} else {
$pieces = self::posts_clauses_low_performance( $pieces );
}
$order = strtoupper( $query->query['order'] );
$pieces['orderby'] = "CAST(last_payment AS DATETIME) {$order}";
// fields and order are identical in both cases
$pieces['fields'] .= ', COALESCE(lp.last_payment, o.post_date_gmt, 0) as lp';
$pieces['orderby'] = "CAST(lp AS DATETIME) {$order}";
return $pieces;
}
/**
* Check is database user is capable of doing high performance things, such as creating temporary tables,
* indexing them, and then dropping them after.
*
* @return bool
*/
public function is_db_user_privileged() {
$permissions = $this->get_special_database_privileges();
return ( in_array( 'CREATE TEMPORARY TABLES', $permissions ) && in_array( 'INDEX', $permissions ) && in_array( 'DROP', $permissions ) );
}
/**
* Return the privileges a database user has out of CREATE TEMPORARY TABLES, INDEX and DROP. This is so we can use
* these discrete values on a debug page.
*
* @return array
*/
public function get_special_database_privileges() {
global $wpdb;
$permissions = $wpdb->get_col( "SELECT PRIVILEGE_TYPE FROM information_schema.user_privileges WHERE GRANTEE = CONCAT( '''', REPLACE( CURRENT_USER(), '@', '''@''' ), '''' ) AND PRIVILEGE_TYPE IN ('CREATE TEMPORARY TABLES', 'INDEX', 'DROP')" );
return $permissions;
}
/**
* Modifies the query for a slightly faster, yet still pretty slow query in case the user does not have
* the necessary privileges to run
*
* @param $pieces
*
* @return mixed
*/
private function posts_clauses_low_performance( $pieces ) {
global $wpdb;
$pieces['join'] .= "LEFT JOIN
(SELECT
MAX( p.post_date_gmt ) as last_payment,
pm.meta_value
FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_subscription_renewal'
GROUP BY pm.meta_value) lp
ON {$wpdb->posts}.ID = lp.meta_value
LEFT JOIN {$wpdb->posts} o on {$wpdb->posts}.post_parent = o.ID";
return $pieces;
}
/**
* Modifies the query in such a way that makes use of the CREATE TEMPORARY TABLE, DROP and INDEX
* MySQL privileges.
*
* @param array $pieces
*
* @return array $pieces
*/
private function posts_clauses_high_performance( $pieces ) {
global $wpdb;
// in case multiple users sort at the same time
$session = wp_get_session_token();
$table_name = substr( "{$wpdb->prefix}tmp_{$session}_lastpayment", 0, 64 );
// Let's create a temporary table, drop the previous one, because otherwise this query is hella slow
$wpdb->query( "DROP TEMPORARY TABLE IF EXISTS {$table_name}" );
$wpdb->query( "CREATE TEMPORARY TABLE {$table_name} (id INT, INDEX USING BTREE (id), last_payment DATETIME) AS SELECT pm.meta_value as id, MAX( p.post_date_gmt ) as last_payment FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_subscription_renewal' GROUP BY pm.meta_value" );
// Magic ends here
$pieces['join'] .= "LEFT JOIN {$table_name} lp
ON {$wpdb->posts}.ID = lp.id
LEFT JOIN {$wpdb->posts} o on {$wpdb->posts}.post_parent = o.ID";
return $pieces;
}
@@ -295,6 +374,8 @@ class WCS_Admin_Post_Types {
echo '<div class="error"><p>' . esc_html( $message ) . '</p></div>';
}
$_SERVER['REQUEST_URI'] = remove_query_arg( array( 'error_count', 'marked_active' ), $_SERVER['REQUEST_URI'] );
break;
}
}
@@ -481,26 +562,25 @@ class WCS_Admin_Post_Types {
if ( wc_product_sku_enabled() && $_product && $_product->get_sku() ) {
$item_name .= $_product->get_sku() . ' - ';
}
$item_name .= $item['name'];
$item_name = apply_filters( 'woocommerce_order_item_name', $item_name, $item );
$item_name .= apply_filters( 'woocommerce_order_item_name', $item['name'], $item );
$item_name = esc_html( $item_name );
if ( $item_quantity > 1 ) {
$item_name = sprintf( '%s &times; %s', absint( $item_quantity ), $item_name );
}
if ( $_product ) {
$item_name = sprintf( '<a href="%s">%s</a>', get_edit_post_link( $_product->id ), $item_name );
}
ob_start();
?>
<div class="order-item">
<?php echo wp_kses( $item_name, array( 'a' => array( 'href' => array() ) ) ); ?>
<?php if ( $item_meta_html ) : ?>
<a class="tips" href="#" data-tip="<?php echo esc_attr( $item_meta_html ); ?>">[?]</a>
<?php endif; ?>
</div>
<?php
$column_content .= ob_get_clean();
$column_content .= '<div class="order-item">';
$column_content .= wp_kses( $item_name, array( 'a' => array( 'href' => array() ) ) );
if ( $item_meta_html ) {
$column_content .= wcs_help_tip( $item_meta_html );
}
$column_content .= '</div>';
}
break;
default :
@@ -517,13 +597,22 @@ class WCS_Admin_Post_Types {
<td class="qty"><?php echo absint( $item['qty'] ); ?></td>
<td class="name">
<?php
$item_name = '';
if ( wc_product_sku_enabled() && $_product && $_product->get_sku() ) {
echo esc_html( $_product->get_sku() ) . ' - ';
$item_name .= $_product->get_sku() . ' - ';
}
echo esc_html( apply_filters( 'woocommerce_order_item_name', $item['name'], $item ) );
if ( $item_meta_html ) { ?>
<a class="tips" href="#" data-tip="<?php echo esc_attr( $item_meta_html ); ?>">[?]</a>
<?php } ?>
$item_name .= apply_filters( 'woocommerce_order_item_name', $item['name'], $item );
$item_name = esc_html( $item_name );
if ( $_product ) {
$item_name = sprintf( '<a href="%s">%s</a>', get_edit_post_link( $_product->id ), $item_name );
}
echo wp_kses( $item_name, array( 'a' => array( 'href' => array() ) ) );
if ( $item_meta_html ) {
echo wcs_help_tip( $item_meta_html );
} ?>
</td>
</tr>
<?php
@@ -565,7 +654,8 @@ class WCS_Admin_Post_Types {
break;
}
echo wp_kses( apply_filters( 'woocommerce_subscription_list_table_column_content', $column_content, $the_subscription, $column ), array( 'a' => array( 'class' => array(), 'href' => array(), 'data-tip' => array(), 'title' => array() ), 'time' => array( 'class' => array(), 'title' => array() ), 'mark' => array( 'class' => array(), 'data-tip' => array() ), 'small' => array( 'class' => array() ), 'table' => array( 'class' => array(), 'cellspacing' => array(), 'cellpadding' => array() ), 'tr' => array( 'class' => array() ), 'td' => array( 'class' => array() ), 'div' => array( 'class' => array(), 'data-tip' => array() ), 'br' => array(), 'strong' => array(), 'span' => array( 'class' => array() ), 'p' => array( 'class' => array() ), 'button' => array( 'type' => array(), 'class' => array() ) ) );
echo wp_kses( apply_filters( 'woocommerce_subscription_list_table_column_content', $column_content, $the_subscription, $column ), array( 'a' => array( 'class' => array(), 'href' => array(), 'data-tip' => array(), 'title' => array() ), 'time' => array( 'class' => array(), 'title' => array() ), 'mark' => array( 'class' => array(), 'data-tip' => array() ), 'small' => array( 'class' => array() ), 'table' => array( 'class' => array(), 'cellspacing' => array(), 'cellpadding' => array() ), 'tr' => array( 'class' => array() ), 'td' => array( 'class' => array() ), 'div' => array( 'class' => array(), 'data-tip' => array() ), 'br' => array(), 'strong' => array(), 'span' => array( 'class' => array(), 'data-tip' => array() ), 'p' => array( 'class' => array() ), 'button' => array( 'type' => array(), 'class' => array() ) ) );
}
/**
@@ -836,7 +926,7 @@ class WCS_Admin_Post_Types {
7 => __( 'Subscription saved.', 'woocommerce-subscriptions' ),
8 => __( 'Subscription submitted.', 'woocommerce-subscriptions' ),
// translators: php date string
9 => sprintf( __( 'Subscription scheduled for: %1$s.', 'woocommerce-subscriptions' ), '<strong>' . date_i18n( _x( 'M j, Y @ G:i', 'used in "Subscription scheduled for <date>"', 'woocommerce-subscriptions' ), strtotime( $post->post_date ) ) . '</strong>' ),
9 => sprintf( __( 'Subscription scheduled for: %1$s.', 'woocommerce-subscriptions' ), '<strong>' . date_i18n( _x( 'M j, Y @ G:i', 'used in "Subscription scheduled for <date>"', 'woocommerce-subscriptions' ), wcs_date_to_time( $post->post_date ) ) . '</strong>' ),
10 => __( 'Subscription draft updated.', 'woocommerce-subscriptions' ),
);

View File

@@ -0,0 +1,171 @@
<?php
/**
* Reports Admin
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
} // Exit if accessed directly
if ( class_exists( 'WCS_Admin_Reports' ) ) {
return new WCS_Admin_Reports();
}
/**
* WCS_Admin_Reports Class
*
* Handles the reports screen.
*/
class WCS_Admin_Reports {
/**
* Constructor
*/
public function __construct() {
// Add the reports layout to the WooCommerce -> Reports admin section
add_filter( 'woocommerce_admin_reports', __CLASS__ . '::initialize_reports', 12, 1 );
// Add the reports layout to the WooCommerce -> Reports admin section
add_filter( 'wc_admin_reports_path', __CLASS__ . '::initialize_reports_path', 12, 3 );
// Add any necessary scripts
add_action( 'admin_enqueue_scripts', __CLASS__ . '::reports_scripts' );
// Add any actions we need based on the screen
add_action( 'current_screen', __CLASS__ . '::conditional_reporting_includes' );
}
/**
* Add the 'Subscriptions' report type to the WooCommerce reports screen.
*
* @param array Array of Report types & their labels, excluding the Subscription product type.
* @return array Array of Report types & their labels, including the Subscription product type.
* @since 2.1
*/
public static function initialize_reports( $reports ) {
$reports['subscriptions'] = array(
'title' => __( 'Subscriptions', 'woocommerce-subscriptions' ),
'reports' => array(
'subscription_events_by_date' => array(
'title' => __( 'Subscription Events by Date', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
),
'upcoming_recurring_revenue' => array(
'title' => __( 'Upcoming Recurring Revenue', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
),
'retention_rate' => array(
'title' => __( 'Retention Rate', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
),
'subscription_by_product' => array(
'title' => __( 'Subscriptions by Product', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
),
'subscription_by_customer' => array(
'title' => __( 'Subscriptions by Customer', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
),
),
);
if ( WCS_Retry_Manager::is_retry_enabled() ) {
$reports['subscriptions']['reports']['subscription_payment_retry'] = array(
'title' => __( 'Failed Payment Retries', 'woocommerce-subscriptions' ),
'description' => '',
'hide_title' => true,
'callback' => array( 'WC_Admin_Reports', 'get_report' ),
);
}
return $reports;
}
/**
* If we hit one of our reports in the WC get_report function, change the path to our dir.
*
* @param report_path the parth to the report.
* @param name the name of the report.
* @param class the class of the report.
* @return string path to the report template.
* @since 2.1
*/
public static function initialize_reports_path( $report_path, $name, $class ) {
if ( in_array( strtolower( $class ), array( 'wc_report_subscription_events_by_date', 'wc_report_upcoming_recurring_revenue', 'wc_report_retention_rate', 'wc_report_subscription_by_product', 'wc_report_subscription_by_customer', 'wc_report_subscription_payment_retry' ) ) ) {
$report_path = dirname( __FILE__ ) . '/reports/class-wcs-report-' . $name . '.php';
}
return $report_path;
}
/**
* Add any subscriptions report javascript to the admin pages.
*
* @since 1.5
*/
public static function reports_scripts() {
global $wp_query, $post;
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
$screen = get_current_screen();
$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce-subscriptions' ) );
// Reports Subscriptions Pages
if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] ) {
wp_enqueue_script( 'wcs-reports', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/admin/reports.js', array( 'jquery', 'jquery-ui-datepicker', 'wc-reports', 'accounting' ), WC_Subscriptions::$version );
// Add currency localisation params for axis label
wp_localize_script( 'wcs-reports', 'wcs_reports', array(
'currency_format_num_decimals' => wc_get_price_decimals(),
'currency_format_symbol' => get_woocommerce_currency_symbol(),
'currency_format_decimal_sep' => esc_js( wc_get_price_decimal_separator() ),
'currency_format_thousand_sep' => esc_js( wc_get_price_thousand_separator() ),
'currency_format' => esc_js( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS
) );
wp_enqueue_script( 'flot-order', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/admin/jquery.flot.orderBars' . $suffix . '.js', array( 'jquery', 'flot' ), WC_Subscriptions::$version );
wp_enqueue_script( 'flot-axis-labels', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/admin/jquery.flot.axislabels' . $suffix . '.js', array( 'jquery', 'flot' ), WC_Subscriptions::$version );
}
}
/**
* Add any reporting files we may need conditionally
*
* @since 2.1
*/
public static function conditional_reporting_includes() {
$screen = get_current_screen();
switch ( $screen->id ) {
case 'dashboard' :
include( 'reports/class-wcs-report-dashboard.php' );
break;
}
}
}
new WCS_Admin_Reports();

View File

@@ -0,0 +1,33 @@
<?php
/**
* Automatic Failed Payment Retries Meta Box
*
* Display the automatic failed payment retries on the Edit Order screen for an order that has been automatically retried.
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Meta Boxes
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WCS_Meta_Box_Payment_Retries Class
*/
class WCS_Meta_Box_Payment_Retries {
/**
* Output the metabox
*/
public static function output( $post ) {
$retries = WCS_Retry_Manager::store()->get_retries_for_order( $post->ID );
include_once( 'views/html-retries-table.php' );
do_action( 'woocommerce_subscriptions_retries_meta_box', $post->ID, $retries );
}
}

View File

@@ -77,7 +77,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
</p>
<p class="form-field form-field-wide">
<label for="order_status"><?php esc_html_e( 'Subscription Status:', 'woocommerce-subscriptions' ); ?></label>
<label for="order_status"><?php esc_html_e( 'Subscription status:', 'woocommerce-subscriptions' ); ?></label>
<select id="order_status" name="order_status">
<?php
$statuses = wcs_get_subscription_statuses();
@@ -123,7 +123,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
// Display help tip
if ( ! empty( $subscription->payment_method ) && ! $subscription->is_manual() ) {
echo '<img class="help_tip" data-tip="Gateway ID: [' . esc_attr( $subscription->payment_gateway->id ) . ']" src="' . esc_url( WC()->plugin_url() ) . '/assets/images/help.png" height="16" width="16" />';
echo wcs_help_tip( sprintf( _x( 'Gateway ID: [%s]', 'The gateway ID displayed on the Edit Subscriptions screen when editing payment method.', 'woocommerce-subscriptions' ), $subscription->payment_gateway->id ) );
}
echo '</p>';

View File

@@ -66,7 +66,7 @@ class WCS_Meta_Box_Schedule {
continue;
}
$dates[ $date_key ] = date( 'Y-m-d H:i:s', $datetime );
$dates[ $date_key ] = gmdate( 'Y-m-d H:i:s', $datetime );
}
try {

View File

@@ -20,25 +20,11 @@ if ( ! defined( 'ABSPATH' ) ) {
</td>
<td>
<?php
$timestamp_gmt = strtotime( $order->post->post_date_gmt );
$timestamp_gmt = wcs_date_to_time( $order->post->post_date_gmt );
if ( $timestamp_gmt > 0 ) {
// translators: php date format
$t_time = get_the_time( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $order->post );
$time_diff = $timestamp_gmt - current_time( 'timestamp', true );
if ( $time_diff > 0 && $time_diff < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
$date_to_display = sprintf( __( 'In %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} elseif ( $time_diff < 0 && absint( $time_diff ) < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
$date_to_display = sprintf( __( '%s ago', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} else {
$timestamp_site = strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp_gmt ) ) );
$date_to_display = date_i18n( wc_date_format(), $timestamp_site ) . ' ' . date_i18n( wc_time_format(), $timestamp_site );
}
$date_to_display = wcs_get_human_time_diff( $timestamp_gmt );
} else {
$t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' );
} ?>

View File

@@ -0,0 +1,75 @@
<?php
/**
* Display the automatic failed payment retires for an order
*
* @var array $retries An array of WCS_Retry objects
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<div class="woocommerce_subscriptions_related_orders">
<table>
<thead>
<tr>
<th><?php esc_html_e( 'Retry Date', 'woocommerce-subscriptions' ); ?></th>
<th>
<?php esc_html_e( 'Retry Status', 'woocommerce-subscriptions' ); ?>
<?php echo wcs_help_tip( __( 'The status of the automatic payment retry: pending means the retry will be processed in the future, failed means the payment was not successful when retried and completed means the payment succeeded when retried.', 'woocommerce-subscriptions' ) ); ?>
</th>
<th>
<?php esc_html_e( 'Status of Order', 'woocommerce-subscriptions' ); ?>
<?php echo wcs_help_tip( __( 'The status applied to the order for the time between when the renewal payment failed or last retry occurred and when this retry was processed.', 'woocommerce-subscriptions' ) ); ?>
</th>
<th>
<?php esc_html_e( 'Status of Subscription', 'woocommerce-subscriptions' ); ?>
<?php echo wcs_help_tip( __( 'The status applied to the subscription for the time between when the renewal payment failed or last retry occurred and when this retry was processed.', 'woocommerce-subscriptions' ) ); ?>
</th>
<th>
<?php esc_html_e( 'Email', 'woocommerce-subscriptions' ); ?>
<?php echo wcs_help_tip( __( 'The email sent to the customer when the renewal payment or payment retry failed to notify them that the payment would be retried.', 'woocommerce-subscriptions' ) ); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ( $retries as $retry ) : ?>
<?php $rule = $retry->get_rule(); ?>
<tr>
<td>
<?php
if ( $retry->get_time() > 0 ) {
// translators: php date format
$t_time = date( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $retry->get_time() );
$date_to_display = wcs_get_human_time_diff( $retry->get_time() );
} else {
$t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' );
} ?>
<abbr title="<?php echo esc_attr( $t_time ); ?>">
<?php echo esc_html( apply_filters( 'post_date_column_time', $date_to_display, $retry->get_id() ) ); ?>
</abbr>
</td>
<td>
<?php echo esc_html( ucwords( $retry->get_status() ) ); ?>
</td>
<td>
<?php echo esc_html( ucwords( $rule->get_status_to_apply( 'order' ) ) ); ?>
</td>
<td>
<?php echo esc_html( ucwords( $rule->get_status_to_apply( 'subscription' ) ) ); ?>
</td>
<td>
<?php $email_class = $rule->get_email_template(); ?>
<?php if ( ! empty( $email_class ) && class_exists( $email_class ) ) : ?>
<?php $email = new $email_class(); ?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=email&section=' . strtolower( $email_class ) ) ); ?>">
<?php echo esc_html( $email->get_title() ); ?>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@@ -44,7 +44,7 @@ if ( ! defined( 'ABSPATH' ) ) {
</div>
<?php foreach ( wcs_get_subscription_date_types() as $date_key => $date_label ) : ?>
<?php if ( 'last_payment' === $date_key ) : ?>
<?php if ( false === wcs_display_date_type( $date_key, $the_subscription ) ) : ?>
<?php continue; ?>
<?php endif;?>
<div id="subscription-<?php echo esc_attr( $date_key ); ?>-date" class="date-fields">

View File

@@ -0,0 +1,264 @@
<?php
/**
* Subscriptions Report Cache Manager
*
* Update report data caches on appropriate events, like renewal order payment.
*
* @class WCS_Cache_Manager
* @since 2.1
* @package WooCommerce Subscriptions/Classes
* @category Class
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
} // Exit if accessed directly
class WCS_Report_Cache_Manager {
/**
* Array of event => report classes to determine which reports need to be updated on certain events.
*
* The index for each report's class is specified as its used later to determine when to schedule the report and we want
* it to be consistently at the same time, regardless of the hook which triggered the cache update. The indexes are based
* on the order of the reports in the menu on the WooCommerce > Reports > Subscriptions screen, which is why the indexes
* are not sequential (because not all reports need caching).
*
*/
private $update_events_and_classes = array(
'woocommerce_subscriptions_reports_schedule_cache_updates' => array( // a custom hook that can be called to schedule a full cache update, used by WC_Subscriptions_Upgrader
0 => 'WC_Report_Subscription_Events_By_Date',
1 => 'WC_Report_Upcoming_Recurring_Revenue',
3 => 'WC_Report_Subscription_By_Product',
4 => 'WC_Report_Subscription_By_Customer',
),
'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments
0 => 'WC_Report_Subscription_Events_By_Date',
4 => 'WC_Report_Subscription_By_Customer',
),
'woocommerce_subscriptions_switch_completed' => array(
0 => 'WC_Report_Subscription_Events_By_Date',
),
'woocommerce_subscription_status_changed' => array(
0 => 'WC_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience
4 => 'WC_Report_Subscription_By_Customer',
),
'woocommerce_subscription_status_active' => array(
1 => 'WC_Report_Upcoming_Recurring_Revenue',
),
'woocommerce_order_add_product' => array(
3 => 'WC_Report_Subscription_By_Product',
),
'woocommerce_order_edit_product' => array(
3 => 'WC_Report_Subscription_By_Product',
),
);
/**
* Record of all the report calsses to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events.
*/
private $reports_to_update = array();
/**
* The hook name to use for our WP-Cron entry for updating report cache.
*/
private $cron_hook = 'wcs_report_update_cache';
/**
* The hook name to use for our WP-Cron entry for updating report cache.
*/
protected $use_large_site_cache;
/**
* Attach callbacks to manage cache updates
*
* @since 2.1
* @return null
*/
public function __construct() {
add_action( $this->cron_hook, array( $this, 'update_cache' ), 10, 1 );
foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
add_action( $event_hook, array( $this, 'set_reports_to_update' ), 10 );
}
add_action( 'shutdown', array( $this, 'schedule_cache_updates' ), 10 );
// Notify store owners that report data can be out-of-date
add_action( 'admin_notices', array( $this, 'admin_notices' ), 0 );
}
/**
* Check if the given hook has reports associated with it, and if so, add them to our $this->reports_to_update
* property so we know to schedule an event to update their cache at the end of the request.
*
* This function is attached as a callback on the events in the $update_events_and_classes property.
*
* @since 2.1
* @return null
*/
public function set_reports_to_update() {
if ( isset( $this->update_events_and_classes[ current_filter() ] ) ) {
$this->reports_to_update = array_unique( array_merge( $this->reports_to_update, $this->update_events_and_classes[ current_filter() ] ) );
}
}
/**
* At the end of the request, schedule cache updates for any events that occured during this request.
*
* For large sites, cache updates are run only once per day to avoid overloading the DB where the queries are very resource intensive
* (as reported during beta testing in https://github.com/Prospress/woocommerce-subscriptions/issues/1732). We do this at 4am in the
* site's timezone, which helps avoid running the queries during busy periods and also runs them after all the renewals for synchronised
* subscriptions should have finished for the day (which begins at 3am and rarely takes more than 1 hours of processing to get through
* an entire queue).
*
* This function is attached as a callback on 'shutdown' and will schedule cache updates for any reports found to need updates by
* @see $this->set_reports_to_update().
*
* @since 2.1
* @return null
*/
public function schedule_cache_updates() {
if ( ! empty( $this->reports_to_update ) ) {
// On large sites, we want to run the cache update once at 4am in the site's timezone
if ( $this->use_large_site_cache() ) {
$four_am_site_time = new DateTime( '4 am', wcs_get_sites_timezone() );
// Convert to a UTC timestamp for scheduling
$cache_update_timestamp = $four_am_site_time->format( 'U' );
// PHP doesn't support a "next 4am" time format equivalent, so we need to manually handle getting 4am from earlier today (which will always happen when this is run after 4am and before midnight in the site's timezone)
if ( $cache_update_timestamp <= gmdate( 'U' ) ) {
$cache_update_timestamp += DAY_IN_SECONDS;
}
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
$cron_args = array( 'report_class' => $report_class );
if ( false === wp_next_scheduled( $this->cron_hook, $cron_args ) ) {
// Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
wp_schedule_single_event( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
}
}
} else { // Otherwise, run it 10 minutes after the last cache invalidating event
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
$cron_args = array( 'report_class' => $report_class );
if ( false !== ( $next_scheduled = wp_next_scheduled( $this->cron_hook, $cron_args ) ) ) {
wp_unschedule_event( $next_scheduled, $this->cron_hook, $cron_args );
}
// Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
wp_schedule_single_event( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
}
}
}
}
/**
* Update the cache data for a given report, as specified with $report_class, by call it's get_data() method.
*
* @since 2.1
* @return null
*/
public function update_cache( $report_class ) {
// Validate the report class
$valid_report_class = false;
foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
if ( in_array( $report_class, $report_classes ) ) {
$valid_report_class = true;
break;
}
}
if ( false === $valid_report_class ) {
return;
}
// Load report class dependencies
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
require_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
$report_name = strtolower( str_replace( '_', '-', str_replace( 'WC_Report_', '', $report_class ) ) );
$report_path = WCS_Admin_Reports::initialize_reports_path( '', $report_name, $report_class );
require_once( $report_path );
$reflector = new ReflectionMethod( $report_class, 'get_data' );
// Some report classes extend WP_List_Table which has a constructor using methods not available on WP-Cron (and unable to be loaded with a __doing_it_wrong() notice), so they have a static get_data() method and do not need to be instantiated
if ( $reflector->isStatic() ) {
call_user_func( array( $report_class, 'get_data' ), array( 'no_cache' => true ) );
} else {
$report = new $report_class();
// Classes with a non-static get_data() method can be displayed for different time series, so we need to update the cache for each of those ranges
foreach ( array( 'year', 'last_month', 'month', '7day' ) as $range ) {
$report->calculate_current_range( $range );
$report->get_data( array( 'no_cache' => true ) );
}
}
}
/**
* Boolean flag to check whether to use a the large site cache method or not, which is determined based on the number of
* subscriptions and orders on the site (using arbitrary counts).
*
* @since 2.1
* @return bool
*/
protected function use_large_site_cache() {
if ( null === $this->use_large_site_cache ) {
if ( false == get_option( 'wcs_report_use_large_site_cache' ) ) {
$subscription_counts = (array) wp_count_posts( 'shop_subscription' );
$order_counts = (array) wp_count_posts( 'shop_order' );
if ( array_sum( $subscription_counts ) > 3000 || array_sum( $order_counts ) > 25000 ) {
update_option( 'wcs_report_use_large_site_cache', 'true', false );
$this->use_large_site_cache = true;
} else {
$this->use_large_site_cache = false;
}
} else {
$this->use_large_site_cache = true;
}
}
return apply_filters( 'wcs_report_use_large_site_cache', $this->use_large_site_cache );
}
/**
* Make it clear to store owners that data for some reports can be out-of-date.
*
* @since 2.1
*/
public function admin_notices() {
$screen = get_current_screen();
$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce-subscriptions' ) );
if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] && ( ! isset( $_GET['report'] ) || in_array( $_GET['report'], array( 'subscription_events_by_date', 'upcoming_recurring_revenue', 'subscription_by_product', 'subscription_by_customer' ) ) ) && $this->use_large_site_cache() ) {
wcs_add_admin_notice( __( 'Please note: data for this report is cached. The data displayed may be out of date by up to 24 hours. The cache is updated each morning at 4am in your site\'s timezone.', 'woocommerce-subscriptions' ) );
}
}
}
return new WCS_Report_Cache_Manager();

View File

@@ -0,0 +1,100 @@
<?php
/**
* Subscriptions Admin Report - Dashboard Stats
*
* Creates the subscription admin reports area.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Report_Dashboard {
/**
* Hook in additional reporting to WooCommerce dashboard widget
*/
public function __construct() {
// Add the dashboard widget text
add_action( 'woocommerce_after_dashboard_status_widget', __CLASS__ . '::add_stats_to_dashboard' );
// Add any necessary scripts / styles
add_action( 'admin_enqueue_scripts', __CLASS__ . '::dashboard_scripts' );
}
/**
* Add the subscription specific details to the bottom of the dashboard widget
*
* @since 2.1
*/
public static function add_stats_to_dashboard() {
global $wpdb;
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
FROM {$wpdb->posts} AS wcsubs
INNER JOIN {$wpdb->posts} AS wcorder
ON wcsubs.post_parent = wcorder.ID
WHERE wcorder.post_type IN ( 'shop_order' )
AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'",
date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d H:i:s', current_time( 'timestamp' ) )
);
$signup_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) );
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcorder.ID) AS count
FROM {$wpdb->posts} AS wcorder
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
ON (
wcorder.id = meta__subscription_renewal.post_id
AND
meta__subscription_renewal.meta_key = '_subscription_renewal'
)
WHERE wcorder.post_type IN ( 'shop_order' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'",
date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d H:i:s', current_time( 'timestamp' ) )
);
$renewal_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) );
?>
<li class="signup-count">
<a href="<?php echo esc_html( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date' ) ); ?>">
<?php printf( wp_kses_post( _n( '<strong>%s signup</strong> subscription signups this month', '<strong>%s signups</strong> subscription signups this month', $signup_count, 'woocommerce-subscriptions' ) ), esc_html( $signup_count ) ); ?>
</a>
</li>
<li class="renewal-count">
<a href="<?php echo esc_html( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date' ) ); ?>">
<?php printf( wp_kses_post( _n( '<strong>%s renewal</strong> subscription renewals this month', '<strong>%s renewals</strong> subscription renewals this month', $renewal_count, 'woocommerce-subscriptions' ) ), esc_html( $renewal_count ) ); ?>
</a>
</li>
<?php
}
/**
* Add the subscription specific details to the bottom of the dashboard widget
*
* @since 2.1
*/
public static function dashboard_scripts() {
wp_enqueue_style( 'wcs-dashboard-report', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/css/dashboard.css', array(), WC_Subscriptions::$version );
}
}
return new WCS_Report_Dashboard();

View File

@@ -0,0 +1,243 @@
<?php
/**
* Subscriptions Admin Report - Retention Rate
*
* Find the number of periods between when each subscription is created and ends or ended
* then plot all subscriptions using this data to provide a curve of retention rates.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Retention_Rate extends WC_Admin_Report {
public $chart_colours = array();
private $report_data;
/**
* Get report data
*
* @since 2.1
* @return array
*/
public function get_report_data() {
if ( empty( $this->report_data ) ) {
$this->query_report_data();
}
return $this->report_data;
}
/**
* Get the number of periods each subscription has between sign-up and end.
*
* This function uses a new "living" and "age" terminology to refer to the time between when a subscription
* is created and when it ends (i.e. expires or is cancelled). The function can't use "active" because the
* subscription may not have been active all of that time. Instead, it may have been on-hold for part of it.
*
* @since 2.1
* @return null
*/
private function query_report_data() {
global $wpdb;
$this->report_data = new stdClass;
// First, let's find the age of the longest living subscription in days
$oldest_subscription_age_in_days = $wpdb->get_var( $wpdb->prepare(
"SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
WHERE posts.post_type = 'shop_subscription'
AND postmeta.meta_key = %s
AND postmeta.meta_value <> '0'
ORDER BY age_in_days DESC
LIMIT 1",
wcs_get_date_meta_key( 'end' )
) );
// Now determine what interval to use based on that length
if ( $oldest_subscription_age_in_days > 365 ) {
$this->report_data->interval_period = 'month';
} elseif ( $oldest_subscription_age_in_days > 182 ) {
$this->report_data->interval_period = 'week';
} else {
$this->report_data->interval_period = 'day';
}
// Use the number of days in the chosen interval period to determine how many periods between each start/end date
$days_in_interval_period = wcs_get_days_in_cycle( $this->report_data->interval_period, 1 );
// Find the number of these periods in the longest living subscription
$oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period );
// Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription)
$base_query = $wpdb->prepare(
"SELECT
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
COUNT(posts.ID) as count
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
ON posts.ID = cancelled_date.post_id
AND cancelled_date.meta_key = %s
AND cancelled_date.meta_value <> '0'
LEFT JOIN {$wpdb->prefix}postmeta end_date
ON posts.ID = end_date.post_id
AND end_date.meta_key = %s
WHERE posts.post_type = 'shop_subscription'
AND posts.post_status NOT IN( 'wc-pending', 'trash' )
GROUP BY periods_active
ORDER BY periods_active ASC",
$days_in_interval_period,
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subsription
wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime
wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscritions that have expired
);
$subscription_ages = $wpdb->get_results( $base_query, OBJECT_K );
$this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
$this->report_data->living_subscriptions = array();
// At day zero, no subscriptions have ended
$this->report_data->living_subscriptions[0] = $this->report_data->total_subscriptions;
// Fill out the report data to provide a smooth curve
for ( $i = 0; $i <= $oldest_subscription_age; $i++ ) {
// We want to push the the array keys ahead by one to make sure out the 0 index represents the total subscriptions
$periods_after_sign_up = $i + 1;
// Only reduce the number of living subscriptions when we have a new number for a given period as that indicates a new set of subscriptions have ended
if ( isset( $subscription_ages[ $i ] ) ) {
$this->report_data->living_subscriptions[ $periods_after_sign_up ] = $this->report_data->living_subscriptions[ $i ] - $subscription_ages[ $i ]->count;
$this->report_data->unended_subscriptions -= $subscription_ages[ $i ]->count;
} else {
$this->report_data->living_subscriptions[ $periods_after_sign_up ] = $this->report_data->living_subscriptions[ $i ];
}
}
}
/**
* Output the report
*
* Use a custom report as we don't need the date filters provided by the WooCommerce html-report-by-date.php template.
*
* @since 2.1
* @return null
*/
public function output_report() {
include( plugin_dir_path( WC_Subscriptions::$plugin_file ) . '/includes/admin/views/html-report-by-period.php' );
}
/**
* Output the HTML and JavaScript to plot the chart
*
* @since 2.1
* @return null
*/
public function get_main_chart() {
$this->get_report_data();
$data_to_plot = array();
foreach ( $this->report_data->living_subscriptions as $periods_since_sign_up => $living_subscription_count ) {
$data_to_plot[] = array(
absint( $periods_since_sign_up ),
absint( $living_subscription_count ),
);
}
switch ( $this->report_data->interval_period ) {
case 'day':
$x_axes_label = _x( 'Number of days after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
break;
case 'week':
$x_axes_label = _x( 'Number of weeks after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
break;
case 'month':
$x_axes_label = _x( 'Number of months after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
break;
}
?>
<div class="chart-container" id="woocommerce_subscriptions_retention_chart">
<div class="chart-placeholder main"></div>
</div>
<script type="text/javascript">
var main_chart;
jQuery(function(){
var subscription_lifespans = jQuery.parseJSON( '<?php echo json_encode( $data_to_plot ); ?>' ),
unended_subscriptions = <?php echo esc_js( $this->report_data->unended_subscriptions ); ?>;
var drawGraph = function( highlight ) {
var series = [
{
data: subscription_lifespans,
color: '#5da5da',
points: { show: true, radius: 4, lineWidth: 3, fillColor: '#efefef', fill: true },
lines: { show: true, lineWidth: 4, fill: false },
shadowSize: 0
},
];
main_chart = jQuery.plot(
jQuery('.chart-placeholder.main'),
series,
{
legend: {
show: false
},
axisLabels: {
show: true
},
grid: {
color: '#aaa',
borderColor: 'transparent',
borderWidth: 0,
hoverable: true,
markings: [ {
xaxis: { from: 1, to: 1 },
yaxis: { from: 1, to: 1 },
color: "#ccc"
} ]
},
xaxes: [ {
color: '#aaa',
position: "bottom",
tickDecimals: 0,
axisLabel: "<?php echo esc_js( $x_axes_label ); ?>",
axisLabelPadding: 18,
font: {
color: "#aaa"
}
} ],
yaxes: [ {
min: unended_subscriptions - 1, // exaggerate change by only plotting between total subscription count and unended count
minTickSize: 1,
tickDecimals: 0,
color: '#d4d9dc',
axisLabel: "<?php echo esc_js( __( 'Unended Subscription Count', 'woocommerce-subscriptions' ) ); ?>",
axisLabelPadding: 18,
font: {
color: "#aaa"
}
} ],
}
);
jQuery('.chart-placeholder').resize();
}
drawGraph();
});
</script>
<?php
}
}

View File

@@ -0,0 +1,278 @@
<?php
/**
* Subscriptions Admin Report - Subscriptions by customer
*
* Creates the subscription admin reports area.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Subscription_By_Customer extends WP_List_Table {
private $totals;
/**
* Constructor.
*/
public function __construct() {
parent::__construct( array(
'singular' => __( 'Customer', 'woocommerce-subscriptions' ),
'plural' => __( 'Customers', 'woocommerce-subscriptions' ),
'ajax' => false,
) );
}
/**
* No subscription products found text.
*/
public function no_items() {
esc_html_e( 'No customers found.', 'woocommerce-subscriptions' );
}
/**
* Output the report.
*/
public function output_report() {
$this->prepare_items();
echo '<div id="poststuff" class="woocommerce-reports-wide">';
echo ' <div id="postbox-container-1" class="postbox-container" style="width: 280px;"><div class="postbox" style="padding: 10px;">';
echo ' <h3>' . esc_html__( 'Customer Totals', 'woocommerce-subscriptions' ) . '</h3>';
echo ' <p><strong>' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->total_customers ) . '<br />';
echo ' <strong>' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->active_subscriptions ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->total_subscriptions ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . '<br />';
echo ' <strong>' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . '</strong> : ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . '</p>';
echo '</div></div>';
$this->display();
echo '</div>';
}
/**
* Get column value.
*
* @param WP_User $user
* @param string $column_name
* @return string
*/
public function column_default( $user, $column_name ) {
global $wpdb;
switch ( $column_name ) {
case 'customer_name' :
$user_info = get_userdata( $user->customer_id );
return '<a href="' . get_edit_user_link( $user->customer_id ) . '">' . $user_info->user_email . '</a>';
case 'active_subscription_count' :
return $user->active_subscriptions;
case 'total_subscription_count' :
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions );
case 'total_subscription_order_count' :
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_order&_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count );
case 'customer_lifetime_value' :
return wc_price( $user->initial_order_total + $user->renewal_switch_total );
}
return '';
}
/**
* Get columns.
*
* @return array
*/
public function get_columns() {
$columns = array(
'customer_name' => __( 'Customer', 'woocommerce-subscriptions' ),
'active_subscription_count' => sprintf( __( 'Active Subscriptions %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The number of subscriptions this customer has with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) ),
'total_subscription_count' => sprintf( __( 'Total Subscriptions %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The number of subscriptions this customer has with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) ),
'total_subscription_order_count' => sprintf( __( 'Total Subscription Orders %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The number of sign-up, switch and renewal orders this customer has placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) ),
'customer_lifetime_value' => sprintf( __( 'Lifetime Value from Subscriptions %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The total value of this customer\'s sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) ),
);
return $columns;
}
/**
* Prepare subscription list items.
*/
public function prepare_items() {
global $wpdb;
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
$current_page = absint( $this->get_pagenum() );
$per_page = absint( apply_filters( 'wcs_reports_customers_per_page', 20 ) );
$offset = absint( ( $current_page - 1 ) * $per_page );
$this->totals = self::get_data();
$customer_query = apply_filters( 'wcs_reports_current_customer_query',
"SELECT customer_ids.meta_value as customer_id,
COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE
WHEN subscription_posts.post_status
IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1
ELSE 0
END) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts
INNER JOIN {$wpdb->postmeta} customer_ids
ON customer_ids.post_id = subscription_posts.ID
AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' )
LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
GROUP BY customer_ids.meta_value
ORDER BY customer_id DESC
LIMIT {$offset}, {$per_page}" );
$this->items = $wpdb->get_results( $customer_query );
// Now get each customer's renewal and switch total
$customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query',
"SELECT
customer_ids.meta_value as customer_id,
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
ON renewal_order_ids.meta_value = subscription_posts.ID
AND subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
INNER JOIN {$wpdb->postmeta} customer_ids
ON renewal_order_ids.meta_value = customer_ids.post_id
AND customer_ids.meta_key = '_customer_user'
AND customer_ids.meta_value IN ('" . implode( "','", wp_list_pluck( $this->items, 'customer_id' ) ) . "' )
INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'
GROUP BY customer_id
ORDER BY customer_id"
);
$customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K );
foreach ( $this->items as $index => $item ) {
if ( isset( $customer_renewal_switch_totals[ $item->customer_id ] ) ) {
$this->items[ $index ]->renewal_switch_total = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_total;
$this->items[ $index ]->renewal_switch_count = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_count;
} else {
$this->items[ $index ]->renewal_switch_total = $this->items[ $index ]->renewal_switch_count = 0;
}
}
/**
* Pagination.
*/
$this->set_pagination_args( array(
'total_items' => $this->totals->total_customers,
'per_page' => $per_page,
'total_pages' => ceil( $this->totals->total_customers / $per_page ),
) );
}
/**
* Gather totals for customers
*/
public static function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
);
$args = apply_filters( 'wcs_reports_customer_total_args', $args );
$args = wp_parse_args( $args, $default_args );
$total_query = apply_filters( 'wcs_reports_customer_total_query',
"SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers,
COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE
WHEN subscription_posts.post_status
IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1
ELSE 0
END) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts
INNER JOIN {$wpdb->postmeta} customer_ids
ON customer_ids.post_id = subscription_posts.ID
AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
");
$cached_results = get_transient( strtolower( __CLASS__ ) );
$query_hash = md5( $total_query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
// Enable big selects for reports
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
$customer_totals = $cached_results[ $query_hash ];
$renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query',
"SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
ON renewal_order_ids.meta_value = subscription_posts.ID
AND subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'"
);
$query_hash = md5( $renewal_switch_total_query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
// Enable big selects for reports
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
$customer_totals->renewal_switch_total = $cached_results[ $query_hash ]->renewal_switch_total;
$customer_totals->renewal_switch_count = $cached_results[ $query_hash ]->renewal_switch_count;
return $customer_totals;
}
}

View File

@@ -0,0 +1,260 @@
<?php
/**
* Subscriptions Admin Report - Subscriptions by product
*
* Creates the subscription admin reports area.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Subscription_By_Product extends WP_List_Table {
/**
* Constructor.
*/
public function __construct() {
parent::__construct( array(
'singular' => __( 'Product', 'woocommerce-subscriptions' ),
'plural' => __( 'Products', 'woocommerce-subscriptions' ),
'ajax' => false,
) );
}
/**
* No subscription products found text.
*/
public function no_items() {
esc_html_e( 'No products found.', 'woocommerce-subscriptions' );
}
/**
* Output the report.
*/
public function output_report() {
$this->prepare_items();
echo '<div id="poststuff" class="woocommerce-reports-wide" style="width:50%; float: left; min-width: 0px;">';
$this->display();
echo '</div>';
$this->product_breakdown_chart();
}
/**
* Get column value.
*
* @param object $report_item
* @param string $column_name
* @return string
*/
public function column_default( $report_item, $column_name ) {
global $wpdb;
switch ( $column_name ) {
case 'product_name' :
return edit_post_link( $report_item->product_name, null, null, $report_item->product_id );
case 'subscription_count' :
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_subscription&_wcs_product=' ), $report_item->product_id, $report_item->subscription_count );
case 'average_recurring_total' :
$average_subscription_amount = ( 0 !== $report_item->subscription_count ? wc_price( $report_item->recurring_total / $report_item->subscription_count ) : '-' );
return $average_subscription_amount;
case 'average_lifetime_value' :
$average_subscription_amount = ( 0 !== $report_item->subscription_count ? wc_price( $report_item->product_total / $report_item->subscription_count ) : '-' );
return $average_subscription_amount;
}
return '';
}
/**
* Get columns.
*
* @return array
*/
public function get_columns() {
$columns = array(
'product_name' => __( 'Subscription Product', 'woocommerce-subscriptions' ),
'subscription_count' => sprintf( __( 'Subscription Count %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The number of subscriptions that include this product as a line item and have a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) ),
'average_recurring_total' => sprintf( __( 'Average Recurring Line Total %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The average line total for this product on each subscription.', 'woocommerce-subscriptions' ) ) ),
'average_lifetime_value' => sprintf( __( 'Average Lifetime Value %s', 'woocommerce-subscriptions' ), wcs_help_tip( __( 'The average line total on all orders for this product line item.', 'woocommerce-subscriptions' ) ) ),
);
return $columns;
}
/**
* Prepare subscription list items.
*/
public function prepare_items() {
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
$this->items = self::get_data();
}
/**
* Get subscription product data, either from the cache or the database.
*/
public static function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
);
$args = apply_filters( 'wcs_reports_product_args', $args );
$args = wp_parse_args( $args, $default_args );
$query = apply_filters( 'wcs_reports_product_query',
"SELECT product.id as product_id,
product.post_title as product_name,
mo.product_type,
COUNT(subscription_line_items.subscription_id) as subscription_count,
SUM(subscription_line_items.product_total) as recurring_total
FROM {$wpdb->posts} AS product
LEFT JOIN (
SELECT tr.object_id AS product_id, t.slug AS product_type
FROM {$wpdb->prefix}term_relationships AS tr
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
INNER JOIN {$wpdb->prefix}terms AS t
ON t.term_id = x.term_id
) AS mo
ON product.id = mo.product_id
LEFT JOIN (
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE wcoitems.order_item_type = 'line_item'
AND wcoimeta.meta_key = '_product_id'
AND wcoimeta2.meta_key = '_line_total'
) as subscription_line_items
ON product.id = subscription_line_items.product_id
LEFT JOIN {$wpdb->posts} as subscriptions
ON subscriptions.ID = subscription_line_items.subscription_id
WHERE product.post_status = 'publish'
AND product.post_type = 'product'
AND subscriptions.post_type = 'shop_subscription'
AND subscriptions.post_status NOT IN( 'wc-pending', 'trash' )
GROUP BY product.id
ORDER BY COUNT(subscription_line_items.subscription_id) DESC" );
$cached_results = get_transient( strtolower( __CLASS__ ) );
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
$report_data = $cached_results[ $query_hash ];
// Now let's get the total revenue for each product so we can provide an average lifetime value for that product
$query = apply_filters( 'wcs_reports_product_lifetime_value_query',
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->posts} AS wcorders
ON wcoitems.order_id = wcorders.ID
AND wcorders.post_type = 'shop_order'
AND wcorders.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE wcoimeta.meta_key = '_product_id'
AND wcoimeta2.meta_key = '_line_total'
GROUP BY product_id" );
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_lifetime_value_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
// Add the product total to each item
foreach ( array_keys( $report_data ) as $product_id ) {
$report_data[ $product_id ]->product_total = isset( $cached_results[ $query_hash ][ $product_id ] ) ? $cached_results[ $query_hash ][ $product_id ]->product_total : 0;
}
return $report_data;
}
/**
* Output product breakdown chart.
*/
public function product_breakdown_chart() {
$chart_colors = array( '#33a02c', '#1f78b4', '#6a3d9a', '#e31a1c', '#ff7f00', '#b15928', '#a6cee3', '#b2df8a', '#fb9a99', '#ffff99', '#fdbf6f', '#cab2d6' );
//We only will display the first 12 plans in the chart
$products = array_slice( $this->items, 0, 12 );
?>
<div class="chart-container" style="float: left; padding-top: 50px; min-width: 0px;">
<div class="data-container" style="display: inline-block; margin-left: 30px; border: 1px solid #e5e5e5; background-color: #FFF; padding: 20px;">
<div class="chart-placeholder product_breakdown_chart pie-chart" style="height:200px; width: 200px; float: left;"></div>
<div class="legend-container" style="margin-left: 10px; float: left;"></div>
<div style="clear:both;"></div>
</div>
</div>
<script type="text/javascript">
jQuery(function(){
jQuery.plot(
jQuery('.chart-placeholder.product_breakdown_chart'),
[
<?php
$i = 0;
foreach ( $products as $product ) {
?>
{
label: '<?php echo esc_js( $product->product_name ); ?>',
data: '<?php echo esc_js( $product->subscription_count ); ?>',
color: '<?php echo esc_js( $chart_colors[ $i ] ); ?>'
},
<?php
$i++;
}
?>
],
{
grid: {
hoverable: true
},
series: {
pie: {
show: true,
radius: 1,
innerRadius: 0.6,
label: {
show: false
}
},
enable_tooltip: true,
append_tooltip: "<?php echo ' ' . esc_js( __( 'subscriptions', 'woocommerce-subscriptions' ) ); ?>",
},
legend: {
show: true,
container: jQuery('.legend-container'),
}
}
);
jQuery('.chart-placeholder.product_breakdown_chart').resize();
});
</script>
<?php
}
}

View File

@@ -0,0 +1,921 @@
<?php
/**
* Subscriptions Admin Report - Subscription Events by Date
*
* Display important historical data for subscription revenue and events, like switches and cancellations.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Subscription_Events_By_Date extends WC_Admin_Report {
public $chart_colours = array();
private $report_data;
/**
* Get report data
* @return array
*/
public function get_report_data() {
if ( empty( $this->report_data ) ) {
$this->get_data();
}
return $this->report_data;
}
/**
* Get all data needed for this report and store in the class
*/
public function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
'order_status' => apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ),
);
$args = apply_filters( 'wcs_reports_subscription_events_args', $args );
$args = wp_parse_args( $args, $default_args );
$query_end_date = date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) );
$this->report_data = new stdClass;
$this->report_data->new_subscriptions = (array) $this->get_order_report_data(
array(
'data' => array(
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'count',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
),
'group_by' => $this->group_by_query,
'order_status' => '',
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
'order_types' => array( 'shop_subscription' ),
'nocache' => $args['no_cache'],
)
);
$this->report_data->renewal_data = (array) $this->get_order_report_data(
array(
'data' => array(
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'count',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
'_subscription_renewal' => array(
'type' => 'meta',
'function' => '',
'name' => 'renewal_orders',
),
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'renewal_totals',
'join_type' => 'LEFT', // To avoid issues if there is no renewal_total meta
),
),
'group_by' => $this->group_by_query,
'order_status' => $args['order_status'],
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
'order_types' => wc_get_order_types( 'order-count' ),
'nocache' => $args['no_cache'],
)
);
$this->report_data->resubscribe_data = (array) $this->get_order_report_data(
array(
'data' => array(
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'count',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
'_subscription_resubscribe' => array(
'type' => 'meta',
'function' => '',
'name' => 'resubscribe_orders',
),
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'resubscribe_totals',
'join_type' => 'LEFT', // To avoid issues if there is no resubscribe_total meta
),
),
'group_by' => $this->group_by_query,
'order_status' => $args['order_status'],
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
'order_types' => wc_get_order_types( 'order-count' ),
'nocache' => $args['no_cache'],
)
);
$this->report_data->switch_counts = (array) $this->get_order_report_data(
array(
'data' => array(
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'count',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date',
),
'_subscription_switch' => array(
'type' => 'meta',
'function' => '',
'name' => 'switch_orders',
),
),
'group_by' => $this->group_by_query,
'order_status' => $args['order_status'],
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
'order_types' => wc_get_order_types( 'order-count' ),
'nocache' => $args['no_cache'],
)
);
$cached_results = get_transient( strtolower( get_class( $this ) ) );
/*
* New subscription orders
*/
$query = $wpdb->prepare(
"SELECT SUM(subscriptions.count) as count,
order_posts.post_date as post_date,
SUM(order_total_post_meta.meta_value) as signup_totals
FROM {$wpdb->posts} AS order_posts
INNER JOIN (
SELECT COUNT(DISTINCT(subscription_posts.ID)) as count,
subscription_posts.post_parent as order_id
FROM {$wpdb->posts} as subscription_posts
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_date >= %s
AND subscription_posts.post_date < %s
GROUP BY order_id
) AS subscriptions ON subscriptions.order_id = order_posts.ID
LEFT JOIN {$wpdb->postmeta} AS order_total_post_meta
ON order_posts.ID = order_total_post_meta.post_id
WHERE order_posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' )
AND order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
AND order_posts.post_date >= %s
AND order_posts.post_date < %s
AND order_total_post_meta.meta_key = '_order_total'
GROUP BY YEAR(order_posts.post_date), MONTH(order_posts.post_date), DAY(order_posts.post_date)
ORDER BY post_date ASC",
date( 'Y-m-d', $this->start_date ),
$query_end_date,
date( 'Y-m-d', $this->start_date ),
$query_end_date
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_sign_up_data', (array) $wpdb->get_results( $query ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
$this->report_data->signup_data = $cached_results[ $query_hash ];
/*
* Subscribers by date
*/
$query = $wpdb->prepare(
"SELECT searchdate.Date as date, COUNT( DISTINCT wcsubs.ID) as count
FROM (
SELECT DATE_FORMAT(a.Date,'%%Y-%%m-%%d') as Date, 0 as cnt
FROM (
SELECT DATE(%s) - INTERVAL(a.a + (10 * b.a) + (100 * c.a)) DAY as Date
FROM (
SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9
) as a
CROSS JOIN (
SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9
) as b
CROSS JOIN (
SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2
UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9
) AS c
) a
WHERE a.Date >= %s AND a.Date <= %s
) searchdate
LEFT JOIN (
{$wpdb->posts} AS wcsubs
LEFT JOIN {$wpdb->postmeta} AS wcsmeta
ON wcsubs.ID = wcsmeta.post_id AND wcsmeta.meta_key = %s
) ON DATE( wcsubs.post_date ) <= searchdate.Date
AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcsubs.post_status NOT IN( 'wc-pending', 'trash' )
AND (
DATE( wcsmeta.meta_value ) >= searchdate.Date
OR wcsmeta.meta_value = 0
OR wcsmeta.meta_value IS NULL
)
GROUP BY searchdate.Date
ORDER BY searchdate.Date ASC",
$query_end_date,
date( 'Y-m-d', $this->start_date ),
$query_end_date,
wcs_get_date_meta_key( 'end' )
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_subscriber_count_data', (array) $wpdb->get_results( $query ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
$this->report_data->subscriber_counts = $cached_results[ $query_hash ];
/*
* Subscription cancellations
*/
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) as count, wcsmeta_cancel.meta_value as cancel_date
FROM {$wpdb->posts} as wcsubs
JOIN {$wpdb->postmeta} AS wcsmeta_cancel
ON wcsubs.ID = wcsmeta_cancel.post_id
AND wcsmeta_cancel.meta_key = %s
WHERE wcsmeta_cancel.meta_value BETWEEN %s AND %s
GROUP BY YEAR(wcsmeta_cancel.meta_value), MONTH(wcsmeta_cancel.meta_value), DAY(wcsmeta_cancel.meta_value)
ORDER BY wcsmeta_cancel.meta_value ASC",
wcs_get_date_meta_key( 'cancelled' ),
date( 'Y-m-d', $this->start_date ),
$query_end_date
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_cancel_count_data', (array) $wpdb->get_results( $query ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
$this->report_data->cancel_counts = $cached_results[ $query_hash ];
/*
* Subscriptions ended
*/
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) as count, wcsmeta_end.meta_value as end_date
FROM {$wpdb->posts} as wcsubs
JOIN {$wpdb->postmeta} AS wcsmeta_end
ON wcsubs.ID = wcsmeta_end.post_id
AND wcsmeta_end.meta_key = %s
WHERE
wcsmeta_end.meta_value BETWEEN %s AND %s
GROUP BY YEAR(wcsmeta_end.meta_value), MONTH(wcsmeta_end.meta_value), DAY(wcsmeta_end.meta_value)
ORDER BY wcsmeta_end.meta_value ASC",
wcs_get_date_meta_key( 'end' ),
date( 'Y-m-d', $this->start_date ),
$query_end_date
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_subscription_events_ended_count_data', (array) $wpdb->get_results( $query ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
$this->report_data->ended_counts = $cached_results[ $query_hash ];
// Total up the query data
$this->report_data->signup_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ) );
$this->report_data->renewal_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) );
$this->report_data->resubscribe_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ) );
$this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) );
$this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) );
$this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
$this->report_data->resubscribe_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'count' ) ) );
$this->report_data->switch_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->switch_counts, 'count' ) ) );
$this->report_data->total_subscriptions_cancelled = absint( array_sum( wp_list_pluck( $this->report_data->cancel_counts, 'count' ) ) );
$this->report_data->total_subscriptions_ended = absint( array_sum( wp_list_pluck( $this->report_data->ended_counts, 'count' ) ) );
$this->report_data->total_subscriptions_at_period_end = absint( end( $this->report_data->subscriber_counts )->count );
$this->report_data->total_subscriptions_at_period_start = isset( $this->report_data->subscriber_counts[0]->count ) ? absint( $this->report_data->subscriber_counts[0]->count ) : 0;
}
/**
* Get the legend for the main chart sidebar
* @return array
*/
public function get_chart_legend() {
$legend = array();
$data = $this->get_report_data();
$legend[] = array(
'title' => sprintf( __( '%s signup revenue in this period', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $data->signup_orders_total_amount ) . '</strong>' ),
'placeholder' => __( 'The sum of all subscription parent orders, including other items, fees, tax and shipping.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['signup_total'],
'highlight_series' => 8,
);
$legend[] = array(
'title' => sprintf( __( '%s renewal revenue in this period', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $data->renewal_orders_total_amount ) . '</strong>' ),
'placeholder' => __( 'The sum of all renewal orders including tax and shipping.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['renewal_total'],
'highlight_series' => 10,
);
$legend[] = array(
'title' => sprintf( __( '%s resubscribe revenue in this period', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $data->resubscribe_orders_total_amount ) . '</strong>' ),
'placeholder' => __( 'The sum of all resubscribe orders including tax and shipping.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['resubscribe_total'],
'highlight_series' => 9,
);
$legend[] = array(
'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '<strong>' . $this->report_data->new_subscription_total_count . '</strong>' ),
'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['new_count'],
'highlight_series' => 1,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription signups', 'woocommerce-subscriptions' ), '<strong>' . $this->report_data->signup_orders_total_count . '</strong>' ),
'placeholder' => __( 'The number of subscription parent orders created during this period. This represents the new subscriptions created by customers placing an order via checkout.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['signup_count'],
'highlight_series' => 2,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription resubscribes', 'woocommerce-subscriptions' ), '<strong>' . $data->resubscribe_orders_total_count . '</strong>' ),
'placeholder' => __( 'The number of resubscribe orders processed during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['resubscribe_count'],
'highlight_series' => 3,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription renewals', 'woocommerce-subscriptions' ), '<strong>' . $data->renewal_orders_total_count . '</strong>' ),
'placeholder' => __( 'The number of renewal orders processed during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['renewal_count'],
'highlight_series' => 4,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription switches', 'woocommerce-subscriptions' ), '<strong>' . $data->switch_orders_total_count . '</strong>' ),
'placeholder' => __( 'The number of subscriptions upgraded, downgraded or cross-graded during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['switch_count'],
'highlight_series' => 0,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription cancellations', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_cancelled . '</strong>' ),
'placeholder' => __( 'The number of subscriptions cancelled by the customer or store manager during this period. The pre-paid term may not yet have ended during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['cancel_count'],
'highlight_series' => 7,
);
$legend[] = array(
'title' => sprintf( __( '%s subscriptions ended', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_ended . '</strong>' ),
'placeholder' => __( 'The number of subscriptions which have either expired or reached the end of the prepaid term if it was previously cancelled.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['ended_count'],
'highlight_series' => 6,
);
$legend[] = array(
'title' => sprintf( __( '%s current subscriptions', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_at_period_end . '</strong>' ),
'placeholder' => __( 'The number of subscriptions during this period with an end date in the future and a status other than pending.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['subscriber_count'],
'highlight_series' => 5,
);
$subscription_change_count = ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start > 0 ) ? '+' . ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start ) : ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start );
if ( $data->total_subscriptions_at_period_start === 0 ) {
$subscription_change_percent = '&#x221e;%'; // infinite percentage increase if the starting subs is 0
} elseif ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start >= 0 ) {
$subscription_change_percent = '+' . number_format( ( ( ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start ) / $data->total_subscriptions_at_period_start ) * 100 ), 2 ) . '%';
} else {
$subscription_change_percent = number_format( ( ( ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start ) / $data->total_subscriptions_at_period_start ) * 100 ), 2 ) . '%';
}
if ( $data->total_subscriptions_at_period_end - $data->total_subscriptions_at_period_start >= 0 ) {
$legend_title = __( '%s net subscription gain', 'woocommerce-subscriptions' );
} else {
$legend_title = __( '%s net subscription loss', 'woocommerce-subscriptions' );
}
$legend[] = array(
'title' => sprintf( $legend_title, '<strong>' . $subscription_change_count . ' <span style="font-size:65%;">(' . $subscription_change_percent . ')</span></strong>' ),
'placeholder' => __( 'Change in subscriptions between the start and end of the period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['subscriber_change'],
'highlight_series' => 5,
);
return $legend;
}
/**
* Output the report
*/
public function output_report() {
$ranges = array(
'year' => __( 'Year', 'woocommerce-subscriptions' ),
'last_month' => __( 'Last Month', 'woocommerce-subscriptions' ),
'month' => __( 'This Month', 'woocommerce-subscriptions' ),
'7day' => __( 'Last 7 Days', 'woocommerce-subscriptions' ),
);
$this->chart_colours = array(
'signup_total' => '#439ad9',
'renewal_total' => '#b1d4ea',
'resubscribe_total' => '#7ab7e2',
'new_count' => '#9adbb5',
'signup_count' => '#5cc488',
'resubscribe_count' => '#449163',
'renewal_count' => '#b9e6cc',
'switch_count' => '#f1c40f',
'cancel_count' => '#e74c3c',
'ended_count' => '#f8ccc7',
'subscriber_count' => '#ecf0f1',
'subscriber_change' => '#ecf0f1',
);
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
$current_range = '7day';
}
$this->calculate_current_range( $current_range );
include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' );
}
/**
* Output an export link
*/
public function get_export_button() {
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
?>
<a
href="#"
download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
class="export_csv"
data-export="chart"
data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce-subscriptions' ); ?>"
data-exclude_series="2"
data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
>
<?php esc_attr_e( 'Export CSV', 'woocommerce-subscriptions' ); ?>
</a>
<?php
}
/**
* Get the main chart
*
* @return string
*/
public function get_main_chart() {
global $wp_locale;
// Prepare data for report
$signup_orders_amount = $this->prepare_chart_data( $this->report_data->signup_data, 'post_date', 'signup_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_orders_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
$resubscribe_orders_amount = $this->prepare_chart_data( $this->report_data->resubscribe_data, 'post_date', 'resubscribe_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
$new_subscriptions_count = $this->prepare_chart_data( $this->report_data->new_subscriptions, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$signup_orders_count = $this->prepare_chart_data( $this->report_data->signup_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_orders_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$resubscribe_orders_count = $this->prepare_chart_data( $this->report_data->resubscribe_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$switch_orders_count = $this->prepare_chart_data( $this->report_data->switch_counts, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$subscriber_count = $this->prepare_chart_data_daily_average( $this->report_data->subscriber_counts, 'date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$cancel_count = $this->prepare_chart_data( $this->report_data->cancel_counts, 'cancel_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$ended_count = $this->prepare_chart_data( $this->report_data->ended_counts, 'end_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
// Encode in json format
$chart_data = array(
'signup_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $signup_orders_amount ) ),
'renewal_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $renewal_orders_amount ) ),
'resubscribe_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $resubscribe_orders_amount ) ),
'new_subscriptions_count' => array_values( $new_subscriptions_count ),
'signup_orders_count' => array_values( $signup_orders_count ),
'renewal_orders_count' => array_values( $renewal_orders_count ),
'resubscribe_orders_count' => array_values( $resubscribe_orders_count ),
'switch_orders_count' => array_values( $switch_orders_count ),
'subscriber_count' => array_values( $subscriber_count ),
'cancel_count' => array_values( $cancel_count ),
'ended_count' => array_values( $ended_count ),
);
$timeformat = ( $this->chart_groupby == 'day' ? '%d %b' : '%b' );
?>
<div class="chart-container">
<div class="chart-placeholder main"></div>
</div>
<script type="text/javascript">
var main_chart;
jQuery(function(){
var order_data = jQuery.parseJSON( '<?php echo json_encode( $chart_data ); ?>' );
var drawGraph = function( highlight ) {
var series = [
{
label: "<?php echo esc_js( __( 'Switched subscriptions', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.switch_orders_count,
color: '<?php echo esc_js( $this->chart_colours['switch_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['switch_count'] ); ?>',
order: 0,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'New Subscriptions', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.new_subscriptions_count,
color: '<?php echo esc_js( $this->chart_colours['new_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['new_count'] ); ?>',
order: 1,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Subscriptions signups', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.signup_orders_count,
color: '<?php echo esc_js( $this->chart_colours['signup_count'] ); ?>',
bars: {
order: 1,
fill: 0.5,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Number of resubscribes', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.resubscribe_orders_count,
color: '<?php echo esc_js( $this->chart_colours['resubscribe_count'] ); ?>',
bars: {
order: 1,
fill: 0.5,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Number of renewals', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.renewal_orders_count,
color: '<?php echo esc_js( $this->chart_colours['renewal_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['renewal_count'] ); ?>',
order: 2,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Subscriptions', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.subscriber_count,
color: '<?php echo esc_js( $this->chart_colours['subscriber_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['subscriber_count'] ); ?>',
order: 3,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Subscriptions Ended', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.ended_count,
color: '<?php echo esc_js( $this->chart_colours['ended_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['ended_count'] ); ?>',
order: 3,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Cancellations', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.cancel_count,
color: '<?php echo esc_js( $this->chart_colours['cancel_count'] ); ?>',
bars: {
order: 3,
fill: 0.5,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.25,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Signup Totals', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.signup_orders_amount,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['signup_total'] ); ?>',
points: {
show: true,
radius: 5,
lineWidth: 5,
fillColor: '#fff',
fill: true
},
lines: {
show: true,
lineWidth: 4,
fill: false
},
shadowSize: 0,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
{
label: "<?php echo esc_js( __( 'Resubscribe Totals', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.resubscribe_orders_amount,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['resubscribe_total'] ); ?>',
points: {
show: true,
radius: 5,
lineWidth: 4,
fillColor: '#fff',
fill: true
},
lines: {
show: true,
lineWidth: 5,
fill: false
},
shadowSize: 0,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
{
label: "<?php echo esc_js( __( 'Renewal Totals', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.renewal_orders_amount,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['renewal_total'] ); ?>',
points: {
show: true,
radius: 5,
lineWidth: 4,
fillColor: '#fff',
fill: true
},
lines: {
show: true,
lineWidth: 5,
fill: false
},
shadowSize: 0,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
];
if ( highlight !== 'undefined' && series[ highlight ] ) {
highlight_series = series[ highlight ];
highlight_series.color = '#9c5d90';
if ( highlight_series.bars ) {
highlight_series.bars.fillColor = '#9c5d90';
}
if ( highlight_series.lines ) {
highlight_series.lines.lineWidth = 5;
}
}
main_chart = jQuery.plot(
jQuery('.chart-placeholder.main'),
series,
{
legend: {
show: false
},
grid: {
color: '#aaa',
borderColor: 'transparent',
borderWidth: 0,
hoverable: true
},
xaxes: [ {
color: '#aaa',
position: "bottom",
tickColor: 'transparent',
mode: "time",
timeformat: "<?php echo esc_js( $timeformat ) ?>",
monthNames: <?php echo json_encode( array_values( $wp_locale->month_abbrev ) ) ?>,
tickLength: 1,
minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"],
font: {
color: "#aaa"
}
} ],
yaxes: [
{
min: 0,
minTickSize: 1,
tickDecimals: 0,
color: '#d4d9dc',
font: { color: "#aaa" }
},
{
position: "right",
min: 0,
tickDecimals: 2,
tickFormatter: function (tick) {
// Localise and format axis labels
return jQuery.wcs_format_money(tick,0);
},
alignTicksWithAxis: 1,
color: 'transparent',
font: { color: "#aaa" }
}
],
stack: true,
}
);
jQuery('.chart-placeholder').resize();
}
drawGraph();
jQuery('.highlight_series').hover(
function() {
drawGraph( jQuery(this).data('series') );
},
function() {
drawGraph();
}
);
});
</script>
<?php
}
/**
* Round our totals correctly.
* @param string $amount
* @return string
*/
private function round_chart_totals( $amount ) {
if ( is_array( $amount ) ) {
return array( $amount[0], wc_format_decimal( $amount[1], wc_get_price_decimals() ) );
} else {
return wc_format_decimal( $amount, wc_get_price_decimals() );
}
}
/**
* Put data with post_date's into an array of times averaged by day
*
* If the data is grouped by day already, we can just call @see $this->prepare_chart_data() otherwise,
* we need to figure out how many days in each period and average the aggregate over that count.
*
* @param array $data array of your data
* @param string $date_key key for the 'date' field. e.g. 'post_date'
* @param string $data_key key for the data you are charting
* @param int $interval
* @param string $start_date
* @param string $group_by
* @return array
*/
private function prepare_chart_data_daily_average( $data, $date_key, $data_key, $interval, $start_date, $group_by ) {
$prepared_data = array();
if ( 'day' == $group_by ) {
$prepared_data = $this->prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by );
} else {
// Ensure all days (or months) have values first in this range
for ( $i = 0; $i <= $interval; $i ++ ) {
$time = strtotime( date( 'Ym', strtotime( "+{$i} MONTH", $start_date ) ) . '01' ) . '000';
if ( ! isset( $prepared_data[ $time ] ) ) {
$prepared_data[ $time ] = array( esc_js( $time ), 0, 'count' => 0 );
}
}
foreach ( $data as $days_data ) {
$time = strtotime( date( 'Ym', strtotime( $days_data->$date_key ) ) . '01' ) . '000';
if ( ! isset( $prepared_data[ $time ] ) ) {
continue;
}
if ( $data_key ) {
$prepared_data[ $time ][1] += $days_data->$data_key;
} else {
$prepared_data[ $time ][1] ++;
}
$prepared_data[ $time ]['count']++;
}
foreach ( $prepared_data as $time => $aggregated_data ) {
if ( 0 === $aggregated_data['count'] ) {
$prepared_data[ $time ][1] = 0;
} else {
$prepared_data[ $time ][1] = round( $prepared_data[ $time ][1] / $aggregated_data['count'] );
}
unset( $prepared_data[ $time ]['count'] );
}
}
return $prepared_data;
}
}

View File

@@ -0,0 +1,389 @@
<?php
/**
* Subscriptions Admin Report - Subscription Events by Date
*
* Creates the subscription admin reports area.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Subscription_Payment_Retry extends WC_Admin_Report {
private $chart_colours = array();
private $report_data;
/**
* Get report data
* @return array
*/
public function get_report_data() {
if ( empty( $this->report_data ) ) {
$this->query_report_data();
}
return $this->report_data;
}
/**
* Get all data needed for this report and store in the class
*/
private function query_report_data() {
global $wpdb;
$this->report_data = new stdClass;
$query_start_date = get_gmt_from_date( date( 'Y-m-d', $this->start_date ) );
$query_end_date = get_gmt_from_date( date( 'Y-m-d', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
// Get the sum of order totals for completed retires (i.e. retries which eventually succeeded in processing the failed payment)
$renewal_query = $wpdb->prepare(
"SELECT COUNT(DISTINCT posts.ID) as count, posts.post_date as post_date, SUM(meta_order_total.meta_value) as renewal_totals
FROM {$wpdb->prefix}posts AS orders
INNER JOIN {$wpdb->prefix}posts AS posts ON ( orders.ID = posts.post_parent )
LEFT JOIN {$wpdb->prefix}postmeta AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
WHERE posts.post_type = 'payment_retry'
AND posts.post_status = 'complete'
AND posts.post_modified_gmt >= %s
AND posts.post_modified_gmt < %s
GROUP BY {$this->group_by_query}
ORDER BY post_date ASC",
$query_start_date,
$query_end_date
);
$this->report_data->renewal_data = $wpdb->get_results( $renewal_query );
// Get the counts for all retries, grouped by day or month and status
$retry_query = $wpdb->prepare(
"SELECT COUNT(DISTINCT posts.ID) as count, posts.post_status as status, posts.post_date as post_date
FROM {$wpdb->prefix}posts AS posts
WHERE posts.post_type = 'payment_retry'
AND posts.post_status IN ( 'complete','failed','pending' )
AND posts.post_modified_gmt >= %s
AND posts.post_modified_gmt < %s
GROUP BY {$this->group_by_query}, posts.post_status
ORDER BY posts.post_date_gmt ASC",
$query_start_date,
$query_end_date
);
$this->report_data->retry_data = $wpdb->get_results( $retry_query );
// Total up the query data
$this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );
$this->report_data->retry_success_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'count' ) ) );
$this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) );
$this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
$this->report_data->renewal_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) );
}
/**
* Get the legend for the main chart sidebar
* @return array
*/
public function get_chart_legend() {
$legend = array();
$data = $this->get_report_data();
$legend[] = array(
'title' => sprintf( __( '%s renewal revenue recovered', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $data->renewal_total_amount ) . '</strong>' ),
'placeholder' => __( 'The total amount of revenue, including tax and shipping, recovered with the failed payment retry system for renewal orders with a failed payment.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['renewal_total'],
'highlight_series' => 3,
);
$legend[] = array(
'title' => sprintf( __( '%s renewal orders', 'woocommerce-subscriptions' ), '<strong>' . $data->renewal_total_count . '</strong>' ),
'placeholder' => __( 'The number of renewal orders which had a failed payment use the retry system.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['renewal_count'],
);
$legend[] = array(
'title' => sprintf( __( '%s retry attempts succeeded', 'woocommerce-subscriptions' ), '<strong>' . $data->retry_success_count . '</strong>' ),
'placeholder' => __( 'The number of renewal payment retries for this period which were able to process the payment which had previously failed one or more times.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['retry_success_count'],
'highlight_series' => 0,
);
$legend[] = array(
'title' => sprintf( __( '%s retry attempts failed', 'woocommerce-subscriptions' ), '<strong>' . $data->retry_failed_count . '</strong>' ),
'placeholder' => __( 'The number of renewal payment retries for this period which did not result in a successful payment.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['retry_failure_count'],
'highlight_series' => 1,
);
$legend[] = array(
'title' => sprintf( __( '%s retry attempts pending', 'woocommerce-subscriptions' ), '<strong>' . $data->retry_pending_count . '</strong>' ),
'placeholder' => __( 'The number of renewal payment retries not yet processed.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['retry_pending_count'],
'highlight_series' => 2,
);
return $legend;
}
/**
* Output the report
*/
public function output_report() {
$ranges = array(
'year' => __( 'Year', 'woocommerce-subscriptions' ),
'last_month' => __( 'Last Month', 'woocommerce-subscriptions' ),
'month' => __( 'This Month', 'woocommerce-subscriptions' ),
'7day' => __( 'Last 7 Days', 'woocommerce-subscriptions' ),
);
$this->chart_colours = array(
'retry_success_count' => '#5cc488',
'retry_failure_count' => '#e74c3c',
'retry_pending_count' => '#dbe1e3',
'renewal_count' => '#b1d4ea',
'renewal_total' => '#3498db',
);
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
$current_range = '7day';
}
$this->calculate_current_range( $current_range );
include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' );
}
/**
* Output an export link
*/
public function get_export_button() {
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
?>
<a
href="#"
download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
class="export_csv"
data-export="chart"
data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce-subscriptions' ); ?>"
data-exclude_series="2"
data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
>
<?php esc_attr_e( 'Export CSV', 'woocommerce-subscriptions' ); ?>
</a>
<?php
}
/**
* Get the main chart
*
* @return string
*/
public function get_main_chart() {
global $wp_locale;
// Prepare data for report
$retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
// Encode in json format
$chart_data = array(
'retry_count' => array_values( $retry_count ),
'retry_success_count' => array_values( $retry_success_count ),
'retry_failure_count' => array_values( $retry_failure_count ),
'retry_pending_count' => array_values( $retry_pending_count ),
'renewal_count' => array_values( $renewal_count ),
'renewal_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $renewal_amount ) ),
);
$timeformat = ( $this->chart_groupby == 'day' ? '%d %b' : '%b' );
?>
<div id="woocommerce_subscriptions_payment_retry_chart" class="chart-container">
<div class="chart-placeholder main"></div>
</div>
<script type="text/javascript">
var main_chart;
jQuery(function(){
var chart_data = jQuery.parseJSON( '<?php echo json_encode( $chart_data ); ?>' );
var drawGraph = function( highlight ) {
var series = [
{
label: "<?php echo esc_js( __( 'Successful retries', 'woocommerce-subscriptions' ) ) ?>",
data: chart_data.retry_success_count,
color: '<?php echo esc_js( $this->chart_colours['retry_success_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['retry_success_count'] ); ?>',
order: 1,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.33,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Failed retries', 'woocommerce-subscriptions' ) ) ?>",
data: chart_data.retry_failure_count,
color: '<?php echo esc_js( $this->chart_colours['retry_failure_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['retry_failure_count'] ); ?>',
order: 2,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.33,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Pending retries', 'woocommerce-subscriptions' ) ) ?>",
data: chart_data.retry_pending_count,
color: '<?php echo esc_js( $this->chart_colours['retry_pending_count'] ); ?>',
bars: {
fillColor: '<?php echo esc_js( $this->chart_colours['retry_pending_count'] ); ?>',
order: 3,
fill: true,
show: true,
lineWidth: 0,
barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.33,
align: 'center'
},
shadowSize: 0,
hoverable: false,
},
{
label: "<?php echo esc_js( __( 'Recovered Renewal Revenue', 'woocommerce-subscriptions' ) ) ?>",
data: chart_data.renewal_amount,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['renewal_total'] ); ?>',
points: {
show: true,
radius: 6,
lineWidth: 4,
fillColor: '#fff',
fill: true
},
lines: {
show: true,
lineWidth: 5,
fill: false
},
shadowSize: 0,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
];
if ( highlight !== 'undefined' && series[ highlight ] ) {
highlight_series = series[ highlight ];
highlight_series.color = '#9c5d90';
if ( highlight_series.bars ) {
highlight_series.bars.fillColor = '#9c5d90';
}
if ( highlight_series.lines ) {
highlight_series.lines.lineWidth = 5;
}
}
main_chart = jQuery.plot(
jQuery('.chart-placeholder.main'),
series,
{
legend: {
show: false
},
grid: {
color: '#aaa',
borderColor: 'transparent',
borderWidth: 0,
hoverable: true
},
xaxes: [ {
color: '#aaa',
position: "bottom",
tickColor: 'transparent',
mode: "time",
timeformat: "<?php echo esc_js( $timeformat ) ?>",
monthNames: <?php echo json_encode( array_values( $wp_locale->month_abbrev ) ) ?>,
tickLength: 1,
minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"],
font: {
color: "#aaa"
}
} ],
yaxes: [
{
min: 0,
minTickSize: 1,
tickDecimals: 0,
color: '#d4d9dc',
font: { color: "#aaa" }
},
{
position: "right",
min: 0,
tickDecimals: 2,
tickFormatter: function (tick) {
// Localise and format axis labels
return jQuery.wcs_format_money(tick,0);
},
alignTicksWithAxis: 1,
color: 'transparent',
font: { color: "#aaa" }
}
],
stack: true,
}
);
jQuery('.chart-placeholder').resize();
}
drawGraph();
jQuery('.highlight_series').hover(
function() {
drawGraph( jQuery(this).data('series') );
},
function() {
drawGraph();
}
);
});
</script>
<?php
}
/**
* Round our totals correctly.
* @param string $amount
* @return string
*/
private function round_chart_totals( $amount ) {
if ( is_array( $amount ) ) {
return array( $amount[0], wc_format_decimal( $amount[1], wc_get_price_decimals() ) );
} else {
return wc_format_decimal( $amount, wc_get_price_decimals() );
}
}
}

View File

@@ -0,0 +1,421 @@
<?php
/**
* Subscriptions Admin Report - Upcoming Recurring Revenue
*
* Display the renewal order count and revenue that will be processed for all currently active subscriptions
* for a given period of time in the future.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Admin_Reports
* @category Class
* @author Prospress
* @since 2.1
*/
class WC_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
public $chart_colours = array();
public $order_ids_recurring_totals = null;
/**
* Get the legend for the main chart sidebar
* @return array
*/
public function get_chart_legend() {
$this->order_ids_recurring_totals = $this->get_data();
$total_renewal_revenue = 0;
$total_renewal_count = 0;
foreach ( $this->order_ids_recurring_totals as $r ) {
if ( strtotime( $r->scheduled_date ) >= $this->start_date ) {
$total_renewal_revenue += $r->recurring_total;
$total_renewal_count += $r->total_renewals;
}
$subscription_ids = explode( ',', $r->subscription_ids );
$billing_intervals = explode( ',', $r->billing_intervals );
$billing_periods = explode( ',', $r->billing_periods );
$scheduled_ends = explode( ',', $r->scheduled_ends );
$subscription_totals = explode( ',', $r->subscription_totals );
// Loop through each returned subscription ID and check if there are any more renewals in this period.
foreach ( $subscription_ids as $key => $subscription_id ) {
$next_payment_timestamp = strtotime( $r->scheduled_date );
// Keep calculating all the new payments until we hit the end date of the search
do {
$next_payment_timestamp = wcs_add_time( $billing_intervals[ $key ], $billing_periods[ $key ], $next_payment_timestamp );
// If there are more renewals add them to the existing object or create a new one
if ( $next_payment_timestamp <= $this->end_date && isset( $scheduled_ends[ $key ] ) && ( 0 == $scheduled_ends[ $key ] || $next_payment_timestamp < strtotime( $scheduled_ends[ $key ] ) ) ) {
$update_key = date( 'Y-m-d', $next_payment_timestamp );
if ( $next_payment_timestamp >= $this->start_date ) {
if ( ! isset( $this->order_ids_recurring_totals[ $update_key ] ) ) {
$this->order_ids_recurring_totals[ $update_key ] = new stdClass();
$this->order_ids_recurring_totals[ $update_key ]->scheduled_date = $update_key;
$this->order_ids_recurring_totals[ $update_key ]->recurring_total = 0;
$this->order_ids_recurring_totals[ $update_key ]->total_renewals = 0;
}
$this->order_ids_recurring_totals[ $update_key ]->total_renewals += 1;
$this->order_ids_recurring_totals[ $update_key ]->recurring_total += $subscription_totals[ $key ];
$total_renewal_revenue += $subscription_totals[ $key ];;
$total_renewal_count += 1;
}
}
} while ( $next_payment_timestamp <= $this->end_date && isset( $scheduled_ends[ $key ] ) && ( 0 == $scheduled_ends[ $key ] || $next_payment_timestamp < strtotime( $scheduled_ends[ $key ] ) ) );
}
}
$legend = array();
$this->average_sales = ( 0 != $total_renewal_count ? $total_renewal_revenue / $total_renewal_count : 0);
$legend[] = array(
'title' => sprintf( __( '%s renewal income in this period', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $total_renewal_revenue ) . '</strong>' ),
'color' => $this->chart_colours['renewals_amount'],
'highlight_series' => 1,
);
$legend[] = array(
'title' => sprintf( __( '%s renewal orders', 'woocommerce-subscriptions' ), '<strong>' . $total_renewal_count . '</strong>' ),
'color' => $this->chart_colours['renewals_count'],
'highlight_series' => 0,
);
$legend[] = array(
'title' => sprintf( __( '%s average renewal amount', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $this->average_sales ) . '</strong>' ),
'color' => $this->chart_colours['renewals_average'],
);
return $legend;
}
/**
* Get report data.
* @return stdClass
*/
public function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
);
$args = apply_filters( 'wcs_reports_upcoming_recurring_revenue_args', $args );
$args = wp_parse_args( $args, $default_args );
// Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours
$base_query = $wpdb->prepare(
"SELECT
DATE_FORMAT(ms.meta_value, '%s') as scheduled_date,
SUM(mo.meta_value) as recurring_total,
COUNT(mo.meta_value) as total_renewals,
group_concat(p.ID) as subscription_ids,
group_concat(mi.meta_value) as billing_intervals,
group_concat(mp.meta_value) as billing_periods,
group_concat(me.meta_value) as scheduled_ends,
group_concat(mo.meta_value) as subscription_totals
FROM {$wpdb->prefix}posts p
LEFT JOIN {$wpdb->prefix}postmeta ms
ON p.ID = ms.post_id
LEFT JOIN {$wpdb->prefix}postmeta mo
ON p.ID = mo.post_id
LEFT JOIN {$wpdb->prefix}postmeta mi
ON p.ID = mi.post_id
LEFT JOIN {$wpdb->prefix}postmeta mp
ON p.ID = mp.post_id
LEFT JOIN {$wpdb->prefix}postmeta me
ON p.ID = me.post_id
WHERE p.post_type = 'shop_subscription'
AND p.post_status = 'wc-active'
AND mo.meta_key = '_order_total'
AND ms.meta_key = '_schedule_next_payment'
AND ms.meta_value BETWEEN '%s' AND '%s'
AND mi.meta_key = '_billing_interval'
AND mp.meta_key = '_billing_period'
AND me.meta_key = '_schedule_end '
GROUP BY {$this->group_by_query}
ORDER BY ms.meta_value ASC",
'%Y-%m-%d',
date( 'Y-m-d', $this->start_date ),
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) )
);
$cached_results = get_transient( strtolower( get_class( $this ) ) );
$query_hash = md5( $base_query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $wpdb->get_results( $base_query, OBJECT_K ), $args );
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
return $cached_results[ $query_hash ];
}
/**
* Output the report
*/
public function output_report() {
$ranges = array(
'year' => __( 'Next 12 Months', 'woocommerce-subscriptions' ),
'month' => __( 'Next 30 Days', 'woocommerce-subscriptions' ),
'last_month' => __( 'Next Month', 'woocommerce-subscriptions' ), // misnomer to match historical reports keys, handy for caching
'7day' => __( 'Next 7 Days', 'woocommerce-subscriptions' ),
);
$this->chart_colours = array(
'renewals_amount' => '#1abc9c',
'renewals_count' => '#e67e22',
'renewals_average' => '#d4d9dc',
);
$current_range = $this->get_current_range();
$this->calculate_current_range( $current_range );
include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' );
}
/**
* Output an export link
*/
public function get_export_button() {
?>
<a
href="#"
download="report-<?php echo esc_attr( $this->get_current_range() ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
class="export_csv"
data-export="chart"
data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce-subscriptions' ); ?>"
data-exclude_series="2"
data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
>
<?php esc_html_e( 'Export CSV', 'woocommerce-subscriptions' ); ?>
</a>
<?php
}
/**
* Get the main chart
* @return string
*/
public function get_main_chart() {
global $wp_locale;
// Prepare data for report
$renewal_amounts = $this->prepare_chart_data( $this->order_ids_recurring_totals, 'scheduled_date', 'recurring_total', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_counts = $this->prepare_chart_data( $this->order_ids_recurring_totals, 'scheduled_date', 'total_renewals', $this->chart_interval, $this->start_date, $this->chart_groupby );
$chart_data = array(
'renewal_amounts' => array_values( $renewal_amounts ),
'renewal_counts' => array_values( $renewal_counts ),
);
?>
<div id="woocommerce_subscriptions_upcoming_recurring_revenue_chart" class="chart-container">
<div class="chart-placeholder main"></div>
</div>
<script type="text/javascript">
var main_chart;
jQuery(function(){
var order_data = jQuery.parseJSON( '<?php echo json_encode( $chart_data ); ?>' );
var drawGraph = function( highlight ) {
var series = [
{
label: "<?php echo esc_js( __( 'Renewals count', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.renewal_counts,
yaxis: 1,
color: '<?php echo esc_js( $this->chart_colours['renewals_count'] ); ?>',
points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 4, fill: false },
shadowSize: 0
},
{
label: "<?php echo esc_js( __( 'Renewals amount', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.renewal_amounts,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['renewals_amount'] ); ?>',
points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 4, fill: false },
shadowSize: 0,
prepend_tooltip: "<?php echo esc_js( get_woocommerce_currency_symbol() ); ?>"
}
];
if ( highlight !== 'undefined' && series[ highlight ] ) {
highlight_series = series[ highlight ];
highlight_series.color = '#9c5d90';
if ( highlight_series.bars )
highlight_series.bars.fillColor = '#9c5d90';
if ( highlight_series.lines ) {
highlight_series.lines.lineWidth = 5;
}
}
main_chart = jQuery.plot(
jQuery('.chart-placeholder.main'),
series,
{
legend: {
show: false
},
grid: {
color: '#aaa',
borderColor: 'transparent',
borderWidth: 0,
hoverable: true
},
xaxes: [ {
color: '#aaa',
position: "bottom",
tickColor: 'transparent',
mode: "time",
timeformat: "<?php echo esc_js( ( $this->chart_groupby == 'day' ? '%d %b' : '%b' ) ); ?>",
monthNames: <?php echo json_encode( array_values( $wp_locale->month_abbrev ) ) ?>,
tickLength: 1,
minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"],
font: {
color: "#aaa"
}
} ],
yaxes: [
{
min: 0,
minTickSize: 1,
tickDecimals: 0,
color: '#d4d9dc',
font: {
color: "#aaa"
}
},
{
position: "right",
min: 0,
tickDecimals: 2,
tickFormatter: function (tick) {
// Localise and format axis labels
return jQuery.wcs_format_money(tick,0);
},
alignTicksWithAxis: 1,
color: 'transparent',
font: {
color: "#aaa"
}
}
],
}
);
jQuery('.chart-placeholder').resize();
}
drawGraph();
jQuery('.highlight_series').hover(
function() {
drawGraph( jQuery(this).data('series') );
},
function() {
drawGraph();
}
);
});
</script>
<?php
}
/**
* Get the current range and calculate the start and end dates
*
* @param string $current_range
*/
public function calculate_current_range( $current_range ) {
switch ( $current_range ) {
case 'custom' :
$this->start_date = strtotime( sanitize_text_field( $_GET['start_date'] ) );
$this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) );
if ( ! $this->end_date ) {
$this->end_date = current_time( 'timestamp' );
}
$interval = 0;
$min_date = $this->start_date;
while ( ( $min_date = wcs_add_months( $min_date, '1' ) ) <= $this->end_date ) {
$interval ++;
}
// 3 months max for day view
if ( $interval > 3 ) {
$this->chart_groupby = 'month';
} else {
$this->chart_groupby = 'day';
}
break;
case 'year' :
$this->start_date = strtotime( 'now', current_time( 'timestamp' ) );
$this->end_date = strtotime( 'last day', strtotime( '+1 YEAR', current_time( 'timestamp' ) ) );
$this->chart_groupby = 'month';
break;
case 'last_month' : // misnomer to match historical reports keys, handy for caching
$this->start_date = strtotime( date( 'Y-m-01', wcs_add_months( current_time( 'timestamp' ), '1' ) ) );
$this->end_date = strtotime( date( 'Y-m-t', $this->start_date ) );
$this->chart_groupby = 'day';
break;
case 'month' :
$this->start_date = strtotime( 'now', current_time( 'timestamp' ) );
$this->end_date = wcs_add_months( current_time( 'timestamp' ), '1' );
$this->chart_groupby = 'day';
break;
case '7day' :
$this->start_date = strtotime( 'now', current_time( 'timestamp' ) );
$this->end_date = strtotime( '+7 days', current_time( 'timestamp' ) );
$this->chart_groupby = 'day';
break;
}
// Group by
switch ( $this->chart_groupby ) {
case 'day' :
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value), DAY(ms.meta_value)';
$this->chart_interval = ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) );
$this->barwidth = 60 * 60 * 24 * 1000;
break;
case 'month' :
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value)';
$this->chart_interval = 0;
$min_date = $this->start_date;
while ( ( $min_date = wcs_add_months( $min_date, '1' ) ) <= $this->end_date ) {
$this->chart_interval ++;
}
$this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000;
break;
}
}
/**
* Helper function to get the report's current range
*/
protected function get_current_range() {
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
if ( ! in_array( $current_range, array( 'custom', 'year', 'month', 'last_month', '7day' ) ) ) {
$current_range = '7day';
}
return $current_range;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Admin View: Report by Period
*
* Based on WooCommerce's Report by Date template but without date filters or sidebar.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<div id="poststuff" class="woocommerce-reports-wide">
<div class="postbox">
<div class="inside">
<?php $this->get_main_chart(); ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<?php
/**
* REST API subscription notes controller
*
* Handles requests to the /subscription/<id>/notes endpoint.
*
* @author Prospress
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Subscription Notes controller class.
*
* @package WooCommerce_Subscriptions/API
* @extends WC_REST_Order_Notes_Controller
*/
class WC_REST_Subscription_Notes_Controller extends WC_REST_Order_Notes_Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'subscriptions/(?P<order_id>[\d]+)/notes';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_subscription';
}

View File

@@ -0,0 +1,412 @@
<?php
/**
* REST API Subscriptions controller
*
* Handles requests to the /subscription endpoint.
*
* @author Prospress
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Subscriptions controller class.
*
* @package WooCommerce_Subscriptions/API
* @extends WC_REST_Orders_Controller
*/
class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'subscriptions';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_subscription';
/**
* Initialize subscription actions and filters
*/
public function __construct() {
add_filter( 'woocommerce_rest_prepare_shop_subscription', array( $this, 'filter_get_subscription_response' ), 10, 3 );
add_filter( 'woocommerce_rest_shop_subscription_query', array( $this, 'query_args' ), 10, 2 );
add_filter( 'woocommerce_rest_pre_insert_shop_subscription', array( $this, 'prepare_subscription_args' ), 10, 2 );
}
/**
* Register the routes for subscriptions.
*/
public function register_routes() {
parent::register_routes();
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/orders', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_subscription_orders' ),
'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 . '/statuses', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_statuses' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Filter WC_REST_Orders_Controller::get_item response for subscription post types
*
* @since 2.1
* @param WP_REST_Response $response
* @param WP_POST $post
* @param WP_REST_Request $request
*/
public function filter_get_subscription_response( $response, $post, $request ) {
if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) {
$subscription = wcs_get_subscription( $post->ID );
$response->data['billing_period'] = $subscription->billing_period;
$response->data['billing_interval'] = $subscription->billing_interval;
$response->data['start_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'start' ) );
$response->data['trial_end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'trial_end' ) );
$response->data['next_payment_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'next_payment' ) );
$response->data['end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'end_date' ) );
}
return $response;
}
/**
* Sets the order_total value on the subscription after WC_REST_Orders_Controller::create_order
* calls calculate_totals(). This allows store admins to create a recurring payment via the api
* without needing to attach a product to the subscription.
*
* @since 2.1
* @param WP_REST_Request $request
*/
protected function create_order( $request ) {
$post_id = parent::create_order( $request );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
if ( isset( $request['order_total'] ) ) {
update_post_meta( $post_id, '_order_total', wc_format_decimal( $request['order_total'], get_option( 'woocommerce_price_num_decimals' ) ) );
}
return $post_id;
}
/**
* Overrides WC_REST_Orders_Controller::update_order to update subscription specific meta
* calls parent::update_order to update the rest.
*
* @since 2.1
* @param WP_REST_Request $request
* @param WP_POST $post
*/
protected function update_order( $request, $post ) {
try {
$post_id = parent::update_order( $request, $post );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
$subscription = wcs_get_subscription( $post_id );
$this->update_schedule( $subscription, $request );
if ( empty( $request['payment_details']['method_id'] ) && ! empty( $request['payment_method'] ) ) {
$request['payment_details']['method_id'] = $request['payment_method'];
}
$this->update_payment_method( $subscription, $request['payment_details'], true );
return $post_id;
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
} catch ( Exception $e ) {
return new WP_Error( 'woocommerce_rest_cannot_update_subscription', $e->getMessage(), array( 'status' => 400 ) );
}
}
/**
* Get subscription orders
*
* @since 2.1
* @param WP_REST_Request $request
* @return WP_Error|WP_REST_Response $response
*/
public function get_subscription_orders( $request ) {
$id = (int) $request['id'];
if ( empty( $id ) || ! wcs_is_subscription( $id ) ) {
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
}
$this->post_type = 'shop_order';
$subscription = wcs_get_subscription( $id );
$subscription_orders = $subscription->get_related_orders();
$orders = array();
foreach ( $subscription_orders as $order_id ) {
$post = get_post( $order_id );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
continue;
}
$response = $this->prepare_item_for_response( $post, $request );
foreach ( array( 'parent', 'renewal', 'switch' ) as $order_type ) {
if ( wcs_order_contains_subscription( $order_id, $order_type ) ) {
$response->data['order_type'] = $order_type . '_order';
break;
}
}
$orders[] = $this->prepare_response_for_collection( $response );
}
$response = rest_ensure_response( $orders );
$response->header( 'X-WP-Total', count( $orders ) );
$response->header( 'X-WP-TotalPages', 1 );
return apply_filters( 'wcs_rest_subscription_orders_response', $response, $request );
}
/**
* Get subscription statuses
*
* @since 2.1
*/
public function get_statuses() {
return rest_ensure_response( wcs_get_subscription_statuses() );
}
/**
* Overrides WC_REST_Orders_Controller::get_order_statuses() so that subscription statuses are
* validated correctly in WC_REST_Orders_Controller::get_collection_params()
*
* @since 2.1
*/
protected function get_order_statuses() {
$subscription_statuses = array();
foreach ( array_keys( wcs_get_subscription_statuses() ) as $status ) {
$subscription_statuses[] = str_replace( 'wc-', '', $status );
}
return $subscription_statuses;
}
/**
* Create WC_Subscription object.
*
* @since 2.1
* @param array $args subscription args.
* @return WC_Subscription
*/
protected function create_base_order( $args ) {
$subscription = wcs_create_subscription( $args );
if ( is_wp_error( $subscription ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_subscription', sprintf( __( 'Cannot create subscription: %s.', 'woocommerce-subscriptions' ), implode( ', ', $subscription->get_error_messages() ) ), 400 );
}
$this->update_schedule( $subscription, $args );
if ( empty( $args['payment_details']['method_id'] ) && ! empty( $args['payment_method'] ) ) {
$args['payment_details']['method_id'] = $args['payment_method'];
}
$this->update_payment_method( $subscription, $args['payment_details'] );
return $subscription;
}
/**
* Update or set the subscription schedule with the request data
*
* @since 2.1
* @param WC_Subscription $subscription
* @param array $data
*/
public function update_schedule( $subscription, $data ) {
if ( isset( $data['billing_interval'] ) ) {
update_post_meta( $subscription->id, '_billing_interval', absint( $data['billing_interval'] ) );
}
if ( ! empty( $data['billing_period'] ) ) {
update_post_meta( $subscription->id, '_billing_period', $data['billing_period'] );
}
try {
$dates_to_update = array();
foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
if ( isset( $data[ $date_type . '_date' ] ) ) {
$dates_to_update[ $date_type ] = $data[ $date_type . '_date' ];
}
}
if ( ! empty( $dates_to_update ) ) {
$subscription->update_dates( $dates_to_update );
}
} catch ( Exception $e ) {
throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_subscription_dates', sprintf( __( 'Updating subscription dates errored with message: %s', 'woocommerce-subscriptions' ), $e->getMessage() ), 400 );
}
}
/**
* Validate and update payment method on a subscription
*
* @since 2.1
* @param WC_Subscription $subscription
* @param array $data
* @param bool $updating
*/
public function update_payment_method( $subscription, $data, $updating = false ) {
$payment_gateways = WC()->payment_gateways->get_available_payment_gateways();
$payment_method = ( ! empty( $data['method_id'] ) ) ? $data['method_id'] : 'manual';
$payment_gateway = ( ! empty( $payment_gateways[ $payment_method ] ) ) ? $payment_gateways[ $payment_method ] : '';
try {
if ( $updating && ! array_key_exists( $payment_method, WCS_Change_Payment_Method_Admin::get_valid_payment_methods( $subscription ) ) ) {
throw new Exception( __( 'Gateway does not support admin changing the payment method on a Subscription.', 'woocommerce-subscriptions' ) );
}
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
if ( ! empty( $payment_gateway ) && isset( $payment_method_meta[ $payment_gateway->id ] ) ) {
$payment_method_meta = $payment_method_meta[ $payment_gateway->id ];
if ( ! empty( $payment_method_meta ) ) {
foreach ( $payment_method_meta as $meta_table => &$meta ) {
if ( ! is_array( $meta ) ) {
continue;
}
foreach ( $meta as $meta_key => &$meta_data ) {
if ( isset( $data[ $meta_table ][ $meta_key ] ) ) {
$meta_data['value'] = $data[ $meta_table ][ $meta_key ];
}
}
}
}
}
if ( empty( $subscription->payment_gateway ) ) {
$subscription->payment_gateway = $payment_gateway;
}
$subscription->set_payment_method( $payment_gateway, $payment_method_meta );
} catch ( Exception $e ) {
// translators: 1$: gateway id, 2$: error message
throw new WC_REST_Exception( 'woocommerce_rest_invalid_payment_data', sprintf( __( 'Subscription payment method could not be set to %1$s with error message: %2$s', 'woocommerce-subscriptions' ), $payment_method, $e->getMessage() ), 400 );
}
}
/**
* Adds additional item schema information for subscription requests
*
* @since 2.1
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$subscriptions_schema = array(
'billing_interval' => array(
'description' => __( 'The number of billing periods between subscription renewals.', 'woocommerce-subscriptions' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'billing_period' => array(
'description' => __( 'Billing period for the subscription.', 'woocommerce-subscriptions' ),
'type' => 'string',
'enum' => array_keys( wcs_get_subscription_period_strings() ),
'context' => array( 'view', 'edit' ),
),
'payment_details' => array(
'description' => __( 'Subscription payment details.', 'woocommerce-subscriptions' ),
'type' => 'array',
'context' => array( 'edit' ),
'properties' => array(
'method_id' => array(
'description' => __( 'Payment gateway ID.', 'woocommerce-subscriptions' ),
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
'start_date' => array(
'description' => __( "The subscription's start date.", 'woocommerce-subscriptions' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'trial_date' => array(
'description' => __( "The subscription's trial date", 'woocommerce-subscriptions' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'next_payment_date' => array(
'description' => __( "The subscription's next payment date.", 'woocommerce-subscriptions' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'end_date' => array(
'description' => __( "The subscription's end date.", 'woocommerce-subscriptions' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
);
$schema['properties'] += $subscriptions_schema;
return $schema;
}
/**
* Prepare subscription data for create.
*
* @since 2.1
* @param stdClass $data
* @param WP_REST_Request $request Request object.
* @return stdClass
*/
public function prepare_subscription_args( $data, $request ) {
$data->billing_interval = $request['billing_interval'];
$data->billing_period = $request['billing_period'];
foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
if ( ! empty( $request[ $date_type . '_date' ] ) ) {
$data->{$date_type . '_date'} = $request[ $date_type . '_date' ];
}
}
$data->payment_details = ! empty( $request['payment_details'] ) ? $request['payment_details'] : '';
$data->payment_method = ! empty( $request['payment_method'] ) ? $request['payment_method'] : '';
return $data;
}
}

View File

@@ -142,19 +142,7 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
*/
function is_purchasable() {
$purchasable = $this->parent->is_purchasable();
// if we have a limited subscription product, make sure the customer doesn't already have another variation for the same variable product in their cart, but only if we're not on the order received or PayPal return pages (we can't use is_order_received_page() to check that becuase get_cart_from_session() is called before the query vars are setup)
if ( 'no' != $this->parent->limit_subscriptions && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
foreach ( WC()->cart->cart_contents as $cart_item ) { // can't use WC()->cart->get_cart() because it will trigger an infinite loop when this is called within WC_Cart::get_cart_from_session()
if ( $this->id == $cart_item['data']->id && $this->variation_id != $cart_item['data']->variation_id ) {
$purchasable = false;
break;
}
}
}
$purchasable = WCS_Limiter::is_purchasable( $this->parent->is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_variation_is_purchasable', $purchasable, $this );
}

View File

@@ -77,14 +77,21 @@ class WC_Product_Subscription extends WC_Product_Simple {
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
$this->limit_subscriptions = 'no';
} elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
$this->limit_subscriptions = 'any';
} else {
$this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
}
/**
* Auto-load in-accessible properties on demand.
*
* @param mixed $key
* @return mixed
*/
public function __get( $key ) {
if ( 'limit_subscriptions' === $key ) {
_deprecated_argument( 'WC_Product_Subscription->limit_subscriptions', '2.1', 'Use wcs_get_product_limitation directly' );
return wcs_get_product_limitation( $this );
} else {
return parent::__get( $key );
}
}
/**
@@ -182,12 +189,7 @@ class WC_Product_Subscription extends WC_Product_Simple {
* @return bool
*/
function is_purchasable() {
$purchasable = parent::is_purchasable();
if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
$purchasable = false;
}
$purchasable = WCS_Limiter::is_purchasable( parent::is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}

View File

@@ -78,15 +78,22 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable {
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
$this->limit_subscriptions = 'no';
} elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
$this->limit_subscriptions = 'any';
} else {
$this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
add_filter( 'woocommerce_add_to_cart_handler', array( &$this, 'add_to_cart_handler' ), 10, 2 );
}
add_filter( 'woocommerce_add_to_cart_handler', array( &$this, 'add_to_cart_handler' ), 10, 2 );
/**
* Auto-load in-accessible properties on demand.
*
* @param mixed $key
* @return mixed
*/
public function __get( $key ) {
if ( 'limit_subscriptions' === $key ) {
_deprecated_argument( 'WC_Product_Subscription->limit_subscriptions', '2.1', 'Use wcs_get_product_limitation directly' );
return wcs_get_product_limitation( $this );
} else {
return parent::__get( $key );
}
}
/**
@@ -594,12 +601,7 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable {
* @return bool
*/
function is_purchasable() {
$purchasable = parent::is_purchasable();
if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
$purchasable = false;
}
$purchasable = WCS_Limiter::is_purchasable( parent::is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}

View File

@@ -20,6 +20,9 @@ class WC_Subscription extends WC_Order {
/** @public string Order type */
public $order_type = 'shop_subscription';
/** @private int Stores get_completed_payment_count when used multiple times in payment_complete() */
private $cached_completed_payment_count = false;
/**
* Initialize the subscription object.
*
@@ -155,13 +158,6 @@ class WC_Subscription extends WC_Order {
'value' => $this->id,
'type' => 'numeric',
),
array(
'key' => '_subscription_switch',
'compare' => '=',
'value' => $this->id,
'type' => 'numeric',
),
'relation' => 'OR',
),
) );
@@ -332,14 +328,17 @@ class WC_Subscription extends WC_Order {
$end_date = $this->calculate_date( 'end_of_prepaid_term' );
// If there is no future payment and no expiration date set, the customer has no prepaid term (this shouldn't be possible as only active subscriptions can be set to pending cancellation and an active subscription always has either an end date or next payment)
if ( 0 == $end_date ) {
$end_date = current_time( 'mysql', true );
// If there is no future payment and no expiration date set, or the end date is before now, the customer has no prepaid term (this shouldn't be possible as only active subscriptions can be set to pending cancellation and an active subscription always has either an end date or next payment), so set the end date and cancellation date to now
if ( 0 == $end_date || wcs_date_to_time( $end_date ) < current_time( 'timestamp', true ) ) {
$cancelled_date = $end_date = current_time( 'mysql', true );
} else {
// the cancellation date is now, and the end date is the end of prepaid term date
$cancelled_date = current_time( 'mysql', true );
}
$this->delete_date( 'trial_end' );
$this->delete_date( 'next_payment' );
$this->update_dates( array( 'end' => $end_date ) );
$this->update_dates( array( 'cancelled' => $cancelled_date, 'end' => $end_date ) );
break;
case 'completed' : // core WC order status mapped internally to avoid exceptions
@@ -373,14 +372,21 @@ class WC_Subscription extends WC_Order {
case 'expired' :
$this->delete_date( 'trial_end' );
$this->delete_date( 'next_payment' );
$this->update_dates( array( 'end' => current_time( 'mysql', true ) ) );
$dates_to_update = array(
'end' => current_time( 'mysql', true ),
);
// Also set the cancelled date to now if it wasn't set previously (when the status was changed to pending-cancellation)
if ( 'cancelled' === $new_status && 0 == $this->get_date( 'cancelled' ) ) {
$dates_to_update['cancelled'] = $dates_to_update['end'];
}
$this->update_dates( $dates_to_update );
wcs_maybe_make_user_inactive( $this->customer_user );
break;
}
// translators: $1 note why the status changes (if any), $2: old status, $3: new status
$this->add_order_note( trim( sprintf( __( '%1$s Status changed from %2$s to %3$s.', 'woocommerce-subscriptions' ), $note, wcs_get_subscription_status_name( $old_status ), wcs_get_subscription_status_name( $new_status ) ) ), 0, $manual );
// dynamic hooks for convenience
do_action( 'woocommerce_subscription_status_' . $new_status, $this );
do_action( 'woocommerce_subscription_status_' . $old_status . '_to_' . $new_status, $this );
@@ -391,13 +397,16 @@ class WC_Subscription extends WC_Order {
// Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes
do_action( 'woocommerce_subscription_status_changed', $this->id, $old_status, $new_status );
// translators: $1 note why the status changes (if any), $2: old status, $3: new status
$this->add_order_note( trim( sprintf( __( '%1$s Status changed from %2$s to %3$s.', 'woocommerce-subscriptions' ), $note, wcs_get_subscription_status_name( $old_status ), wcs_get_subscription_status_name( $new_status ) ) ), 0, $manual );
} catch ( Exception $e ) {
// Make sure the old status is restored
wp_update_post( array( 'ID' => $this->id, 'post_status' => $old_status_key ) );
$this->post_status = $old_status_key;
$this->add_order_note( sprintf( __( 'Unable to change subscription status to "%s".', 'woocommerce-subscriptions' ), $new_status ) );
$this->add_order_note( sprintf( __( 'Unable to change subscription status to "%s". Exception: %s', 'woocommerce-subscriptions' ), $new_status, $e->getMessage() ) );
do_action( 'woocommerce_subscription_unable_to_update_status', $this, $new_status, $old_status );
@@ -497,9 +506,29 @@ class WC_Subscription extends WC_Order {
*/
public function get_completed_payment_count() {
// If not cached, calculate the completed payment count otherwise return the cached version
if ( false === $this->cached_completed_payment_count ) {
$completed_payment_count = ( false !== $this->order && ( isset( $this->order->paid_date ) || $this->order->has_status( $this->get_paid_order_statuses() ) ) ) ? 1 : 0;
// not all gateways will call $order->payment_complete() so we need to find renewal orders with a paid status rather than just a _paid_date
// Get all renewal orders - for large sites its more efficient to find the two different sets of renewal orders below using post__in than complicated meta queries
$renewal_orders = get_posts( array(
'posts_per_page' => -1,
'post_status' => 'any',
'post_type' => 'shop_order',
'fields' => 'ids',
'orderby' => 'date',
'order' => 'desc',
'meta_key' => '_subscription_renewal',
'meta_compare' => '=',
'meta_type' => 'numeric',
'meta_value' => $this->id,
'update_post_term_cache' => false,
) );
if ( ! empty( $renewal_orders ) ) {
// Not all gateways will call $order->payment_complete() so we need to find renewal orders with a paid status rather than just a _paid_date
$paid_status_renewal_orders = get_posts( array(
'posts_per_page' => -1,
'post_status' => $this->get_paid_order_statuses(),
@@ -507,17 +536,10 @@ class WC_Subscription extends WC_Order {
'fields' => 'ids',
'orderby' => 'date',
'order' => 'desc',
'meta_query' => array(
array(
'key' => '_subscription_renewal',
'compare' => '=',
'value' => $this->id,
'type' => 'numeric',
),
),
'post__in' => $renewal_orders,
) );
// because some stores may be using custom order status plugins, we also can't rely on order status to find paid orders, so also check for a _paid_date
// Some stores may be using custom order status plugins, we also can't rely on order status to find paid orders, so also check for a _paid_date
$paid_date_renewal_orders = get_posts( array(
'posts_per_page' => -1,
'post_status' => 'any',
@@ -525,18 +547,10 @@ class WC_Subscription extends WC_Order {
'fields' => 'ids',
'orderby' => 'date',
'order' => 'desc',
'meta_query' => array(
array(
'key' => '_subscription_renewal',
'compare' => '=',
'value' => $this->id,
'type' => 'numeric',
),
array(
'key' => '_paid_date',
'compare' => 'EXISTS',
),
),
'post__in' => $renewal_orders,
'meta_key' => '_paid_date',
'meta_compare' => 'EXISTS',
'update_post_term_cache' => false,
) );
$paid_renewal_orders = array_unique( array_merge( $paid_date_renewal_orders, $paid_status_renewal_orders ) );
@@ -544,8 +558,15 @@ class WC_Subscription extends WC_Order {
if ( ! empty( $paid_renewal_orders ) ) {
$completed_payment_count += count( $paid_renewal_orders );
}
}
} else {
$completed_payment_count = $this->cached_completed_payment_count;
}
return apply_filters( 'woocommerce_subscription_payment_completed_count', $completed_payment_count, $this );
// Store the completed payment count to avoid hitting the database again
$this->cached_completed_payment_count = apply_filters( 'woocommerce_subscription_payment_completed_count', $completed_payment_count, $this );
return $this->cached_completed_payment_count;
}
/**
@@ -627,16 +648,11 @@ class WC_Subscription extends WC_Order {
case 'start' :
$this->schedule->{$date_type} = ( '0000-00-00 00:00:00' != $this->post->post_date_gmt ) ? $this->post->post_date_gmt : get_gmt_from_date( $this->post->post_date ); // why not always use post_date_gmt? Because when a post is first created via the Add Subscription screen, it has a post_date but not a post_date_gmt value yet
break;
case 'next_payment' :
case 'trial_end' :
case 'end' :
$this->schedule->{$date_type} = get_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), true );
break;
case 'last_payment' :
$this->schedule->{$date_type} = $this->get_last_payment_date();
break;
default :
$this->schedule->{$date_type} = 0;
$this->schedule->{$date_type} = get_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), true );
break;
}
@@ -690,6 +706,9 @@ class WC_Subscription extends WC_Order {
case 'end' :
$date_to_display = __( 'Not yet ended', 'woocommerce-subscriptions' );
break;
case 'cancelled' :
$date_to_display = __( 'Not cancelled', 'woocommerce-subscriptions' );
break;
case 'next_payment' :
case 'trial_end' :
default :
@@ -712,7 +731,7 @@ class WC_Subscription extends WC_Order {
$datetime = $this->get_date( $date_type, $timezone );
if ( 0 !== $datetime ) {
$datetime = strtotime( $datetime );
$datetime = wcs_date_to_time( $datetime );
}
return $datetime;
@@ -764,7 +783,7 @@ class WC_Subscription extends WC_Order {
$datetime = get_gmt_from_date( $datetime );
}
$timestamps[ $date_type ] = strtotime( $datetime );
$timestamps[ $date_type ] = wcs_date_to_time( $datetime );
}
}
@@ -789,6 +808,11 @@ class WC_Subscription extends WC_Order {
foreach ( $timestamps as $date_type => $datetime ) {
switch ( $date_type ) {
case 'end' :
if ( array_key_exists( 'cancelled', $timestamps ) && $datetime < $timestamps['cancelled'] ) {
$messages[] = sprintf( __( 'The %s date must occur after the cancellation date.', 'woocommerce-subscriptions' ), $date_type );
}
case 'cancelled' :
if ( array_key_exists( 'last_payment', $timestamps ) && $datetime < $timestamps['last_payment'] ) {
$messages[] = sprintf( __( 'The %s date must occur after the last payment date.', 'woocommerce-subscriptions' ), $date_type );
}
@@ -815,18 +839,13 @@ class WC_Subscription extends WC_Order {
$is_updated = false;
foreach ( $timestamps as $date_type => $timestamp ) {
$datetime = date( 'Y-m-d H:i:s', $timestamp );
$datetime = gmdate( 'Y-m-d H:i:s', $timestamp );
if ( $datetime == $this->get_date( $date_type ) ) {
continue;
}
switch ( $date_type ) {
case 'next_payment' :
case 'trial_end' :
case 'end' :
$is_updated = update_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), $datetime );
break;
case 'start' :
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_date = %s, post_date_gmt = %s WHERE ID = %s", get_date_from_gmt( $datetime ), $datetime, $this->id ) ); // Don't use wp_update_post() to avoid infinite loops here
$is_updated = true;
@@ -835,6 +854,9 @@ class WC_Subscription extends WC_Order {
$this->update_last_payment_date( $datetime );
$is_updated = true;
break;
default :
$is_updated = update_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), $datetime );
break;
}
if ( $is_updated ) {
@@ -888,6 +910,7 @@ class WC_Subscription extends WC_Order {
}
break;
case 'trial_end' :
$this->cached_completed_payment_count = false;
if ( $this->get_completed_payment_count() < 2 && ! $this->has_status( wcs_get_subscription_ended_statuses() ) && ( $this->has_status( 'pending' ) || $this->payment_method_supports( 'subscription_date_changes' ) ) ) {
$can_date_be_updated = true;
} else {
@@ -1003,12 +1026,12 @@ class WC_Subscription extends WC_Order {
}
// If the subscription has an end date and the next billing period comes after that, return 0
if ( 0 != $end_time && ( $next_payment_timestamp + 120 ) > $end_time ) {
if ( 0 != $end_time && ( $next_payment_timestamp + 23 * HOUR_IN_SECONDS ) > $end_time ) {
$next_payment_timestamp = 0;
}
if ( $next_payment_timestamp > 0 ) {
$next_payment_date = date( 'Y-m-d H:i:s', $next_payment_timestamp );
$next_payment_date = gmdate( 'Y-m-d H:i:s', $next_payment_timestamp );
}
return $next_payment_date;
@@ -1249,6 +1272,9 @@ class WC_Subscription extends WC_Order {
*/
public function payment_complete( $transaction_id = '' ) {
// Clear the cached completed payment count
$this->cached_completed_payment_count = false;
// Make sure the last order's status is updated
$last_order = $this->get_last_order( 'all', 'any' );
@@ -1276,7 +1302,7 @@ class WC_Subscription extends WC_Order {
do_action( 'woocommerce_subscription_payment_complete', $this );
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
do_action( 'woocommerce_subscription_renewal_payment_complete', $this );
do_action( 'woocommerce_subscription_renewal_payment_complete', $this, $last_order );
}
}
@@ -1309,7 +1335,7 @@ class WC_Subscription extends WC_Order {
do_action( 'woocommerce_subscription_payment_failed', $this, $new_status );
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
do_action( 'woocommerce_subscription_renewal_payment_failed', $this );
do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $last_order );
}
}
@@ -1449,7 +1475,7 @@ class WC_Subscription extends WC_Order {
public function get_last_order( $return_fields = 'ids', $order_types = array( 'parent', 'renewal' ) ) {
$return_fields = ( 'ids' == $return_fields ) ? $return_fields : 'all';
$order_types = ( 'any' == $order_types ) ? array( 'parent', 'renewal', 'switch' ) : $order_types;
$order_types = ( 'any' == $order_types ) ? array( 'parent', 'renewal', 'switch' ) : (array) $order_types;
$related_orders = array();
foreach ( $order_types as $order_type ) {

View File

@@ -91,7 +91,7 @@ class WC_Subscriptions_Cart {
add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' );
add_action( 'woocommerce_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' );
add_action( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 3 );
add_action( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 6 );
add_filter( 'woocommerce_cart_needs_shipping', __CLASS__ . '::cart_needs_shipping', 11, 1 );
@@ -310,10 +310,6 @@ class WC_Subscriptions_Cart {
$total = max( 0, round( WC()->cart->cart_contents_total + WC()->cart->tax_total + WC()->cart->shipping_tax_total + WC()->cart->shipping_total + WC()->cart->fee_total, WC()->cart->dp ) );
if ( isset( WC()->cart->discount_total ) && 0 !== WC()->cart->discount_total ) { // WC < 2.3, deduct deprecated after tax discount total
$total = max( 0, round( $total - WC()->cart->discount_total, WC()->cart->dp ) );
}
if ( ! self::charge_shipping_up_front() ) {
$total = max( 0, $total - WC()->cart->shipping_tax_total - WC()->cart->shipping_total );
WC()->cart->shipping_taxes = array();
@@ -948,7 +944,7 @@ class WC_Subscriptions_Cart {
$cart_key = '';
$product = $cart_item['data'];
$product_id = ! empty( $product->variation_id ) ? $product->variation_id : $product->id;
$product_id = wcs_get_canonical_product_id( $product );
$renewal_time = ! empty( $renewal_time ) ? $renewal_time : WC_Subscriptions_Product::get_first_renewal_payment_time( $product_id );
$interval = WC_Subscriptions_Product::get_interval( $product );
$period = WC_Subscriptions_Product::get_period( $product );
@@ -957,7 +953,7 @@ class WC_Subscriptions_Cart {
$trial_length = WC_Subscriptions_Product::get_trial_length( $product );
if ( $renewal_time > 0 ) {
$cart_key .= date( 'Y_m_d_', $renewal_time );
$cart_key .= gmdate( 'Y_m_d_', $renewal_time );
}
// First start with the billing interval and period
@@ -996,13 +992,13 @@ class WC_Subscriptions_Cart {
}
/**
* Don't allow other subscriptions to be added to the cart while it contains a renewal
* Don't allow new subscription products to be added to the cart if it contains a subscription renewal already.
*
* @since 2.0
*/
public static function check_valid_add_to_cart( $is_valid, $product, $quantity ) {
public static function check_valid_add_to_cart( $is_valid, $product_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) {
if ( $is_valid && wcs_cart_contains_renewal() && WC_Subscriptions_Product::is_subscription( $product ) ) {
if ( $is_valid && ! isset( $item_data['subscription_renewal'] ) && wcs_cart_contains_renewal() && WC_Subscriptions_Product::is_subscription( $product_id ) ) {
wc_add_notice( __( 'That subscription product can not be added to your cart as it already contains a subscription renewal.', 'woocommerce-subscriptions' ), 'error' );
$is_valid = false;
@@ -1206,197 +1202,6 @@ class WC_Subscriptions_Cart {
/* Deprecated */
/**
* Returns the formatted subscription price string for an item
*
* @since 1.0
*/
public static function get_cart_item_price_html( $price_string, $cart_item ) {
_deprecated_function( __METHOD__, '1.2' );
return $price_string;
}
/**
* Returns either the total if prices include tax because this doesn't include tax, or the
* subtotal if prices don't includes tax, because this doesn't include tax.
*
* @return string formatted price
*
* @since 1.0
*/
public static function get_cart_contents_total( $cart_contents_total ) {
_deprecated_function( __METHOD__, '1.2' );
return $cart_contents_total;
}
/**
* Calculate totals for the sign-up fees in the cart, based on @see WC_Cart::calculate_totals()
*
* @since 1.0
*/
public static function calculate_sign_up_fee_totals() {
_deprecated_function( __METHOD__, '1.2' );
}
/**
* Function to apply discounts to a product and get the discounted price (before tax is applied)
*
* @param mixed $values
* @param mixed $price
* @param bool $add_totals (default: false)
* @return float price
* @since 1.0
*/
public static function get_discounted_price( $values, $price, $add_totals = false ) {
_deprecated_function( __METHOD__, '1.2' );
return $price;
}
/**
* Function to apply product discounts after tax
*
* @param mixed $values
* @param mixed $price
* @since 1.0
*/
public static function apply_product_discounts_after_tax( $values, $price ) {
_deprecated_function( __METHOD__, '1.2' );
}
/**
* Function to apply cart discounts after tax
*
* @since 1.0
*/
public static function apply_cart_discounts_after_tax() {
_deprecated_function( __METHOD__, '1.2' );
}
/**
* Get tax row amounts with or without compound taxes includes
*
* @return float price
*/
public static function get_sign_up_taxes_total( $compound = true ) {
_deprecated_function( __METHOD__, '1.2' );
return 0;
}
public static function get_sign_up_fee_fields() {
_deprecated_function( __METHOD__, '1.2' );
return array(
'cart_contents_sign_up_fee_total',
'cart_contents_sign_up_fee_count',
'sign_up_fee_total',
'sign_up_fee_subtotal',
'sign_up_fee_subtotal_ex_tax',
'sign_up_fee_tax_total',
'sign_up_fee_taxes',
'sign_up_fee_discount_cart',
'sign_up_fee_discount_total',
);
}
/**
* Returns the subtotal for a cart item including the subscription period and duration details
*
* @since 1.0
*/
public static function get_product_subtotal( $product_subtotal, $product ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_product_subtotal( $product_subtotal, $product )' );
return self::get_formatted_product_subtotal( $product_subtotal, $product );
}
/**
* Returns a string with the cart discount and subscription period.
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_discounts_before_tax( $discount, $cart ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_discounts_before_tax( $discount )' );
return self::get_formatted_discounts_before_tax( $discount );
}
/**
* Gets the order discount amount - these are applied after tax
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_discounts_after_tax( $discount, $cart ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_discounts_after_tax( $discount )' );
return self::get_formatted_discounts_after_tax( $discount );
}
/**
* Includes the sign-up fee subtotal in the subtotal displayed in the cart.
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_cart_subtotal( $cart_subtotal, $compound, $cart ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_cart_subtotal( $cart_subtotal, $compound, $cart )' );
return self::get_formatted_cart_subtotal( $cart_subtotal, $compound, $cart );
}
/**
* Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it.
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_total( $total ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_total( $total )' );
return self::get_formatted_total( $total );
}
/**
* Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it.
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_total_ex_tax( $total_ex_tax ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_total_ex_tax( $total_ex_tax )' );
return self::get_formatted_total_ex_tax( $total_ex_tax );
}
/**
* Displays each cart tax in a subscription string and calculates the sign-up fee taxes (if any)
* to display in the string.
*
* @since 1.2
*/
public static function get_formatted_taxes( $formatted_taxes, $cart ) {
_deprecated_function( __METHOD__, '1.4.9', __CLASS__ .'::get_recurring_tax_totals( $total_ex_tax )' );
if ( self::cart_contains_subscription() ) {
$recurring_taxes = self::get_recurring_taxes();
foreach ( $formatted_taxes as $tax_id => $tax_amount ) {
$formatted_taxes[ $tax_id ] = self::get_cart_subscription_string( $tax_amount, $recurring_taxes[ $tax_id ] );
}
// Add any recurring tax not already handled - when a subscription has a free trial and a sign-up fee, we get a recurring shipping tax with no initial shipping tax
foreach ( $recurring_taxes as $tax_id => $tax_amount ) {
if ( ! array_key_exists( $tax_id, $formatted_taxes ) ) {
$formatted_taxes[ $tax_id ] = self::get_cart_subscription_string( '', $tax_amount );
}
}
}
return $formatted_taxes;
}
/**
* Checks the cart to see if it contains a subscription product renewal.
*

View File

@@ -632,30 +632,6 @@ class WC_Subscriptions_Coupon {
return $cart;
}
/**
* Determines if cart contains a recurring fee discount code
*
* Does not check if the code is valid, etc
*
* @since 1.2
*/
public static function cart_contains_recurring_discount() {
_deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "recurring_fee" )' );
return self::cart_contains_discount( 'recurring_fee' );
}
/**
* Determines if cart contains a sign up fee discount code
*
* Does not check if the code is valid, etc
*
* @since 1.2
*/
public static function cart_contains_sign_up_discount() {
_deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "sign_up_fee" )' );
return self::cart_contains_discount( 'sign_up_fee' );
}
/**
* Restores discount coupons which had been removed for special subscription calculations.
*

View File

@@ -7,7 +7,7 @@
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Email
* @category Class
* @author Brent Shepherd
* @author Prospress
*/
class WC_Subscriptions_Email {
@@ -26,6 +26,7 @@ class WC_Subscriptions_Email {
add_filter( 'woocommerce_resend_order_emails_available', __CLASS__ . '::renewal_order_emails_available', -1 ); // run before other plugins so we don't remove their emails
add_action( 'woocommerce_subscriptions_email_order_details', __CLASS__ . '::order_details', 10, 4 );
}
/**
@@ -42,6 +43,8 @@ class WC_Subscriptions_Email {
require_once( 'emails/class-wcs-email-customer-completed-switch-order.php' );
require_once( 'emails/class-wcs-email-customer-renewal-invoice.php' );
require_once( 'emails/class-wcs-email-cancelled-subscription.php' );
require_once( 'emails/class-wcs-email-expired-subscription.php' );
require_once( 'emails/class-wcs-email-on-hold-subscription.php' );
$email_classes['WCS_Email_New_Renewal_Order'] = new WCS_Email_New_Renewal_Order();
$email_classes['WCS_Email_New_Switch_Order'] = new WCS_Email_New_Switch_Order();
@@ -50,6 +53,8 @@ class WC_Subscriptions_Email {
$email_classes['WCS_Email_Completed_Switch_Order'] = new WCS_Email_Completed_Switch_Order();
$email_classes['WCS_Email_Customer_Renewal_Invoice'] = new WCS_Email_Customer_Renewal_Invoice();
$email_classes['WCS_Email_Cancelled_Subscription'] = new WCS_Email_Cancelled_Subscription();
$email_classes['WCS_Email_Expired_Subscription'] = new WCS_Email_Expired_Subscription();
$email_classes['WCS_Email_On_Hold_Subscription'] = new WCS_Email_On_Hold_Subscription();
return $email_classes;
}
@@ -67,6 +72,8 @@ class WC_Subscriptions_Email {
}
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::send_cancelled_email', 10, 2 );
add_action( 'woocommerce_subscription_status_expired', __CLASS__ . '::send_expired_email', 10, 2 );
add_action( 'woocommerce_customer_changed_subscription_to_on-hold', __CLASS__ . '::send_on_hold_email', 10, 2 );
$order_email_actions = array(
'woocommerce_order_status_pending_to_processing',
@@ -102,6 +109,30 @@ class WC_Subscriptions_Email {
}
}
/**
* Init the mailer and call for the expired email notification hook.
*
* @param $subscription WC Subscription
* @since 2.1
*/
public static function send_expired_email( $subscription ) {
WC()->mailer();
do_action( 'expired_subscription_notification', $subscription );
}
/**
* Init the mailer and call for the suspended email notification hook.
*
* @param $subscription WC Subscription
* @since 2.1
*/
public static function send_on_hold_email( $subscription ) {
WC()->mailer();
do_action( 'on-hold_subscription_notification', $subscription );
}
/**
* Init the mailer and call the notifications for the renewal orders.
*
@@ -139,7 +170,7 @@ class WC_Subscriptions_Email {
*/
public static function maybe_reattach_woocommerce_email( $order_id ) {
if ( wcs_order_contains_renewal( $order_id ) || wcs_order_contains_switch( $order_id ) ) {
add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
}
}
@@ -231,6 +262,44 @@ class WC_Subscriptions_Email {
return $items_table;
}
/**
* Show the order details table
*
* @param WC_Order $order
* @param bool $sent_to_admin Whether the email is sent to admin - defaults to false
* @param bool $plain_text Whether the email should use plain text templates - defaults to false
* @param WC_Email $email
* @since 2.1
*/
public static function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) {
$order_items_table_args = array(
'show_download_links' => ( $sent_to_admin ) ? false : $order->is_download_permitted(),
'show_sku' => $sent_to_admin,
'show_purchase_note' => ( $sent_to_admin ) ? false : $order->has_status( apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ) ),
'show_image' => '',
'image_size' => '',
'plain_text' => $plain_text,
);
$template_path = ( $plain_text ) ? 'emails/plain/email-order-details.php' : 'emails/email-order-details.php';
$order_type = ( wcs_is_subscription( $order ) ) ? 'subscription' : 'order';
wc_get_template(
$template_path,
array(
'order' => $order,
'sent_to_admin' => $sent_to_admin,
'plain_text' => $plain_text,
'email' => $email,
'order_type' => $order_type,
'order_items_table_args' => $order_items_table_args,
),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/'
);
}
/**
* Init the mailer and call the notifications for the current filter.
*

View File

@@ -105,7 +105,7 @@ class WC_Subscriptions_Manager {
}
}
if ( 0 == $subscription->get_total() ) {
if ( 0 == $renewal_order->get_total() ) {
$renewal_order->payment_complete();
@@ -434,7 +434,7 @@ class WC_Subscriptions_Manager {
$billing_interval = WC_Subscriptions_Product::get_interval( $product_id );
// Support passing timestamps
$args['start_date'] = is_numeric( $args['start_date'] ) ? date( 'Y-m-d H:i:s', $args['start_date'] ) : $args['start_date'];
$args['start_date'] = is_numeric( $args['start_date'] ) ? gmdate( 'Y-m-d H:i:s', $args['start_date'] ) : $args['start_date'];
$product = wc_get_product( $product_id );
@@ -494,7 +494,7 @@ class WC_Subscriptions_Manager {
// Adding a new subscription so set the expiry date/time from the order date
if ( ! empty( $args['expiry_date'] ) ) {
if ( is_numeric( $args['expiry_date'] ) ) {
$args['expiry_date'] = date( 'Y-m-d H:i:s', $args['expiry_date'] );
$args['expiry_date'] = gmdate( 'Y-m-d H:i:s', $args['expiry_date'] );
}
$expiration = $args['expiry_date'];
@@ -1171,7 +1171,7 @@ class WC_Subscriptions_Manager {
public static function set_expiration_date( $subscription_key, $user_id = '', $expiration_date = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "end" => $expiration_date ) )' );
if ( is_int( $expiration_date ) ) {
$expiration_date = date( 'Y-m-d H:i:s', $expiration_date );
$expiration_date = gmdate( 'Y-m-d H:i:s', $expiration_date );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
return apply_filters( 'woocommerce_subscriptions_set_expiration_date', $subscription->update_dates( array( 'end' => $expiration_date ) ), $subscription->get_date( 'end' ), $subscription_key, $user_id );
@@ -1232,7 +1232,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "next_payment" => $next_payment ) )' );
if ( is_int( $next_payment ) ) {
$next_payment = date( 'Y-m-d H:i:s', $next_payment );
$next_payment = gmdate( 'Y-m-d H:i:s', $next_payment );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
@@ -1284,7 +1284,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::calculate_date( "next_payment" )' );
$subscription = wcs_get_subscription_from_key( $subscription_key );
$next_payment = $subscription->calculate_date( 'next_payment' );
return ( 'mysql' == $type ) ? $next_payment : strtotime( $next_payment );
return ( 'mysql' == $type ) ? $next_payment : wcs_date_to_time( $next_payment );
}
/**
@@ -1314,7 +1314,7 @@ class WC_Subscriptions_Manager {
public static function set_trial_expiration_date( $subscription_key, $user_id = '', $trial_expiration_date = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "trial_end" => $expiration_date ) )' );
if ( is_int( $trial_expiration_date ) ) {
$trial_expiration_date = date( 'Y-m-d H:i:s', $trial_expiration_date );
$trial_expiration_date = gmdate( 'Y-m-d H:i:s', $trial_expiration_date );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
return apply_filters( 'woocommerce_subscriptions_set_trial_expiration_date', $subscription->update_dates( array( 'trial_end' => $trial_expiration_date ) ), $subscription->get_date( 'trial_end' ), $subscription_key, $user_id );
@@ -1333,7 +1333,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::calculate_date( "trial_end" )' );
$subscription = wcs_get_subscription_from_key( $subscription_key );
$trial_end = $subscription->calculate_date( 'trial_end' );
$trial_end = ( 'mysql' == $type ) ? $trial_end : strtotime( $trial_end );
$trial_end = ( 'mysql' == $type ) ? $trial_end : wcs_date_to_time( $trial_end );
return apply_filters( 'woocommerce_subscription_calculated_trial_expiration_date' , $trial_end, $subscription_key, $user_id );
}
@@ -1601,14 +1601,14 @@ class WC_Subscriptions_Manager {
public static function update_next_payment_date( $new_payment_date, $subscription_key, $user_id = '', $timezone = 'server' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "next_payment" => $new_payment_date ) )' );
$new_payment_timestamp = ( is_numeric( $new_payment_date ) ) ? $new_payment_date : strtotime( $new_payment_date );
$new_payment_timestamp = ( is_numeric( $new_payment_date ) ) ? $new_payment_date : wcs_date_to_time( $new_payment_date );
// The date needs to be converted to GMT/UTC
if ( 'server' != $timezone ) {
$new_payment_timestamp = $new_payment_timestamp - ( get_option( 'gmt_offset' ) * 3600 );
}
$new_payment_date = date( 'Y-m-d H:i:s', $new_payment_timestamp );
$new_payment_date = gmdate( 'Y-m-d H:i:s', $new_payment_timestamp );
$subscription = wcs_get_subscription_from_key( $subscription_key );
@@ -1860,122 +1860,6 @@ class WC_Subscriptions_Manager {
/* Deprecated Functions */
/**
* @deprecated 1.1
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @since 1.0
*/
public static function can_subscription_be_cancelled( $subscription_key, $user_id = '' ) {
_deprecated_function( __METHOD__, '1.1', __CLASS__ . '::can_subscription_be_changed_to( "cancelled", $subscription_key, $user_id )' );
$subscription_can_be_cancelled = self::can_subscription_be_changed_to( 'cancelled', $subscription_key, $user_id );
return apply_filters( 'woocommerce_subscription_can_be_cancelled', $subscription_can_be_cancelled, $subscription, $order );
}
/**
* @deprecated 1.1
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @since 1.0
*/
public static function get_users_cancellation_link( $subscription_key ) {
_deprecated_function( __METHOD__, '1.1', __CLASS__ . '::get_users_cancellation_link( $subscription_key, "cancel" )' );
return apply_filters( 'woocommerce_subscriptions_users_cancellation_link', self::get_users_change_status_link( $subscription_key, 'cancel' ), $subscription_key );
}
/**
* @deprecated 1.1
* @since 1.0
*/
public static function maybe_cancel_users_subscription() {
_deprecated_function( __METHOD__, '1.1', __CLASS__ . '::maybe_change_users_subscription()' );
self::maybe_change_users_subscription();
}
/**
* @deprecated 1.1
* @param int $user_id The ID of the user who owns the subscriptions.
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @since 1.0
*/
public static function get_failed_payment_count( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.1', __CLASS__ . '::get_subscriptions_failed_payment_count( $subscription_key, $user_id )' );
return self::get_subscriptions_failed_payment_count( $subscription_key, $user_id );
}
/**
* Deprecated in favour of a more correctly named @see maybe_reschedule_subscription_payment()
*
* @deprecated 1.1.5
* @since 1.0
*/
public static function reschedule_subscription_payment( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.1.5', __CLASS__ . '::maybe_reschedule_subscription_payment( $user_id, $subscription_key )' );
self::maybe_reschedule_subscription_payment( $user_id, $subscription_key );
}
/**
* Suspended a single subscription on a users account by placing it in the "suspended" status.
*
* Subscriptions version 1.2 replaced the "suspended" status with the "on-hold" status to match WooCommerce core.
*
* @param int $user_id The id of the user whose subscription should be suspended.
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @deprecated 1.2
* @since 1.0
*/
public static function suspend_subscription( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::put_subscription_on_hold( $user_id, $subscription_key )' );
self::put_subscription_on_hold( $user_id, $subscription_key );
}
/**
* Suspended all the subscription products in an order.
*
* Subscriptions version 1.2 replaced the "suspended" status with the "on-hold" status to match WooCommerce core.
*
* @param WC_Order|int $order The order or ID of the order for which subscriptions should be marked as activated.
* @deprecated 1.2
* @since 1.0
*/
public static function suspend_subscriptions_for_order( $order ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::put_subscription_on_hold_for_order( $order )' );
self::put_subscription_on_hold_for_order( $order );
}
/**
* Gets a specific subscription for a user, as specified by $subscription_key
*
* Subscriptions version 1.4 moved subscription details out of user meta and into item meta, meaning it can be accessed
* efficiently without a user ID.
*
* @param int $user_id (optional) The id of the user whose subscriptions you want. Defaults to the currently logged in user.
* @param string $subscription_key A subscription key of the form created by @see self::subscription_key()
* @deprecated 1.4
* @since 1.0
*/
public static function get_users_subscription( $user_id = 0, $subscription_key ) {
_deprecated_function( __METHOD__, '1.4', __CLASS__ . '::get_subscription( $subscription_key )' );
return apply_filters( 'woocommerce_users_subscription', self::get_subscription( $subscription_key ), $user_id, $subscription_key );
}
/**
* Removed a specific subscription for a user, as specified by $subscription_key, but as subscriptions are no longer stored
* against a user and are instead stored against the order, this is no longer required (changing the user on the order effectively
* performs the same thing without requiring the subscription to have any changes).
*
* @param int $user_id (optional) The id of the user whose subscriptions you want. Defaults to the currently logged in user.
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @deprecated 1.4
* @since 1.0
*/
public static function remove_users_subscription( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.4' );
}
/**
* When a scheduled subscription payment hook is fired, automatically process the subscription payment
* if the amount is for $0 (and therefore, there is no payment to be processed by a gateway, and likely
@@ -2310,7 +2194,7 @@ class WC_Subscriptions_Manager {
} else {
$new_payment_date = sprintf( '%s-%s-%s %s', (int) $_POST['wcs_year'], zeroise( (int) $_POST['wcs_month'], 2 ), zeroise( (int) $_POST['wcs_day'], 2 ), date( 'H:i:s', current_time( 'timestamp' ) ) );
$new_payment_date = sprintf( '%s-%s-%s %s', (int) $_POST['wcs_year'], zeroise( (int) $_POST['wcs_month'], 2 ), zeroise( (int) $_POST['wcs_day'], 2 ), gmdate( 'H:i:s', current_time( 'timestamp' ) ) );
$new_payment_timestamp = self::update_next_payment_date( $new_payment_date, $_POST['wcs_subscription_key'], self::get_user_id_from_subscription_key( $_POST['wcs_subscription_key'] ), 'user' );
if ( is_wp_error( $new_payment_timestamp ) ) {

View File

@@ -36,8 +36,13 @@ class WC_Subscriptions_Order {
add_action( 'woocommerce_thankyou', __CLASS__ . '::subscription_thank_you' );
add_action( 'manage_shop_order_posts_custom_column', __CLASS__ . '::add_contains_subscription_hidden_field', 10, 1 );
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::contains_subscription_hidden_field', 10, 1 );
// Add column that indicates whether an order is parent or renewal for a subscription
add_filter( 'manage_edit-shop_order_columns', __CLASS__ . '::add_contains_subscription_column' );
add_action( 'manage_shop_order_posts_custom_column', __CLASS__ . '::add_contains_subscription_column_content', 10, 1 );
// Record initial payment against the subscription & set start date based on that payment
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_record_subscription_payment', 9, 3 );
@@ -358,11 +363,16 @@ class WC_Subscriptions_Order {
if ( wcs_order_contains_subscription( $order_id, 'any' ) ) {
$subscription_count = count( wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) ) );
$thank_you_message = '<p>' . _n( 'Your subscription will be activated when payment clears.', 'Your subscriptions will be activated when payment clears.', $subscription_count, 'woocommerce-subscriptions' ) . '</p>';
$my_account_subscriptions_url = get_permalink( wc_get_page_id( 'myaccount' ) );
// Post WC 2.6 link directly to the My Account subscriptions endpoint
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
$my_account_subscriptions_url = wc_get_endpoint_url( 'subscriptions', '', wc_get_page_permalink( 'myaccount' ) );
}
// translators: placeholders are opening and closing link tags
$thank_you_message .= '<p>' . sprintf( _n( 'View the status of your subscription in %syour account%s.', 'View the status of your subscriptions in %syour account%s.', $subscription_count, 'woocommerce-subscriptions' ), '<a href="' . get_permalink( wc_get_page_id( 'myaccount' ) ) . '">', '</a>' ) . '</p>';
$thank_you_message .= '<p>' . sprintf( _n( 'View the status of your subscription in %syour account%s.', 'View the status of your subscriptions in %syour account%s.', $subscription_count, 'woocommerce-subscriptions' ), '<a href="' . $my_account_subscriptions_url . '">', '</a>' ) . '</p>';
echo wp_kses( apply_filters( 'woocommerce_subscriptions_thank_you_message', $thank_you_message, $order_id ), array( 'a' => array( 'href' => array(), 'title' => array() ), 'p' => array(), 'em' => array(), 'strong' => array() ) );
}
@@ -401,6 +411,46 @@ class WC_Subscriptions_Order {
echo '<input type="hidden" name="contains_subscription" value="' . esc_attr( $has_subscription ) . '">';
}
/**
* Add a column to the WooCommerce -> Orders admin screen to indicate whether an order is a
* parent of a subscription, a renewal order for a subscription, or a regular order.
*
* @param array $columns The current list of columns
* @since 2.1
*/
public static function add_contains_subscription_column( $columns ) {
$column_header = '<span class="subscription_head tips" data-tip="' . esc_attr__( 'Subscription Relationship', 'woocommerce-subscriptions' ) . '">' . esc_attr__( 'Subscription Relationship', 'woocommerce-subscriptions' ) . '</span>';
$new_columns = wcs_array_insert_after( 'shipping_address', $columns, 'subscription_relationship', $column_header );
return $new_columns;
}
/**
* Add column content to the WooCommerce -> Orders admin screen to indicate whether an
* order is a parent of a subscription, a renewal order for a subscription, or a
* regular order.
*
* @param string $column The string of the current column
* @since 2.1
*/
public static function add_contains_subscription_column_content( $column ) {
global $post;
if ( 'subscription_relationship' == $column ) {
if ( wcs_order_contains_subscription( $post->ID, 'renewal' ) ) {
echo '<span class="subscription_renewal_order tips" data-tip="' . esc_attr__( 'Renewal Order', 'woocommerce-subscriptions' ) . '"></span>';
} elseif ( wcs_order_contains_subscription( $post->ID, 'resubscribe' ) ) {
echo '<span class="subscription_resubscribe_order tips" data-tip="' . esc_attr__( 'Resubscribe Order', 'woocommerce-subscriptions' ) . '"></span>';
} elseif ( wcs_order_contains_subscription( $post->ID, 'parent' ) ) {
echo '<span class="subscription_parent_order tips" data-tip="' . esc_attr__( 'Parent Order', 'woocommerce-subscriptions' ) . '"></span>';
} else {
echo '<span class="normal_order">&ndash;</span>';
}
}
}
/**
* Records the initial payment against a subscription.
*
@@ -417,9 +467,9 @@ class WC_Subscriptions_Order {
*/
public static function maybe_record_subscription_payment( $order_id, $old_order_status, $new_order_status ) {
if ( wcs_order_contains_subscription( $order_id ) ) {
if ( wcs_order_contains_subscription( $order_id, 'parent' ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order_id );
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'parent' ) );
$was_activated = false;
$order_completed = in_array( $new_order_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) ) && in_array( $old_order_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
@@ -441,7 +491,7 @@ class WC_Subscriptions_Order {
$next_payment = $subscription->get_time( 'next_payment' );
// if either there is a free trial date or a next payment date that falls before now, we need to recalculate all the sync'd dates
if ( ( $trial_end > 0 && $trial_end < strtotime( $dates['start'] ) ) || ( $next_payment > 0 && $next_payment < strtotime( $dates['start'] ) ) ) {
if ( ( $trial_end > 0 && $trial_end < wcs_date_to_time( $dates['start'] ) ) || ( $next_payment > 0 && $next_payment < wcs_date_to_time( $dates['start'] ) ) ) {
foreach ( $subscription->get_items() as $item ) {
$product_id = wcs_get_canonical_product_id( $item );
@@ -626,33 +676,31 @@ class WC_Subscriptions_Order {
* @since version 1.5
*/
public static function restrict_manage_subscriptions() {
global $typenow, $wp_query;
global $typenow;
if ( 'shop_order' != $typenow ) {
return;
}?>
<select name='shop_order_subtype' id='dropdown_shop_order_subtype'>
<option value=""><?php esc_html_e( 'Show all types', 'woocommerce-subscriptions' ); ?></option>
<option value=""><?php esc_html_e( 'All orders types', 'woocommerce-subscriptions' ); ?></option>
<?php
$terms = array( 'Original', 'Renewal' );
$order_types = array(
'original' => _x( 'Original', 'An order type', 'woocommerce-subscriptions' ),
'parent' => _x( 'Subscription Parent', 'An order type', 'woocommerce-subscriptions' ),
'renewal' => _x( 'Subscription Renewal', 'An order type', 'woocommerce-subscriptions' ),
'resubscribe' => _x( 'Subscription Resubscribe', 'An order type', 'woocommerce-subscriptions' ),
'switch' => _x( 'Subscription Switch', 'An order type', 'woocommerce-subscriptions' ),
'regular' => _x( 'Non-subscription', 'An order type', 'woocommerce-subscriptions' ),
);
foreach ( $terms as $term ) {
echo '<option value="' . esc_attr( $term ) . '"';
foreach ( $order_types as $order_type_key => $order_type_description ) {
echo '<option value="' . esc_attr( $order_type_key ) . '"';
if ( isset( $_GET['shop_order_subtype'] ) && $_GET['shop_order_subtype'] ) {
selected( $term, $_GET['shop_order_subtype'] );
selected( $order_type_key, $_GET['shop_order_subtype'] );
}
switch ( $term ) {
case 'Original':
$term_text = _x( 'Original', 'An order type', 'woocommerce-subscriptions' );
break;
case 'Renewal':
$term_text = _x( 'Renewal', 'An order type', 'woocommerce-subscriptions' );
break;
}
echo '>' . esc_html( $term_text ) . '</option>';
echo '>' . esc_html( $order_type_description ) . '</option>';
}
?>
</select>
@@ -668,21 +716,51 @@ class WC_Subscriptions_Order {
* @since 1.5
*/
public static function orders_by_type_query( $vars ) {
global $typenow, $wp_query;
global $typenow, $wpdb;
if ( 'shop_order' == $typenow && isset( $_GET['shop_order_subtype'] ) ) {
if ( 'shop_order' == $typenow && ! empty( $_GET['shop_order_subtype'] ) ) {
if ( 'Original' == $_GET['shop_order_subtype'] ) {
$compare_operator = 'NOT EXISTS';
} elseif ( 'Renewal' == $_GET['shop_order_subtype'] ) {
$compare_operator = 'EXISTS';
}
if ( 'original' == $_GET['shop_order_subtype'] || 'regular' == $_GET['shop_order_subtype'] ) {
$vars['meta_query']['relation'] = 'AND';
if ( ! empty( $compare_operator ) ) {
$vars['meta_query'][] = array(
'key' => '_subscription_renewal',
'compare' => $compare_operator,
'compare' => 'NOT EXISTS',
);
$vars['meta_query'][] = array(
'key' => '_subscription_switch',
'compare' => 'NOT EXISTS',
);
} elseif ( 'parent' == $_GET['shop_order_subtype'] ) {
$vars['post__in'] = wcs_get_subscription_orders();
} else {
switch ( $_GET['shop_order_subtype'] ) {
case 'renewal' :
$meta_key = '_subscription_renewal';
break;
case 'resubscribe' :
$meta_key = '_subscription_resubscribe';
break;
case 'switch' :
$meta_key = '_subscription_switch';
break;
}
$vars['meta_query'][] = array(
'key' => $meta_key,
'compare' => 'EXISTS',
);
}
// Also exclude parent orders from non-subscription query
if ( 'regular' == $_GET['shop_order_subtype'] ) {
$vars['post__not_in'] = wcs_get_subscription_orders();
}
}
@@ -941,134 +1019,6 @@ class WC_Subscriptions_Order {
/* Deprecated Functions */
/**
* Returned the recurring amount for a subscription in an order.
*
* @deprecated 1.2
* @since 1.0
*/
public static function get_price_per_period( $order, $product_id = '' ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::get_recurring_total( $order, $product_id )' );
return self::get_recurring_total( $order, $product_id );
}
/**
* Creates a new order for renewing a subscription product based on the details of a previous order.
*
* @param WC_Order|int $order The WC_Order object or ID of the order for which the a new order should be created.
* @param string $product_id The ID of the subscription product in the order which needs to be added to the new order.
* @param string $new_order_role A flag to indicate whether the new order should become the master order for the subscription. Accepts either 'parent' or 'child'. Defaults to 'parent' - replace the existing order.
* @deprecated 1.2
* @since 1.0
*/
public static function generate_renewal_order( $original_order, $product_id, $new_order_role = 'parent' ) {
_deprecated_function( __METHOD__, '1.2', 'WC_Subscriptions_Renewal_Order::generate_renewal_order( $original_order, $product_id, array( "new_order_role" => $new_order_role ) )' );
return WC_Subscriptions_Renewal_Order::generate_renewal_order( $original_order, $product_id, array( 'new_order_role' => $new_order_role ) );
}
/**
* Hooks to the renewal order created action to determine if the order should be emailed to the customer.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @deprecated 1.2
* @since 1.0
*/
public static function maybe_send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.2', 'WC_Subscriptions_Renewal_Order::maybe_send_customer_renewal_order_email( $order )' );
WC_Subscriptions_Renewal_Order::maybe_send_customer_renewal_order_email( $order );
}
/**
* Processing Order
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @deprecated 1.2
* @since 1.0
*/
public static function send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.2', 'WC_Subscriptions_Renewal_Order::send_customer_renewal_order_email( $order )' );
WC_Subscriptions_Renewal_Order::send_customer_renewal_order_email( $order );
}
/**
* Check if a given order is a subscription renewal order
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @deprecated 1.2
* @since 1.0
*/
public static function is_renewal( $order ) {
_deprecated_function( __METHOD__, '1.2', 'wcs_order_contains_renewal( $order )' );
return wcs_order_contains_renewal( $order );
}
/**
* Once payment is completed on an order, record the payment against the subscription automatically so that
* payment gateway extension developers don't have to do this.
*
* @param int $order_id The id of the order to record payment against
* @deprecated 1.2
* @since 1.1.2
*/
public static function record_order_payment( $order_id ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::maybe_record_order_payment( $order_id )' );
return self::maybe_record_order_payment( $order_id );
}
/**
* Checks an order item to see if it is a subscription. The item needs to exist and have been a subscription
* product at the time of purchase for the function to return true.
*
* @param mixed $order A WC_Order object or the ID of the order which the subscription was purchased in.
* @param int $product_id The ID of a WC_Product object purchased in the order.
* @return bool True if the order contains a subscription, otherwise false.
* @deprecated 1.2.4
*/
public static function is_item_a_subscription( $order, $product_id ) {
_deprecated_function( __METHOD__, '1.2.4', __CLASS__ . '::is_item_subscription( $order, $product_id )' );
return self::is_item_subscription( $order, $product_id );
}
/**
* Deprecated due to change of order item ID/API in WC 2.0.
*
* @param WC_Order|int $order The WC_Order object or ID of the order for which the meta should be sought.
* @param int $item_id The product/post ID of a subscription. Option - if no product id is provided, the first item's meta will be returned
* @since 1.2
* @deprecated 1.2.5
*/
public static function get_item( $order, $product_id = '' ) {
_deprecated_function( __METHOD__, '1.2.5', __CLASS__ . '::get_item_by_product_id( $order, $product_id )' );
return self::get_item_by_product_id( $order, $product_id );
}
/**
* Deprecated due to different totals calculation method.
*
* Determined the proportion of the order total that a recurring amount accounts for and
* returns that proportion.
*
* If there is only one subscription in the order and no sign up fee for the subscription,
* this function will return 1 (i.e. 100%).
*
* Shipping only applies to recurring amounts so is deducted from both the order total and
* recurring amount so it does not distort the proportion.
*
* @param WC_Order|int $order A WC_Order object or ID of a WC_Order order.
* @return float The proportion of the order total which the recurring amount accounts for
* @since 1.2
* @deprecated 1.4
*/
public static function get_recurring_total_proportion( $order, $product_id = '' ) {
_deprecated_function( __METHOD__, '1.4' );
$order_shipping_total = self::get_recurring_shipping_total( $order ) + self::get_recurring_shipping_tax_total( $order );
$order_total_sans_shipping = $order->get_total() - $order_shipping_total;
$recurring_total_sans_shipping = self::get_recurring_total( $order, $product_id ) - $order_shipping_total;
return $recurring_total_sans_shipping / $order_total_sans_shipping;
}
/**
* Checks an order to see if it contains a subscription.
*
@@ -1835,7 +1785,7 @@ class WC_Subscriptions_Order {
$next_payment = $subscription->calculate_date( 'next_payment' );
}
$next_payment = ( 'mysql' == $type && 0 != $next_payment ) ? $next_payment : strtotime( $next_payment );
$next_payment = ( 'mysql' == $type && 0 != $next_payment ) ? $next_payment : wcs_date_to_time( $next_payment );
return apply_filters( 'woocommerce_subscriptions_calculated_next_payment_date', $next_payment, $order, $product_id, $type, $from_date, $from_date );
}

View File

@@ -15,9 +15,6 @@ class WC_Subscriptions_Product {
/* cache the check on whether the session has an order awaiting payment for a given product */
protected static $order_awaiting_payment_for_product = array();
/* cache whether a given product is purchasable or not to save running lots of queries for the same product in the same request */
protected static $is_purchasable_cache = array();
protected static $subscription_meta_fields = array(
'_subscription_price',
'_subscription_sign_up_fee',
@@ -587,7 +584,7 @@ class WC_Subscriptions_Product {
$first_renewal_timestamp = self::get_first_renewal_payment_time( $product_id, $from_date, $timezone );
if ( $first_renewal_timestamp > 0 ) {
$first_renewal_date = date( 'Y-m-d H:i:s', $first_renewal_timestamp );
$first_renewal_date = gmdate( 'Y-m-d H:i:s', $first_renewal_timestamp );
} else {
$first_renewal_date = 0;
}
@@ -626,18 +623,11 @@ class WC_Subscriptions_Product {
// If the subscription has a free trial period, the first renewal is the same as the expiration of the free trial
if ( $trial_length > 0 ) {
$first_renewal_timestamp = strtotime( self::get_trial_expiration_date( $product_id, $from_date ) );
$first_renewal_timestamp = wcs_date_to_time( self::get_trial_expiration_date( $product_id, $from_date ) );
} else {
$from_timestamp = strtotime( $from_date );
$billing_period = self::get_period( $product_id );
if ( 'month' == $billing_period ) {
$first_renewal_timestamp = wcs_add_months( $from_timestamp, $billing_interval );
} else {
$first_renewal_timestamp = strtotime( "+ $billing_interval {$billing_period}s", $from_timestamp );
}
$first_renewal_timestamp = wcs_add_time( $billing_interval, self::get_period( $product_id ), wcs_date_to_time( $from_date ) );
if ( 'site' == $timezone ) {
$first_renewal_timestamp += ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
@@ -672,7 +662,7 @@ class WC_Subscriptions_Product {
$from_date = self::get_trial_expiration_date( $product_id, $from_date );
}
$expiration_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription_length, self::get_period( $product_id ), strtotime( $from_date ) ) );
$expiration_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription_length, self::get_period( $product_id ), wcs_date_to_time( $from_date ) ) );
} else {
@@ -694,7 +684,6 @@ class WC_Subscriptions_Product {
*/
public static function get_trial_expiration_date( $product_id, $from_date = '' ) {
$trial_period = self::get_trial_period( $product_id );
$trial_length = self::get_trial_length( $product_id );
if ( $trial_length > 0 ) {
@@ -703,11 +692,8 @@ class WC_Subscriptions_Product {
$from_date = gmdate( 'Y-m-d H:i:s' );
}
if ( 'month' == $trial_period ) {
$trial_expiration_date = date( 'Y-m-d H:i:s', wcs_add_months( strtotime( $from_date ), $trial_length ) );
} else { // Safe to just add the billing periods
$trial_expiration_date = date( 'Y-m-d H:i:s', strtotime( "+ {$trial_length} {$trial_period}s", strtotime( $from_date ) ) );
}
$trial_expiration_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $trial_length, self::get_trial_period( $product_id ), wcs_date_to_time( $from_date ) ) );
} else {
$trial_expiration_date = 0;
@@ -903,31 +889,6 @@ class WC_Subscriptions_Product {
}
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 2.0
* @return bool
*/
public static function is_purchasable( $is_purchasable, $product ) {
global $wp;
if ( ! isset( self::$is_purchasable_cache[ $product->id ] ) ) {
self::$is_purchasable_cache[ $product->id ] = $is_purchasable;
if ( self::is_subscription( $product->id ) && 'no' != $product->limit_subscriptions && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
if ( ( ( 'active' == $product->limit_subscriptions && wcs_user_has_subscription( 0, $product->id, 'on-hold' ) ) || wcs_user_has_subscription( 0, $product->id, $product->limit_subscriptions ) ) && ! self::order_awaiting_payment_for_product( $product->id ) ) {
self::$is_purchasable_cache[ $product->id ] = false;
}
}
}
return self::$is_purchasable_cache[ $product->id ];
}
/**
* Save variation meta data when it is bulk edited from the Edit Product screen
*
@@ -983,50 +944,6 @@ class WC_Subscriptions_Product {
}
}
/**
* Check if the current session has an order awaiting payment for a subscription to a specific product line item.
*
* @return 2.0.13
* @return bool
**/
protected static function order_awaiting_payment_for_product( $product_id ) {
global $wp;
if ( ! isset( self::$order_awaiting_payment_for_product[ $product_id ] ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = false;
if ( ! empty( WC()->session->order_awaiting_payment ) || isset( $_GET['pay_for_order'] ) ) {
$order_id = ! empty( WC()->session->order_awaiting_payment ) ? WC()->session->order_awaiting_payment : $wp->query_vars['order-pay'];
$order = wc_get_order( absint( $order_id ) );
if ( is_object( $order ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
foreach ( $order->get_items() as $item ) {
if ( $item['product_id'] == $product_id || $item['variation_id'] == $product_id ) {
$subscriptions = wcs_get_subscriptions( array(
'order_id' => $order->id,
'product_id' => $product_id,
) );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
if ( $subscription->has_status( array( 'pending', 'on-hold' ) ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = true;
}
}
break;
}
}
}
}
}
return self::$order_awaiting_payment_for_product[ $product_id ];
}
/**
* Processes an AJAX request to check if a product has a variation which is either sync'd or has a trial.
* Once at least one variation with a trial or sync date is found, this will terminate and return true, otherwise false.
@@ -1092,87 +1009,61 @@ class WC_Subscriptions_Product {
}
/**
* Calculates a price (could be per period price or sign-up fee) for a subscription less tax
* if the subscription is taxable and the prices in the store include tax.
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* Based on the WC_Product::get_price_excluding_tax() function.
*
* @param float $price The price to adjust based on taxes
* @param WC_Product $product The product the price belongs too (needed to determine tax class)
* @since 1.0
* @since 2.0
* @return bool
*/
public static function calculate_tax_for_subscription( $price, $product, $deduct_base_taxes = false ) {
_deprecated_function( __METHOD__, '1.5.8', 'WC_Product::get_price_including_tax()' );
if ( $product->is_taxable() ) {
$tax = new WC_Tax();
$base_tax_rates = $tax->get_shop_base_rate( $product->tax_class );
$tax_rates = $tax->get_rates( $product->get_tax_class() ); // This will get the base rate unless we're on the checkout page
if ( $deduct_base_taxes && wc_prices_include_tax() ) {
$base_taxes = $tax->calc_tax( $price, $base_tax_rates, true );
$taxes = $tax->calc_tax( $price - array_sum( $base_taxes ), $tax_rates, false );
} elseif ( get_option( 'woocommerce_prices_include_tax' ) == 'yes' ) {
$taxes = $tax->calc_tax( $price, $base_tax_rates, true );
} else {
$taxes = $tax->calc_tax( $price, $base_tax_rates, false );
}
$tax_amount = $tax->get_tax_total( $taxes );
} else {
$tax_amount = 0;
}
return $tax_amount;
}
/**
* Deprecated in favour of native get_price_html() method on the Subscription Product classes (e.g. WC_Product_Subscription)
*
* Output subscription string as the price html
*
* @since 1.0
* @deprecated 1.5.18
*/
public static function get_price_html( $price, $product ) {
_deprecated_function( __METHOD__, '1.5.18', __CLASS__ . '::get_price_string()' );
if ( self::is_subscription( $product ) ) {
$price = self::get_price_string( $product, array( 'price' => $price ) );
}
return $price;
public static function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_product' );
return WCS_Limiter::is_purchasable_product( $is_purchasable, $product );
}
/**
* Deprecated in favour of native get_price_html() method on the Subscription Product classes (e.g. WC_Product_Subscription)
* Check if the current session has an order awaiting payment for a subscription to a specific product line item.
*
* Set the subscription string for products which have a $0 recurring fee, but a sign-up fee
*
* @since 1.3.4
* @deprecated 1.5.18
*/
public static function get_free_price_html( $price, $product ) {
_deprecated_function( __METHOD__, '1.5.18', __CLASS__ . '::get_price_string()' );
* @return 2.0.13
* @return bool
**/
protected static function order_awaiting_payment_for_product( $product_id ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::order_awaiting_payment_for_product' );
// Check if it has a sign-up fee (we already know it has no recurring fee)
if ( self::is_subscription( $product ) && self::get_sign_up_fee( $product ) > 0 ) {
$price = self::get_price_string( $product, array( 'price' => $price ) );
global $wp;
if ( ! isset( self::$order_awaiting_payment_for_product[ $product_id ] ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = false;
if ( ! empty( WC()->session->order_awaiting_payment ) || isset( $_GET['pay_for_order'] ) ) {
$order_id = ! empty( WC()->session->order_awaiting_payment ) ? WC()->session->order_awaiting_payment : $wp->query_vars['order-pay'];
$order = wc_get_order( absint( $order_id ) );
if ( is_object( $order ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
foreach ( $order->get_items() as $item ) {
if ( $item['product_id'] == $product_id || $item['variation_id'] == $product_id ) {
$subscriptions = wcs_get_subscriptions( array(
'order_id' => $order->id,
'product_id' => $product_id,
) );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
if ( $subscription->has_status( array( 'pending', 'on-hold' ) ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = true;
}
}
break;
}
}
}
}
}
return $price;
return self::$order_awaiting_payment_for_product[ $product_id ];
}
}

View File

@@ -191,108 +191,6 @@ class WC_Subscriptions_Renewal_Order {
/* Deprecated functions */
/**
* Hooks to the renewal order created action to determine if the order should be emailed to the customer.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 1.4
*/
public static function maybe_send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_email_renewal_order' ) ) {
self::send_customer_renewal_order_email( $order );
}
}
/**
* Processing Order
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 1.4
*/
public static function send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( ! is_object( $order ) ) {
$order = new WC_Order( $order );
}
$mailer = WC()->mailer();
$mails = $mailer->get_emails();
$mails['WCS_Email_Customer_Renewal_Invoice']->trigger( $order->id );
}
/**
* Change the email subject of the new order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_new_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
// translators: 1$: blog name, 2$: order number
$subject = apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', sprintf( _x( '[%1$s] New Subscription Renewal Order (%2$s)', 'used in new renewal order email, deprecated', 'woocommerce-subscriptions' ), $blogname, $order->get_order_number() ), $order );
}
return $subject;
}
/**
* Change the email subject of the processing order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_customer_procesing_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$subject = apply_filters(
'woocommerce_subscriptions_email_subject_customer_procesing_renewal_order',
// translators: placeholder is blog name
sprintf( _x( '[%s] Subscription Renewal Order', 'used as email subject for renewal order notification email to customer', 'woocommerce-subscriptions' ), $blogname ),
$order
);
}
return $subject;
}
/**
* Change the email subject of the completed order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_customer_completed_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$subject = apply_filters(
'woocommerce_subscriptions_email_subject_customer_completed_renewal_order',
// translators: placeholder is blog name
sprintf( _x( '[%s] Subscription Renewal Order', 'used as email subject for renewal order notification email to customer', 'woocommerce-subscriptions' ), $blogname ),
$order
);
}
return $subject;
}
/**
* Generate an order to record an automatic subscription payment.
*

View File

@@ -10,9 +10,6 @@
*/
class WC_Subscriptions_Switcher {
/* cache whether a given product is purchasable or not to save running lots of queries for the same product in the same request */
protected static $is_purchasable_cache = array();
/**
* Bootstraps the class and hooks required actions & filters.
*
@@ -63,10 +60,6 @@ class WC_Subscriptions_Switcher {
// Don't display free trials when switching a subscription, because no free trials are provided
add_filter( 'woocommerce_subscriptions_product_price_string_inclusions', __CLASS__ . '::customise_product_string_inclusions', 12, 2 );
// Allow switching between variations on a limited subscription
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable', 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable', 12, 2 );
// Autocomplete subscription switch orders
add_action( 'woocommerce_payment_complete_order_status', __CLASS__ . '::subscription_switch_autocomplete', 10, 2 );
@@ -101,12 +94,15 @@ class WC_Subscriptions_Switcher {
// Display/indicate whether a cart switch item is a upgrade/downgrade/crossgrade
add_filter( 'woocommerce_cart_item_subtotal', __CLASS__ . '::add_cart_item_switch_direction', 10, 3 );
// Check if the new order was to record a switch request and maybe call a "switch completed" action.
add_action( 'subscriptions_created_for_order', __CLASS__ . '::maybe_add_switched_callback', 10, 1 );
// Check if the order was to record a switch request and maybe call a "switch completed" action.
add_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::maybe_add_switched_callback', 10, 1 );
// Revoke download permissions from old switch item
add_action( 'woocommerce_subscriptions_switched_item', __CLASS__ . '::remove_download_permissions_after_switch', 10, 3 );
// Process subscription switch changes on completed switch orders status
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::process_subscription_switches', 10, 3 );
// Check if we need to force payment on this switch, just after calculating the prorated totals in @see self::calculate_prorated_totals()
add_filter( 'woocommerce_subscriptions_calculated_total', __CLASS__ . '::set_force_payment_flag_in_cart', 10, 1 );
@@ -114,7 +110,10 @@ class WC_Subscriptions_Switcher {
add_filter( 'woocommerce_cart_needs_payment', __CLASS__ . '::cart_needs_payment' , 50, 2 );
// Require payment when switching from a $0 / period subscription to a non-zero subscription to process automatic payments
add_filter( 'woocommerce_payment_successful_result', __CLASS__ . '::maybe_set_payment_method' , 10, 2 );
add_filter( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::maybe_set_payment_method_after_switch' , 10, 1 );
// Do not reduce product stock when the order item is simply to record a switch
add_filter( 'woocommerce_order_item_quantity', __CLASS__ . '::maybe_do_not_reduce_stock', 10, 3 );
// Mock a free trial on the cart item to make sure the switch total doesn't include any recurring amount
add_filter( 'woocommerce_before_calculate_totals', __CLASS__ . '::maybe_set_free_trial', 100, 1 );
@@ -183,45 +182,43 @@ class WC_Subscriptions_Switcher {
}
} elseif ( is_product() && $product = wc_get_product( $post ) ) { // Automatically initiate the switch process for limited variable subscriptions
if ( wcs_is_product_switchable_type( $product ) && 'no' != $product->limit_subscriptions ) {
$limited_switchable_products = array();
// Check if the user has an active subscription for this product, and if so, initiate the switch process
$subscriptions = wcs_get_users_subscriptions();
if ( $product->is_type( 'grouped' ) ) { // If we're on a grouped product's page, we need to check if this grouped product has children which are limited and may need to be switched
$child_ids = $product->get_children();
foreach ( $subscriptions as $subscription ) {
// If we're on a grouped product's page, we need to check if the subscription has a child of this grouped product that needs to be switched
$subscription_product_id = false;
if ( $product->is_type( 'grouped' ) ) {
foreach ( $child_ids as $child_id ) {
if ( $subscription->has_product( $child_id ) ) {
$subscription_product_id = $child_id;
break;
$product = wc_get_product( $child_id );
if ( 'no' != wcs_get_product_limitation( $product ) && wcs_is_product_switchable_type( $product ) ) {
$limited_switchable_products[] = $product;
}
}
} elseif ( 'no' != wcs_get_product_limitation( $product ) && wcs_is_product_switchable_type( $product ) ) {
// If we're on a limited variation or single product within a group which is switchable
// we only need to look for if the customer is subscribed to this product
$limited_switchable_products[] = $product;
}
if ( $subscription->has_product( $product->id ) || $subscription_product_id ) {
// If we have limited switchable products, check if the customer is already subscribed and needs to be switched
if ( ! empty( $limited_switchable_products ) ) {
// For grouped products, we need to check the child products limitations, not the grouped product's (which will have no limitation)
if ( $subscription_product_id ) {
$child_product = wc_get_product( $subscription_product_id );
$limitation = $child_product->limit_subscriptions;
} else {
$limitation = $product->limit_subscriptions;
$subscriptions = wcs_get_users_subscriptions();
foreach ( $subscriptions as $subscription ) {
foreach ( $limited_switchable_products as $product ) {
if ( ! $subscription->has_product( $product->id ) ) {
continue;
}
// If the product is limited
$limitation = wcs_get_product_limitation( $product );
if ( 'any' == $limitation || $subscription->has_status( $limitation ) ) {
$subscribed_notice = __( 'You have already subscribed to this product and it is limited to one per customer. You can not purchase the product again.', 'woocommerce-subscriptions' );
// If switching is enabled for this product type, initiate the auto-switch process
if ( wcs_is_product_switchable_type( $product ) ) {
// Don't initiate auto-switching when the subscription requires payment
if ( $subscription->needs_payment() ) {
@@ -237,11 +234,9 @@ class WC_Subscriptions_Switcher {
} else {
$product_id = ( $subscription_product_id ) ? $subscription_product_id : $product->id;
// Get the matching item
foreach ( $subscription->get_items() as $line_item_id => $line_item ) {
if ( $line_item['product_id'] == $product_id || $line_item['variation_id'] == $product_id ) {
if ( $line_item['product_id'] == $product->id || $line_item['variation_id'] == $product->id ) {
$item_id = $line_item_id;
$item = $line_item;
break;
@@ -253,11 +248,6 @@ class WC_Subscriptions_Switcher {
exit;
}
}
} else {
WC_Subscriptions::add_notice( $subscribed_notice, 'notice' );
break;
}
}
}
}
@@ -318,7 +308,7 @@ class WC_Subscriptions_Switcher {
'name' => __( 'Switching', 'woocommerce-subscriptions' ),
'type' => 'title',
// translators: placeholders are opening and closing link tags
'desc' => sprintf( __( 'Allow subscribers to switch (upgrade or downgrade) between different subscriptions. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woothemes.com/document/subscriptions/switching-guide/' ) . '">', '</a>' ),
'desc' => sprintf( __( 'Allow subscribers to switch (upgrade or downgrade) between different subscriptions. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woocommerce.com/document/subscriptions/switching-guide/' ) . '">', '</a>' ),
'id' => WC_Subscriptions_Admin::$option_prefix . '_switch_settings',
),
@@ -441,15 +431,21 @@ class WC_Subscriptions_Switcher {
}
$product = wc_get_product( $item['product_id'] );
$additional_query_args = array();
// Grouped product
if ( 0 !== $product->post->post_parent ) {
$switch_url = get_permalink( $product->post->post_parent );
} else {
$switch_url = get_permalink( $product->id );
if ( ! empty( $_GET ) && is_product() ) {
$product_variations = $product->get_variation_attributes();
$additional_query_args = array_intersect_key( $_GET, $product_variations );
}
}
$switch_url = self::add_switch_query_args( $subscription->id, $item_id, $switch_url );
$switch_url = self::add_switch_query_args( $subscription->id, $item_id, $switch_url, $additional_query_args );
return apply_filters( 'woocommerce_subscriptions_switch_url', $switch_url, $item_id, $item, $subscription );
}
@@ -460,12 +456,14 @@ class WC_Subscriptions_Switcher {
* @param int $subscription_id A subscription's post ID
* @param int $item_id The order item ID of a subscription line item
* @param string $permalink The permalink of the product
* @param array $additional_query_args (optional) Additional query args to add to the switch URL
* @since 2.0
*/
protected static function add_switch_query_args( $subscription_id, $item_id, $permalink ) {
protected static function add_switch_query_args( $subscription_id, $item_id, $permalink, $additional_query_args = array() ) {
// manually add a nonce because we can't use wp_nonce_url() (it would escape the URL)
$permalink = add_query_arg( array( 'switch-subscription' => absint( $subscription_id ), 'item' => absint( $item_id ), '_wcsnonce' => wp_create_nonce( 'wcs_switch_request' ) ), $permalink );
$query_args = array_merge( $additional_query_args, array( 'switch-subscription' => absint( $subscription_id ), 'item' => absint( $item_id ), '_wcsnonce' => wp_create_nonce( 'wcs_switch_request' ) ) );
$permalink = add_query_arg( $query_args, $permalink );
return apply_filters( 'woocommerce_subscriptions_add_switch_query_args', $permalink, $subscription_id, $item_id );
}
@@ -476,7 +474,7 @@ class WC_Subscriptions_Switcher {
* For an item to be switchable, switching must be enabled, and the item must be for a variable subscription or
* part of a grouped product (at the time the check is made, not at the time the subscription was purchased)
*
* The subscription must also be active or on-hold and use manual renewals or use a payment method which supports cancellation.
* The subscription must also be active and use manual renewals or use a payment method which supports cancellation.
*
* @param array $item An order item on the subscription
* @param WC_Subscription $subscription An instance of WC_Subscription
@@ -546,6 +544,9 @@ class WC_Subscriptions_Switcher {
*/
public static function add_order_meta( $order_id, $posted ) {
// delete all the existing subscription switch links before adding new ones
delete_post_meta( $order_id, '_subscription_switch' );
$switches = self::cart_contains_switches();
if ( false !== $switches ) {
@@ -615,6 +616,8 @@ class WC_Subscriptions_Switcher {
}
$order = wc_get_order( $order_id );
$order_items = $order->get_items();
$switch_order_data = array();
try {
// Start transaction if available
@@ -642,6 +645,11 @@ class WC_Subscriptions_Switcher {
$is_different_billing_schedule = false;
}
// If we haven't calculated a first payment date, fall back to the recurring cart's next payment date
if ( 0 == $cart_item['subscription_switch']['first_payment_timestamp'] ) {
$cart_item['subscription_switch']['first_payment_timestamp'] = strtotime( $recurring_cart->next_payment_date );
}
if ( 0 !== $cart_item['subscription_switch']['first_payment_timestamp'] && $next_payment_timestamp !== $cart_item['subscription_switch']['first_payment_timestamp'] ) {
$is_different_payment_date = true;
} elseif ( 0 !== $cart_item['subscription_switch']['first_payment_timestamp'] && 0 == $subscription->get_time( 'next_payment' ) ) { // if the subscription doesn't have a next payment but the switched item does
@@ -650,7 +658,7 @@ class WC_Subscriptions_Switcher {
$is_different_payment_date = false;
}
if ( date( 'Y-m-d', strtotime( $recurring_cart->end_date ) ) !== date( 'Y-m-d', $subscription->get_time( 'end' ) ) ) {
if ( gmdate( 'Y-m-d', wcs_date_to_time( $recurring_cart->end_date ) ) !== gmdate( 'Y-m-d', $subscription->get_time( 'end' ) ) ) {
$is_different_length = true;
} else {
$is_different_length = false;
@@ -663,11 +671,34 @@ class WC_Subscriptions_Switcher {
$is_single_item_subscription = false;
}
$order_item_id = '';
foreach ( $order_items as $item_id => $item ) {
if ( wcs_get_canonical_product_id( $item ) == wcs_get_canonical_product_id( $cart_item ) && ( empty( $switch_order_data['switches'] ) || ! in_array( $item_id, array_keys( $switch_order_data['switches'] ) ) ) ) {
$order_item_id = $item_id;
$switch_order_data[ $subscription->id ]['switches'][ $item_id ]['subscription_item_id'] = $cart_item['subscription_switch']['item_id'];
break;
}
}
// If the item is on the same schedule, we can just add it to the new subscription and remove the old item
if ( $is_single_item_subscription || ( false === $is_different_billing_schedule && false === $is_different_payment_date && false === $is_different_length ) ) {
// Add the new item
$item_id = WC_Subscriptions_Checkout::add_cart_item( $subscription, $cart_item, $cart_item_key );
$item_meta = wc_get_order_item_meta( $item_id, '' );
// We can't use the prorated order item price upon successful payment so store the cart price
$switch_order_data[ $subscription->id ]['switches'][ $order_item_id ]['add_order_item_data'] = array(
'totals' => array(
'subtotal' => $cart_item['line_subtotal'],
'subtotal_tax' => $cart_item['line_subtotal_tax'],
'total' => $cart_item['line_total'],
'tax' => $cart_item['line_tax'],
'tax_data' => $cart_item['line_tax_data'],
),
'meta' => $item_meta,
);
// Remove the item from the cart so that WC_Subscriptions_Checkout doesn't add it to a subscription
if ( 1 == count( WC()->cart->recurring_carts[ $recurring_cart_key ]->get_cart() ) ) {
@@ -676,8 +707,6 @@ class WC_Subscriptions_Switcher {
} else {
unset( WC()->cart->recurring_carts[ $recurring_cart_key ]->cart_contents[ $cart_item_key ] );
}
do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $item_id, $cart_item['subscription_switch']['item_id'] );
}
// If the old subscription has just one item, we can safely update its billing schedule
@@ -686,14 +715,18 @@ class WC_Subscriptions_Switcher {
if ( $is_different_billing_schedule ) {
update_post_meta( $subscription->id, '_billing_period', $cart_item['data']->subscription_period );
update_post_meta( $subscription->id, '_billing_interval', absint( $cart_item['data']->subscription_period_interval ) );
$switch_order_data[ $subscription->id ]['billing_schedule']['_billing_period'] = $cart_item['data']->subscription_period;
$switch_order_data[ $subscription->id ]['billing_schedule']['_billing_interval'] = absint( $cart_item['data']->subscription_period_interval );
}
$updated_dates = array();
if ( '1' == $cart_item['data']->subscription_length || ( 0 != $recurring_cart->end_date && date( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] ) >= $recurring_cart->end_date ) ) {
if ( '1' == $cart_item['data']->subscription_length || ( 0 != $recurring_cart->end_date && gmdate( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] ) >= $recurring_cart->end_date ) ) {
$subscription->delete_date( 'next_payment' );
$switch_order_data[ $subscription->id ]['dates']['delete'][] = 'next_payment';
} else if ( $is_different_payment_date ) {
$updated_dates['next_payment'] = date( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] );
$updated_dates['next_payment'] = gmdate( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] );
}
if ( $is_different_length ) {
@@ -702,33 +735,45 @@ class WC_Subscriptions_Switcher {
if ( ! empty( $updated_dates ) ) {
$subscription->update_dates( $updated_dates );
$switch_order_data[ $subscription->id ]['dates']['update'] = $updated_dates;
}
}
// Remove the old item from the subscription but don't delete it completely by changing its line item type to "line_item_switched"
wc_update_order_item( $cart_item['subscription_switch']['item_id'], array( 'order_item_type' => 'line_item_switched' ) );
$old_item_name = wcs_get_order_item_name( $existing_item, array( 'attributes' => true ) );
$new_item_name = wcs_get_cart_item_name( $cart_item, array( 'attributes' => true ) );
// translators: 1$: old item name, 2$: new item name when switching
$subscription->add_order_note( sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_item_name, $new_item_name ) );
// Change the shipping
self::update_shipping_methods( $subscription, $recurring_cart );
$switch_order_data[ $subscription->id ]['shipping_methods'] = $subscription->get_shipping_methods();
// Finally, change the addresses but only if they've changed
self::maybe_update_subscription_address( $order, $subscription );
$subscription->calculate_totals();
}
}
// Everything seems to be in order
$wpdb->query( 'COMMIT' );
// Everything seems to be in order.
// Rollback the changes and store the required meta on the order so it can be processed on successful payment.
$wpdb->query( 'ROLLBACK' );
foreach ( $switch_order_data as $subscription_id => $switch_data ) {
// Cancel all the switch orders linked to the switched subscription(s) which haven't been completed yet - excluding this one.
$switch_orders = wcs_get_switch_orders_for_subscription( $subscription_id );
foreach ( $switch_orders as $switch_order_id => $switch_order ) {
if ( $order->id !== $switch_order_id && in_array( $switch_order->get_status(), apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed', 'on-hold' ), $switch_order ) ) ) {
$switch_order->update_status( 'cancelled', sprintf( __( 'Switch order cancelled due to a new switch order being created #%s.', 'woocommerce-subscriptions' ), $order->get_order_number() ) );
}
}
// Despite rolling back the DB queries, the cache can still contain subscription changes (eg _billing_period post meta), so make sure we delete the cache for all subscriptions we've altered.
wp_cache_delete( $subscription_id, 'post_meta' );
}
update_post_meta( $order_id, '_subscription_switch_data', $switch_order_data );
} catch ( Exception $e ) {
// There was an error adding the subscription, roll back and delete pending order for switch
// There was an error updating the subscription, roll back and delete pending order for switch
$wpdb->query( 'ROLLBACK' );
wp_delete_post( $order_id, true );
throw $e;
@@ -919,6 +964,11 @@ class WC_Subscriptions_Switcher {
$item_id = absint( $_GET['item'] );
$item = wcs_get_order_item( $item_id, $subscription );
// Prevent switching to non-subscription product
if ( ! WC_Subscriptions_Product::is_subscription( $product_id ) ) {
throw new Exception( __( 'You can only switch to a subscription product.', 'woocommerce-subscriptions' ) );
}
// Check if the chosen variation's attributes are different to the existing subscription's attributes (to support switching between a "catch all" variation)
if ( empty( $item ) ) {
@@ -1143,7 +1193,7 @@ class WC_Subscriptions_Switcher {
// Set when the first payment and end date for the new subscription should occur
WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $cart_item['subscription_switch']['next_payment_timestamp'];
WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['end_timestamp'] = $end_timestamp = strtotime( WC_Subscriptions_Product::get_expiration_date( $product_id, $subscription->get_date( 'last_payment' ) ) );
WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['end_timestamp'] = $end_timestamp = wcs_date_to_time( WC_Subscriptions_Product::get_expiration_date( $product_id, $subscription->get_date( 'last_payment' ) ) );
// Add any extra sign up fees required to switch to the new subscription
if ( 'yes' == $apportion_sign_up_fee ) {
@@ -1228,11 +1278,12 @@ class WC_Subscriptions_Switcher {
// Find out how many days at the new price per day the customer would receive for the total amount already paid
// (e.g. if the customer paid $10 / month previously, and was switching to a $5 / week subscription, she has pre-paid 14 days at the new price)
$pre_paid_days = 0;
do {
$pre_paid_days = $new_total_paid = 0;
while ( $new_total_paid < $old_recurring_total ) {
$pre_paid_days++;
$new_total_paid = $pre_paid_days * $new_price_per_day;
} while ( $new_total_paid < $old_recurring_total );
}
// If the total amount the customer has paid entitles her to more days at the new price than she has received, there is no gap payment, just shorten the pre-paid term the appropriate number of days
if ( $days_since_last_payment < $pre_paid_days ) {
@@ -1321,8 +1372,8 @@ class WC_Subscriptions_Switcher {
public static function recurring_cart_next_payment_date( $first_renewal_date, $cart ) {
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
if ( isset( $cart_item['subscription_switch']['first_payment_timestamp'] ) ) {
$first_renewal_date = ( '1' != $cart_item['data']->subscription_length ) ? date( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] ) : 0;
if ( isset( $cart_item['subscription_switch']['first_payment_timestamp'] ) && 0 != $cart_item['subscription_switch']['first_payment_timestamp'] ) {
$first_renewal_date = ( '1' != $cart_item['data']->subscription_length ) ? gmdate( 'Y-m-d H:i:s', $cart_item['subscription_switch']['first_payment_timestamp'] ) : 0;
}
}
@@ -1345,7 +1396,7 @@ class WC_Subscriptions_Switcher {
// if the subscription is length 1 and prorated, we want to use the prorated the next payment date as the end date
if ( 1 == $cart_item['data']->subscription_length && 0 !== $next_payment_time && isset( $cart_item['subscription_switch']['recurring_payment_prorated'] ) ) {
$end_date = date( 'Y-m-d H:i:s', $next_payment_time );
$end_date = gmdate( 'Y-m-d H:i:s', $next_payment_time );
// if the subscription is more than 1 (and not 0) and we have a next payment date (prorated or not) we want to calculate the new end date from that
} elseif ( 0 !== $next_payment_time && $cart_item['data']->subscription_length > 1 ) {
@@ -1353,14 +1404,14 @@ class WC_Subscriptions_Switcher {
$trial_length = $cart_item['data']->subscription_trial_length;
$cart_item['data']->subscription_trial_length = 0;
$end_date = WC_Subscriptions_Product::get_expiration_date( $cart_item['data'], date( 'Y-m-d H:i:s', $next_payment_time ) );
$end_date = WC_Subscriptions_Product::get_expiration_date( $cart_item['data'], gmdate( 'Y-m-d H:i:s', $next_payment_time ) );
// add back the trial length if it has been spoofed
$cart_item['data']->subscription_trial_length = $trial_length;
// elseif fallback to using the end date set on the cart item
} elseif ( ! empty( $end_timestamp ) ) {
$end_date = date( 'Y-m-d H:i:s', $end_timestamp );
$end_date = gmdate( 'Y-m-d H:i:s', $end_timestamp );
}
break;
@@ -1420,41 +1471,11 @@ class WC_Subscriptions_Switcher {
*
* @since 1.4.4
* @return bool
* @deprecated 2.1
*/
public static function is_purchasable( $is_purchasable, $product ) {
$product_key = ! empty( $product->variation_id ) ? $product->variation_id : $product->id;
if ( ! isset( self::$is_purchasable_cache[ $product_key ] ) ) {
if ( false === $is_purchasable && wcs_is_product_switchable_type( $product ) && WC_Subscriptions_Product::is_subscription( $product->id ) && 'no' != $product->limit_subscriptions && is_user_logged_in() && wcs_user_has_subscription( 0, $product->id, $product->limit_subscriptions ) ) {
// Adding to cart from the product page
if ( isset( $_GET['switch-subscription'] ) ) {
$is_purchasable = true;
// Validating when restring cart from session
} elseif ( self::cart_contains_switches() ) {
$is_purchasable = true;
// Restoring cart from session, so need to check the cart in the session (self::cart_contains_subscription_switch() only checks the cart)
} elseif ( isset( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item['subscription_switch'] ) ) {
$is_purchasable = true;
break;
}
}
}
}
self::$is_purchasable_cache[ $product_key ] = $is_purchasable;
}
return self::$is_purchasable_cache[ $product_key ];
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_switch' );
return WCS_Limiter::is_purchasable_switch( $is_purchasable, $product );
}
/**
@@ -1513,6 +1534,49 @@ class WC_Subscriptions_Switcher {
return $add_to_cart_url;
}
/**
* Completes subscription switches on completed order status changes.
*
* Commits all the changes calculated and saved by @see WC_Subscriptions_Switcher::process_checkout(), updating subscription
* line items, schedule, dates and totals to reflect the changes made in this switch order.
*
* @param int $order_id The post_id of a shop_order post/WC_Order object
* @param array $order_old_status The old order status
* @param array $order_new_status The new order status
* @since 2.1
*/
public static function process_subscription_switches( $order_id, $order_old_status, $order_new_status ) {
global $wpdb;
$switch_processed = get_post_meta( $order_id, '_completed_subscription_switch', true );
$order = wc_get_order( $order_id );
if ( ! wcs_order_contains_switch( $order_id ) || 'true' == $switch_processed ) {
return;
}
$order_completed = in_array( $order_new_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) );
if ( $order_completed ) {
try {
// Start transaction if available
$wpdb->query( 'START TRANSACTION' );
self::complete_subscription_switches( $order );
update_post_meta( $order_id, '_completed_subscription_switch', 'true' );
$wpdb->query( 'COMMIT' );
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
throw $e;
}
do_action( 'woocommerce_subscriptions_switch_completed', $order );
}
}
/**
* Checks if a product can be switched based on it's type and the types which can be switched
*
@@ -1552,7 +1616,7 @@ class WC_Subscriptions_Switcher {
*/
public static function hidden_order_itemmeta( $hidden_meta_keys ) {
if ( ! defined( 'WCS_DEBUG' ) || true !== WCS_DEBUG ) {
if ( apply_filters( 'woocommerce_subscriptions_hide_switch_itemmeta', ! defined( 'WCS_DEBUG' ) || true !== WCS_DEBUG ) ) {
$hidden_meta_keys = array_merge( $hidden_meta_keys, array(
'_switched_subscription_item_id',
'_switched_subscription_new_item_id',
@@ -1656,7 +1720,6 @@ class WC_Subscriptions_Switcher {
foreach ( $subscriptions as $subscription ) {
foreach ( $subscription->get_items() as $new_order_item ) {
if ( isset( $new_order_item['switched_subscription_item_id'] ) ) {
$product_id = wcs_get_canonical_product_id( $new_order_item );
// we need to check if the switch order contains the line item that has just been switched so that we don't call the hook on items that were previously switched in another order
foreach ( $order->get_items() as $order_item ) {
@@ -1687,6 +1750,142 @@ class WC_Subscriptions_Switcher {
$product_id = wcs_get_canonical_product_id( $old_item );
WCS_Download_Handler::revoke_downloadable_file_permission( $product_id, $subscription->id, $subscription->customer_user );
}
/**
* Completes subscription switches for switch order.
*
* Performs all the changes calculated and saved by @see WC_Subscriptions_Switcher::process_checkout(), updating subscription
* line items, schedule, dates and totals to reflect the changes made in this switch order.
*
* @param WC_Order $order
* @since 2.1
*/
public static function complete_subscription_switches( $order ) {
// Get the switch meta
$switch_order_data = get_post_meta( $order->id, '_subscription_switch_data', true );
// if we don't have an switch data, there is nothing to do here. Switch orders created prior to v2.1 won't have any data to process.
if ( empty( $switch_order_data ) || ! is_array( $switch_order_data ) ) {
return;
}
foreach ( $switch_order_data as $subcription_id => $switch_data ) {
$subscription = wcs_get_subscription( $subcription_id );
if ( ! $subscription instanceof WC_Subscription ) {
continue;
}
// Add the new line items
if ( ! empty( $switch_data['switches'] ) ) {
foreach ( $switch_data['switches'] as $order_item_id => $switch_item_data ) {
$order_item = wcs_get_order_item( $order_item_id, $order );
// if we are simply adding this product to an existing subscription
if ( isset( $switch_item_data['add_order_item_data'] ) ) {
$product = WC_Subscriptions::get_product( wcs_get_canonical_product_id( $order_item ) );
$line_tax_data = wc_get_order_item_meta( $order_item_id, '_line_tax_data', true );
$variation_attributes = ( method_exists( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array();
$item_id = $subscription->add_product( $product, $order_item['qty'], array(
'variation' => $variation_attributes,
'totals' => $switch_item_data['add_order_item_data']['totals'],
) );
foreach ( $switch_item_data['add_order_item_data']['meta'] as $key => $value ) {
if ( ! array_key_exists( 'attribute_' . $key, $variation_attributes ) ) {
wc_add_order_item_meta( $item_id, $key, reset( $value ), true );
}
}
do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $order_item_id, $switch_item_data['subscription_item_id'] );
}
// remove the existing subscription item
$old_order_item = wcs_get_order_item( $switch_item_data['subscription_item_id'], $subscription );
if ( empty( $old_order_item ) ) {
throw new Exception( __( 'The original subscription item being switched cannot be found.', 'woocommerce-subscriptions' ) );
} else {
// We dont want to include switch item meta in order item name
add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' );
$new_order_item_name = wcs_get_order_item_name( $order_item, array( 'attributes' => true ) );
$old_subscription_item_name = wcs_get_order_item_name( $old_order_item, array( 'attributes' => true ) );
remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' );
wc_update_order_item( $switch_item_data['subscription_item_id'], array( 'order_item_type' => 'line_item_switched' ) );
// translators: 1$: old item, 2$: new item when switching
$subscription->add_order_note( sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_subscription_item_name, $new_order_item_name ) );
}
}
}
if ( ! empty( $switch_data['billing_schedule'] ) ) {
// Update the billing schedule
foreach ( $switch_data['billing_schedule'] as $meta_key => $value ) {
update_post_meta( $subcription_id, $meta_key, $value );
}
}
// Update subscription dates
if ( ! empty( $switch_data['dates'] ) ) {
if ( ! empty( $switch_data['dates']['delete'] ) ) {
foreach ( $switch_data['dates']['delete'] as $date ) {
$subscription->delete_date( $date );
}
}
if ( ! empty( $switch_data['dates']['update'] ) ) {
$subscription->update_dates( $switch_order_data[ $subscription->id ]['dates']['update'] );
}
}
if ( ! empty( $switch_data['shipping_methods'] ) ) {
// Archive the old subscription shipping methods
foreach ( $subscription->get_shipping_methods() as $shipping_line_item_id => $item ) {
wc_update_order_item( $shipping_line_item_id, array( 'order_item_type' => 'shipping_switched' ) );
}
// Add the new shipping line item
foreach ( $switch_data['shipping_methods'] as $shipping_line_item ) {
$item_id = wc_add_order_item( $subscription->id, array(
'order_item_name' => $shipping_line_item['name'],
'order_item_type' => 'shipping',
) );
if ( ! $item_id || empty( $shipping_line_item['method_id'] ) || empty( $shipping_line_item['cost'] ) || empty( $shipping_line_item['taxes'] ) ) {
throw new Exception( __( 'Failed to update the subscription shipping method.', 'woocommerce-subscriptions' ) );
}
// Add shipping order item meta
wc_add_order_item_meta( $item_id, 'method_id', $shipping_line_item['method_id'] );
wc_add_order_item_meta( $item_id, 'cost', wc_format_decimal( $shipping_line_item['cost'] ) );
$taxes = array_map( 'wc_format_decimal', maybe_unserialize( $shipping_line_item['taxes'] ) );
wc_add_order_item_meta( $item_id, 'taxes', $taxes );
// Add custom shipping order item meta added by third-party plugins
foreach ( $shipping_line_item['item_meta'] as $key => $value ) {
wc_add_order_item_meta( $item_id, $key, $value );
}
}
}
// Update the subscription address
self::maybe_update_subscription_address( $order, $subscription );
$subscription->calculate_totals();
}
}
/**
@@ -1771,18 +1970,12 @@ class WC_Subscriptions_Switcher {
* payment was completed with a payment method which supports automatic payments, update the payment on the subscription
* and the manual renewals flag so that future renewals are processed automatically.
*
* @param array $payment_processing_result
* @param int $order_id
* @since 2.0.16
* @param WC_Order $order
* @since 2.1
*/
public static function maybe_set_payment_method( $payment_processing_result, $order_id ) {
public static function maybe_set_payment_method_after_switch( $order ) {
// Only update the payment method the order contains a switch, and payment was processed (i.e. a paid date has been set) not just setup for processing, which is the case with PayPal Standard (which is handled by WCS_PayPal_Standard_Switcher)
if ( wcs_order_contains_switch( $order_id ) && false != get_post_meta( $order_id, '_paid_date', true ) ) {
$order = wc_get_order( $order_id );
foreach ( wcs_get_subscriptions_for_switch_order( $order_id ) as $subscription ) {
foreach ( wcs_get_subscriptions_for_switch_order( $order->id ) as $subscription ) {
if ( false === $subscription->is_manual() ) {
continue;
@@ -1802,9 +1995,50 @@ class WC_Subscriptions_Switcher {
}
}
/** Deprecated Methods **/
/**
* Once payment is processed on a switch from a $0 / period subscription to a non-zero $ / period subscription, if
* payment was completed with a payment method which supports automatic payments, update the payment on the subscription
* and the manual renewals flag so that future renewals are processed automatically.
*
* @param array $payment_processing_result
* @param int $order_id
* @since 2.0.16
* @deprecated 2.1
*/
public static function maybe_set_payment_method( $payment_processing_result, $order_id ) {
_deprecated_function( __METHOD__, '2.1', __CLASS__ . '::maybe_set_payment_method_after_switch( $order )' );
if ( wcs_order_contains_switch( $order_id ) && false != get_post_meta( $order_id, '_paid_date', true ) ) {
$order = wc_get_order( $order_id );
self::maybe_set_payment_method_after_switch( $order );
}
return $payment_processing_result;
}
/**
* Override the order item quantity used to reduce stock levels when the order item is to record a switch and where no
* prorated amount is being charged.
*
* @param int $quantity the original order item quantity used to reduce stock
* @param WC_Order $order
* @param array $order_item
*
* @return int
*/
public static function maybe_do_not_reduce_stock( $quantity, $order, $order_item ) {
if ( isset( $order_item['switched_subscription_price_prorated'] ) && 0 == $order_item['line_total'] ) {
$quantity = 0;
}
return $quantity;
}
/**
* Make sure switch cart item price doesn't include any recurring amount by setting a free trial.
*
@@ -1999,7 +2233,7 @@ class WC_Subscriptions_Switcher {
$first_payment_timestamp = get_post_meta( $subscription->order->id, '_switched_subscription_first_payment_timestamp', true );
if ( 0 != $first_payment_timestamp ) {
$next_payment_date = ( 'mysql' == $type ) ? date( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
$next_payment_date = ( 'mysql' == $type ) ? gmdate( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
}
}

View File

@@ -44,10 +44,10 @@ class WC_Subscriptions_Synchroniser {
self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_sync_payments';
self::$setting_id_proration = WC_Subscriptions_Admin::$option_prefix . '_prorate_synced_payments';
self::$sync_field_label = __( 'Synchronise Renewals', 'woocommerce-subscriptions' );
self::$sync_field_label = __( 'Synchronise renewals', 'woocommerce-subscriptions' );
self::$sync_description = __( 'Align the payment date for all customers who purchase this subscription to a specific day of the week or month.', 'woocommerce-subscriptions' );
// translators: placeholder is a year (e.g. "2016")
self::$sync_description_year = sprintf( _x( 'Align the payment date for this subscription to a specific day of the year. If the date has already taken place this year, the first payment will be processed in %s. Set the day to 0 to disable payment syncing for this product.', 'used in subscription product edit screen', 'woocommerce-subscriptions' ), date( 'Y', strtotime( '+1 year' ) ) );
self::$sync_description_year = sprintf( _x( 'Align the payment date for this subscription to a specific day of the year. If the date has already taken place this year, the first payment will be processed in %s. Set the day to 0 to disable payment syncing for this product.', 'used in subscription product edit screen', 'woocommerce-subscriptions' ), gmdate( 'Y', wcs_date_to_time( '+1 year' ) ) );
// Add the settings to control whether syncing is enabled and how it will behave
add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::add_settings' );
@@ -149,7 +149,7 @@ class WC_Subscriptions_Synchroniser {
'name' => __( 'Synchronisation', 'woocommerce-subscriptions' ),
'type' => 'title',
// translators: placeholders are opening and closing link tags
'desc' => sprintf( _x( 'Align subscription renewal to a specific day of the week, month or year. For example, the first day of the month. %sLearn more%s.', 'used in the general subscription options page', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woothemes.com/document/subscriptions/renewal-synchronisation/' ) . '">', '</a>' ),
'desc' => sprintf( _x( 'Align subscription renewal to a specific day of the week, month or year. For example, the first day of the month. %sLearn more%s.', 'used in the general subscription options page', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'http://docs.woocommerce.com/document/subscriptions/renewal-synchronisation/' ) . '">', '</a>' ),
'id' => self::$setting_id . '_title',
),
@@ -208,7 +208,7 @@ class WC_Subscriptions_Synchroniser {
$payment_month = $payment_day['month'];
$payment_day = $payment_day['day'];
} else {
$payment_month = date( 'm' );
$payment_month = gmdate( 'm' );
}
echo '<div class="options_group subscription_pricing subscription_sync show_if_subscription">';
@@ -216,8 +216,8 @@ class WC_Subscriptions_Synchroniser {
woocommerce_wp_select( array(
'id' => self::$post_meta_key,
'class' => 'wc_input_subscription_payment_sync',
'label' => self::$sync_field_label . ':',
'class' => 'wc_input_subscription_payment_sync select short',
'label' => self::$sync_field_label,
'options' => self::get_billing_period_ranges( $subscription_period ),
'description' => self::$sync_description,
'desc_tip' => true,
@@ -229,26 +229,22 @@ class WC_Subscriptions_Synchroniser {
echo '<div class="subscription_sync_annual" style="' . esc_attr( $display_annual_select ) . '">';
woocommerce_wp_text_input( array(
'id' => self::$post_meta_key_day,
'class' => 'wc_input_subscription_payment_sync',
'label' => self::$sync_field_label . ':',
'placeholder' => _x( 'Day', 'input field placeholder for day field for annual subscriptions', 'woocommerce-subscriptions' ),
'value' => $payment_day,
'type' => 'number',
)
);
?><p class="form-field _subscription_payment_sync_date_day_field">
<label for="_subscription_payment_sync_date_day"><?php echo esc_html( self::$sync_field_label ); ?></label>
<span class="wrap">
<input type="number" id="<?php echo esc_attr( self::$post_meta_key_day ); ?>" name="<?php echo esc_attr( self::$post_meta_key_day ); ?>" class="wc_input_subscription_payment_sync" value="<?php echo esc_attr( $payment_day ); ?>" placeholder="<?php echo esc_attr_x( 'Day', 'input field placeholder for day field for annual subscriptions', 'woocommerce-subscriptions' ); ?>" />
woocommerce_wp_select( array(
'id' => self::$post_meta_key_month,
'class' => 'wc_input_subscription_payment_sync',
'label' => '',
'options' => $wp_locale->month,
'description' => self::$sync_description_year,
'desc_tip' => true,
'value' => $payment_month, // Explicity set value in to ensure backward compatibility
)
);
<label for="<?php echo esc_attr( self::$post_meta_key_month ); ?>" class="wcs_hidden_label"><?php esc_html_e( 'Month for Synchronisation', 'woocommerce-subscriptions' ); ?></label>
<select id="<?php echo esc_attr( self::$post_meta_key_month ); ?>" name="<?php echo esc_attr( self::$post_meta_key_month ); ?>" class="wc_input_subscription_payment_sync last" >
<?php foreach ( $wp_locale->month as $value => $label ) { ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $value, $payment_month, true ) ?>><?php echo esc_html( $label ); ?></option>
<?php } ?>
</select>
</select>
</span>
<?php echo wcs_help_tip( self::$sync_description_year ); ?>
</p><?php
echo '</div>';
echo '</div>';
@@ -280,7 +276,7 @@ class WC_Subscriptions_Synchroniser {
$payment_month = $payment_day['month'];
$payment_day = $payment_day['day'];
} else {
$payment_month = date( 'm' );
$payment_month = gmdate( 'm' );
}
include( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/admin/html-variation-synchronisation.php' );
@@ -494,14 +490,14 @@ class WC_Subscriptions_Synchroniser {
$from_date = WC_Subscriptions_Product::get_trial_expiration_date( $product, $from_date );
}
$from_timestamp = strtotime( $from_date ) + ( get_option( 'gmt_offset' ) * 3600 ); // Site time
$from_timestamp = wcs_date_to_time( $from_date ) + ( get_option( 'gmt_offset' ) * 3600 ); // Site time
$payment_day = self::get_products_payment_day( $product );
if ( 'week' == $period ) {
// strtotime() will figure out if the day is in the future or today (see: https://gist.github.com/thenbrent/9698083)
$first_payment_timestamp = strtotime( self::$weekdays[ $payment_day ], $from_timestamp );
$first_payment_timestamp = wcs_strtotime_dark_knight( self::$weekdays[ $payment_day ], $from_timestamp );
} elseif ( 'month' == $period ) {
@@ -513,7 +509,7 @@ class WC_Subscriptions_Synchroniser {
} elseif ( gmdate( 'j', $from_timestamp ) > $payment_day ) { // today is later than specified day in the from date, we need the next month
$month = date( 'F', wcs_add_months( $from_timestamp, 1 ) );
$month = gmdate( 'F', wcs_add_months( $from_timestamp, 1 ) );
} else { // specified day is either today or still to come in the month of the from date
@@ -521,7 +517,7 @@ class WC_Subscriptions_Synchroniser {
}
$first_payment_timestamp = strtotime( "{$payment_day} {$month}", $from_timestamp );
$first_payment_timestamp = wcs_strtotime_dark_knight( "{$payment_day} {$month}", $from_timestamp );
} elseif ( 'year' == $period ) {
@@ -565,7 +561,7 @@ class WC_Subscriptions_Synchroniser {
break;
}
$first_payment_timestamp = strtotime( "{$payment_day['day']} {$month}", $from_timestamp );
$first_payment_timestamp = wcs_strtotime_dark_knight( "{$payment_day['day']} {$month}", $from_timestamp );
}
// Make sure the next payment is in the future and after the $from_date, as strtotime() will return the date this year for any day in the past when adding months or years (see: https://gist.github.com/thenbrent/9698083)
@@ -576,7 +572,7 @@ class WC_Subscriptions_Synchroniser {
$i = 1;
// Then make sure the date and time of the payment is in the future
while ( ( $first_payment_timestamp < gmdate( 'U' ) || $first_payment_timestamp < $from_timestamp ) && $i < 30 ) {
$first_payment_timestamp = strtotime( "+ 1 {$period}", $first_payment_timestamp );
$first_payment_timestamp = wcs_add_time( 1, $period, $first_payment_timestamp );
$i = $i + 1;
}
}
@@ -588,7 +584,7 @@ class WC_Subscriptions_Synchroniser {
// And convert it to the UTC equivalent of 3am on that day
$first_payment_timestamp -= ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
$first_payment = ( 'mysql' == $type && 0 != $first_payment_timestamp ) ? date( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
$first_payment = ( 'mysql' == $type && 0 != $first_payment_timestamp ) ? gmdate( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
return apply_filters( 'woocommerce_subscriptions_synced_first_payment_date', $first_payment, $product, $type, $from_date, $from_date_param );
}
@@ -834,7 +830,7 @@ class WC_Subscriptions_Synchroniser {
// Convert timestamp to site's time
$timestamp += get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
return ( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) == date( 'Y-m-d', $timestamp ) ) ? true : false;
return ( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) == gmdate( 'Y-m-d', $timestamp ) ) ? true : false;
}
/**
@@ -876,10 +872,10 @@ class WC_Subscriptions_Synchroniser {
$days_in_cycle = 7 * $product->subscription_period_interval;
break;
case 'month' :
$days_in_cycle = date( 't' ) * $product->subscription_period_interval;
$days_in_cycle = gmdate( 't' ) * $product->subscription_period_interval;
break;
case 'year' :
$days_in_cycle = ( 365 + date( 'L' ) ) * $product->subscription_period_interval;
$days_in_cycle = ( 365 + gmdate( 'L' ) ) * $product->subscription_period_interval;
break;
}
@@ -1194,7 +1190,7 @@ class WC_Subscriptions_Synchroniser {
$first_payment_timestamp = self::calculate_first_payment_date( $product_id, 'timestamp', $order->order_date );
if ( 0 != $first_payment_timestamp ) {
$first_payment_date = ( 'mysql' == $type ) ? date( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
$first_payment_date = ( 'mysql' == $type ) ? gmdate( 'Y-m-d H:i:s', $first_payment_timestamp ) : $first_payment_timestamp;
}
}
}
@@ -1217,7 +1213,7 @@ class WC_Subscriptions_Synchroniser {
$first_payment_date = self::get_first_payment_date( $payment_date, $order, $product_id, 'timestamp' );
if ( ! self::is_today( $first_payment_date ) ) {
$payment_date = ( 'timestamp' == $type ) ? $first_payment_date : date( 'Y-m-d H:i:s', $first_payment_date );
$payment_date = ( 'timestamp' == $type ) ? $first_payment_date : gmdate( 'Y-m-d H:i:s', $first_payment_date );
}
return $payment_date;
@@ -1326,7 +1322,7 @@ class WC_Subscriptions_Synchroniser {
public static function recalculate_trial_end_date( $trial_end_date, $recurring_cart, $product ) {
_deprecated_function( __METHOD__, '2.0.14' );
if ( self::is_product_synced( $product ) ) {
$product_id = ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id;
$product_id = wcs_get_canonical_product_id( $product );
$trial_end_date = WC_Subscriptions_Product::get_trial_expiration_date( $product_id );
}
@@ -1343,7 +1339,7 @@ class WC_Subscriptions_Synchroniser {
public static function recalculate_end_date( $end_date, $recurring_cart, $product ) {
_deprecated_function( __METHOD__, '2.0.14' );
if ( self::is_product_synced( $product ) ) {
$product_id = ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id;
$product_id = wcs_get_canonical_product_id( $product );
$end_date = WC_Subscriptions_Product::get_expiration_date( $product_id );
}

View File

@@ -14,15 +14,15 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
protected $action_hooks = array(
'woocommerce_scheduled_subscription_trial_end' => 'trial_end',
'woocommerce_scheduled_subscription_payment' => 'next_payment',
'woocommerce_scheduled_subscription_payment_retry' => 'payment_retry',
'woocommerce_scheduled_subscription_expiration' => 'end',
);
/**
* Maybe set a schedule action if the new date is in the future
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'payment_retry', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
*/
public function update_date( $subscription, $date_type, $datetime ) {
@@ -33,8 +33,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
if ( ! empty( $action_hook ) ) {
$action_args = array( 'subscription_id' => $subscription->id );
$timestamp = strtotime( $datetime );
$action_args = $this->get_action_args( $date_type, $subscription );
$timestamp = wcs_date_to_time( $datetime );
$next_scheduled = wc_next_scheduled_action( $action_hook, $action_args );
if ( $next_scheduled !== $timestamp ) {
@@ -45,8 +45,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
}
// Only reschedule if it's in the future
if ( $timestamp > current_time( 'timestamp', true ) && 'active' == $subscription->get_status() ) {
wc_schedule_single_action( $datetime, $action_hook, $action_args );
if ( $timestamp > current_time( 'timestamp', true ) && ( 'payment_retry' == $date_type || 'active' == $subscription->get_status() ) ) {
wc_schedule_single_action( $timestamp, $action_hook, $action_args );
}
}
}
@@ -72,13 +72,12 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
*/
public function update_status( $subscription, $new_status, $old_status ) {
$action_args = array( 'subscription_id' => $subscription->id );
switch ( $new_status ) {
case 'active' :
foreach ( $this->action_hooks as $action_hook => $date_type ) {
$action_args = $this->get_action_args( $date_type, $subscription );
$next_scheduled = wc_next_scheduled_action( $action_hook, $action_args );
$event_time = $subscription->get_time( $date_type );
@@ -98,9 +97,10 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
// Now that we have the current times, clear the scheduled hooks
foreach ( $this->action_hooks as $action_hook => $date_type ) {
wc_unschedule_action( $action_hook, $action_args );
wc_unschedule_action( $action_hook, $this->get_action_args( $date_type, $subscription ) );
}
$action_args = $this->get_action_args( 'end', $subscription );
$next_scheduled = wc_next_scheduled_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
if ( false !== $next_scheduled && $next_scheduled != $end_time ) {
@@ -118,9 +118,9 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
case 'expired' :
case 'trash' :
foreach ( $this->action_hooks as $action_hook => $date_type ) {
wc_unschedule_action( $action_hook, $action_args );
wc_unschedule_action( $action_hook, $this->get_action_args( $date_type, $subscription ) );
}
wc_unschedule_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
wc_unschedule_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) );
break;
}
}
@@ -128,8 +128,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
/**
* Get the hook to use in the action scheduler for the date type
*
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'expiration', 'end_of_prepaid_term' or a custom date type
* @param object $subscription An instance of WC_Subscription to get the hook for
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'expiration', 'end_of_prepaid_term' or a custom date type
*/
protected function get_scheduled_action_hook( $subscription, $date_type ) {
@@ -139,6 +139,9 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
case 'next_payment' :
$hook = 'woocommerce_scheduled_subscription_payment';
break;
case 'payment_retry' :
$hook = 'woocommerce_scheduled_subscription_payment_retry';
break;
case 'trial_end' :
$hook = 'woocommerce_scheduled_subscription_trial_end';
break;
@@ -154,4 +157,24 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
return apply_filters( 'woocommerce_subscriptions_scheduled_action_hook', $hook, $date_type );
}
/**
* Get the args to set on the scheduled action.
*
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'expiration', 'end_of_prepaid_term' or a custom date type
* @param object $subscription An instance of WC_Subscription to get the hook for
*/
protected function get_action_args( $date_type, $subscription ) {
if ( 'payment_retry' == $date_type ) {
$last_order_id = $subscription->get_last_order( 'ids', 'renewal' );
$action_args = array( 'order_id' => $last_order_id );
} else {
$action_args = array( 'subscription_id' => $subscription->id );
}
return apply_filters( 'woocommerce_subscriptions_scheduled_action_args', $action_args, $date_type, $subscription );
}
}

View File

@@ -17,6 +17,7 @@ class WCS_API {
public static function init() {
add_filter( 'woocommerce_api_classes', __CLASS__ . '::includes' );
add_action( 'rest_api_init', __CLASS__ . '::register_routes', 15 );
}
/**
@@ -28,16 +29,40 @@ class WCS_API {
* @return array
*/
public static function includes( $wc_api_classes ) {
// include the subscription api classes
require_once( 'api/class-wc-api-subscriptions.php' );
require_once( 'api/class-wc-api-subscriptions-customers.php' );
if ( ! defined( 'WC_API_REQUEST_VERSION' ) || 3 == WC_API_REQUEST_VERSION ) {
require_once( 'api/legacy/class-wc-api-subscriptions.php' );
require_once( 'api/legacy/class-wc-api-subscriptions-customers.php' );
array_push( $wc_api_classes, 'WC_API_Subscriptions' );
array_push( $wc_api_classes, 'WC_API_Subscriptions_Customers' );
}
return $wc_api_classes;
}
/**
* Load the new REST API subscription endpoints
*
* @since 2.1
*/
public static function register_routes() {
global $wp_version;
if ( version_compare( $wp_version, 4.4, '<' ) || WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
return;
}
require_once( 'api/class-wc-rest-subscriptions-controller.php' );
require_once( 'api/class-wc-rest-subscription-notes-controller.php' );
foreach ( array( 'WC_REST_Subscriptions_Controller', 'WC_REST_Subscription_Notes_Controller' ) as $api_class ) {
$controller = new $api_class();
$controller->register_routes();
}
}
}
WCS_API::init();

View File

@@ -39,18 +39,26 @@ class WCS_Cart_Initial_Payment extends WCS_Cart_Renewal {
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] );
$order = wc_get_order( $wp->query_vars['order-pay'] );
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && ! wcs_order_contains_subscription( $order, array( 'renewal', 'resubscribe' ) ) ) {
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_subscription( $order, 'parent' ) && ! wcs_order_contains_subscription( $order, 'resubscribe' ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) );
if ( ! is_user_logged_in() ) {
if ( get_current_user_id() !== $order->get_user_id() ) {
$redirect = add_query_arg( array(
'wcs_redirect' => 'pay_for_order',
'wcs_redirect_id' => $order_id,
), get_permalink( wc_get_page_id( 'myaccount' ) ) );
wp_safe_redirect( $redirect );
exit;
} elseif ( ! current_user_can( 'pay_for_order', $order_id ) ) {
wc_add_notice( __( 'That doesn\'t appear to be your order.', 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) );
exit;
} elseif ( ! empty( $subscriptions ) ) {
} else {
// Setup cart with all the original order's line items
$this->setup_cart( $order, array(

View File

@@ -37,6 +37,12 @@ class WCS_Cart_Renewal {
// When a failed renewal order is paid for via checkout, make sure WC_Checkout::create_order() preserves its "failed" status until it is paid
add_filter( 'woocommerce_default_order_status', array( &$this, 'maybe_preserve_order_status' ) );
// When a failed/pending renewal order is paid for via checkout, ensure a new order isn't created due to mismatched cart hashes
add_filter( 'woocommerce_create_order', array( &$this, 'set_renewal_order_cart_hash' ), 10, 1 );
// When a user is prevented from paying for a failed/pending renewal order because they aren't logged in, redirect them back after login
add_filter( 'woocommerce_login_redirect', array( &$this, 'maybe_redirect_after_login' ), 10 , 1 );
}
/**
@@ -53,10 +59,6 @@ class WCS_Cart_Renewal {
// Make sure fees are added to the cart
add_action( 'woocommerce_cart_calculate_fees', array( &$this, 'maybe_add_fees' ), 10, 1 );
// Allow renewal of limited subscriptions
add_filter( 'woocommerce_subscription_is_purchasable', array( &$this, 'is_purchasable' ), 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', array( &$this, 'is_purchasable' ), 12, 2 );
// Check if a user is requesting to create a renewal order for a subscription, needs to happen after $wp->query_vars are set
add_action( 'template_redirect', array( &$this, 'maybe_setup_cart' ), 100 );
@@ -75,6 +77,12 @@ class WCS_Cart_Renewal {
// Use original order price when resubscribing to products with addons (to ensure the adds on prices are included)
add_filter( 'woocommerce_product_addons_adjust_price', array( &$this, 'product_addons_adjust_price' ), 10, 2 );
// When loading checkout address details, use the renewal order address details for renewals
add_filter( 'woocommerce_checkout_get_value', array( &$this, 'checkout_get_value' ), 10, 2 );
// If the shipping address on a renewal order differs to the order's billing address, check the "Ship to different address" automatically to make sure the renewal order's fields are used by default
add_filter( 'woocommerce_ship_to_different_address_checked', array( &$this, 'maybe_check_ship_to_different_address' ), 100, 1 );
}
/**
@@ -97,6 +105,25 @@ class WCS_Cart_Renewal {
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_renewal( $order ) ) {
// If a user isn't logged in, allow them to login first and then redirect back
if ( ! is_user_logged_in() ) {
$redirect = add_query_arg( array(
'wcs_redirect' => 'pay_for_order',
'wcs_redirect_id' => $order_id,
), get_permalink( wc_get_page_id( 'myaccount' ) ) );
wp_safe_redirect( $redirect );
exit;
} elseif ( ! current_user_can( 'pay_for_order', $order_id ) ) {
wc_add_notice( __( 'That doesn\'t appear to be your order.', 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) );
exit;
}
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
do_action( 'wcs_before_renewal_setup_cart_subscriptions', $subscriptions, $order );
@@ -106,7 +133,7 @@ class WCS_Cart_Renewal {
do_action( 'wcs_before_renewal_setup_cart_subscription', $subscription, $order );
// Add the existing subscription items to the cart
$this->setup_cart( $subscription, array(
$this->setup_cart( $order, array(
'subscription_id' => $subscription->id,
'renewal_order_id' => $order_id,
) );
@@ -119,9 +146,7 @@ class WCS_Cart_Renewal {
if ( WC()->cart->cart_contents_count != 0 ) {
// Store renewal order's ID in session so it can be re-used after payment
WC()->session->set( 'order_awaiting_payment', $order_id );
// Set cart hash for orders paid in WC >= 2.6
$this->set_cart_hash( $order_id );
wc_add_notice( __( 'Complete checkout to renew your subscription.', 'woocommerce-subscriptions' ), 'success' );
}
wp_safe_redirect( WC()->cart->get_checkout_url() );
@@ -146,6 +171,7 @@ class WCS_Cart_Renewal {
$quantity = (int) $line_item['qty'];
$variation_id = (int) $line_item['variation_id'];
$variations = array();
$item_data = array();
foreach ( $line_item['item_meta'] as $meta_name => $meta_value ) {
if ( taxonomy_is_product_attribute( $meta_name ) ) {
@@ -177,11 +203,15 @@ class WCS_Cart_Renewal {
}
}
if ( wcs_is_subscription( $subscription ) ) {
$cart_item_data['subscription_line_item_id'] = $item_id;
$cart_item_data['line_item_id'] = $item_id;
$item_data = apply_filters( 'woocommerce_order_again_cart_item_data', array( $this->cart_item_key => $cart_item_data ), $line_item, $subscription );
if ( ! apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations, $item_data ) ) {
continue;
}
$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations, apply_filters( 'woocommerce_order_again_cart_item_data', array( $this->cart_item_key => $cart_item_data ), $line_item, $subscription ) );
$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations, $item_data );
$success = $success && (bool) $cart_item_key;
}
@@ -340,12 +370,12 @@ class WCS_Cart_Renewal {
$_product = $cart_item_session_data['data'];
// Need to get the original subscription price, not the current price
$subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
// Need to get the original subscription or order price, not the current price
$subscription = $this->get_order( $cart_item );
if ( $subscription ) {
$subscription_items = $subscription->get_items();
$item_to_renew = $subscription_items[ $cart_item_session_data[ $this->cart_item_key ]['subscription_line_item_id'] ];
$item_to_renew = $subscription_items[ $cart_item_session_data[ $this->cart_item_key ]['line_item_id'] ];
$price = $item_to_renew['line_subtotal'];
@@ -376,6 +406,68 @@ class WCS_Cart_Renewal {
return $cart_item_session_data;
}
/**
* Returns address details from the renewal order if the checkout is for a renewal.
*
* @param string $value Default checkout field value.
* @param string $key The checkout form field name/key
* @return string $value Checkout field value.
*/
public function checkout_get_value( $value, $key ) {
// Only hook in after WC()->checkout() has been initialised
if ( did_action( 'woocommerce_checkout_init' ) > 0 ) {
$address_fields = array_merge( WC()->checkout()->checkout_fields['billing'], WC()->checkout()->checkout_fields['shipping'] );
if ( array_key_exists( $key, $address_fields ) && false !== ( $item = $this->cart_contains() ) ) {
// Get the most specific order object, which will be the renewal order for renewals, initial order for initial payments, or a subscription for switches/resubscribes
$order = $this->get_order( $item );
if ( isset( $order->$key ) ) {
$value = $order->$key;
}
}
}
return $value;
}
/**
* If the cart contains a renewal order that needs to ship to an address that is different
* to the order's billing address, tell the checkout to toggle the ship to a different address
* checkbox and make sure the shipping fields are displayed by default.
*
* @param bool $ship_to_different_address Whether the order will ship to a different address
* @return bool $ship_to_different_address
*/
public function maybe_check_ship_to_different_address( $ship_to_different_address ) {
if ( ! $ship_to_different_address && false !== ( $item = $this->cart_contains() ) ) {
$order = $this->get_order( $item );
$renewal_shipping_address = $order->get_address( 'shipping' );
$renewal_billing_address = $order->get_address( 'billing' );
if ( isset( $renewal_billing_address['email'] ) ) {
unset( $renewal_billing_address['email'] );
}
if ( isset( $renewal_billing_address['phone'] ) ) {
unset( $renewal_billing_address['phone'] );
}
// If the order's addresses are different, we need to display the shipping fields otherwise the billing address will override it
if ( $renewal_shipping_address != $renewal_billing_address ) {
$ship_to_different_address = 1;
}
}
return $ship_to_different_address;
}
/**
* When completing checkout for a subscription renewal, update the address on the subscription to use
* the shipping/billing address entered in case it has changed since the subscription was first created.
@@ -421,28 +513,9 @@ class WCS_Cart_Renewal {
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
// If the product is being set as not-purchasable by Subscriptions (due to limiting)
if ( false === $is_purchasable && false === WC_Subscriptions_Product::is_purchasable( $is_purchasable, $product ) ) {
// Adding to cart from the product page or paying for a renewal
if ( isset( $_GET[ $this->cart_item_key ] ) || isset( $_GET['subscription_renewal'] ) || $this->cart_contains() ) {
$is_purchasable = true;
} else if ( WC()->session->cart ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item['subscription_renewal'] ) ) {
$is_purchasable = true;
break;
}
}
}
}
return $is_purchasable;
}
/**
@@ -565,7 +638,7 @@ class WCS_Cart_Renewal {
public function items_removed_title( $product_title, $cart_item ) {
if ( isset( $cart_item[ $this->cart_item_key ]['subscription_id'] ) ) {
$subscription = wcs_get_subscription( absint( $cart_item[ $this->cart_item_key ]['subscription_id'] ) );
$subscription = $this->get_order( $cart_item );
$product_title = ( count( $subscription->get_items() ) > 1 ) ? esc_html_x( 'All linked subscription items were', 'Used in WooCommerce by removed item notification: "_All linked subscription items were_ removed. Undo?" Filter for item title.', 'woocommerce-subscriptions' ) : $product_title;
}
@@ -785,6 +858,42 @@ class WCS_Cart_Renewal {
update_post_meta( $order_id, '_cart_hash', md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ) );
}
/**
* Right before WC processes a renewal cart through the checkout, set the cart hash.
* This ensures legitimate changes to taxes and shipping methods don't cause a new order to be created.
*
* @param Mixed | An order generated by third party plugins
* @return Mixed | The unchanged order param
* @since 2.1.0
*/
public function set_renewal_order_cart_hash( $order ) {
if ( $item = wcs_cart_contains_renewal() ) {
$this->set_cart_hash( $item[ $this->cart_item_key ]['renewal_order_id'] );
}
return $order;
}
/**
* Redirect back to pay for an order after successfully logging in.
*
* @param string | redirect URL after successful login
* @return string
* @since 2.1.0
*/
function maybe_redirect_after_login( $redirect ) {
if ( isset( $_GET['wcs_redirect'], $_GET['wcs_redirect_id'] ) && 'pay_for_order' == $_GET['wcs_redirect'] ) {
$order = wc_get_order( $_GET['wcs_redirect_id'] );
if ( $order ) {
$redirect = $order->get_checkout_payment_url();
}
}
return $redirect;
}
/* Deprecated */
/**

View File

@@ -29,6 +29,24 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
// When a resubscribe order is created on checkout, record the resubscribe, attached after WC_Subscriptions_Checkout::process_checkout()
add_action( 'woocommerce_checkout_subscription_created', array( &$this, 'maybe_record_resubscribe' ), 10, 3 );
add_filter( 'woocommerce_subscriptions_recurring_cart_key', array( &$this, 'get_recurring_cart_key' ), 10, 2 );
add_filter( 'wcs_recurring_cart_next_payment_date', array( &$this, 'recurring_cart_next_payment_date' ), 100, 2 );
// Mock a free trial on the cart item to make sure the resubscribe total doesn't include any recurring amount when honoring prepaid term
add_filter( 'woocommerce_before_calculate_totals', array( &$this, 'maybe_set_free_trial' ), 100, 1 );
add_action( 'woocommerce_subscription_cart_before_grouping', array( &$this, 'maybe_unset_free_trial' ) );
add_action( 'woocommerce_subscription_cart_after_grouping', array( &$this, 'maybe_set_free_trial' ) );
add_action( 'wcs_recurring_cart_start_date', array( &$this, 'maybe_unset_free_trial' ), 0, 1 );
add_action( 'wcs_recurring_cart_end_date', array( &$this, 'maybe_set_free_trial' ), 100, 1 );
add_filter( 'woocommerce_subscriptions_calculated_total', array( &$this, 'maybe_unset_free_trial' ), 10000, 1 );
add_action( 'woocommerce_cart_totals_before_shipping', array( &$this, 'maybe_set_free_trial' ) );
add_action( 'woocommerce_cart_totals_after_shipping', array( &$this, 'maybe_unset_free_trial' ) );
add_action( 'woocommerce_review_order_before_shipping', array( &$this, 'maybe_set_free_trial' ) );
add_action( 'woocommerce_review_order_after_shipping', array( &$this, 'maybe_unset_free_trial' ) );
add_action( 'woocommerce_order_status_changed', array( &$this, 'maybe_cancel_existing_subscription' ), 10, 3 );
}
/**
@@ -85,14 +103,31 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_resubscribe( $order ) ) {
if ( ! is_user_logged_in() ) {
$redirect = add_query_arg( array(
'wcs_redirect' => 'pay_for_order',
'wcs_redirect_id' => $order_id,
), get_permalink( wc_get_page_id( 'myaccount' ) ) );
wp_safe_redirect( $redirect );
exit;
}
wc_add_notice( __( 'Complete checkout to resubscribe.', 'woocommerce-subscriptions' ), 'success' );
$subscriptions = wcs_get_subscriptions_for_resubscribe_order( $order );
foreach ( $subscriptions as $subscription ) {
if ( current_user_can( 'subscribe_again', $subscription->id ) ) {
$this->setup_cart( $subscription, array(
'subscription_id' => $subscription->id,
) );
} else {
wc_add_notice( __( 'That doesn\'t appear to be one of your subscriptions.', 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) );
exit;
}
}
$redirect_to = WC()->cart->get_checkout_url();
@@ -153,40 +188,9 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
// If the product is being set as not-purchasable by Subscriptions (due to limiting)
if ( false === $is_purchasable && false === WC_Subscriptions_Product::is_purchasable( $is_purchasable, $product ) ) {
// Validating when restoring cart from session
if ( false !== $this->cart_contains() ) {
$resubscribe_cart_item = $this->cart_contains();
$subscription = wcs_get_subscription( $resubscribe_cart_item['subscription_resubscribe']['subscription_id'] );
if ( $subscription->has_product( $product->id ) ) {
$is_purchasable = true;
}
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart)
} elseif ( isset( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item[ $this->cart_item_key ] ) ) {
$is_purchasable = true;
break;
}
}
} elseif ( isset( $_GET['resubscribe'] ) ) { // Is a request to resubscribe
$subscription = wcs_get_subscription( absint( $_GET['resubscribe'] ) );
if ( false !== $subscription && $subscription->has_product( $product->id ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$is_purchasable = true;
}
}
}
return $is_purchasable;
}
/**
@@ -221,5 +225,93 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
return $subscription;
}
/**
* Make sure that a resubscribe item's cart key is based on the end of the pre-paid term if the user already has a subscription that is pending-cancel, not the date calculated for the product.
*
* @since 2.1
*/
public function get_recurring_cart_key( $cart_key, $cart_item ) {
$subscription = $this->get_order( $cart_item );
if ( false !== $subscription && $subscription->has_status( 'pending-cancel' ) ) {
remove_filter( 'woocommerce_subscriptions_recurring_cart_key', array( &$this, 'get_recurring_cart_key' ), 10, 2 );
$cart_key = WC_Subscriptions_Cart::get_recurring_cart_key( $cart_item, $subscription->get_time( 'end' ) );
add_filter( 'woocommerce_subscriptions_recurring_cart_key', array( &$this, 'get_recurring_cart_key' ), 10, 2 );
}
return $cart_key;
}
/**
* Make sure when displaying the next payment date for a subscription, the date takes into
* account the end of the pre-paid term if the user is resubscribing to a subscription that is pending-cancel.
*
* @since 2.1
*/
public function recurring_cart_next_payment_date( $first_renewal_date, $cart ) {
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
$subscription = $this->get_order( $cart_item );
if ( false !== $subscription && $subscription->has_status( 'pending-cancel' ) ) {
$first_renewal_date = ( '1' != $cart_item['data']->subscription_length ) ? $subscription->get_date( 'end' ) : 0;
break;
}
}
return $first_renewal_date;
}
/**
* Make sure resubscribe cart item price doesn't include any recurring amount by setting a free trial.
*
* @since 2.1
*/
public function maybe_set_free_trial( $total = '' ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
$subscription = $this->get_order( $cart_item );
if ( false !== $subscription && $subscription->has_status( 'pending-cancel' ) ) {
WC()->cart->cart_contents[ $cart_item_key ]['data']->subscription_trial_length = 1;
break;
}
}
return $total;
}
/**
* Remove mock free trials from resubscribe cart items.
*
* @since 2.1
*/
public function maybe_unset_free_trial( $total = '' ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
$subscription = $this->get_order( $cart_item );
if ( false !== $subscription && $subscription->has_status( 'pending-cancel' ) ) {
WC()->cart->cart_contents[ $cart_item_key ]['data']->subscription_trial_length = 0;
break;
}
}
return $total;
}
/**
* When the user resubscribes to a subscription that is pending-cancel, cancel the existing subscription.
*
* @since 2.1
*/
public function maybe_cancel_existing_subscription( $order_id, $old_order_status, $new_order_status ) {
if ( wcs_order_contains_subscription( $order_id ) && wcs_order_contains_resubscribe( $order_id ) ) {
$order_completed = in_array( $new_order_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) );
$order_needed_payment = in_array( $old_order_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
foreach ( wcs_get_subscriptions_for_resubscribe_order( $order_id ) as $subscription ) {
if ( $subscription->has_status( 'pending-cancel' ) ) {
$cancel_note = sprintf( __( 'Customer resubscribed in order #%s', 'woocommerce-subscriptions' ), wc_get_order( $order_id )->get_order_number() );
$subscription->update_status( 'cancelled', $cancel_note );
}
}
}
}
}
new WCS_Cart_Resubscribe();

View File

@@ -0,0 +1,119 @@
<?php
/**
* Subscriptions switching cart
*
*
* @author Prospress
* @since 2.1
*/
class WCS_Cart_Switch extends WCS_Cart_Renewal{
/**
* Initialise class hooks & filters when the file is loaded
*
* @since 2.1
*/
public function __construct() {
// Set checkout payment URL parameter for subscription switch orders
add_filter( 'woocommerce_get_checkout_payment_url', array( &$this, 'get_checkout_payment_url' ), 10, 2 );
// Check if a user is requesting to pay for a switch order, needs to happen after $wp->query_vars are set
add_action( 'template_redirect', array( &$this, 'maybe_setup_cart' ), 99 );
}
/**
* Add flag to payment url for failed/ pending switch orders.
*
* @since 2.1
*/
public function get_checkout_payment_url( $pay_url, $order ) {
if ( wcs_order_contains_switch( $order ) ) {
$switch_order_data = get_post_meta( $order->id, '_subscription_switch_data', true );
if ( ! empty( $switch_order_data ) ) {
$pay_url = add_query_arg( array(
'subscription_switch' => 'true',
'_wcsnonce' => wp_create_nonce( 'wcs_switch_request' ),
), $pay_url );
}
}
return $pay_url;
}
/**
* Check if a payment is being made on a switch order from 'My Account'. If so,
* reconstruct the cart with the order contents. If the order item is part of a switch, load the necessary data
* into $_GET and $_POST to ensure the switch validation occurs and the switch cart item meta is correctly loaded.
*
* @since 2.1
*/
public function maybe_setup_cart() {
global $wp;
if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && isset( $wp->query_vars['order-pay'] ) && isset( $_GET['subscription_switch'] ) ) {
// Pay for existing order
$order_key = $_GET['key'];
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] );
$order = wc_get_order( $wp->query_vars['order-pay'] );
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_switch( $order ) ) {
WC()->cart->empty_cart( true );
$switch_order_data = get_post_meta( $order_id, '_subscription_switch_data', true );
foreach ( $order->get_items() as $item_id => $line_item ) {
unset( $_GET['switch-subscription'] );
unset( $_GET['item'] );
// check if this order item is for a switch
foreach ( $switch_order_data as $subscription_id => $switch_data ) {
if ( isset( $switch_data['switches'] ) && in_array( $item_id, array_keys( $switch_data['switches'] ) ) ) {
$_GET['switch-subscription'] = $subscription_id;
$_GET['item'] = $switch_data['switches'][ $item_id ]['subscription_item_id'];
break;
}
}
$order_item = wcs_get_order_item( $item_id, $order );
$product = WC_Subscriptions::get_product( wcs_get_canonical_product_id( $order_item ) );
$order_product_data = array(
'_qty' => 0,
'_variation_id' => '',
);
$variations = array();
foreach ( $order_item['item_meta'] as $meta_key => $meta_value ) {
if ( taxonomy_is_product_attribute( $meta_key ) || meta_is_product_attribute( $meta_key, $meta_value[0], $product->id ) ) {
$variations[ $meta_key ] = $meta_value[0];
$_POST[ 'attribute_' . $meta_key ] = $meta_value[0];
} else if ( array_key_exists( $meta_key, $order_product_data ) ) {
$order_product_data[ $meta_key ] = (int) $meta_value[0];
}
}
$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product->id, $order_product_data['_qty'], $order_product_data['_variation_id'] );
if ( $passed_validation ) {
$cart_item_key = WC()->cart->add_to_cart( $product->id, $order_product_data['_qty'], $order_product_data['_variation_id'], $variations, array() );
}
}
}
WC()->session->set( 'order_awaiting_payment', $order_id );
$this->set_cart_hash( $order_id );
wp_safe_redirect( WC()->cart->get_checkout_url() );
exit;
}
}
}
new WCS_Cart_Switch();

View File

@@ -49,7 +49,7 @@ class WCS_Change_Payment_Method_Admin {
} elseif ( count( $valid_payment_methods ) == 1 ) {
echo '<strong>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':</strong><br/>' . esc_html( current( $valid_payment_methods ) );
echo '<img class="help_tip" data-tip="Gateway ID: [' . esc_attr( key( $valid_payment_methods ) ) . ']" src="' . esc_url( WC()->plugin_url() ) . '/assets/images/help.png" height="16" width="16" />';
echo wcs_help_tip( sprintf( _x( 'Gateway ID: [%s]', 'The gateway ID displayed on the Edit Subscriptions screen when editing payment method.', 'woocommerce-subscriptions' ), key( $valid_payment_methods ) ) );
echo '<input type="hidden" value="' . esc_attr( key( $valid_payment_methods ) ) . '" id="_payment_method" name="_payment_method">';
}

View File

@@ -0,0 +1,241 @@
<?php
/**
* A class to make it possible to limit a subscription product.
*
* @package WooCommerce Subscriptions
* @category Class
* @since 2.1
*/
class WCS_Limiter {
/* cache whether a given product is purchasable or not to save running lots of queries for the same product in the same request */
protected static $is_purchasable_cache = array();
/* cache the check on whether the session has an order awaiting payment for a given product */
protected static $order_awaiting_payment_for_product = array();
public static function init() {
//Add limiting subscriptions options on edit product page
add_action( 'woocommerce_product_options_reviews', __CLASS__ . '::admin_edit_product_fields' );
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
}
/**
* Adds limit options to 'Edit Product' screen.
*
* @since 2.1 Moved from WC_Subscriptions_Admin
*/
public static function admin_edit_product_fields() {
global $post;
echo '</div>';
echo '<div class="options_group limit_subscription show_if_subscription show_if_variable-subscription">';
// Only one Subscription per customer
woocommerce_wp_select( array(
'id' => '_subscription_limit',
'label' => __( 'Limit subscription', 'woocommerce-subscriptions' ),
// translators: placeholders are opening and closing link tags
'description' => sprintf( __( 'Only allow a customer to have one subscription to this product. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#limit-subscription">', '</a>' ),
'options' => array(
'no' => __( 'Do not limit', 'woocommerce-subscriptions' ),
'active' => __( 'Limit to one active subscription', 'woocommerce-subscriptions' ),
'any' => __( 'Limit to one of any status', 'woocommerce-subscriptions' ),
),
) );
do_action( 'woocommerce_subscriptions_product_options_advanced' );
}
/**
* Canonical is_purchasable method to be called by product classes.
*
* @since 2.1
* @param bool $purchasable Whether the product is purchasable as determined by parent class
* @param mixed $product The product in question to be checked if it is purchasable.
* @param string $product_class Determines the subscription type of the product. Controls switch logic.
*
* @return bool
*/
public static function is_purchasable( $purchasable, $product ) {
switch ( $product->get_type() ) {
case 'subscription' :
case 'variable-subscription' :
if ( true === $purchasable && false === self::is_purchasable_product( $purchasable, $product ) ) {
$purchasable = false;
}
break;
case 'subscription_variation' :
if ( 'no' != wcs_get_product_limitation( $product->parent ) && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
foreach ( WC()->cart->cart_contents as $cart_item ) {
if ( $product->id == $cart_item['data']->id && $product->variation_id != $cart_item['data']->variation_id ) {
$purchasable = false;
break;
}
}
}
break;
}
return $purchasable;
}
/**
* If a product is limited and the customer already has a subscription, mark it as not purchasable.
*
* @since 2.1 Moved from WC_Subscriptions_Product
* @return bool
*/
public static function is_purchasable_product( $is_purchasable, $product ) {
//Set up cache
if ( ! isset( self::$is_purchasable_cache[ $product->id ] ) ) {
self::$is_purchasable_cache[ $product->id ] = array();
}
if ( ! isset( self::$is_purchasable_cache[ $product->id ]['standard'] ) ) {
self::$is_purchasable_cache[ $product->id ]['standard'] = $is_purchasable;
if ( WC_Subscriptions_Product::is_subscription( $product->id ) && 'no' != wcs_get_product_limitation( $product ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
if ( wcs_is_product_limited_for_user( $product ) && ! self::order_awaiting_payment_for_product( $product->id ) ) {
self::$is_purchasable_cache[ $product->id ]['standard'] = false;
}
}
}
return self::$is_purchasable_cache[ $product->id ]['standard'];
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to switch the subscription, then mark it as purchasable.
*
* @since 2.1 Moved from WC_Subscriptions_Switcher::is_purchasable
* @return bool
*/
public static function is_purchasable_switch( $is_purchasable, $product ) {
$product_key = wcs_get_canonical_product_id( $product );
if ( ! isset( self::$is_purchasable_cache[ $product_key ] ) ) {
self::$is_purchasable_cache[ $product_key ] = array();
}
if ( ! isset( self::$is_purchasable_cache[ $product_key ]['switch'] ) ) {
if ( false === $is_purchasable && wcs_is_product_switchable_type( $product ) && WC_Subscriptions_Product::is_subscription( $product->id ) && 'no' != wcs_get_product_limitation( $product ) && is_user_logged_in() && wcs_user_has_subscription( 0, $product->id, wcs_get_product_limitation( $product ) ) ) {
//Adding to cart
if ( isset( $_GET['switch-subscription'] ) ) {
$is_purchasable = true;
//Validating when restring cart from session
} elseif ( WC_Subscriptions_Switcher::cart_contains_switches() ) {
$is_purchasable = true;
// Restoring cart from session, so need to check the cart in the session (WC_Subscriptions_Switcher::cart_contains_subscription_switch() only checks the cart)
} elseif ( isset( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item['subscription_switch'] ) ) {
$is_purchasable = true;
break;
}
}
}
}
self::$is_purchasable_cache[ $product_key ]['switch'] = $is_purchasable;
}
return self::$is_purchasable_cache[ $product_key ]['switch'];
}
/**
* Determines whether a product is purchasable based on whether the cart is to resubscribe or renew.
*
* @since 2.1 Combines WCS_Cart_Renewal::is_purchasable and WCS_Cart_Resubscribe::is_purchasable
* @return bool
*/
public static function is_purchasable_renewal( $is_purchasable, $product ) {
if ( false === $is_purchasable && false === self::is_purchasable_product( $is_purchasable, $product ) ) {
// Resubscribe logic
if ( isset( $_GET['resubscribe'] ) || false !== ( $resubscribe_cart_item = wcs_cart_contains_resubscribe() ) ) {
$subscription_id = ( isset( $_GET['resubscribe'] ) ) ? absint( $_GET['resubscribe'] ) : $resubscribe_cart_item['subscription_resubscribe']['subscription_id'];
$subscription = wcs_get_subscription( $subscription_id );
if ( false != $subscription && $subscription->has_product( $product->id ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$is_purchasable = true;
}
// Renewal logic
} elseif ( isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() ) {
$is_purchasable = true;
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart)
} elseif ( WC()->session->cart ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && ( isset( $cart_item['subscription_renewal'] ) || isset( $cart_item['subscription_resubscribe'] ) ) ) {
$is_purchasable = true;
break;
}
}
}
}
return $is_purchasable;
}
/**
* Check if the current session has an order awaiting payment for a subscription to a specific product line item.
*
* @since 2.1 Moved from WC_Subscriptions_Product
* @return bool
**/
protected static function order_awaiting_payment_for_product( $product_id ) {
global $wp;
if ( ! isset( self::$order_awaiting_payment_for_product[ $product_id ] ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = false;
if ( ! empty( WC()->session->order_awaiting_payment ) || isset( $_GET['pay_for_order'] ) ) {
$order_id = ! empty( WC()->session->order_awaiting_payment ) ? WC()->session->order_awaiting_payment : $wp->query_vars['order-pay'];
$order = wc_get_order( absint( $order_id ) );
if ( is_object( $order ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
foreach ( $order->get_items() as $item ) {
if ( $item['product_id'] == $product_id || $item['variation_id'] == $product_id ) {
$subscriptions = wcs_get_subscriptions( array(
'order_id' => $order->id,
'product_id' => $product_id,
) );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
if ( $subscription->has_status( array( 'pending', 'on-hold' ) ) ) {
self::$order_awaiting_payment_for_product[ $product_id ] = true;
}
}
break;
}
}
}
}
}
return self::$order_awaiting_payment_for_product[ $product_id ];
}
}
WCS_Limiter::init();

View File

@@ -66,6 +66,9 @@ class WCS_Query extends WC_Query {
foreach ( $this->query_vars as $key => $query_var ) {
if ( $this->is_query( $query_var ) ) {
$title = $this->get_endpoint_title( $key );
// unhook after we've returned our title to prevent it from overriding others
remove_filter( 'the_title', array( $this, __FUNCTION__ ), 11 );
}
}
}

View File

@@ -0,0 +1,321 @@
<?php
/**
* Manage the process of retrying a failed renewal payment that previously failed.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Manager
* @category Class
* @author Prospress
* @since 2.1
*/
require_once( 'payment-retry/class-wcs-retry-admin.php' );
class WCS_Retry_Manager {
/* the rules that control the retry schedule and behaviour of each retry */
protected static $retry_rules = array();
/* an instance of the class responsible for storing retry data */
protected static $store;
/* the setting ID for enabling/disabling the automatic retry system */
protected static $setting_id;
/* property to store the instance of WCS_Retry_Admin */
protected static $admin;
/**
* Attach callbacks and set the retry rules
*
* @codeCoverageIgnore
* @since 2.1
*/
public static function init() {
self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_retry';
self::$admin = new WCS_Retry_Admin( self::$setting_id );
if ( self::is_retry_enabled() ) {
self::load_classes();
add_filter( 'init', array( self::store(), 'init' ) );
add_filter( 'woocommerce_subscription_dates', __CLASS__ . '::add_retry_date_type' );
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::maybe_cancel_retry', 0, 3 );
add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 );
add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 );
add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' );
}
}
/**
* A helper function to check if the retry system has been enabled or not
*
* @since 2.1
*/
public static function is_retry_enabled() {
return apply_filters( 'wcs_is_retry_enabled', ( 'yes' == get_option( self::$setting_id, 'no' ) ) ? true : false );
}
/**
* Load all the retry classes if the retry system is enabled
*
* @codeCoverageIgnore
* @since 2.1
*/
protected static function load_classes() {
require_once( 'abstracts/abstract-wcs-retry-store.php' );
require_once( 'payment-retry/class-wcs-retry.php' );
require_once( 'payment-retry/class-wcs-retry-rule.php' );
require_once( 'payment-retry/class-wcs-retry-rules.php' );
require_once( 'payment-retry/class-wcs-retry-post-store.php' );
require_once( 'payment-retry/class-wcs-retry-email.php' );
require_once( 'admin/meta-boxes/class-wcs-meta-box-payment-retries.php' );
}
/**
* Add a renewal retry date type to Subscriptions date types
*
* @since 2.1
*/
public static function add_retry_date_type( $subscription_date_types ) {
$subscription_date_types = wcs_array_insert_after( 'next_payment', $subscription_date_types, 'payment_retry', _x( 'Renewal Payment Retry', 'table heading', 'woocommerce-subscriptions' ) );
return $subscription_date_types;
}
/**
* When a subscription's status is updated, if the new status isn't the expected retry subscription status, cancel the retry.
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $new_status A valid subscription status
* @param string $old_status A valid subscription status
*/
public static function maybe_cancel_retry( $subscription, $new_status, $old_status ) {
if ( $subscription->get_date( 'payment_retry' ) > 0 ) {
$last_order = $subscription->get_last_order( 'all' );
$last_retry = ( $last_order ) ? self::store()->get_last_retry_for_order( $last_order->id ) : null;
if ( null !== $last_retry && 'cancelled' !== $last_retry->get_status() && null !== ( $last_retry_rule = $last_retry->get_rule() ) ) {
$retry_subscription_status = $last_retry_rule->get_status_to_apply( 'subscription' );
$applying_retry_rule = did_action( 'woocommerce_subscriptions_before_apply_retry_rule' ) !== did_action( 'woocommerce_subscriptions_after_apply_retry_rule' );
$retrying_payment = did_action( 'woocommerce_subscriptions_before_payment_retry' ) !== did_action( 'woocommerce_subscriptions_after_payment_retry' );
// If the new status isn't the expected retry subscription status and we aren't in the process of applying a retry rule or retrying payment, cancel the retry
if ( $new_status != $retry_subscription_status && ! $applying_retry_rule && ! $retrying_payment ) {
$last_retry->update_status( 'cancelled' );
}
}
}
}
/**
* When a retry's status is updated, if it's no longer pending or processing and it's the most recent retry,
* delete the retry date on the subscriptions related to the order
*
* @param object $retry An instance of a WCS_Retry object
* @param string $new_status A valid retry status
*/
public static function maybe_delete_payment_retry_date( $retry, $new_status ) {
if ( ! in_array( $new_status, array( 'pending', 'processing' ) ) ) {
$last_retry = self::store()->get_last_retry_for_order( $retry->get_order_id() );
if ( $retry->get_id() === $last_retry->get_id() ) {
foreach ( wcs_get_subscriptions_for_renewal_order( $retry->get_order_id() ) as $subscription ) {
$subscription->delete_date( 'payment_retry' );
}
}
}
}
/**
* When a payment fails, apply a retry rule, if one exists that applies to this failure.
*
* @param WC_Subscription The subscription on which the payment failed
* @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param)
* @since 2.1
*/
public static function maybe_apply_retry_rule( $subscription, $last_order ) {
if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) {
return;
}
$retry_count = self::store()->get_retry_count_for_order( $last_order->id );
if ( self::rules()->has_rule( $retry_count, $last_order->id ) ) {
$retry_rule = self::rules()->get_rule( $retry_count, $last_order->id );
do_action( 'woocommerce_subscriptions_before_apply_retry_rule', $retry_rule, $last_order, $subscription );
$retry_id = self::store()->save( new WCS_Retry( array(
'status' => 'pending',
'order_id' => $last_order->id,
'date_gmt' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval() ),
'rule_raw' => $retry_rule->get_raw_data(),
) ) );
foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_key => $object ) {
$new_status = $retry_rule->get_status_to_apply( $object_key );
if ( '' !== $new_status && ! $object->has_status( $new_status ) ) {
$object->update_status( $new_status, _x( 'Retry rule applied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) );
}
}
if ( $retry_rule->get_retry_interval() > 0 ) {
// by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action
$subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) );
}
do_action( 'woocommerce_subscriptions_after_apply_retry_rule', $retry_rule, $last_order, $subscription );
}
}
/**
* When a retry hook is triggered, check if the rules for that retry are still valid
* and if so, retry the payment.
*
* @param WC_Order|int The order on which the payment failed
* @since 2.1
*/
public static function maybe_retry_payment( $last_order ) {
if ( ! is_object( $last_order ) ) {
$last_order = wc_get_order( $last_order );
}
if ( false === $last_order ) {
return;
}
$subscriptions = wcs_get_subscriptions_for_renewal_order( $last_order );
$last_retry = self::store()->get_last_retry_for_order( $last_order->id );
// we only need to retry the payment if we have applied a retry rule for the order and it still needs payment
if ( null !== $last_retry && 'pending' === $last_retry->get_status() ) {
do_action( 'woocommerce_subscriptions_before_payment_retry', $last_retry, $last_order );
if ( $last_order->needs_payment() ) {
$last_retry->update_status( 'processing' );
$expected_order_status = $last_retry->get_rule()->get_status_to_apply( 'order' );
$valid_order_status = ( '' == $expected_order_status || $last_order->has_status( $expected_order_status ) ) ? true : false;
$expected_subscription_status = $last_retry->get_rule()->get_status_to_apply( 'subscription' );
if ( '' == $expected_subscription_status ) {
$valid_subscription_status = true;
} else {
$valid_subscription_status = true;
foreach ( $subscriptions as $subscription ) {
if ( ! $subscription->has_status( $expected_subscription_status ) ) {
$valid_subscription_status = false;
break;
}
}
}
// if both statuses are still the same or there no special status was applied and the order still needs payment (i.e. there has been no manual intervention), trigger the payment hook
if ( $valid_order_status && $valid_subscription_status ) {
// Make sure the subscription is on hold in case something goes wrong while trying to process renewal and in case gateways expect the subscription to be on-hold, which is normally the case with a renewal payment
foreach ( $subscriptions as $subscription ) {
$subscription->update_status( 'on-hold', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
}
WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $last_order );
// Now that we've attempted to process the payment, refresh the order
$last_order = wc_get_order( $last_order->id );
// if the order still needs payment, payment failed
if ( $last_order->needs_payment() ) {
$last_retry->update_status( 'failed' );
} else {
$last_retry->update_status( 'complete' );
}
} else {
// order or subscription statuses have been manually updated, so we'll cancel the retry
$last_retry->update_status( 'cancelled' );
}
} else {
// last order must have been paid for some other way, so we'll cancel the retry
$last_retry->update_status( 'cancelled' );
}
do_action( 'woocommerce_subscriptions_after_payment_retry', $last_retry, $last_order );
}
}
/**
* Access the object used to interface with the database
*
* @since 2.1
*/
public static function store() {
if ( empty( self::$store ) ) {
$class = self::get_store_class();
self::$store = new $class();
}
return self::$store;
}
/**
* Get the class used for instantiating retry storage via self::store()
*
* @since 2.1
*/
protected static function get_store_class() {
return apply_filters( 'wcs_retry_store_class', 'WCS_Retry_Post_Store' );
}
/**
* Setup and access the object used to interface with retry rules
*
* @since 2.1
*/
public static function rules() {
if ( empty( self::$retry_rules ) ) {
$class = self::get_rules_class();
self::$retry_rules = new $class();
}
return self::$retry_rules;
}
/**
* Get the class used for instantiating retry rules via self::rules()
*
* @since 2.1
*/
protected static function get_rules_class() {
return apply_filters( 'wcs_retry_rules_class', 'WCS_Retry_Rules' );
}
}
WCS_Retry_Manager::init();

View File

@@ -32,10 +32,14 @@ class WCS_Webhooks {
add_filter( 'woocommerce_valid_webhook_resources', __CLASS__ . '::add_resource', 10, 1 );
add_filter( 'woocommerce_valid_webhook_events', __CLASS__ . '::add_event', 10, 1 );
add_action( 'woocommerce_checkout_subscription_created', __CLASS__ . '::add_subscription_created_callback', 10, 1 );
add_action( 'woocommerce_subscription_date_updated', __CLASS__ . '::add_subscription_updated_callback', 10, 1 );
add_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::add_subscription_switched_callback', 10, 1 );
add_filter( 'woocommerce_webhook_topics' , __CLASS__ . '::add_topics_admin_menu', 10, 1 );
}
@@ -66,6 +70,9 @@ class WCS_Webhooks {
'woocommerce_subscription_deleted',
'woocommerce_api_delete_subscription',
),
'subscription.switched' => array(
'wcs_webhook_subscription_switched',
),
), $webhook );
}
@@ -83,6 +90,7 @@ class WCS_Webhooks {
'subscription.created' => __( ' Subscription Created', 'woocommerce-subscriptions' ),
'subscription.updated' => __( ' Subscription Updated', 'woocommerce-subscriptions' ),
'subscription.deleted' => __( ' Subscription Deleted', 'woocommerce-subscriptions' ),
'subscription.switched' => __( ' Subscription Switched', 'woocommerce-subscriptions' ),
);
return array_merge( $topics, $front_end_topics );
@@ -126,6 +134,19 @@ class WCS_Webhooks {
return $resources;
}
/**
* Add webhook event for subscription switched.
*
* @param array $events
* @since 2.1
*/
public static function add_event( $events ) {
$events[] = 'switched';
return $events;
}
/**
* Call a "subscription created" action hook with the first parameter being a subscription id so that it can be used
* for webhooks.
@@ -145,5 +166,17 @@ class WCS_Webhooks {
do_action( 'wcs_webhook_subscription_updated', $subscription->id );
}
/**
* For each switched subscription in an order, call a "subscription switched" action hook with a subscription id as the first parameter to be used for webhooks payloads.
*
* @since 2.1
*/
public static function add_subscription_switched_callback( $order ) {
$switched_subscriptions = wcs_get_subscriptions_for_switch_order( $order );
foreach ( array_keys( $switched_subscriptions ) as $subscription_id ) {
do_action( 'wcs_webhook_subscription_switched', $subscription_id );
}
}
}
WCS_Webhooks::init();

View File

@@ -8,9 +8,9 @@ if ( ! defined( 'ABSPATH' ) ) {
* An email sent to the admin when a subscription is cancelled (either by a store manager, or the customer).
*
* @class WCS_Email_Cancelled_Subscription
* @version 1.4
* @version 2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Brent Shepherd
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Cancelled_Subscription extends WC_Email {
@@ -81,6 +81,9 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -101,6 +104,9 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -64,9 +64,9 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -118,6 +118,9 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -138,6 +141,9 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -63,9 +63,9 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -120,6 +120,9 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -141,6 +144,9 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -0,0 +1,134 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Customer Retry
*
* Email sent to the customer when an attempt to automatically process a subscription renewal payment has failed
* and a retry rule has been applied to retry the payment in the future.
*
* @version 2.1
* @package WooCommerce_Subscriptions/Includes/Emails
* @author Prospress
* @extends WCS_Email_Customer_Renewal_Invoice
*/
class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoice {
var $find;
var $replace;
/**
* Constructor
*/
function __construct() {
$this->id = 'customer_payment_retry';
$this->title = __( 'Customer Payment Retry', 'woocommerce-subscriptions' );
$this->description = __( 'Sent to a customer when an attempt to automatically process a subscription renewal payment has failed and a retry rule has been applied to retry the payment in the future. The email contains the renewal order information, date of the scheduled retry and payment links to allow the customer to pay for the renewal order manually instead of waiting for the automatic retry.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->template_html = 'emails/customer-payment-retry.php';
$this->template_plain = 'emails/plain/customer-payment-retry.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
$this->subject = __( 'Automatic payment failed for {order_number}, we will retry {retry_time}', 'woocommerce-subscriptions' );
$this->heading = __( 'Automatic payment failed for order {order_number}', 'woocommerce-subscriptions' );
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
}
/**
* trigger function.
*
* We can use most of WCS_Email_Customer_Renewal_Invoice's trigger method but we need to set up the
* retry data ourselves before calling it as WCS_Email_Customer_Renewal_Invoice has no retry
* associated with it.
*
* @access public
* @return void
*/
function trigger( $order ) {
$this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( $order->id );
$retry_time_index = array_search( '{retry_time}', $this->find );
if ( false === $retry_time_index ) {
$this->find[] = '{retry_time}';
$this->replace[] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
} else {
$this->replace[ $retry_time_index ] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
}
parent::trigger( $order );
}
/**
* get_subject function.
*
* @access public
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_customer_retry', parent::get_subject(), $this->object );
}
/**
* get_heading function.
*
* @access public
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_retry', parent::get_heading(), $this->object );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'retry' => $this->retry,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'retry' => $this->retry,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -58,9 +58,9 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -112,6 +112,9 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -132,6 +135,9 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -7,17 +7,21 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* An email sent to the customer via admin.
*
* @class WC_Email_Customer_Invoice
* @version 2.0.0
* @package WooCommerce/Classes/Emails
* @author WooThemes
* @extends WC_Email
* @class WCS_Email_Customer_Renewal_Invoice
* @version 1.4
* @package WooCommerce_Subscriptions/Includes/Emails
* @author Prospress
* @extends WC_Email_Customer_Invoice
*/
class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
var $find;
var $replace;
// fields used in WC_Email_Customer_Invoice this class doesn't need
var $subject_paid = null;
var $heading_paid = null;
/**
* Constructor
*/
@@ -25,7 +29,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
$this->id = 'customer_renewal_invoice';
$this->title = __( 'Customer Renewal Invoice', 'woocommerce-subscriptions' );
$this->description = __( 'Sent to a customer when the subscription is due for renewal and the renewal requires a manual payment, either because it uses manual renewals or the automatic recurring payment failed. The email contains renewal order information and payment links.', 'woocommerce-subscriptions' );
$this->description = __( 'Sent to a customer when the subscription is due for renewal and the renewal requires a manual payment, either because it uses manual renewals or the automatic recurring payment failed for the initial attempt and all automatic retries (if any). The email contains renewal order information and payment links.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->template_html = 'emails/customer-renewal-invoice.php';
@@ -35,9 +39,6 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
$this->subject = __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
$this->heading = __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
$this->subject_paid = null;
$this->heading_paid = null;
// Triggers for this email
add_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_renewal_notification', array( $this, 'trigger' ) );
@@ -68,9 +69,9 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -122,6 +123,9 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -142,6 +146,9 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -0,0 +1,165 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Expired Subscription Email
*
* An email sent to the admin when a subscription is expired.
*
* @class WCS_Email_Expired_Subscription
* @version 2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Expired_Subscription extends WC_Email {
/**
* Create an instance of the class.
*
* @access public
* @return void
*/
function __construct() {
$this->id = 'expired_subscription';
$this->title = __( 'Expired Subscription', 'woocommerce-subscriptions' );
$this->description = __( 'Expired Subscription emails are sent when a customer\'s subscription expires.', 'woocommerce-subscriptions' );
$this->heading = __( 'Subscription Expired', 'woocommerce-subscriptions' );
// translators: placeholder is {blogname}, a variable that will be substituted when email is sent out
$this->subject = sprintf( _x( '[%s] Subscription Expired', 'default email subject for expired emails sent to the admin', 'woocommerce-subscriptions' ), '{blogname}' );
$this->template_html = 'emails/expired-subscription.php';
$this->template_plain = 'emails/plain/expired-subscription.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
add_action( 'expired_subscription_notification', array( $this, 'trigger' ) );
parent::__construct();
$this->recipient = $this->get_option( 'recipient' );
if ( ! $this->recipient ) {
$this->recipient = get_option( 'admin_email' );
}
}
/**
* trigger function.
*
* @access public
* @return void
*/
function trigger( $subscription ) {
$this->object = $subscription;
if ( ! is_object( $subscription ) ) {
throw new InvalidArgumentException( __( 'Subscription argument passed in is not an object.', 'woocommerce-subscriptions' ) );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Initialise Settings Form Fields
*
* @access public
* @return void
*/
function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => _x( 'Enable/Disable', 'an email notification', 'woocommerce-subscriptions' ),
'type' => 'checkbox',
'label' => __( 'Enable this email notification', 'woocommerce-subscriptions' ),
'default' => 'no',
),
'recipient' => array(
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ),
'placeholder' => '',
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ),
'placeholder' => '',
'default' => '',
),
'email_type' => array(
'title' => _x( 'Email type', 'text, html or multipart', 'woocommerce-subscriptions' ),
'type' => 'select',
'description' => __( 'Choose which format of email to send.', 'woocommerce-subscriptions' ),
'default' => 'html',
'class' => 'email_type',
'options' => array(
'plain' => _x( 'Plain text', 'email type', 'woocommerce-subscriptions' ),
'html' => _x( 'HTML', 'email type', 'woocommerce-subscriptions' ),
'multipart' => _x( 'Multipart', 'email type', 'woocommerce-subscriptions' ),
),
),
);
}
}

View File

@@ -65,9 +65,9 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -99,6 +99,9 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -119,6 +122,9 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -65,9 +65,9 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), wcs_date_to_time( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
@@ -102,6 +102,9 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
@@ -123,6 +126,9 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base

View File

@@ -0,0 +1,165 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Suspended Subscription Email
*
* An email sent to the admin when a subscription is expired.
*
* @class WCS_Email_On_Hold_Subscription
* @version 2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_On_Hold_Subscription extends WC_Email {
/**
* Create an instance of the class.
*
* @access public
* @return void
*/
function __construct() {
$this->id = 'suspended_subscription';
$this->title = __( 'Suspended Subscription', 'woocommerce-subscriptions' );
$this->description = __( 'Suspended Subscription emails are sent when a customer manually suspends their subscription.', 'woocommerce-subscriptions' );
$this->heading = __( 'Subscription Suspended', 'woocommerce-subscriptions' );
// translators: placeholder is {blogname}, a variable that will be substituted when email is sent out
$this->subject = sprintf( _x( '[%s] Subscription Suspended', 'default email subject for suspended emails sent to the admin', 'woocommerce-subscriptions' ), '{blogname}' );
$this->template_html = 'emails/on-hold-subscription.php';
$this->template_plain = 'emails/plain/on-hold-subscription.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
add_action( 'on-hold_subscription_notification', array( $this, 'trigger' ) );
parent::__construct();
$this->recipient = $this->get_option( 'recipient' );
if ( ! $this->recipient ) {
$this->recipient = get_option( 'admin_email' );
}
}
/**
* trigger function.
*
* @access public
* @return void
*/
function trigger( $subscription ) {
$this->object = $subscription;
if ( ! is_object( $subscription ) ) {
throw new InvalidArgumentException( __( 'Subscription argument passed in is not an object.', 'woocommerce-subscriptions' ) );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Initialise Settings Form Fields
*
* @access public
* @return void
*/
function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => _x( 'Enable/Disable', 'an email notification', 'woocommerce-subscriptions' ),
'type' => 'checkbox',
'label' => __( 'Enable this email notification', 'woocommerce-subscriptions' ),
'default' => 'no',
),
'recipient' => array(
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ),
'placeholder' => '',
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ),
'placeholder' => '',
'default' => '',
),
'email_type' => array(
'title' => _x( 'Email type', 'text, html or multipart', 'woocommerce-subscriptions' ),
'type' => 'select',
'description' => __( 'Choose which format of email to send.', 'woocommerce-subscriptions' ),
'default' => 'html',
'class' => 'email_type',
'options' => array(
'plain' => _x( 'Plain text', 'email type', 'woocommerce-subscriptions' ),
'html' => _x( 'HTML', 'email type', 'woocommerce-subscriptions' ),
'multipart' => _x( 'Multipart', 'email type', 'woocommerce-subscriptions' ),
),
),
);
}
}

View File

@@ -0,0 +1,107 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Admin payment retry email
*
* Email sent to admins when an attempt to automatically process a subscription renewal payment has failed
* and a retry rule has been applied to retry the payment in the future.
*
* @class WCS_Email_Payment_Retry
* @version 2.1
* @package WooCommerce_Subscriptions/Includes/Emails
* @author Prospress
* @extends WC_Email_Failed_Order
*/
class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
/**
* Constructor
*/
public function __construct() {
$this->id = 'payment_retry';
$this->title = __( 'Payment Retry', 'woocommerce-subscriptions' );
$this->description = __( 'Payment retry emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment has failed and a retry rule has been applied to retry the payment in the future.', 'woocommerce-subscriptions' );
$this->heading = __( 'Automatic renewal payment failed', 'woocommerce-subscriptions' );
$this->subject = __( '[{site_title}] Automatic payment failed for {order_number}, retry scheduled to run {retry_time}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/admin-payment-retry.php';
$this->template_plain = 'emails/plain/admin-payment-retry.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
// Other settings
$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
}
/**
* Trigger.
*
* @param int $order_id
*/
public function trigger( $order ) {
$this->object = $order;
$this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( $order->id );;
$this->find['order-date'] = '{order_date}';
$this->find['order-number'] = '{order_number}';
$this->find['retry-time'] = '{retry_time}';
$this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace['order-number'] = $this->object->get_order_number();
$this->replace['retry-time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* Get content html.
*
* @access public
* @return string
*/
public function get_content_html() {
return wc_get_template_html(
$this->template_html,
array(
'order' => $this->object,
'retry' => $this->retry,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => false,
'email' => $this,
),
'',
$this->template_base
);
}
/**
* Get content plain.
*
* @return string
*/
public function get_content_plain() {
return wc_get_template_html(
$this->template_plain,
array(
'order' => $this->object,
'retry' => $this->retry,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
),
'',
$this->template_base
);
}
}

View File

@@ -21,13 +21,13 @@ class WC_Subscriptions_Payment_Gateways {
*/
public static function init() {
add_action( 'init', __CLASS__ . '::init_paypal', 10 );
add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' );
add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' );
// Create a custom hook for gateways that need to manually charge recurring payments
// Trigger a hook for gateways to charge recurring payments
add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 );
// Create a gateway specific hooks for subscription events
@@ -164,6 +164,21 @@ class WC_Subscriptions_Payment_Gateways {
do_action( $hook_prefix . $subscription->payment_method, $subscription );
}
/**
* Fire a gateway specific hook for when a subscription renewal payment is due.
*
* @since 2.1.0
*/
public static function trigger_gateway_renewal_payment_hook( $renewal_order ) {
if ( ! empty( $renewal_order ) && $renewal_order->get_total() > 0 && ! empty( $renewal_order->payment_method ) ) {
// Make sure gateways are setup
WC()->payment_gateways();
do_action( 'woocommerce_scheduled_subscription_payment_' . $renewal_order->payment_method, $renewal_order->get_total(), $renewal_order );
}
}
/**
* Fire a gateway specific hook for when a subscription payment is due.
*
@@ -175,16 +190,19 @@ class WC_Subscriptions_Payment_Gateways {
if ( null != $deprecated ) {
_deprecated_argument( __METHOD__, '2.0', 'Second parameter is deprecated' );
$subscription = wcs_get_subscription_from_key( $deprecated );
} else {
} elseif ( ! is_object( $subscription_id ) ) {
$subscription = wcs_get_subscription( $subscription_id );
} else {
// Support receiving a full subscription object for unit testing
$subscription = $subscription_id;
}
if ( false === $subscription ) {
throw new InvalidArgumentException( sprintf( __( 'Subscription doesn\'t exist in scheduled action: %d', 'woocommerce-subscriptions' ), $subscription_id ) );
}
if ( ! $subscription->is_manual() && $subscription->get_total() > 0 && ! empty( $subscription->payment_method ) ) {
do_action( 'woocommerce_scheduled_subscription_payment_' . $subscription->payment_method, $subscription->get_total(), $subscription->get_last_order( 'all' ) );
if ( ! $subscription->is_manual() ) {
self::trigger_gateway_renewal_payment_hook( $subscription->get_last_order( 'all', 'renewal' ) );
}
}
@@ -237,18 +255,6 @@ class WC_Subscriptions_Payment_Gateways {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'expired' );
}
/**
* Fired a gateway specific when a subscription was suspended. Suspended status was changed in 1.2 to match
* WooCommerce with the "on-hold" status.
*
* @deprecated 1.2
* @since 1.0
*/
public static function trigger_gateway_suspended_subscription_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::trigger_gateway_subscription_put_on_hold_hook( $subscription_key, $user_id )' );
self::trigger_gateway_subscription_put_on_hold_hook( $subscription_key, $user_id );
}
}
WC_Subscriptions_Payment_Gateways::init();

View File

@@ -25,6 +25,7 @@ require_once( 'includes/class-wcs-paypal-standard-change-payment-method.php' );
require_once( 'includes/admin/class-wcs-paypal-admin.php' );
require_once( 'includes/admin/class-wcs-paypal-change-payment-method-admin.php' );
require_once( 'includes/deprecated/class-wc-paypal-standard-subscriptions.php' );
require_once( 'includes/class-wcs-paypal-standard-ipn-failure-handler.php' );
class WCS_PayPal {
@@ -90,6 +91,10 @@ class WCS_PayPal {
add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' );
// Run the IPN failure handler attach and detach functions before and after processing to catch and log any unexpected shutdowns
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::attach', -1, 1 );
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::detach', 1, 1 );
WCS_PayPal_Supports::init();
WCS_PayPal_Status_Manager::init();
WCS_PayPal_Standard_Switcher::init();
@@ -154,7 +159,7 @@ class WCS_PayPal {
update_option( 'wcs_paypal_rt_enabled_accounts', wcs_json_encode( $accounts_with_reference_transactions_enabled ) );
$reference_transactions_enabled = true;
} else {
set_transient( $transient_key, $api_username, DAY_IN_SECONDS );
set_transient( $transient_key, $api_username, WEEK_IN_SECONDS );
}
}
}
@@ -296,6 +301,7 @@ class WCS_PayPal {
*/
public static function process_ipn_request( $transaction_details ) {
try {
require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' );
require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' );
@@ -311,6 +317,9 @@ class WCS_PayPal {
} elseif ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) {
self::get_ipn_handler( 'reference' )->valid_response( $transaction_details );
}
} catch ( Exception $e ) {
WCS_PayPal_Standard_IPN_Failure_Handler::log_unexpected_exception( $e );
}
}
/**

View File

@@ -107,7 +107,7 @@ class WCS_PayPal_Admin {
'type' => 'warning',
// translators: placeholders are opening and closing link tags. 1$-2$: to docs on woothemes, 3$-4$ to gateway settings on the site
'text' => sprintf( esc_html__( 'PayPal is inactive for subscription transactions. Please %1$sset up the PayPal IPN%2$s and %3$senter your API credentials%4$s to enable PayPal for Subscriptions.', 'woocommerce-subscriptions' ),
'<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'<a href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'</a>',
'<a href="' . esc_url( $payment_gateway_tab_url ) . '">',
'</a>'
@@ -122,11 +122,11 @@ class WCS_PayPal_Admin {
'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are not enabled on your account%2$s, some subscription management features are not enabled. Please contact PayPal and request they %3$senable PayPal Reference Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %7$sLearn more %8$s', 'woocommerce-subscriptions' ),
'<strong>',
'</strong>',
'<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'<a href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'</a>',
'</p><p><a class="button" href="' . esc_url( wp_nonce_url( add_query_arg( 'wcs_paypal', 'check_reference_transaction_support' ), __CLASS__ ) ) . '">',
'</a>',
'<a class="button button-primary" href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'<a class="button button-primary" href="http://docs.woocommerce.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'&raquo;</a>'
),
);
@@ -147,11 +147,11 @@ class WCS_PayPal_Admin {
if ( false !== get_option( 'wcs_paypal_credentials_error' ) ) {
$notices[] = array(
'type' => 'error',
// translators: placeholders are link opening and closing tags. 1$-2$: to gateway settings, 3$-4$: support docs on woothemes.com
// translators: placeholders are link opening and closing tags. 1$-2$: to gateway settings, 3$-4$: support docs on woocommerce.com
'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your API credentials may be incorrect. Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s.', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( $payment_gateway_tab_url ) . '">',
'</a>',
'<a href="https://support.woothemes.com/hc/en-us/articles/202882473#paypal-credentials" target="_blank">',
'<a href="https://docs.woocommerce.com/document/subscriptions-canceled-suspended-paypal/#section-2" target="_blank">',
'</a>'
),
);
@@ -162,13 +162,31 @@ class WCS_PayPal_Admin {
'type' => 'error',
// translators: placeholders are opening and closing link tags. 1$-2$: docs on woothemes, 3$-4$: dismiss link
'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your PayPal account is issuing out-of-date subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s.', 'woocommerce-subscriptions' ),
'<a href="https://support.woothemes.com/hc/en-us/articles/202882473#old-paypal-account" target="_blank">',
'<a href="https://docs.woocommerce.com/document/subscriptions-canceled-suspended-paypal/#section-3" target="_blank">',
'</a>',
'<a href="' . esc_url( add_query_arg( 'wcs_disable_paypal_invalid_profile_id_notice', 'true' ) ) . '">',
'</a>'
),
);
}
$last_ipn_error = get_option( 'wcs_fatal_error_handling_ipn', '' );
if ( ! empty( $last_ipn_error ) && ( false == get_option( 'wcs_fatal_error_handling_ipn_ignored', false ) || isset( $_GET['wcs_reveal_your_ipn_secrets'] ) ) ) {
$notices[] = array(
'type' => 'error',
'text' => sprintf( esc_html__( '%sA fatal error has occurred when processing a recent subscription payment with PayPal. Please %sopen a new ticket at WooThemes Support%s immediately to get this resolved.%sIn order to get the quickest possible response please attach a %sTemporary Admin Login%s and a copy of your PHP error logs to your support ticket.%sLast recorded error: %s', 'woocommerce-subscriptions' ),
'<p>',
'<a href="https://www.woocommerce.com/my-account/create-a-ticket/" target="_blank">',
'</a>',
'<br>',
'<a href="https://docs.woocommerce.com/document/create-new-admin-account-wordpress/" target="_blank">',
'</a>',
'</p>',
'<code>' . esc_html( $last_ipn_error ) . '</code><div style="margin: 5px 0;"><a class="button" href="' . esc_url( wp_nonce_url( add_query_arg( 'wcs_ipn_error_notice', 'ignore' ), 'wcs_ipn_error_notice', '_wcsnonce' ) ) . '">' . esc_html__( 'Ignore this error (not recommended!)', 'woocommerce-subscriptions' ) . '</a> <a class="button button-primary" href="https://www.woocommerce.com/my-account/create-a-ticket/">' . esc_html__( 'Open up a ticket now!', 'woocommerce-subscriptions' ) . '</a></div>'
),
);
}
}
if ( ! empty( $notices ) ) {
@@ -185,6 +203,10 @@ class WCS_PayPal_Admin {
if ( isset( $_GET['wcs_disable_paypal_invalid_profile_id_notice'] ) ) {
update_option( 'wcs_paypal_invalid_profile_id', 'disabled' );
}
if ( isset( $_GET['wcs_ipn_error_notice'] ) ) {
update_option( 'wcs_fatal_error_handling_ipn_ignored', true );
}
}
/**

View File

@@ -115,9 +115,8 @@ class WCS_PayPal_Reference_Transaction_API_Response_Payment extends WCS_PayPal_R
} elseif ( 'echeck' == $this->get_payment_type() ) {
// add some additional info for eCheck payments
// translators: placeholder is localised datetime
$message = sprintf( __( 'expected clearing date %s', 'woocommerce-subscriptions' ), date_i18n( wc_date_format(), strtotime( $this->get_payment_parameter( 'EXPECTEDECHECKCLEARDATE' ) ) ) );
$message = sprintf( __( 'expected clearing date %s', 'woocommerce-subscriptions' ), date_i18n( wc_date_format(), wcs_date_to_time( $this->get_payment_parameter( 'EXPECTEDECHECKCLEARDATE' ) ) ) );
}
// add fraud filters

View File

@@ -57,6 +57,37 @@ class WCS_PayPal_Reference_Transaction_API_Response extends WC_Gateway_Paypal_Re
return ( 'Success' !== $this->get_parameter( 'ACK' ) && 'SuccessWithWarning' !== $this->get_parameter( 'ACK' ) );
}
/**
* Checks if response contains an API error code or message relating to invalid credentails
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/
*
* @return bool true if has API error relating to incorrect credentials, false otherwise
* @since 2.1
*/
public function has_api_error_for_credentials() {
$has_api_error_for_credentials = false;
// assume something went wrong if ACK is missing
if ( $this->has_api_error() ) {
foreach ( range( 0, 9 ) as $index ) {
// Error codes refer to multiple errors, go figure, so we need to compare both error codes and error messages
$has_credentials_error_code = $this->has_parameter( "L_ERRORCODE{$index}" ) && in_array( $this->get_parameter( "L_ERRORCODE{$index}" ), array( 10002, 10008 ) );
$has_credentials_error_message = $this->has_parameter( "L_LONGMESSAGE{$index}" ) && in_array( $this->get_parameter( "L_LONGMESSAGE{$index}" ), array( 'Username/Password is incorrect', 'Security header is not valid' ) );
if ( $has_credentials_error_code && $has_credentials_error_message ) {
$has_api_error_for_credentials = true;
break;
}
}
}
return $has_api_error_for_credentials;
}
/**
* Gets the API error code
*

View File

@@ -128,6 +128,11 @@ class WCS_PayPal_Reference_Transaction_API extends WCS_SV_API_Base {
$reference_transactions_enabled = true;
} else {
$reference_transactions_enabled = false;
// And set a flag to display invalid credentials notice
if ( $response->has_api_error_for_credentials() ) {
update_option( 'wcs_paypal_credentials_error', 'yes' );
}
}
} catch ( Exception $e ) {
$reference_transactions_enabled = false;

View File

@@ -2,9 +2,9 @@
/**
* PayPal Standard Change Subscription Payment Method Class.
*
* Handles the process of a customer changing the payment method on a subscription via their My Account page from or two PayPal Standard.
* Handles the process of a customer changing the payment method on a subscription via their My Account page from or to PayPal Standard.
*
* @link http://docs.woothemes.com/document/subscriptions/customers-view/#section-5
* @link http://docs.woocommerce.com/document/subscriptions/customers-view/#section-5
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal

View File

@@ -0,0 +1,140 @@
<?php
/**
* PayPal Standard IPN Failure Handler
*
* Introduces a new handler to take care of failing IPN requests
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0.6
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Standard_IPN_Failure_Handler {
private static $transaction_details = null;
public static $log = null;
/**
* Attaches all IPN failure handler related hooks and filters and also sets logging to enabled.
*
* @since 2.0.6
* @param array $transaction_details
*/
public static function attach( $transaction_details ) {
self::$transaction_details = $transaction_details;
$transient_key = 'wcs_paypal_ipn_error_occurred';
$api_username = WCS_PayPal::get_option( 'api_username' );
WC_Gateway_Paypal::$log_enabled = true;
// try to enable debug logging if errors were previously found
if ( get_transient( $transient_key ) == $api_username && ! defined( 'WP_DEBUG' ) ) {
define( 'WP_DEBUG', true );
if ( ! defined( 'WP_DEBUG_DISPLAY' ) ) {
define( 'WP_DEBUG_DISPLAY', false );
}
}
add_action( 'wcs_paypal_ipn_process_failure', __CLASS__ . '::log_ipn_errors', 10, 2 );
add_action( 'shutdown', __CLASS__ . '::catch_unexpected_shutdown' );
}
/**
* Close up loose ends
*
* @since 2.0.6
* @param $transaction_details
*/
public static function detach( $transaction_details ) {
remove_action( 'wcs_paypal_ipn_process_failure', __CLASS__ . '::log_ipn_errors' );
remove_action( 'shutdown', __CLASS__ . '::catch_unexpected_shutdown' );
self::$transaction_details = null;
}
/**
* On PHP shutdown log any unexpected failures from PayPal IPN processing
*
* @since 2.0.6
*/
public static function catch_unexpected_shutdown() {
if ( ! empty( self::$transaction_details ) && $error = error_get_last() ) {
if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ) ) ) {
do_action( 'wcs_paypal_ipn_process_failure', self::$transaction_details, $error );
}
}
self::$transaction_details = null;
}
/**
* Log any fatal errors occurred while Subscriptions is trying to process IPN messages
*
* @since 2.0.6
* @param array $transaction_details the current IPN message being processed when the fatal error occurred
* @param array $error
*/
public static function log_ipn_errors( $transaction_details, $error = '' ) {
// we want to make sure the ipn error admin notice is always displayed when a new error occurs
delete_option( 'wcs_fatal_error_handling_ipn_ignored' );
self::log_to_failure( sprintf( 'Subscription transaction details: %s', print_r( $transaction_details, true ) ) );
if ( ! empty( $error ) ) {
update_option( 'wcs_fatal_error_handling_ipn', $error['message'] );
self::log_to_failure( sprintf( 'Error processing PayPal IPN message: %s in %s on line %s.', $error['message'], $error['file'], $error['line'] ) );
if ( ! empty( $error['trace'] ) ) {
self::log_to_failure( sprintf( 'Stack trace: %s', PHP_EOL . $error['trace'] ) );
}
}
set_transient( 'wcs_paypal_ipn_error_occurred', WCS_PayPal::get_option( 'api_username' ), WEEK_IN_SECONDS );
}
/**
* Log any unexpected fatal errors to wcs-ipn-failures log file
*
* @since 2.0.6
* @param string $message
*/
public static function log_to_failure( $message ) {
if ( empty( self::$log ) ) {
self::$log = new WC_Logger();
}
self::$log->add( 'wcs-ipn-failures', $message );
}
/**
* Builds an error array from exception and call @see self::log_ipn_errors() to log unhandled
* exceptions in a separate paypal log.
*
* @since 2.0.6
* @param Exception $exception
*/
public static function log_unexpected_exception( $exception ) {
$error = array(
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
);
if ( empty( $error['message'] ) ) {
$error['message'] = 'Unhandled Exception: no message';
}
self::log_ipn_errors( self::$transaction_details, $error );
}
}

View File

@@ -39,7 +39,7 @@ class WCS_PayPal_Standard_Switcher {
add_action( 'woocommerce_checkout_update_order_meta', __CLASS__ . '::save_old_paypal_meta', 15, 2 );
// Try to cancel a paypal once the switch has been successfully completed
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_cancel_paypal_after_switch', 10, 3 );
add_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::cancel_paypal_standard_after_switch', 10, 1 );
// Do not allow subscriptions to be switched using PayPal Standard as the payment method
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways', 12, 1 );
@@ -174,22 +174,18 @@ class WCS_PayPal_Standard_Switcher {
/**
* Cancel subscriptions with PayPal Standard after the order has been successfully switched.
*
* @param int $order_id
* @param string $old_status
* @param string $new_status
* @since 2.0.15
* @param WC_Order $order
* @since 2.1
*/
public static function maybe_cancel_paypal_after_switch( $order_id, $old_status, $new_status ) {
public static function cancel_paypal_standard_after_switch( $order ) {
$order_completed = in_array( $new_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) ) && in_array( $old_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
if ( 'paypal_standard' == get_post_meta( $order->id, '_old_payment_method', true ) ) {
if ( $order_completed && wcs_order_contains_switch( $order_id ) && 'paypal_standard' == get_post_meta( $order_id, '_old_payment_method', true ) ) {
$old_profile_id = get_post_meta( $order_id, '_old_paypal_subscription_id', true );
$old_profile_id = get_post_meta( $order->id, '_old_paypal_subscription_id', true );
if ( ! empty( $old_profile_id ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'switch' ) );
$subscriptions = wcs_get_subscriptions_for_order( $order->id, array( 'order_type' => 'switch' ) );
foreach ( $subscriptions as $subscription ) {
@@ -229,4 +225,26 @@ class WCS_PayPal_Standard_Switcher {
return $available_gateways;
}
/** Deprecated Methods **/
/**
* Cancel subscriptions with PayPal Standard after the order has been successfully switched.
*
* @param int $order_id
* @param string $old_status
* @param string $new_status
* @since 2.0.15
*/
public static function maybe_cancel_paypal_after_switch( $order_id, $old_status, $new_status ) {
_deprecated_function( __METHOD__, '2.1', __CLASS__ . 'cancel_paypal_standard_after_switch( $order )' );
$order_completed = in_array( $new_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) ) && in_array( $old_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
if ( $order_completed && wcs_order_contains_switch( $order_id ) ) {
$order = wc_get_order( $order_id );
self::cancel_paypal_standard_after_switch( $order );
}
}
}

View File

@@ -93,6 +93,25 @@ class WCS_PayPal_Status_Manager extends WCS_PayPal {
$status_updated = true;
} else {
$status_updated = false;
if ( $response->has_api_error_for_credentials() ) {
// Store the profile ID so we can lookup which profiles are affected
$profile_ids = get_option( 'wcs_paypal_credentials_error_affected_profiles', '' );
if ( ! empty( $profile_ids ) ) {
$profile_ids .= ', ';
}
$profile_ids .= $profile_id;
update_option( 'wcs_paypal_credentials_error_affected_profiles', $profile_ids );
// And set a flag to display notice
update_option( 'wcs_paypal_credentials_error', 'yes' );
// This message will be added as an order note on by WC_Subscription::update_status()
throw new Exception( sprintf( __( 'PayPal API error - credentials are incorrect.', 'woocommerce-subscriptions' ), $new_status ) );
}
}
} else {
$status_updated = false;

View File

@@ -1,28 +1,28 @@
<?php
/*
Plugin Name: Action Scheduler
Plugin URI: https://github.com/flightless/action-scheduler
Plugin URI: https://github.com/prospress/action-scheduler
Description: A robust action scheduler for WordPress
Author: Flightless
Author URI: http://flightless.us/
Version: 1.4-dev
Author: Prospress
Author URI: http://prospress.com/
Version: 1.5
*/
if ( !function_exists('action_scheduler_register_1_dot_4_dev') ) {
if ( ! function_exists( 'action_scheduler_register_1_dot_5' ) ) {
if ( ! class_exists( 'ActionScheduler_Versions' ) ) {
require_once( 'classes/ActionScheduler_Versions.php' );
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
}
add_action( 'plugins_loaded', 'action_scheduler_register_1_dot_4_dev', 0, 0 );
add_action( 'plugins_loaded', 'action_scheduler_register_1_dot_5', 0, 0 );
function action_scheduler_register_1_dot_4_dev() {
function action_scheduler_register_1_dot_5() {
$versions = ActionScheduler_Versions::instance();
$versions->register( '1.4-dev', 'action_scheduler_initialize_1_dot_4_dev' );
$versions->register( '1.5', 'action_scheduler_initialize_1_dot_5' );
}
function action_scheduler_initialize_1_dot_4_dev() {
function action_scheduler_initialize_1_dot_5() {
require_once( 'classes/ActionScheduler.php' );
ActionScheduler::init( __FILE__ );
}

View File

@@ -21,7 +21,7 @@ class ActionScheduler_CronSchedule implements ActionScheduler_Schedule {
*/
public function next( DateTime $after = NULL ) {
$after = empty($after) ? clone $this->start : clone $after;
return $this->cron->getNextRunDate($after, 0, TRUE);
return $this->cron->getNextRunDate($after, 0, false);
}

View File

@@ -126,12 +126,25 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
global $wpdb;
if ( 0 === $post_id ) {
$count = wp_cache_get( 'comments-0', 'counts' );
if ( false !== $count ) {
return $count;
$stats = $this->get_comment_count();
}
return $stats;
}
/**
* Retrieve the comment counts from our cache, or the database if the cached version isn't set.
*
* @return object
*/
protected function get_comment_count() {
global $wpdb;
$stats = get_transient( 'as_comment_count' );
if ( ! $stats ) {
$stats = array();
$count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A );
$total = 0;
@@ -149,6 +162,8 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
}
$stats['total_comments'] = $total;
$stats['all'] = $total;
foreach ( $approved as $key ) {
if ( empty( $stats[ $key ] ) ) {
$stats[ $key ] = 0;
@@ -156,12 +171,22 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
}
$stats = (object) $stats;
wp_cache_set( 'comments-0', $stats, 'counts' );
set_transient( 'as_comment_count', $stats );
}
return $stats;
}
/**
* Delete comment count cache whenever there is new comment or the status of a comment changes. Cache
* will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called.
*
* @return void
*/
public function delete_comment_count_cache() {
delete_transient( 'as_comment_count' );
}
/**
* @codeCoverageIgnore
*/
@@ -177,7 +202,11 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 );
add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 );
add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 );
add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 9, 2 ); // run before WC_Comments::wp_count_comments()
add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs
// Delete comments count cache whenever there is a new comment or a comment status changes
add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) );
add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) );
}
public function disable_comment_counting() {

View File

@@ -0,0 +1,144 @@
<?php
/**
* Create settings and add meta boxes relating to retries
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Retry_Admin {
/**
* Constructor
*/
public function __construct( $setting_id ) {
$this->setting_id = $setting_id;
add_filter( 'woocommerce_subscription_settings', array( $this, 'add_settings' ) );
if ( WCS_Retry_Manager::is_retry_enabled() ) {
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 50 );
add_filter( 'wcs_display_date_type', array( $this, 'maybe_hide_date_type' ), 10, 3 );
// Display the number of retries in the Orders list table
add_action( 'manage_shop_order_posts_custom_column', __CLASS__ . '::add_column_content', 20, 2 );
}
}
/**
* Add a meta box to the Edit Order screen to display the retries relating to that order
*
* @return null
*/
public function add_meta_boxes() {
global $current_screen, $post_ID;
// Only display the meta box if an order relates to a subscription
if ( 'shop_order' === get_post_type( $post_ID ) && wcs_order_contains_renewal( $post_ID ) && WCS_Retry_Manager::store()->get_retry_count_for_order( $post_ID ) > 0 ) {
add_meta_box( 'renewal_payment_retries', __( 'Automatic Failed Payment Retries', 'woocommerce-subscriptions' ), 'WCS_Meta_Box_Payment_Retries::output', 'shop_order', 'normal', 'low' );
}
}
/**
* Only display the retry payment date on the Edit Subscription screen if the subscription has a pending retry
* and when that is the case, do not display the next payment date (because it will still be set to the original
* payment date, in the past).
*
* @param bool $show_date_type
* @param string $date_key
* @param WC_Subscription $the_subscription
* @return bool
*/
public function maybe_hide_date_type( $show_date_type, $date_key, $the_subscription ) {
if ( 'payment_retry' === $date_key && 0 == $the_subscription->get_time( 'payment_retry' ) ) {
$show_date_type = false;
} elseif ( 'next_payment' === $date_key && $the_subscription->get_time( 'payment_retry' ) > 0 ) {
$show_date_type = false;
}
return $show_date_type;
}
/**
* Dispay the number of retries on a renewal order in the Orders list table.
*
* @param string $column The string of the current column
* @param int $post_id The ID of the order
* @since 2.1
*/
public static function add_column_content( $column, $post_id ) {
if ( 'subscription_relationship' == $column && wcs_order_contains_renewal( $post_id ) ) {
$retries = WCS_Retry_Manager::store()->get_retries_for_order( $post_id );
if ( ! empty( $retries ) ) {
$retry_counts = array();
$tool_tip = '';
foreach ( $retries as $retry ) {
$retry_counts[ $retry->get_status() ] = isset( $retry_counts[ $retry->get_status() ] ) ? ++$retry_counts[ $retry->get_status() ] : 1;
}
foreach ( $retry_counts as $retry_status => $retry_count ) {
switch ( $retry_status ) {
case 'pending' :
$tool_tip .= sprintf( _n( '%d Pending Payment Retry', '%d Pending Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count );
break;
case 'processing' :
$tool_tip .= sprintf( _n( '%d Processing Payment Retry', '%d Processing Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count );
break;
case 'failed' :
$tool_tip .= sprintf( _n( '%d Failed Payment Retry', '%d Failed Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count );
break;
case 'complete' :
$tool_tip .= sprintf( _n( '%d Successful Payment Retry', '%d Successful Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count );
break;
case 'cancelled' :
$tool_tip .= sprintf( _n( '%d Cancelled Payment Retry', '%d Cancelled Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count );
break;
}
$tool_tip .= '<br />';
}
echo '<br /><span class="payment_retry tips" data-tip="' . esc_attr( $tool_tip ) . '"></span>';
}
}
}
/**
* Add a setting to enable/disable the retry system
*
* @param array
* @return null
*/
public function add_settings( $settings ) {
$misc_section_end = wp_list_filter( $settings, array( 'id' => 'woocommerce_subscriptions_miscellaneous', 'type' => 'sectionend' ) );
$spliced_array = array_splice( $settings, key( $misc_section_end ), 0, array(
array(
'name' => __( 'Retry Failed Payments', 'woocommerce-subscriptions' ),
'desc' => __( 'Enable automatic retry of failed recurring payments', 'woocommerce-subscriptions' ),
'id' => $this->setting_id,
'default' => 'no',
'type' => 'checkbox',
'desc_tip' => sprintf( __( 'Attempt to recover recurring revenue that would otherwise be lost due to payment methods being declined only temporarily. %sLearn more%s.', 'woocommerce-subscriptions' ), '<a href="https://docs.woocommerce.com/document/subscriptions/failed-payment-retry/">', '</a>' ),
),
) );
return $settings;
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* Manage the emails sent as part of the retry process
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Email
* @category Class
* @author Prospress
* @since 2.1
*/
class WCS_Retry_Email {
/* a property to cache the order ID when detaching/reattaching default emails in favour of retry emails */
protected static $removed_emails_for_order_id;
/**
* Attach callbacks and set the retry rules
*
* @since 2.1
*/
public static function init() {
add_action( 'woocommerce_email_classes', __CLASS__ . '::add_emails', 12, 1 );
add_action( 'woocommerce_subscriptions_after_apply_retry_rule', __CLASS__ . '::send_email', 0, 2 );
add_action( 'woocommerce_order_status_failed', __CLASS__ . '::maybe_detach_email', 9 );
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_reattach_email', 100, 3 );
}
/**
* Add default retry email classes to the available WooCommerce emails
*
* @since 2.1
*/
public static function add_emails( $email_classes ) {
require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/emails/class-wcs-email-customer-payment-retry.php' );
$email_classes['WCS_Email_Customer_Payment_Retry'] = new WCS_Email_Customer_Payment_Retry();
// the WCS_Email_Payment_Retry extends WC_Email_Failed_Order which is only available in WC 2.5+
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/emails/class-wcs-email-payment-retry.php' );
$email_classes['WCS_Email_Payment_Retry'] = new WCS_Email_Payment_Retry();
}
return $email_classes;
}
/**
* After a retry rule has been applied, send relevant emails for that rule.
*
* Attached to 'woocommerce_subscriptions_after_apply_retry_rule' with a low priority.
*
* @param WCS_Retry_Rule The retry rule applied.
* @param WC_Order The order to which the retry rule was applied.
* @since 2.1
*/
public static function send_email( $retry_rule, $last_order ) {
// maybe send emails about the renewal payment failure
foreach ( array( 'customer', 'admin' ) as $recipient ) {
if ( $retry_rule->has_email_template( $recipient ) ) {
$email_class = $retry_rule->get_email_template( $recipient );
if ( class_exists( $email_class ) ) {
$email = new $email_class();
$email->trigger( $last_order );
}
}
}
}
/**
* Don't send the renewal order invoice email to the customer or failed order email to the admin
* when a payment fails if there are retry rules to apply as they define which email/s to send.
*
* @since 2.1
*/
public static function maybe_detach_email( $order_id ) {
// We only want to detach the email if there is a retry
if ( wcs_order_contains_renewal( $order_id ) && WCS_Retry_Manager::rules()->has_rule( WCS_Retry_Manager::store()->get_retry_count_for_order( $order_id ), $order_id ) ) {
// Remove email sent to customer email, which is sent by Subscriptions, which already removes the WooCommerce equivalent email
remove_action( 'woocommerce_order_status_failed', 'WC_Subscriptions_Email::send_renewal_order_email', 10 );
// Remove email sent to admin, which is sent by WooCommerce
remove_action( 'woocommerce_order_status_pending_to_failed', array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
remove_action( 'woocommerce_order_status_on-hold_to_failed', array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
self::$removed_emails_for_order_id = $order_id;
}
}
/**
* Check if we removed emails for a given order, and if we did, reattach them to the corresponding hooks
*
* @since 2.1
*/
public static function maybe_reattach_email( $order_id, $old_status, $new_status ) {
if ( 'failed' === $new_status && $order_id == self::$removed_emails_for_order_id ) {
// Reattach email sent to customer email by Subscriptions, but only reattach it once
add_action( 'woocommerce_order_status_failed', 'WC_Subscriptions_Email::send_renewal_order_email' );
// Reattach email sent to admin, which is sent by WooCommerce
add_action( 'woocommerce_order_status_pending_to_failed', array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
add_action( 'woocommerce_order_status_on-hold_to_failed', array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
self::$removed_emails_for_order_id = null;
}
}
}
WCS_Retry_Email::init();

View File

@@ -0,0 +1,160 @@
<?php
/**
* Store retry details in the WordPress posts table as a custom post type
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Store
* @category Class
* @author Prospress
* @since 2.1
*/
class WCS_Retry_Post_Store extends WCS_Retry_Store {
protected static $post_type = 'payment_retry';
/**
* Setup the class, if required
*
* @return null
*/
public function init() {
register_post_type( self::$post_type, array(
'description' => __( 'Payment retry posts store details about the automatic retry of failed renewal payments.', 'woocommerce-subscriptions' ),
'public' => false,
'map_meta_cap' => true,
'hierarchical' => false,
'supports' => array( 'title', 'editor','comments' ),
'rewrite' => false,
'query_var' => false,
'can_export' => true,
'ep_mask' => EP_NONE,
'labels' => array(
'name' => _x( 'Renewal Payment Retries', 'Post type name', 'woocommerce-subscriptions' ),
'singular_name' => __( 'Renewal Payment Retry', 'woocommerce-subscriptions' ),
'menu_name' => _x( 'Renewal Payment Retries', 'Admin menu name', 'woocommerce-subscriptions' ),
'add_new' => __( 'Add', 'woocommerce-subscriptions' ),
'add_new_item' => __( 'Add New Retry', 'woocommerce-subscriptions' ),
'edit' => __( 'Edit', 'woocommerce-subscriptions' ),
'edit_item' => __( 'Edit Retry', 'woocommerce-subscriptions' ),
'new_item' => __( 'New Retry', 'woocommerce-subscriptions' ),
'view' => __( 'View Retry', 'woocommerce-subscriptions' ),
'view_item' => __( 'View Retry', 'woocommerce-subscriptions' ),
'search_items' => __( 'Search Renewal Payment Retries', 'woocommerce-subscriptions' ),
'not_found' => __( 'No retries found', 'woocommerce-subscriptions' ),
'not_found_in_trash' => __( 'No retries found in trash', 'woocommerce-subscriptions' ),
),
)
);
}
/**
* Save the details of a retry to the database
*
* @param WCS_Retry $retry
* @return int the retry's ID
*/
public function save( WCS_Retry $retry ) {
$post_id = wp_insert_post( array(
'ID' => $retry->get_id(),
'post_type' => self::$post_type,
'post_status' => $retry->get_status(),
'post_parent' => $retry->get_order_id(),
'post_date' => $retry->get_date(),
'post_date_gmt' => $retry->get_date_gmt(),
) );
// keep a record of the rule in post meta
foreach ( $retry->get_rule()->get_raw_data() as $rule_key => $rule_value ) {
update_post_meta( $post_id, '_rule_' . $rule_key, $rule_value );
}
return $post_id;
}
/**
* Get the details of a retry from the database
*
* @param int $retry_id
* @return WCS_Retry
*/
public function get_retry( $retry_id ) {
$retry_post = get_post( $retry_id );
if ( null !== $retry_post ) {
$rule_data = array();
$post_meta = get_post_meta( $retry_id );
foreach ( $post_meta as $meta_key => $meta_value ) {
if ( 0 === strpos( $meta_key, '_rule_' ) ) {
$rule_data[ substr( $meta_key, 6 ) ] = $meta_value[0];
}
}
$retry = new WCS_Retry( array(
'id' => $retry_post->ID,
'status' => $retry_post->post_status,
'order_id' => $retry_post->post_parent,
'date_gmt' => $retry_post->post_date_gmt,
'rule_raw' => $rule_data,
) );
} else {
$retry = null;
}
return $retry;
}
/**
*
*/
public function get_retries( $args ) {
$args = wp_parse_args( $args, array(
'status' => 'any',
'date_query' => array(),
) );
$retry_post_ids = get_posts( array(
'posts_per_page' => -1,
'post_type' => self::$post_type,
'post_status' => $args['status'],
'date_query' => $args['date_query'],
'fields' => 'ids',
'orderby' => 'date',
'order' => 'DESC',
) );
$retries = array();
foreach ( $retry_post_ids as $retry_post_id ) {
$retries[ $retry_post_id ] = $this->get_retry( $retry_post_id );
}
return $retries;
}
/**
* Get the IDs of all retries from the database for a given order
*
* @param int $order_id
* @return array
*/
public function get_retry_ids_for_order( $order_id ) {
$retry_post_ids = get_posts( array(
'posts_per_page' => -1,
'post_type' => self::$post_type,
'post_status' => 'any',
'post_parent' => $order_id,
'fields' => 'ids',
'orderby' => 'ID',
'order' => 'ASC',
) );
return $retry_post_ids;
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* An instance of a failed payment retry rule.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Rule
* @category Class
* @author Prospress
* @since 2.1
*/
class WCS_Retry_Rule {
/* the rule_data that control the retry schedule and behaviour of each retry */
protected $rule_data = array();
/**
* Set up the retry rules
*
* @since 2.1
*/
public function __construct( $rule_data ) {
foreach ( $rule_data as $rule_key => $rule_value ) {
$this->rule_data[ $rule_key ] = $rule_value;
}
}
/**
* Get the time to wait between when this rule is applied (i.e. payment failed) and the retry
* should be processed.
*
* @return int
* @since 2.1
*/
public function get_retry_interval() {
return ( isset( $this->rule_data['retry_after_interval'] ) ) ? $this->rule_data['retry_after_interval'] : 0;
}
/**
* Check if this rule has an email template defined for sending to a specified recipient.
*
* @param string $recipient The email type based on recipient, either 'customer' or 'admin'
* @return bool
* @since 2.1
*/
public function has_email_template( $recipient = 'customer' ) {
return ( isset( $this->rule_data[ 'email_template_' . $recipient ] ) && ! empty( $this->rule_data[ 'email_template_' . $recipient ] ) ) ? true : false;
}
/**
* Get the email template this rule defined for sending to a specified recipient.
*
* @param string $recipient The email type based on recipient, either 'customer' or 'admin'
* @return string
* @since 2.1
*/
public function get_email_template( $recipient = 'customer' ) {
if ( $this->has_email_template( $recipient ) ) {
$email_template = $this->rule_data[ 'email_template_' . $recipient ];
} else {
$email_template = '';
}
return $email_template;
}
/**
* Get the status to apply to one of the related objects when this rule is applied.
*
* @param string $object The object type the status should be applied to, either 'order' or 'subscription'
* @return string
* @since 2.1
*/
public function get_status_to_apply( $object = 'order' ) {
if ( isset( $this->rule_data[ 'status_to_apply_to_' . $object ] ) ) {
$status = $this->rule_data[ 'status_to_apply_to_' . $object ];
} else {
$status = '';
}
return $status;
}
/**
* Get rule data as a raw array.
*
* @return array
* @since 2.1
*/
public function get_raw_data() {
return $this->rule_data;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* Setup the rules for retrying failed automatic renewal payments and provide methods for working with them.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Rules
* @category Class
* @author Prospress
* @since 2.1
*/
class WCS_Retry_Rules {
/* the class used to instantiate an individual retry rule */
protected $retry_rule_class;
/* the rules that control the retry schedule and behaviour of each retry */
protected $default_retry_rules = array();
/**
* Set up the retry rules
*
* @since 2.1
*/
public function __construct() {
$this->retry_rule_class = apply_filters( 'wcs_retry_rule_class', 'WCS_Retry_Rule' );
$this->default_retry_rules = apply_filters( 'wcs_default_retry_rules', array(
array(
'retry_after_interval' => DAY_IN_SECONDS / 2, // how long to wait before retrying
'email_template_customer' => '', // don't bother the customer yet
'email_template_admin' => 'WCS_Email_Payment_Retry',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => DAY_IN_SECONDS / 2,
'email_template_customer' => 'WCS_Email_Customer_Payment_Retry',
'email_template_admin' => 'WCS_Email_Payment_Retry',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => DAY_IN_SECONDS,
'email_template_customer' => '', // avoid spamming the customer by not sending them an email this time either
'email_template_admin' => 'WCS_Email_Payment_Retry',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => DAY_IN_SECONDS * 2,
'email_template_customer' => 'WCS_Email_Customer_Payment_Retry',
'email_template_admin' => 'WCS_Email_Payment_Retry',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => DAY_IN_SECONDS * 3,
'email_template_customer' => 'WCS_Email_Customer_Payment_Retry',
'email_template_admin' => 'WCS_Email_Payment_Retry',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
) );
}
/**
* Check if a retry rule exists for a certain stage of the retry process.
*
* @param int The retry queue position to check for a rule
* @param int The ID of a WC_Order object to which the failed payment relates
* @return bool
* @since 2.1
*/
public function has_rule( $retry_number, $order_id ) {
return ( null !== $this->get_rule( $retry_number, $order_id ) ) ? true : false;
}
/**
* Get an instance of a retry rule for a given order and stage of the retry queue (if any).
*
* @param int The retry queue position to check for a rule
* @param int The ID of a WC_Order object to which the failed payment relates
* @return null|WCS_Retry_Rule If a retry rule exists for this stage of the retry queue and order, WCS_Retry_Rule, otherwise null.
* @since 2.1
*/
public function get_rule( $retry_number, $order_id ) {
$rule = null;
if ( isset( $this->default_retry_rules[ $retry_number ] ) ) {
$rule_array = apply_filters( 'wcs_get_retry_rule_raw', $this->default_retry_rules[ $retry_number ], $retry_number, $order_id );
if ( ! empty( $rule_array ) ) {
$rule = new $this->retry_rule_class( $rule_array );
}
}
return apply_filters( 'wcs_get_retry_rule', $rule, $retry_number, $order_id );
}
/**
* Get the PHP class used ti instaniate a set of raw retry rule data.
*
* @since 2.1
*/
public function get_rule_class() {
return $this->retry_rule_class;
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* An instance of a failed payment retry.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry
* @category Class
* @author Prospress
* @since 2.1
*/
class WCS_Retry {
/* the retry's ID */
protected $id;
/* the renewal order to which the retry relates */
protected $order_id;
/* the status of this retry */
protected $status;
/* the date/time in UTC timezone on which this retry was run */
protected $date_gmt;
/* an instance of the retry rules (WCS_Retry_Rule by default) applied for this retry */
protected $rule;
/* the raw retry rules applied for this retry */
protected $rule_raw;
/**
* Get the Renewal Order which this retry was run for
*
* @return null
*/
public function __construct( $args ) {
$this->id = isset( $args['id'] ) ? $args['id'] : 0;
$this->order_id = $args['order_id'];
$this->status = isset( $args['status'] ) ? $args['status'] : 'pending';
$this->date_gmt = isset( $args['date_gmt'] ) ? $args['date_gmt'] : gmdate( 'Y-m-d H:i:s' );
$this->rule_raw = isset( $args['rule_raw'] ) ? $args['rule_raw'] : array();
}
/**
* Get the Renewal Order which this retry was run for
*
* @return int
*/
public function get_id() {
return $this->id;
}
/**
* Get the ID of the renewal order which this retry was run for
*
* @return int
*/
public function get_order_id() {
return $this->order_id;
}
/**
* Get the Renewal Order which this retry was run for
*
* @return string
*/
public function get_status() {
return $this->status;
}
/**
* Update the status of a retry
*
* @since 2.1
*/
public function update_status( $new_status ) {
WCS_Retry_Manager::store()->save( new WCS_Retry( array(
'id' => $this->get_id(),
'order_id' => $this->get_order_id(),
'date_gmt' => $this->get_date_gmt(),
'status' => $new_status,
'rule_raw' => $this->get_rule()->get_raw_data(),
) ) );
$old_status = $this->status;
$this->status = $new_status;
do_action( 'woocommerce_subscriptions_retry_status_updated', $this, $new_status, $old_status );
}
/**
* Get the date in the site's timezone when this retry was recorded
*
* @return string
*/
public function get_date() {
return get_date_from_gmt( $this->date_gmt );
}
/**
* Get the date in GMT/UTC timezone when this retry was recorded
*
* @return string
*/
public function get_date_gmt() {
return $this->date_gmt;
}
/**
* Update the status of a retry and set the date to reflect that
*
* @since 2.1
*/
public function update_date_gmt( $new_date ) {
WCS_Retry_Manager::store()->save( new WCS_Retry( array(
'id' => $this->get_id(),
'order_id' => $this->get_order_id(),
'date_gmt' => $new_date,
'status' => $this->get_status(),
'rule_raw' => $this->get_rule()->get_raw_data(),
) ) );
$old_date = $this->date_gmt;
$this->date_gmt = $new_date;
do_action( 'woocommerce_subscriptions_retry_date_updated', $this, $new_date, $old_date );
}
/**
* Get the timestamp (in GMT/UTC timezone) when this retry was recorded
*
* @return string
*/
public function get_time() {
return wcs_date_to_time( $this->get_date_gmt() );
}
/**
* Get an instance of the retry rule applied for this retry
*
* @return WCS_Retry_Rule
*/
public function get_rule() {
if ( null === $this->rule ) {
$rule_class = WCS_Retry_Manager::rules()->get_rule_class();
$this->rule = new $rule_class( $this->rule_raw );
}
return $this->rule;
}
}

View File

@@ -85,6 +85,9 @@ class WC_Subscriptions_Upgrader {
// While the upgrade is in progress, we need to block PayPal IPN messages to avoid renewals failing to process
add_action( 'woocommerce_api_wc_gateway_paypal', __CLASS__ . '::maybe_block_paypal_ipn', 0 );
// Sometimes redirect to the Welcome/About page after an upgrade
add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::maybe_redirect_after_upgrade_complete', 100, 2 );
}
/**
@@ -167,6 +170,20 @@ class WC_Subscriptions_Upgrader {
self::ajax_upgrade_handler();
}
if ( '0' != self::$active_version && version_compare( self::$active_version, '2.1.0', '<' ) ) {
// Delete cached subscription length ranges to force an update with 2.1
WC_Subscriptions::$cache->delete_cached( tlc_transient( 'wcs-sub-ranges-' . get_locale() )->key );
WCS_Upgrade_Logger::add( 'v2.1: Deleted cached subscription ranges.' );
include_once( 'class-wcs-upgrade-2-1.php' );
WCS_Upgrade_2_1::set_cancelled_dates();
// Schedule report cache updates in the hopes that the data is ready and waiting for the store owner the first time they visit the reports pages
do_action( 'woocommerce_subscriptions_reports_schedule_cache_updates' );
}
self::upgrade_complete();
}
@@ -183,7 +200,19 @@ class WC_Subscriptions_Upgrader {
delete_option( 'wc_subscriptions_is_upgrading' );
do_action( 'woocommerce_subscriptions_upgraded', WC_Subscriptions::$version );
do_action( 'woocommerce_subscriptions_upgraded', WC_Subscriptions::$version, self::$active_version );
}
/**
* Redirect to the Subscriptions major version Welcome/About page for major version updates
*
* @since 2.1
*/
public static function maybe_redirect_after_upgrade_complete( $current_version, $previously_active_version ) {
if ( version_compare( $previously_active_version, '2.1.0', '<' ) && version_compare( $current_version, '2.1.0', '>=' ) ) {
wp_safe_redirect( self::$about_page_url );
exit();
}
}
/**
@@ -296,7 +325,7 @@ class WC_Subscriptions_Upgrader {
$results = array(
'upgraded_count' => 0,
// translators: 1$: error message, 2$: opening link tag, 3$: closing link tag
'message' => sprintf( __( 'Unable to upgrade subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woothemes.com/my-account/create-a-ticket/' ) . '">', '</a>' ),
'message' => sprintf( __( 'Unable to upgrade subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>' ),
'status' => 'error',
);
}
@@ -347,7 +376,7 @@ class WC_Subscriptions_Upgrader {
'repaired_count' => 0,
'unrepaired_count' => 0,
// translators: 1$: error message, 2$: opening link tag, 3$: closing link tag
'message' => sprintf( _x( 'Unable to repair subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woothemes.com/my-account/create-a-ticket/' ) . '">', '</a>' ),
'message' => sprintf( _x( 'Unable to repair subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>' ),
'status' => 'error',
);
}
@@ -551,7 +580,7 @@ class WC_Subscriptions_Upgrader {
* @since 1.4
*/
public static function updated_welcome_page() {
$about_page = add_dashboard_page( __( 'Welcome to WooCommerce Subscriptions 2.0', 'woocommerce-subscriptions' ), __( 'About WooCommerce Subscriptions', 'woocommerce-subscriptions' ), 'manage_options', 'wcs-about', __CLASS__ . '::about_screen' );
$about_page = add_dashboard_page( __( 'Welcome to WooCommerce Subscriptions 2.1', 'woocommerce-subscriptions' ), __( 'About WooCommerce Subscriptions', 'woocommerce-subscriptions' ), 'manage_options', 'wcs-about', __CLASS__ . '::about_screen' );
add_action( 'admin_print_styles-'. $about_page, __CLASS__ . '::admin_css' );
add_action( 'admin_head', __CLASS__ . '::admin_head' );
}
@@ -580,7 +609,10 @@ class WC_Subscriptions_Upgrader {
* Output the about screen.
*/
public static function about_screen() {
$active_version = self::$active_version;
$settings_page = admin_url( 'admin.php?page=wc-settings&tab=subscriptions' );
include_once( 'templates/wcs-about.php' );
}

View File

@@ -269,7 +269,7 @@ class WCS_Repair_2_0_2 {
// if we have a date, make sure it's valid
if ( null !== $old_next_payment_date ) {
if ( strtotime( $old_next_payment_date ) <= gmdate( 'U' ) ) {
if ( wcs_date_to_time( $old_next_payment_date ) <= gmdate( 'U' ) ) {
$repair_date = $subscription->calculate_date( 'next_payment' );
if ( 0 == $repair_date ) {
$repair_date = false;
@@ -283,7 +283,7 @@ class WCS_Repair_2_0_2 {
// let's just double check we shouldn't have a date set by recalculating it
$calculated_next_payment_date = $subscription->calculate_date( 'next_payment' );
if ( 0 != $calculated_next_payment_date && strtotime( $calculated_next_payment_date ) > gmdate( 'U' ) ) {
if ( 0 != $calculated_next_payment_date && wcs_date_to_time( $calculated_next_payment_date ) > gmdate( 'U' ) ) {
$repair_date = $calculated_next_payment_date;
} else {
$repair_date = false;

View File

@@ -263,11 +263,11 @@ class WCS_Repair_2_0 {
// let's get the last 2 renewal orders
$last_renewal_order = array_shift( $renewal_orders );
$last_renewal_date = $last_renewal_order->order_date;
$last_renewal_timestamp = strtotime( $last_renewal_date );
$last_renewal_timestamp = wcs_date_to_time( $last_renewal_date );
$second_renewal_order = array_shift( $renewal_orders );
$second_renewal_date = $second_renewal_order->order_date;
$second_renewal_timestamp = strtotime( $second_renewal_date );
$second_renewal_timestamp = wcs_date_to_time( $second_renewal_date );
$interval = 1;
@@ -336,11 +336,11 @@ class WCS_Repair_2_0 {
// let's get the last 2 renewal orders
$last_renewal_order = array_shift( $renewal_orders );
$last_renewal_date = $last_renewal_order->order_date;
$last_renewal_timestamp = strtotime( $last_renewal_date );
$last_renewal_timestamp = wcs_date_to_time( $last_renewal_date );
$second_renewal_order = array_shift( $renewal_orders );
$second_renewal_date = $second_renewal_order->order_date;
$second_renewal_timestamp = strtotime( $second_renewal_date );
$second_renewal_timestamp = wcs_date_to_time( $second_renewal_date );
$subscription['interval'] = wcs_estimate_periods_between( $second_renewal_timestamp, $last_renewal_timestamp, $subscription['period'] );
@@ -369,7 +369,7 @@ class WCS_Repair_2_0 {
// If we can calculate it from the effective date and expiry date
if ( 'expired' == $subscription['status'] && array_key_exists( 'expiry_date', $subscription ) && ! empty( $subscription['expiry_date'] ) && null !== $effective_start_date && array_key_exists( 'period', $subscription ) && ! empty( $subscription['period'] ) && array_key_exists( 'interval', $subscription ) && ! empty( $subscription['interval'] ) ) {
$intervals = wcs_estimate_periods_between( strtotime( $effective_start_date ), strtotime( $subscription['expiry_date'] ), $subscription['period'], 'floor' );
$intervals = wcs_estimate_periods_between( wcs_date_to_time( $effective_start_date ), wcs_date_to_time( $subscription['expiry_date'] ), $subscription['period'], 'floor' );
$subscription['length'] = $intervals;
} else {
$subscription['length'] = 0;
@@ -470,7 +470,7 @@ class WCS_Repair_2_0 {
} else {
$subscription['end_date'] = wcs_add_time( 5, 'hours', strtotime( $last_order->order_date ) );
$subscription['end_date'] = wcs_add_time( 5, 'hours', wcs_date_to_time( $last_order->order_date ) );
}
} else {
@@ -547,8 +547,8 @@ class WCS_Repair_2_0 {
* @return integer number of seconds between the two
*/
private static function time_diff( $to, $from ) {
$to = strtotime( $to );
$from = strtotime( $from );
$to = wcs_date_to_time( $to );
$from = wcs_date_to_time( $from );
return abs( $to - $from );
}
@@ -602,7 +602,7 @@ class WCS_Repair_2_0 {
$formatted_date = 0;
WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s": fetch of date unsuccessfull: no action present. Date is 0.', $subscription['order_id'], $type ) );
} else {
$formatted_date = date( 'Y-m-d H:i:s', $next_date_timestamp );
$formatted_date = gmdate( 'Y-m-d H:i:s', $next_date_timestamp );
WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s": fetch of date successfull. New date is %s', $subscription['order_id'], $type, $formatted_date ) );
}
@@ -624,7 +624,7 @@ class WCS_Repair_2_0 {
} elseif ( array_key_exists( 'trial_period', $subscription ) && ! empty( $subscription['trial_period'] ) && array_key_exists( 'trial_length', $subscription ) && ! empty( $subscription['trial_length'] ) && array_key_exists( 'start_date', $subscription ) && ! empty( $subscription['start_date'] ) ) {
// calculate the end of trial from interval, period and start date
$effective_date = date( 'Y-m-d H:i:s', strtotime( '+' . $subscription['trial_length'] . ' ' . $subscription['trial_period'], strtotime( $subscription['start_date'] ) ) );
$effective_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription['trial_length'], $subscription['trial_period'], wcs_date_to_time( $subscription['start_date'] ) ) );
} elseif ( array_key_exists( 'start_date', $subscription ) && ! empty( $subscription['start_date'] ) ) {

View File

@@ -508,7 +508,7 @@ class WCS_Upgrade_2_0 {
if ( 'end_of_prepaid_term' == $new_key ) {
wc_schedule_single_action( $next_scheduled, 'woocommerce_scheduled_subscription_end_of_prepaid_term', array( 'subscription_id' => $new_subscription->id ) );
} else {
$dates_to_update[ $new_key ] = date( 'Y-m-d H:i:s', $next_scheduled );
$dates_to_update[ $new_key ] = gmdate( 'Y-m-d H:i:s', $next_scheduled );
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Methods to upgrade subscriptions data to v2.1
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Upgrade_2_1 {
/**
* Set the _schedule_cancelled post meta value to store a subscription's cancellation
* date for those subscriptions still in the pending cancellation state, and therefore
* where it is possible to determine the cancellation date.
*
* @since 2.1
*/
public static function set_cancelled_dates() {
global $wpdb;
$wpdb->query( 'SET SQL_BIG_SELECTS = 1;' );
$cancelled_date_meta_key = wcs_get_date_meta_key( 'cancelled' );
// Run two separate insert queries for pending cancellation and cancelled subscriptions. This could easily be done in one query, but we'll run it in two separate queries to minimise issues with large databases.
foreach ( array( 'wc-pending-cancel', 'wc-cancelled' ) as $post_status ) {
$rows_inserted = $wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->postmeta}(post_id, meta_key, meta_value)
SELECT ID, %s, post_modified_gmt
FROM {$wpdb->posts} as posts
WHERE post_status = %s
AND NOT EXISTS (
SELECT null
FROM {$wpdb->postmeta} as postmeta
WHERE postmeta.post_id = posts.ID
AND postmeta.meta_key = %s
)
",
$cancelled_date_meta_key,
$post_status,
$cancelled_date_meta_key
) );
WCS_Upgrade_Logger::add( sprintf( 'v2.1: Set _schedule_cancelled date to post_modified_gmt column value for %d subscriptions with %s status.', $rows_inserted, $post_status ) );
}
}
}

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