diff --git a/assets/css/about.css b/assets/css/about.css index 8d2eab9..61667f2 100644 --- a/assets/css/about.css +++ b/assets/css/about.css @@ -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; } diff --git a/assets/css/admin.css b/assets/css/admin.css index 68d27bb..184be13 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -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; +.woocommerce_options_panel ._subscription_price_fields .wrap input, +.woocommerce_options_panel ._subscription_price_fields .wrap select { + width:30.75%; + margin-right:3.8%; } -p._subscription_price_field, -p._subscription_period_field, -p._subscription_length_field { - display: inline-block; +.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%; } -.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_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; } -p._subscription_period_field label, -p._subscription_length_field label, -p._subscription_period_interval_field label, -p._subscription_trial_period_field label, + .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; +} diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..11e0cd6 --- /dev/null +++ b/assets/css/dashboard.css @@ -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; +} diff --git a/assets/images/add-edit-subscription-screen.png b/assets/images/add-edit-subscription-screen.png deleted file mode 100644 index 183d79f..0000000 Binary files a/assets/images/add-edit-subscription-screen.png and /dev/null differ diff --git a/assets/images/admin-change-payment-method.jpg b/assets/images/admin-change-payment-method.jpg deleted file mode 100644 index 19df347..0000000 Binary files a/assets/images/admin-change-payment-method.jpg and /dev/null differ diff --git a/assets/images/billing-schedules-meta-box.png b/assets/images/billing-schedules-meta-box.png deleted file mode 100644 index b156012..0000000 Binary files a/assets/images/billing-schedules-meta-box.png and /dev/null differ diff --git a/assets/images/checkout-recurring-totals.png b/assets/images/checkout-recurring-totals.png deleted file mode 100644 index 150869b..0000000 Binary files a/assets/images/checkout-recurring-totals.png and /dev/null differ diff --git a/assets/images/drip-downloadable-content.jpg b/assets/images/drip-downloadable-content.jpg deleted file mode 100644 index f761dc1..0000000 Binary files a/assets/images/drip-downloadable-content.jpg and /dev/null differ diff --git a/assets/images/view-subscription.png b/assets/images/view-subscription.png deleted file mode 100644 index 2e9d035..0000000 Binary files a/assets/images/view-subscription.png and /dev/null differ diff --git a/assets/js/admin/jquery.flot.axislabels.js b/assets/js/admin/jquery.flot.axislabels.js new file mode 100644 index 0000000..c4b3bca --- /dev/null +++ b/assets/js/admin/jquery.flot.axislabels.js @@ -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 = $('
' + + this.opts.axisLabel + '
'); + 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 = $('
' + + this.opts.axisLabel + '
'); + 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 = $('
' + this.opts.axisLabel + '
'); + 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); diff --git a/assets/js/admin/jquery.flot.axislabels.min.js b/assets/js/admin/jquery.flot.axislabels.min.js new file mode 100644 index 0000000..21b20cd --- /dev/null +++ b/assets/js/admin/jquery.flot.axislabels.min.js @@ -0,0 +1 @@ +!function(a){function c(){return!!document.createElement("canvas").getContext}function d(){if(!c())return!1;var a=document.createElement("canvas"),b=a.getContext("2d");return"function"==typeof b.fillText}function e(){var a=document.createElement("div");return"undefined"!=typeof a.style.MozTransition||"undefined"!=typeof a.style.OTransition||"undefined"!=typeof a.style.webkitTransition||"undefined"!=typeof a.style.transition}function f(a,b,c,d,e){this.axisName=a,this.position=b,this.padding=c,this.plot=d,this.opts=e,this.width=0,this.height=0}function g(a,b,c,d,e){f.prototype.constructor.call(this,a,b,c,d,e)}function h(a,b,c,d,e){f.prototype.constructor.call(this,a,b,c,d,e),this.elem=null}function i(a,b,c,d,e){h.prototype.constructor.call(this,a,b,c,d,e)}function j(a,b,c,d,e){i.prototype.constructor.call(this,a,b,c,d,e),this.requiresResize=!1}function k(b){b.hooks.processOptions.push(function(b,c){if(c.axisLabels.show){var f=!1,k={},m=2;b.hooks.draw.push(function(b,c){var l=!1;f?(f=!1,a.each(b.getAxes(),function(a,c){var d=c.options||b.getOptions()[a];d&&d.axisLabel&&c.show&&k[a].draw(c.box)})):(a.each(b.getAxes(),function(a,c){var f=c.options||b.getOptions()[a];if(a in k&&(c.labelHeight=c.labelHeight-k[a].height,c.labelWidth=c.labelWidth-k[a].width,f.labelHeight=c.labelHeight,f.labelWidth=c.labelWidth,k[a].cleanup(),delete k[a]),f&&f.axisLabel&&c.show){l=!0;var n=null;if(f.axisLabelUseHtml||"Microsoft Internet Explorer"!=navigator.appName)n=f.axisLabelUseHtml||!e()&&!d()&&!f.axisLabelUseCanvas?h:f.axisLabelUseCanvas||!e()?g:i;else{var o=navigator.userAgent,p=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");null!=p.exec(o)&&(rv=parseFloat(RegExp.$1)),n=rv>=9&&!f.axisLabelUseCanvas&&!f.axisLabelUseHtml?i:f.axisLabelUseCanvas||f.axisLabelUseHtml?f.axisLabelUseCanvas?g:h:j}var q=void 0===f.axisLabelPadding?m:f.axisLabelPadding;k[a]=new n(a,c.position,q,b,f),k[a].calculateSize(),f.labelHeight=c.labelHeight+k[a].height,f.labelWidth=c.labelWidth+k[a].width}}),l&&(f=!0,b.setupGrid(),b.draw()))})}})}var b={axisLabels:{show:!0}};f.prototype.cleanup=function(){},g.prototype=new f,g.prototype.constructor=g,g.prototype.calculateSize=function(){this.opts.axisLabelFontSizePixels||(this.opts.axisLabelFontSizePixels=14),this.opts.axisLabelFontFamily||(this.opts.axisLabelFontFamily="sans-serif");this.opts.axisLabelFontSizePixels+this.padding,this.opts.axisLabelFontSizePixels+this.padding;"left"==this.position||"right"==this.position?(this.width=this.opts.axisLabelFontSizePixels+this.padding,this.height=0):(this.width=0,this.height=this.opts.axisLabelFontSizePixels+this.padding)},g.prototype.draw=function(a){this.opts.axisLabelColour||(this.opts.axisLabelColour="black");var b=this.plot.getCanvas().getContext("2d");b.save(),b.font=this.opts.axisLabelFontSizePixels+"px "+this.opts.axisLabelFontFamily,b.fillStyle=this.opts.axisLabelColour;var e,f,c=b.measureText(this.opts.axisLabel).width,d=this.opts.axisLabelFontSizePixels,g=0;"top"==this.position?(e=a.left+a.width/2-c/2,f=a.top+.72*d):"bottom"==this.position?(e=a.left+a.width/2-c/2,f=a.top+a.height-.72*d):"left"==this.position?(e=a.left+.72*d,f=a.height/2+a.top+c/2,g=-Math.PI/2):"right"==this.position&&(e=a.left+a.width-.72*d,f=a.height/2+a.top-c/2,g=Math.PI/2),b.translate(e,f),b.rotate(g),b.fillText(this.opts.axisLabel,0,0),b.restore()},h.prototype=new f,h.prototype.constructor=h,h.prototype.calculateSize=function(){var b=a('
'+this.opts.axisLabel+"
");this.plot.getPlaceholder().append(b),this.labelWidth=b.outerWidth(!0),this.labelHeight=b.outerHeight(!0),b.remove(),this.width=this.height=0,"left"==this.position||"right"==this.position?this.width=this.labelWidth+this.padding:this.height=this.labelHeight+this.padding},h.prototype.cleanup=function(){this.elem&&this.elem.remove()},h.prototype.draw=function(b){this.plot.getPlaceholder().find("#"+this.axisName+"Label").remove(),this.elem=a('
'+this.opts.axisLabel+"
"),this.plot.getPlaceholder().append(this.elem),"top"==this.position?(this.elem.css("left",b.left+b.width/2-this.labelWidth/2+"px"),this.elem.css("top",b.top+"px")):"bottom"==this.position?(this.elem.css("left",b.left+b.width/2-this.labelWidth/2+"px"),this.elem.css("top",b.top+b.height-this.labelHeight+"px")):"left"==this.position?(this.elem.css("top",b.top+b.height/2-this.labelHeight/2+"px"),this.elem.css("left",b.left+"px")):"right"==this.position&&(this.elem.css("top",b.top+b.height/2-this.labelHeight/2+"px"),this.elem.css("left",b.left+b.width-this.labelWidth+"px"))},i.prototype=new h,i.prototype.constructor=i,i.prototype.calculateSize=function(){h.prototype.calculateSize.call(this),this.width=this.height=0,"left"==this.position||"right"==this.position?this.width=this.labelHeight+this.padding:this.height=this.labelHeight+this.padding},i.prototype.transforms=function(a,b,c){var d={"-moz-transform":"","-webkit-transform":"","-o-transform":"","-ms-transform":""};if(0!=b||0!=c){var e=" translate("+b+"px, "+c+"px)";d["-moz-transform"]+=e,d["-webkit-transform"]+=e,d["-o-transform"]+=e,d["-ms-transform"]+=e}if(0!=a){var g=" rotate("+a+"deg)";d["-moz-transform"]+=g,d["-webkit-transform"]+=g,d["-o-transform"]+=g,d["-ms-transform"]+=g}var h="top: 0; left: 0; ";for(var i in d)d[i]&&(h+=i+":"+d[i]+";");return h+=";"},i.prototype.calculateOffsets=function(a){var b={x:0,y:0,degrees:0};return"bottom"==this.position?(b.x=a.left+a.width/2-this.labelWidth/2,b.y=a.top+a.height-this.labelHeight):"top"==this.position?(b.x=a.left+a.width/2-this.labelWidth/2,b.y=a.top):"left"==this.position?(b.degrees=-90,b.x=a.left-this.labelWidth/2+this.labelHeight/2,b.y=a.height/2+a.top):"right"==this.position&&(b.degrees=90,b.x=a.left+a.width-this.labelWidth/2-this.labelHeight/2,b.y=a.height/2+a.top),b.x=Math.round(b.x),b.y=Math.round(b.y),b},i.prototype.draw=function(b){this.plot.getPlaceholder().find("."+this.axisName+"Label").remove();var c=this.calculateOffsets(b);this.elem=a('
'+this.opts.axisLabel+"
"),this.plot.getPlaceholder().append(this.elem)},j.prototype=new i,j.prototype.constructor=j,j.prototype.transforms=function(a,b,c){var d="";if(0!=a){for(var e=a/90;e<0;)e+=4;d+=" filter: progid:DXImageTransform.Microsoft.BasicImage(rotation="+e+"); ",this.requiresResize="right"==this.position}return 0!=b&&(d+="left: "+b+"px; "),0!=c&&(d+="top: "+c+"px; "),d},j.prototype.calculateOffsets=function(a){var b=i.prototype.calculateOffsets.call(this,a);return"top"==this.position?b.y=a.top+1:"left"==this.position?(b.x=a.left,b.y=a.height/2+a.top-this.labelWidth/2):"right"==this.position&&(b.x=a.left+a.width-this.labelHeight,b.y=a.height/2+a.top-this.labelWidth/2),b},j.prototype.draw=function(a){i.prototype.draw.call(this,a),this.requiresResize&&(this.elem=this.plot.getPlaceholder().find("."+this.axisName+"Label"),this.elem.css("width",this.labelWidth),this.elem.css("height",this.labelHeight))},a.plot.plugins.push({init:k,options:b,name:"axisLabels",version:"2.0"})}(jQuery); diff --git a/assets/js/admin/jquery.flot.orderBars.js b/assets/js/admin/jquery.flot.orderBars.js new file mode 100644 index 0000000..a3f8daa --- /dev/null +++ b/assets/js/admin/jquery.flot.orderBars.js @@ -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 , 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); \ No newline at end of file diff --git a/assets/js/admin/jquery.flot.orderBars.min.js b/assets/js/admin/jquery.flot.orderBars.min.js new file mode 100644 index 0000000..a1a1c90 --- /dev/null +++ b/assets/js/admin/jquery.flot.orderBars.min.js @@ -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 , 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;et?-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=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 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 ); + } + }); + } + +}); \ No newline at end of file diff --git a/assets/js/admin/wcs-meta-boxes-order.js b/assets/js/admin/wcs-meta-boxes-order.js new file mode 100644 index 0000000..c99b87b --- /dev/null +++ b/assets/js/admin/wcs-meta-boxes-order.js @@ -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); + } + }); +}); diff --git a/changelog.txt b/changelog.txt index a4672a2..c1a8932 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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) diff --git a/includes/abstracts/abstract-wcs-retry-store.php b/includes/abstracts/abstract-wcs-retry-store.php new file mode 100644 index 0000000..05d7d88 --- /dev/null +++ b/includes/abstracts/abstract-wcs-retry-store.php @@ -0,0 +1,107 @@ +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 ); + } +} diff --git a/includes/abstracts/abstract-wcs-scheduler.php b/includes/abstracts/abstract-wcs-scheduler.php index d723484..f1f1896 100644 --- a/includes/abstracts/abstract-wcs-scheduler.php +++ b/includes/abstracts/abstract-wcs-scheduler.php @@ -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 ); } diff --git a/includes/admin/class-wc-subscriptions-admin.php b/includes/admin/class-wc-subscriptions-admin.php index 9ee7158..fd1d799 100644 --- a/includes/admin/class-wc-subscriptions-admin.php +++ b/includes/admin/class-wc-subscriptions-admin.php @@ -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 '
'; - // 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 + ?>

+ + + + + + + + + +

'_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 - ) ); + ?>

+ + + + + + + +

'_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 '
'; - echo '
'; - - // 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' ), '', '' ), - '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 ) ) : ?> - - - - - - - + + + + + + + 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 [%s]', $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' ), '', '', '', '' ); - } 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' ), '' . $available_gateways[0] . '' ); - } 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' ), '' . implode( ', ', array_slice( $available_gateways, 0, count( $available_gateways ) - 1 ) ) . '', '' . array_pop( $available_gateways ) . '' ); - } - 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' ), '', '' ), + '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' ), '', '' ), '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' ), '', '' ), + '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' ), '', '' ), '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' ), '', '' ), - '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' ), '', '', '', '' ), - 'id' => self::$option_prefix . '_payment_gateways_additional', - 'type' => 'informational', - ), - ) ); } @@ -1209,7 +1142,7 @@ class WC_Subscriptions_Admin {

- +

@@ -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' ), '', '' ); + } + + $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' ), '', '' ), + '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' ), '', '', '', '' ), + '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. * diff --git a/includes/admin/class-wcs-admin-meta-boxes.php b/includes/admin/class-wcs-admin-meta-boxes.php index 75c8afb..254a9df 100644 --- a/includes/admin/class-wcs-admin-meta-boxes.php +++ b/includes/admin/class-wcs-admin-meta-boxes.php @@ -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(); diff --git a/includes/admin/class-wcs-admin-post-types.php b/includes/admin/class-wcs-admin-post-types.php index acedc38..42bd36b 100644 --- a/includes/admin/class-wcs-admin-post-types.php +++ b/includes/admin/class-wcs-admin-post-types.php @@ -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 '

' . esc_html( $message ) . '

'; } + $_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 = esc_html( $item_name ); + $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 × %s', absint( $item_quantity ), $item_name ); } if ( $_product ) { $item_name = sprintf( '%s', get_edit_post_link( $_product->id ), $item_name ); } - ob_start(); - ?> -
- array( 'href' => array() ) ) ); ?> - - [?] - -
- '; + $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 .= ''; } break; default : @@ -517,13 +597,22 @@ class WCS_Admin_Post_Types { 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 ) { ?> - [?] - + + $item_name .= apply_filters( 'woocommerce_order_item_name', $item['name'], $item ); + $item_name = esc_html( $item_name ); + + if ( $_product ) { + $item_name = sprintf( '%s', 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 ); + } ?> 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' ), '' . date_i18n( _x( 'M j, Y @ G:i', 'used in "Subscription scheduled for "', 'woocommerce-subscriptions' ), strtotime( $post->post_date ) ) . '' ), + 9 => sprintf( __( 'Subscription scheduled for: %1$s.', 'woocommerce-subscriptions' ), '' . date_i18n( _x( 'M j, Y @ G:i', 'used in "Subscription scheduled for "', 'woocommerce-subscriptions' ), wcs_date_to_time( $post->post_date ) ) . '' ), 10 => __( 'Subscription draft updated.', 'woocommerce-subscriptions' ), ); diff --git a/includes/admin/class-wcs-admin-reports.php b/includes/admin/class-wcs-admin-reports.php new file mode 100644 index 0000000..124c20d --- /dev/null +++ b/includes/admin/class-wcs-admin-reports.php @@ -0,0 +1,171 @@ + 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(); diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php b/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php new file mode 100644 index 0000000..8755a24 --- /dev/null +++ b/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php @@ -0,0 +1,33 @@ +get_retries_for_order( $post->ID ); + + include_once( 'views/html-retries-table.php' ); + + do_action( 'woocommerce_subscriptions_retries_meta_box', $post->ID, $retries ); + } +} diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php index 6a2a505..0408f22 100644 --- a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php @@ -77,7 +77,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {

- + '; } + /** + * 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 = '' . esc_attr__( 'Subscription Relationship', 'woocommerce-subscriptions' ) . ''; + + $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 ''; + } elseif ( wcs_order_contains_subscription( $post->ID, 'resubscribe' ) ) { + echo ''; + } elseif ( wcs_order_contains_subscription( $post->ID, 'parent' ) ) { + echo ''; + } else { + echo ''; + } + } + } + /** * 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; }?> @@ -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 ); } diff --git a/includes/class-wc-subscriptions-product.php b/includes/class-wc-subscriptions-product.php index b44ab5b..a0f6301 100644 --- a/includes/class-wc-subscriptions-product.php +++ b/includes/class-wc-subscriptions-product.php @@ -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()' ); + 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 ); + } - if ( $product->is_taxable() ) { + /** + * 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 ) { + _deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::order_awaiting_payment_for_product' ); - $tax = new WC_Tax(); + global $wp; - $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 ( ! isset( self::$order_awaiting_payment_for_product[ $product_id ] ) ) { - if ( $deduct_base_taxes && wc_prices_include_tax() ) { + self::$order_awaiting_payment_for_product[ $product_id ] = false; - $base_taxes = $tax->calc_tax( $price, $base_tax_rates, true ); - $taxes = $tax->calc_tax( $price - array_sum( $base_taxes ), $tax_rates, false ); + if ( ! empty( WC()->session->order_awaiting_payment ) || isset( $_GET['pay_for_order'] ) ) { - } elseif ( get_option( 'woocommerce_prices_include_tax' ) == 'yes' ) { + $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 ) ); - $taxes = $tax->calc_tax( $price, $base_tax_rates, true ); + 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 ) { - } else { + $subscriptions = wcs_get_subscriptions( array( + 'order_id' => $order->id, + 'product_id' => $product_id, + ) ); - $taxes = $tax->calc_tax( $price, $base_tax_rates, false ); + 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; + } + } + } } - - $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; - } - - /** - * Deprecated in favour of native get_price_html() method on the Subscription Product classes (e.g. WC_Product_Subscription) - * - * 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()' ); - - // 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 ) ); - } - - return $price; + return self::$order_awaiting_payment_for_product[ $product_id ]; } } diff --git a/includes/class-wc-subscriptions-renewal-order.php b/includes/class-wc-subscriptions-renewal-order.php index 5eec309..14ed2de 100644 --- a/includes/class-wc-subscriptions-renewal-order.php +++ b/includes/class-wc-subscriptions-renewal-order.php @@ -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. * diff --git a/includes/class-wc-subscriptions-switcher.php b/includes/class-wc-subscriptions-switcher.php index 1f22d6e..e138612 100644 --- a/includes/class-wc-subscriptions-switcher.php +++ b/includes/class-wc-subscriptions-switcher.php @@ -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,80 +182,71 @@ 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 ) { + foreach ( $child_ids as $child_id ) { + $product = wc_get_product( $child_id ); - // 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; - } - } + 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() ) { - // Don't initiate auto-switching when the subscription requires payment - if ( $subscription->needs_payment() ) { + $last_order = $subscription->get_last_order( 'all' ); - $last_order = $subscription->get_last_order( 'all' ); - - if ( $last_order->needs_payment() ) { - // translators: 1$: is the "You have already subscribed to this product" notice, 2$-4$: opening/closing link tags, 3$: an order number - $subscribed_notice = sprintf( __( '%1$s Complete payment on %2$sOrder %3$s%4$s to be able to change your subscription.', 'woocommerce-subscriptions' ), $subscribed_notice, sprintf( '', $last_order->get_checkout_payment_url() ), $last_order->get_order_number(), '' ); - } - - WC_Subscriptions::add_notice( $subscribed_notice, 'notice' ); - break; - - } 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 ) { - $item_id = $line_item_id; - $item = $line_item; - break; - } - } - - if ( self::can_item_be_switched_by_user( $item, $subscription ) ) { - wp_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) ); - exit; - } + if ( $last_order->needs_payment() ) { + // translators: 1$: is the "You have already subscribed to this product" notice, 2$-4$: opening/closing link tags, 3$: an order number + $subscribed_notice = sprintf( __( '%1$s Complete payment on %2$sOrder %3$s%4$s to be able to change your subscription.', 'woocommerce-subscriptions' ), $subscribed_notice, sprintf( '', $last_order->get_checkout_payment_url() ), $last_order->get_order_number(), '' ); } - } else { WC_Subscriptions::add_notice( $subscribed_notice, 'notice' ); break; + + } else { + + // 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 ) { + $item_id = $line_item_id; + $item = $line_item; + break; + } + } + + if ( self::can_item_be_switched_by_user( $item, $subscription ) ) { + wp_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) ); + exit; + } } } } @@ -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' ), '', '' ), + 'desc' => sprintf( __( 'Allow subscribers to switch (upgrade or downgrade) between different subscriptions. %sLearn more%s.', 'woocommerce-subscriptions' ), '', '' ), '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 ) { @@ -614,7 +615,9 @@ class WC_Subscriptions_Switcher { return; } - $order = wc_get_order( $order_id ); + $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_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,40 +1970,75 @@ 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 ) ) { + foreach ( wcs_get_subscriptions_for_switch_order( $order->id ) as $subscription ) { - $order = wc_get_order( $order_id ); + if ( false === $subscription->is_manual() ) { + continue; + } - foreach ( wcs_get_subscriptions_for_switch_order( $order_id ) as $subscription ) { + if ( $subscription->payment_method !== $order->payment_method ) { - if ( false === $subscription->is_manual() ) { - continue; - } + // Set the new payment method on the subscription + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $payment_method = isset( $available_gateways[ $order->payment_method ] ) ? $available_gateways[ $order->payment_method ] : false; - if ( $subscription->payment_method !== $order->payment_method ) { - - // Set the new payment method on the subscription - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $payment_method = isset( $available_gateways[ $order->payment_method ] ) ? $available_gateways[ $order->payment_method ] : false; - - if ( $payment_method && $payment_method->supports( 'subscriptions' ) ) { - $subscription->set_payment_method( $payment_method ); - $subscription->update_manual( false ); - } + if ( $payment_method && $payment_method->supports( 'subscriptions' ) ) { + $subscription->set_payment_method( $payment_method ); + $subscription->update_manual( false ); } } } + } + + /** 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; } } diff --git a/includes/class-wc-subscriptions-synchroniser.php b/includes/class-wc-subscriptions-synchroniser.php index 52530fa..c9c3c53 100644 --- a/includes/class-wc-subscriptions-synchroniser.php +++ b/includes/class-wc-subscriptions-synchroniser.php @@ -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' ), '', '' ), + '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' ), '', '' ), '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 '

'; @@ -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 '
'; - 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', - ) - ); + ?>

+ + + - 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 - ) - ); + + + + + + +

'; echo '
'; @@ -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 ); } diff --git a/includes/class-wcs-action-scheduler.php b/includes/class-wcs-action-scheduler.php index 4674bd5..d37636f 100644 --- a/includes/class-wcs-action-scheduler.php +++ b/includes/class-wcs-action-scheduler.php @@ -12,17 +12,17 @@ class WCS_Action_Scheduler extends WCS_Scheduler { /*@protected Array of $action_hook => $date_type values */ protected $action_hooks = array( - 'woocommerce_scheduled_subscription_trial_end' => 'trial_end', - 'woocommerce_scheduled_subscription_payment' => 'next_payment', - 'woocommerce_scheduled_subscription_expiration' => 'end', - + '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 ); + } } diff --git a/includes/class-wcs-api.php b/includes/class-wcs-api.php index 5f9ee48..1eebb91 100644 --- a/includes/class-wcs-api.php +++ b/includes/class-wcs-api.php @@ -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' ); - array_push( $wc_api_classes, 'WC_API_Subscriptions' ); - array_push( $wc_api_classes, 'WC_API_Subscriptions_Customers' ); + 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(); diff --git a/includes/class-wcs-cart-initial-payment.php b/includes/class-wcs-cart-initial-payment.php index 4db7fa1..3f6b08b 100644 --- a/includes/class-wcs-cart-initial-payment.php +++ b/includes/class-wcs-cart-initial-payment.php @@ -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( diff --git a/includes/class-wcs-cart-renewal.php b/includes/class-wcs-cart-renewal.php index e88f8a6..a7a6da0 100644 --- a/includes/class-wcs-cart-renewal.php +++ b/includes/class-wcs-cart-renewal.php @@ -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 */ /** diff --git a/includes/class-wcs-cart-resubscribe.php b/includes/class-wcs-cart-resubscribe.php index e25674c..b3c960b 100644 --- a/includes/class-wcs-cart-resubscribe.php +++ b/includes/class-wcs-cart-resubscribe.php @@ -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 ) { - $this->setup_cart( $subscription, array( - 'subscription_id' => $subscription->id, - ) ); + 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(); diff --git a/includes/class-wcs-cart-switch.php b/includes/class-wcs-cart-switch.php new file mode 100644 index 0000000..eaed1d1 --- /dev/null +++ b/includes/class-wcs-cart-switch.php @@ -0,0 +1,119 @@ +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(); diff --git a/includes/class-wcs-change-payment-method-admin.php b/includes/class-wcs-change-payment-method-admin.php index ec6e1c4..7bd8e95 100644 --- a/includes/class-wcs-change-payment-method-admin.php +++ b/includes/class-wcs-change-payment-method-admin.php @@ -49,7 +49,7 @@ class WCS_Change_Payment_Method_Admin { } elseif ( count( $valid_payment_methods ) == 1 ) { echo '' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':
' . esc_html( current( $valid_payment_methods ) ); - echo ''; + 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 ''; } diff --git a/includes/class-wcs-limiter.php b/includes/class-wcs-limiter.php new file mode 100644 index 0000000..e6249bc --- /dev/null +++ b/includes/class-wcs-limiter.php @@ -0,0 +1,241 @@ +'; + echo '
'; + + // 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' ), '', '' ), + '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(); diff --git a/includes/class-wcs-query.php b/includes/class-wcs-query.php index cb6141d..ad6bd5d 100644 --- a/includes/class-wcs-query.php +++ b/includes/class-wcs-query.php @@ -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 ); } } } diff --git a/includes/class-wcs-retry-manager.php b/includes/class-wcs-retry-manager.php new file mode 100644 index 0000000..5a9d7f4 --- /dev/null +++ b/includes/class-wcs-retry-manager.php @@ -0,0 +1,321 @@ +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(); diff --git a/includes/class-wcs-webhooks.php b/includes/class-wcs-webhooks.php index eccdc01..132ce51 100644 --- a/includes/class-wcs-webhooks.php +++ b/includes/class-wcs-webhooks.php @@ -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 ); } @@ -80,9 +87,10 @@ class WCS_Webhooks { public static function add_topics_admin_menu( $topics ) { $front_end_topics = array( - 'subscription.created' => __( ' Subscription Created', 'woocommerce-subscriptions' ), - 'subscription.updated' => __( ' Subscription Updated', 'woocommerce-subscriptions' ), - 'subscription.deleted' => __( ' Subscription Deleted', 'woocommerce-subscriptions' ), + '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(); diff --git a/includes/emails/class-wcs-email-cancelled-subscription.php b/includes/emails/class-wcs-email-cancelled-subscription.php index 6edf483..283407f 100644 --- a/includes/emails/class-wcs-email-cancelled-subscription.php +++ b/includes/emails/class-wcs-email-cancelled-subscription.php @@ -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 { @@ -79,8 +79,11 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { wc_get_template( $this->template_html, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base @@ -99,8 +102,11 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { wc_get_template( $this->template_plain, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base diff --git a/includes/emails/class-wcs-email-customer-completed-renewal-order.php b/includes/emails/class-wcs-email-customer-completed-renewal-order.php index 6aa9e75..8ea9fc0 100644 --- a/includes/emails/class-wcs-email-customer-completed-renewal-order.php +++ b/includes/emails/class-wcs-email-customer-completed-renewal-order.php @@ -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 diff --git a/includes/emails/class-wcs-email-customer-completed-switch-order.php b/includes/emails/class-wcs-email-customer-completed-switch-order.php index ab083b8..3b7b95a 100644 --- a/includes/emails/class-wcs-email-customer-completed-switch-order.php +++ b/includes/emails/class-wcs-email-customer-completed-switch-order.php @@ -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 diff --git a/includes/emails/class-wcs-email-customer-payment-retry.php b/includes/emails/class-wcs-email-customer-payment-retry.php new file mode 100644 index 0000000..f98391e --- /dev/null +++ b/includes/emails/class-wcs-email-customer-payment-retry.php @@ -0,0 +1,134 @@ +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(); + } +} diff --git a/includes/emails/class-wcs-email-customer-processing-renewal-order.php b/includes/emails/class-wcs-email-customer-processing-renewal-order.php index e4829b3..e97c2db 100644 --- a/includes/emails/class-wcs-email-customer-processing-renewal-order.php +++ b/includes/emails/class-wcs-email-customer-processing-renewal-order.php @@ -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 diff --git a/includes/emails/class-wcs-email-customer-renewal-invoice.php b/includes/emails/class-wcs-email-customer-renewal-invoice.php index 4dad736..2c4cdc3 100644 --- a/includes/emails/class-wcs-email-customer-renewal-invoice.php +++ b/includes/emails/class-wcs-email-customer-renewal-invoice.php @@ -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 diff --git a/includes/emails/class-wcs-email-expired-subscription.php b/includes/emails/class-wcs-email-expired-subscription.php new file mode 100644 index 0000000..47cecfe --- /dev/null +++ b/includes/emails/class-wcs-email-expired-subscription.php @@ -0,0 +1,165 @@ +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 %s.', '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: %s.', '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: %s.', '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' ), + ), + ), + ); + } +} diff --git a/includes/emails/class-wcs-email-new-renewal-order.php b/includes/emails/class-wcs-email-new-renewal-order.php index 100ac68..2a7d2eb 100644 --- a/includes/emails/class-wcs-email-new-renewal-order.php +++ b/includes/emails/class-wcs-email-new-renewal-order.php @@ -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 diff --git a/includes/emails/class-wcs-email-new-switch-order.php b/includes/emails/class-wcs-email-new-switch-order.php index 1156d98..4e8c1a5 100644 --- a/includes/emails/class-wcs-email-new-switch-order.php +++ b/includes/emails/class-wcs-email-new-switch-order.php @@ -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 diff --git a/includes/emails/class-wcs-email-on-hold-subscription.php b/includes/emails/class-wcs-email-on-hold-subscription.php new file mode 100644 index 0000000..26bf84d --- /dev/null +++ b/includes/emails/class-wcs-email-on-hold-subscription.php @@ -0,0 +1,165 @@ +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 %s.', '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: %s.', '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: %s.', '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' ), + ), + ), + ); + } +} diff --git a/includes/emails/class-wcs-email-payment-retry.php b/includes/emails/class-wcs-email-payment-retry.php new file mode 100644 index 0000000..65bfbe7 --- /dev/null +++ b/includes/emails/class-wcs-email-payment-retry.php @@ -0,0 +1,107 @@ +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 + ); + } +} diff --git a/includes/gateways/class-wc-subscriptions-payment-gateways.php b/includes/gateways/class-wc-subscriptions-payment-gateways.php index 151fd36..e8346c6 100644 --- a/includes/gateways/class-wc-subscriptions-payment-gateways.php +++ b/includes/gateways/class-wc-subscriptions-payment-gateways.php @@ -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(); diff --git a/includes/gateways/paypal/class-wcs-paypal.php b/includes/gateways/paypal/class-wcs-paypal.php index 3bd34e4..cd5d215 100644 --- a/includes/gateways/paypal/class-wcs-paypal.php +++ b/includes/gateways/paypal/class-wcs-paypal.php @@ -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,20 +301,24 @@ class WCS_PayPal { */ public static function process_ipn_request( $transaction_details ) { - require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' ); - require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' ); + try { + require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' ); + require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' ); - if ( ! isset( $transaction_details['txn_type'] ) || ! in_array( $transaction_details['txn_type'], array_merge( self::get_ipn_handler( 'standard' )->get_transaction_types(), self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) ) { - return; - } + if ( ! isset( $transaction_details['txn_type'] ) || ! in_array( $transaction_details['txn_type'], array_merge( self::get_ipn_handler( 'standard' )->get_transaction_types(), self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) ) { + return; + } - WC_Gateway_Paypal::log( 'Subscription Transaction Type: ' . $transaction_details['txn_type'] ); - WC_Gateway_Paypal::log( 'Subscription Transaction Details: ' . print_r( $transaction_details, true ) ); + WC_Gateway_Paypal::log( 'Subscription Transaction Type: ' . $transaction_details['txn_type'] ); + WC_Gateway_Paypal::log( 'Subscription Transaction Details: ' . print_r( $transaction_details, true ) ); - if ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'standard' )->get_transaction_types() ) ) { - self::get_ipn_handler( 'standard' )->valid_response( $transaction_details ); - } elseif ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) { - self::get_ipn_handler( 'reference' )->valid_response( $transaction_details ); + if ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'standard' )->get_transaction_types() ) ) { + self::get_ipn_handler( 'standard' )->valid_response( $transaction_details ); + } 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 ); } } diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php index 02aac90..fffbb12 100644 --- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php +++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php @@ -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' ), - '', + '', '', '', '' @@ -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' ), '', '', - '', + '', '', '

', '', - '', + '', '»' ), ); @@ -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' ), '', '', - '', + '', '' ), ); @@ -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' ), - '', + '', '', '', '' ), ); } + + $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' ), + '

', + '', + '', + '
', + '', + '', + '

', + '' . esc_html( $last_ipn_error ) . '' + ), + ); + } } 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 ); + } } /** diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php index a08baeb..e487626 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php @@ -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 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php index 86c594c..75b24ab 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php @@ -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 * diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php index b176b01..63f0f49 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php @@ -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; diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php index d301460..3d61926 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php @@ -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 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php new file mode 100644 index 0000000..c446e3e --- /dev/null +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php @@ -0,0 +1,140 @@ +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 ); + } +} diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php index ad62cd4..f00348a 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php @@ -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 ); + } + } } diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php b/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php index fd6f7dd..87100aa 100644 --- a/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php @@ -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; diff --git a/includes/libraries/action-scheduler/action-scheduler.php b/includes/libraries/action-scheduler/action-scheduler.php index be35b6a..41b60a0 100644 --- a/includes/libraries/action-scheduler/action-scheduler.php +++ b/includes/libraries/action-scheduler/action-scheduler.php @@ -1,29 +1,29 @@ 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() { - require_once('classes/ActionScheduler.php'); + function action_scheduler_initialize_1_dot_5() { + require_once( 'classes/ActionScheduler.php' ); ActionScheduler::init( __FILE__ ); } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php index 6e4af2d..cfcc9eb 100644 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php @@ -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); } @@ -41,4 +41,4 @@ class ActionScheduler_CronSchedule implements ActionScheduler_Schedule { $this->start = as_get_datetime_object($this->start_timestamp); } } - \ No newline at end of file + diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php index aaefa2e..6f34371 100644 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php @@ -126,11 +126,24 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { global $wpdb; if ( 0 === $post_id ) { + $stats = $this->get_comment_count(); + } - $count = wp_cache_get( 'comments-0', 'counts' ); - if ( false !== $count ) { - return $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 ); @@ -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() { diff --git a/includes/payment-retry/class-wcs-retry-admin.php b/includes/payment-retry/class-wcs-retry-admin.php new file mode 100644 index 0000000..656ebb6 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-admin.php @@ -0,0 +1,144 @@ +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 .= '
'; + } + + echo '
'; + } + } + } + + /** + * 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' ), '', '' ), + ), + ) ); + + return $settings; + } +} diff --git a/includes/payment-retry/class-wcs-retry-email.php b/includes/payment-retry/class-wcs-retry-email.php new file mode 100644 index 0000000..29b975f --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-email.php @@ -0,0 +1,118 @@ +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(); diff --git a/includes/payment-retry/class-wcs-retry-post-store.php b/includes/payment-retry/class-wcs-retry-post-store.php new file mode 100644 index 0000000..ff1afcc --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-post-store.php @@ -0,0 +1,160 @@ + __( '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; + } +} diff --git a/includes/payment-retry/class-wcs-retry-rule.php b/includes/payment-retry/class-wcs-retry-rule.php new file mode 100644 index 0000000..bbcb095 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-rule.php @@ -0,0 +1,95 @@ + $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; + } +} diff --git a/includes/payment-retry/class-wcs-retry-rules.php b/includes/payment-retry/class-wcs-retry-rules.php new file mode 100644 index 0000000..5e13093 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-rules.php @@ -0,0 +1,112 @@ +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; + } +} diff --git a/includes/payment-retry/class-wcs-retry.php b/includes/payment-retry/class-wcs-retry.php new file mode 100644 index 0000000..35a5ee2 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry.php @@ -0,0 +1,155 @@ +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; + } +} diff --git a/includes/upgrades/class-wc-subscriptions-upgrader.php b/includes/upgrades/class-wc-subscriptions-upgrader.php index 6bca8f4..648bd2b 100644 --- a/includes/upgrades/class-wc-subscriptions-upgrader.php +++ b/includes/upgrades/class-wc-subscriptions-upgrader.php @@ -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.
Error: %1$s
Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ), + 'message' => sprintf( __( 'Unable to upgrade subscriptions.
Error: %1$s
Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ), '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.
Error: %1$s
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' ), '' . $e->getMessage(). '', '', '' ), + 'message' => sprintf( _x( 'Unable to repair subscriptions.
Error: %1$s
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' ), '' . $e->getMessage(). '', '', '' ), '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' ); } diff --git a/includes/upgrades/class-wcs-repair-2-0-2.php b/includes/upgrades/class-wcs-repair-2-0-2.php index 6b66cf2..de34cfc 100644 --- a/includes/upgrades/class-wcs-repair-2-0-2.php +++ b/includes/upgrades/class-wcs-repair-2-0-2.php @@ -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; diff --git a/includes/upgrades/class-wcs-repair-2-0.php b/includes/upgrades/class-wcs-repair-2-0.php index d792baf..4d6b4f6 100644 --- a/includes/upgrades/class-wcs-repair-2-0.php +++ b/includes/upgrades/class-wcs-repair-2-0.php @@ -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'] ) ) { diff --git a/includes/upgrades/class-wcs-upgrade-2-0.php b/includes/upgrades/class-wcs-upgrade-2-0.php index 25a6ae4..32bb2dc 100644 --- a/includes/upgrades/class-wcs-upgrade-2-0.php +++ b/includes/upgrades/class-wcs-upgrade-2-0.php @@ -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 ); } } } diff --git a/includes/upgrades/class-wcs-upgrade-2-1.php b/includes/upgrades/class-wcs-upgrade-2-1.php new file mode 100644 index 0000000..2b6fcdb --- /dev/null +++ b/includes/upgrades/class-wcs-upgrade-2-1.php @@ -0,0 +1,54 @@ +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 ) ); + } + } +} diff --git a/includes/upgrades/class-wcs-upgrade-logger.php b/includes/upgrades/class-wcs-upgrade-logger.php index e762324..1ef1de5 100644 --- a/includes/upgrades/class-wcs-upgrade-logger.php +++ b/includes/upgrades/class-wcs-upgrade-logger.php @@ -57,7 +57,7 @@ class WCS_Upgrade_Logger { */ public static function schedule_cleanup() { $time_to_cleanup = gmdate( 'U' ) + self::$weeks_until_cleanup * WEEK_IN_SECONDS; - self::add( sprintf( 'Upgrade complete. Scheduling log cleanup for %s GMT/UTC', date( 'Y-m-d H:i:s', $time_to_cleanup ) ) ); + self::add( sprintf( 'Upgrade complete. Scheduling log cleanup for %s GMT/UTC', gmdate( 'Y-m-d H:i:s', $time_to_cleanup ) ) ); wc_schedule_single_action( $time_to_cleanup, 'woocommerce_subscriptions_clear_upgrade_log' ); } } diff --git a/includes/upgrades/templates/wcs-about-2-0.php b/includes/upgrades/templates/wcs-about-2-0.php new file mode 100644 index 0000000..f59daff --- /dev/null +++ b/includes/upgrades/templates/wcs-about-2-0.php @@ -0,0 +1,196 @@ + + +
+ +

+ +
+ + + +
+ +
+ +
+ +

+ + + + +

+ +
+

+
+ +
+
+ +
+ +
+

+

+

+

', '' ); ?> +

+
+
+ +
+ +
+ +
+ +
+

+

+

tags + printf( esc_html__( 'The new interface is also built on the existing %sEdit Order%s screen. If you\'ve ever modified an order, you already know how to modify a subscription.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

', '', '', '' ); ?> +

+
+
+ +
+
+ +
+ +
+

+

+ tags + printf( esc_html__( 'Your customers can now view the full details of a subscription, including line items, billing and shipping address, billing schedule and renewal orders, from a special %sMy Account > View Subscription%s page.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

+

+ ', '' ); ?> +

+
+
+
+
+ +
+ +
+ +

+

) tags + printf( esc_html__( 'By default, adding new files to an existing subscription product will automatically provide active subscribers with access to the new files. However, now you can enable a %snew content dripping setting%s to provide subscribers with access to new files only after the next renewal payment.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

) tags + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> +

+
+ +
+ +

+

tags + printf( esc_html__( 'For a store manager to change a subscription from automatic to manual renewal payments (or manual to automatic) with Subscriptions v1.5, the database needed to be modified directly. Subscriptions now provides a way for payment gateways to allow you to change that from the new %sEdit Subscription%s interface.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

) tags + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> +

+
+ +
+ +

+

tags + printf( esc_html__( 'It was already possible to change a subscription\'s next payment date, but some store managers wanted to provide a customer with an extended free trial or add an extra month to the expiration date. Now you can change all of these dates from the %sEdit Subscription%s screen.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

) tags + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> +

+
+ +
+
+ +
+
+

+

', '' ); ?>

+
+
+ +
+ +

+

+ +
+
+

', '' ); ?> +

+

+

tags + printf( esc_html__( 'Developers can also now use all the familiar WordPress functions, like %sget_posts()%s, to query or modify subscription data.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+
+
+

tags + printf( esc_html__( 'New %sWC_Subscription%s Object', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

+

tags, no need to order them + printf( esc_html__( 'Because the %sWC_Subscription%s class extends %sWC_Order%s, you can use its familiar methods, like %s$subscription->update_status()%s or %s$subscription->get_total()%s.', 'woocommerce-subscriptions' ), '', '', '', '', '', '', '', '' ); ?> +

+
+
+

+

+

tags, no need to order them + printf( esc_html__( 'Want to list all the subscriptions on a site? Get %sexample.com/wc-api/v2/subscriptions/%s. Want the details of a specific subscription? Get %s/wc-api/v2/subscriptions//%s.', 'woocommerce-subscriptions' ), '', '', '', '' ); ?> +

+
+
+
+
+ +
+
diff --git a/includes/upgrades/templates/wcs-about.php b/includes/upgrades/templates/wcs-about.php index 172547e..45da673 100644 --- a/includes/upgrades/templates/wcs-about.php +++ b/includes/upgrades/templates/wcs-about.php @@ -1,27 +1,26 @@
-

+

- + ', '' ); ?>
@@ -33,27 +32,27 @@ $settings_page = admin_url( 'admin.php?page=wc-settings&tab=subscriptions' );

- - + +

-
- +
-

-

-

-

', '' ); ?> +

+

+

+

+

+ +

@@ -61,136 +60,156 @@ $settings_page = admin_url( 'admin.php?page=wc-settings&tab=subscriptions' );
- +
-

-

-

tags - printf( esc_html__( 'The new interface is also built on the existing %sEdit Order%s screen. If you\'ve ever modified an order, you already know how to modify a subscription.', 'woocommerce-subscriptions' ), '', '' ); ?> -

-

', '', '', '' ); ?> +

+

+

+
    +
  • +
  • +
  • +
  • +
+

+

+ +

- +
-

-

- tags - printf( esc_html__( 'Your customers can now view the full details of a subscription, including line items, billing and shipping address, billing schedule and renewal orders, from a special %sMy Account > View Subscription%s page.', 'woocommerce-subscriptions' ), '', '' ); ?> -

-

-

- ', '' ); ?> +

+

+
    +
  • +
  • +
  • +
+

Settings > Emails%s administration screen.', 'woocommerce-subscriptions' ), '', '' ); ?>

+

+ +

-
+ +
+ +

+

- -

+ +

+

+

) tags - printf( esc_html__( 'By default, adding new files to an existing subscription product will automatically provide active subscribers with access to the new files. However, now you can enable a %snew content dripping setting%s to provide subscribers with access to new files only after the next renewal payment.', 'woocommerce-subscriptions' ), '', '' ); ?> -

-

) tags - printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?>

- -

-

tags - printf( esc_html__( 'For a store manager to change a subscription from automatic to manual renewal payments (or manual to automatic) with Subscriptions v1.5, the database needed to be modified directly. Subscriptions now provides a way for payment gateways to allow you to change that from the new %sEdit Subscription%s interface.', 'woocommerce-subscriptions' ), '', '' ); ?> -

+ +

+

+

) tags - printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?>

- -

-

tags - printf( esc_html__( 'It was already possible to change a subscription\'s next payment date, but some store managers wanted to provide a customer with an extended free trial or add an extra month to the expiration date. Now you can change all of these dates from the %sEdit Subscription%s screen.', 'woocommerce-subscriptions' ), '', '' ); ?> -

+ +

+

+

) tags - printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?> + printf( esc_html__( '%sLearn more »%s', 'woocommerce-subscriptions' ), '', '' ); ?>

-
-
-

-

', '' ); ?>

-
-
-

-

', '' ); ?> -

-

-

tags - printf( esc_html__( 'Developers can also now use all the familiar WordPress functions, like %sget_posts()%s, to query or modify subscription data.', 'woocommerce-subscriptions' ), '', '' ); ?> + printf( esc_html__( 'Customise Retry Rules', 'woocommerce-subscriptions' ), '', '' ); ?> + +

+

tags, no need to order them + printf( esc_html__( 'With the %s\'wcs_default_retry_rules\'%s filter, you can define a set of default rules to apply to all failed payments in your store.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

tags, no need to order them + printf( esc_html__( 'To apply a specific rule based on certain conditions, like high value orders or an infrequent renewal schedule, you can use the retry specific %s\'wcs_get_retry_rule\'%s filter. This provides the ID of the renewal order for the failed payment, which can be used to find information about the products, subscription and totals to which the failed payment relates.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

', '' ); ?>

-

tags - printf( esc_html__( 'New %sWC_Subscription%s Object', 'woocommerce-subscriptions' ), '', '' ); ?> -

-

+

+

tag linking to WC API docs, $2: closing tag, $3: opening tag linking to WP API docs, $4: closing tag + printf( esc_html__( 'WooCommerce 2.6 added support for %1$sREST API%2$s endpoints built on WordPress core\'s %3$sREST API%4$s infrastructure.', 'woocommerce-subscriptions' ), '', '', '', '' ); ?> +

+

+

tags, no need to order them - printf( esc_html__( 'Because the %sWC_Subscription%s class extends %sWC_Order%s, you can use its familiar methods, like %s$subscription->update_status()%s or %s$subscription->get_total()%s.', 'woocommerce-subscriptions' ), '', '', '', '', '', '', '', '' ); ?> + printf( esc_html__( 'Want to list all the subscriptions on a site? Get %s/wp-json/wc/v1/subscriptions%s.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

tags, no need to order them + printf( esc_html__( 'Want the details of a specific subscription? Get %s/wp-json/wc/v1/subscriptions//%s.', 'woocommerce-subscriptions' ), '', '' ); ?> +

+

', '' ); ?>

-

-

+

', '' ); ?> +

+

tags, no need to order them - printf( esc_html__( 'Want to list all the subscriptions on a site? Get %sexample.com/wc-api/v2/subscriptions/%s. Want the details of a specific subscription? Get %s/wc-api/v2/subscriptions//%s.', 'woocommerce-subscriptions' ), '', '', '', '' ); ?> + // translators: placeholders are opening and closing tags + printf( esc_html__( 'Subscriptions 2.1 now passes the renewal order\'s total, making it possible to add a fee or discount to the renewal order with simple one-liners like %s$order->add_fee()%s or %s$order->add_coupon()%s.', 'woocommerce-subscriptions' ), '', '', '', '' ); ?> +

+

tags + printf( esc_html__( 'Subscriptions also now uses the renewal order to setup the cart for %smanual renewals%s, making it easier to add products or discounts to a single renewal paid manually.', 'woocommerce-subscriptions' ), '', '' ); ?>

-
+
- +

+

diff --git a/includes/wcs-cart-functions.php b/includes/wcs-cart-functions.php index 5f4880a..d530857 100644 --- a/includes/wcs-cart-functions.php +++ b/includes/wcs-cart-functions.php @@ -65,6 +65,8 @@ function wcs_cart_totals_shipping_html() { $chosen_initial_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : ''; $chosen_recurring_method = isset( WC()->session->chosen_shipping_methods[ $recurring_cart_key . '_' . $i ] ) ? WC()->session->chosen_shipping_methods[ $recurring_cart_key . '_' . $i ] : $chosen_initial_method; + $shipping_selection_displayed = false; + if ( ( 1 === count( $package['rates'] ) ) || ( isset( $package['rates'][ $chosen_initial_method ] ) && isset( $initial_packages[ $i ] ) && $package['rates'] == $initial_packages[ $i ]['rates'] && apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart ) ) ) { $shipping_method = ( 1 === count( $package['rates'] ) ) ? current( $package['rates'] ) : $package['rates'][ $chosen_initial_method ]; // packages match, display shipping amounts only @@ -87,6 +89,8 @@ function wcs_cart_totals_shipping_html() { // Display the options $product_names = array(); + $shipping_selection_displayed = true; + if ( $show_package_name ) { $package_name = apply_filters( 'woocommerce_shipping_package_name', sprintf( _n( 'Shipping', 'Shipping %d', ( $i + 1 ), 'woocommerce-subscriptions' ), ( $i + 1 ) ), $i, $package ); } else { @@ -109,7 +113,7 @@ function wcs_cart_totals_shipping_html() { ); $show_package_name = false; } - do_action( 'woocommerce_subscriptions_after_recurring_shipping_rates', $index, $base_package, $recurring_cart, $chosen_recurring_method ); + do_action( 'woocommerce_subscriptions_after_recurring_shipping_rates', $index, $base_package, $recurring_cart, $chosen_recurring_method, $shipping_selection_displayed ); } } } @@ -318,7 +322,7 @@ function wcs_cart_pluck( $cart, $field, $default = 0 ) { function wcs_add_cart_first_renewal_payment_date( $order_total_html, $cart ) { if ( 0 !== $cart->next_payment_date ) { - $first_renewal_date = date_i18n( wc_date_format(), strtotime( get_date_from_gmt( $cart->next_payment_date ) ) ); + $first_renewal_date = date_i18n( wc_date_format(), wcs_date_to_time( get_date_from_gmt( $cart->next_payment_date ) ) ); // translators: placeholder is a date $order_total_html .= '
' . sprintf( __( 'First renewal: %s', 'woocommerce-subscriptions' ), $first_renewal_date ) . '
'; } diff --git a/includes/wcs-compatibility-functions.php b/includes/wcs-compatibility-functions.php new file mode 100644 index 0000000..fad0da1 --- /dev/null +++ b/includes/wcs-compatibility-functions.php @@ -0,0 +1,45 @@ +', $tip, esc_url( WC()->plugin_url() ) ); + } + + return $help_tip; +} diff --git a/includes/wcs-formatting-functions.php b/includes/wcs-formatting-functions.php index 0b82514..324500f 100644 --- a/includes/wcs-formatting-functions.php +++ b/includes/wcs-formatting-functions.php @@ -211,3 +211,28 @@ function wcs_price_string( $subscription_details ) { return apply_filters( 'woocommerce_subscription_price_string', $subscription_string, $subscription_details ); } + +/** + * Display a human friendly time diff for a given timestamp, e.g. "In 12 hours" or "12 hours ago". + * + * @param int $timestamp_gmt + * @return string A human friendly string to display for the timestamp's date + * @since 2.1 + */ +function wcs_get_human_time_diff( $timestamp_gmt ) { + + $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 = wcs_date_to_time( get_date_from_gmt( gmdate( '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 ); + } + + return $date_to_display; +} diff --git a/includes/wcs-helper-functions.php b/includes/wcs-helper-functions.php index 8d4d347..2331b27 100644 --- a/includes/wcs-helper-functions.php +++ b/includes/wcs-helper-functions.php @@ -116,7 +116,7 @@ function wcs_json_encode( $data ) { * @param $new_value An value to insert * @return The new array if the $needle key exists, otherwise an unmodified $haystack */ -function wcs_array_insert_after( $needle, $haystack, $new_key, $new_value) { +function wcs_array_insert_after( $needle, $haystack, $new_key, $new_value ) { if ( array_key_exists( $needle, $haystack ) ) { diff --git a/includes/wcs-limit-functions.php b/includes/wcs-limit-functions.php new file mode 100644 index 0000000..5e3956b --- /dev/null +++ b/includes/wcs-limit-functions.php @@ -0,0 +1,47 @@ +product_custom_fields['_subscription_limit'][0] ) ) { + return 'no'; + } elseif ( 'yes' == $product->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility + return 'any'; + } else { + return $product->product_custom_fields['_subscription_limit'][0]; + } +} + +/** + * Returns true if product is limited to one active subscription and user currently has this product on-hold. + * + * @param int|WC_Product $product A WC_Product object or the ID of a product + * @return boolean + */ +function wcs_is_product_limited_for_user( $product, $user_id = 0 ) { + if ( ! is_object( $product ) ) { + $product = wc_get_product( $product ); + } + + return ( ( 'active' == wcs_get_product_limitation( $product ) && wcs_user_has_subscription( $user_id, $product->id, 'on-hold' ) ) || ( 'no' !== wcs_get_product_limitation( $product ) && wcs_user_has_subscription( $user_id, $product->id, wcs_get_product_limitation( $product ) ) ) ) ? true : false; +} diff --git a/includes/wcs-order-functions.php b/includes/wcs-order-functions.php index c3e1e8f..c5b4461 100644 --- a/includes/wcs-order-functions.php +++ b/includes/wcs-order-functions.php @@ -236,12 +236,43 @@ function wcs_create_order_from_subscription( $subscription, $type ) { wc_add_order_item_meta( $recurring_item_id, $meta_key, maybe_unserialize( $meta_value ) ); } } + + // If the line item we're adding is a product line item and that product still exists, trigger the 'woocommerce_order_add_product' hook + if ( 'line_item' == $item['type'] && isset( $item['product_id'] ) ) { + + $product_id = wcs_get_canonical_product_id( $item ); + $product = wc_get_product( $product_id ); + + if ( false !== $product ) { + + $args = array( + 'totals' => array( + 'subtotal' => $item['line_subtotal'], + 'total' => $item['line_total'], + 'subtotal_tax' => $item['line_subtotal_tax'], + 'tax' => $item['line_tax'], + 'tax_data' => maybe_unserialize( $item['line_tax_data'] ), + ), + ); + + // If we have a variation, get the attribute meta data from teh item to pass to callbacks + if ( ! empty( $item['variation_id'] ) && ! empty( $product->variation_data ) ) { + foreach ( $product->variation_data as $attribute => $variation ) { + if ( isset( $item[ str_replace( 'attribute_', '', $attribute ) ] ) ) { + $args['variation'][ $attribute ] = $item[ str_replace( 'attribute_', '', $attribute ) ]; + } + } + } + + do_action( 'woocommerce_order_add_product', $new_order->id, $recurring_item_id, $product, $item['qty'], $args ); + } + } } // If we got here, the subscription was created without problems $wpdb->query( 'COMMIT' ); - return apply_filters( 'wcs_new_order_created', $new_order, $subscription ); + return apply_filters( 'wcs_new_order_created', $new_order, $subscription, $type ); } catch ( Exception $e ) { // There was an error adding the subscription @@ -378,6 +409,91 @@ function wcs_order_contains_subscription( $order, $order_type = array( 'parent', return $contains_subscription; } +/** + * Get all the orders that relate to a subscription in some form (rather than only the orders associated with + * a specific subscription). + * + * @param string $return_fields The columns to return, either 'all' or 'ids' + * @param array|string $order_type Can include 'any', 'parent', 'renewal', 'resubscribe' and/or 'switch'. Defaults to 'parent'. + * @return array The orders that relate to a subscription, if any. Will contain either as just IDs or WC_Order objects depending on $return_fields value. + * @since 2.1 + */ +function wcs_get_subscription_orders( $return_fields = 'ids', $order_type = 'parent' ) { + global $wpdb; + + // Accept either an array or string (to make it more convenient for singular types, like 'parent' or 'any') + if ( ! is_array( $order_type ) ) { + $order_type = array( $order_type ); + } + + $any_order_type = in_array( 'any', $order_type ) ? true : false; + $return_fields = ( 'ids' == $return_fields ) ? $return_fields : 'all'; + + $orders = array(); + $order_ids = array(); + + if ( $any_order_type || in_array( 'parent', $order_type ) ) { + $order_ids = array_merge( $order_ids, $wpdb->get_col( + "SELECT DISTINCT post_parent FROM {$wpdb->posts} + WHERE post_type = 'shop_subscription' + AND post_parent <> 0" + ) ); + } + + if ( $any_order_type || in_array( 'renewal', $order_type ) || in_array( 'resubscribe', $order_type ) || in_array( 'switch', $order_type ) ) { + + $meta_query = array( + 'relation' => 'OR', + ); + + if ( $any_order_type || in_array( 'renewal', $order_type ) ) { + $meta_query[] = array( + 'key' => '_subscription_renewal', + 'compare' => 'EXISTS', + ); + } + + if ( $any_order_type || in_array( 'switch', $order_type ) ) { + $meta_query[] = array( + 'key' => '_subscription_switch', + 'compare' => 'EXISTS', + ); + } + + // $any_order_type handled by 'parent' query above as all resubscribe orders are all parent orders + if ( in_array( 'resubscribe', $order_type ) && ! in_array( 'parent', $order_type ) ) { + $meta_query[] = array( + 'key' => '_subscription_resubscribe', + 'compare' => 'EXISTS', + ); + } + + if ( count( $meta_query ) > 1 ) { + $order_ids = array_merge( $order_ids, get_posts( array( + 'posts_per_page' => -1, + 'post_type' => 'shop_order', + 'post_status' => 'any', + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'DESC', + 'meta_query' => $meta_query, + ) ) ); + } + } + + if ( 'all' == $return_fields ) { + foreach ( $order_ids as $order_id ) { + $orders[ $order_id ] = wc_get_order( $order_id ); + } + } else { + foreach ( $order_ids as $order_id ) { + $orders[ $order_id ] = $order_id; + } + } + + return apply_filters( 'wcs_get_subscription_orders', $orders, $return_fields, $order_type ); +} + /** * A wrapper for getting a specific item from a subscription. * diff --git a/includes/wcs-renewal-functions.php b/includes/wcs-renewal-functions.php index 3ed3236..bd33ab4 100644 --- a/includes/wcs-renewal-functions.php +++ b/includes/wcs-renewal-functions.php @@ -115,12 +115,16 @@ function wcs_get_subscriptions_for_renewal_order( $order ) { $order = wc_get_order( $order ); } - $subscriptions = array(); - $subscription_ids = get_post_meta( $order->id, '_subscription_renewal', false ); + $subscriptions = array(); - foreach ( $subscription_ids as $subscription_id ) { - if ( wcs_is_subscription( $subscription_id ) ) { - $subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id ); + // Only use the order if we actually found a valid order object + if ( is_object( $order ) ) { + $subscription_ids = get_post_meta( $order->id, '_subscription_renewal', false ); + + foreach ( $subscription_ids as $subscription_id ) { + if ( wcs_is_subscription( $subscription_id ) ) { + $subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id ); + } } } diff --git a/includes/wcs-resubscribe-functions.php b/includes/wcs-resubscribe-functions.php index f4e00d7..d8d5ad4 100644 --- a/includes/wcs-resubscribe-functions.php +++ b/includes/wcs-resubscribe-functions.php @@ -191,7 +191,7 @@ function wcs_can_user_resubscribe_to( $subscription, $user_id = '' ) { $can_user_resubscribe = false; - } elseif ( ! $subscription->has_status( array( 'cancelled', 'expired', 'trash' ) ) ) { + } elseif ( ! $subscription->has_status( array( 'pending-cancel', 'cancelled', 'expired', 'trash' ) ) ) { $can_user_resubscribe = false; @@ -229,7 +229,7 @@ function wcs_can_user_resubscribe_to( $subscription, $user_id = '' ) { break; } - if ( 'active' == $product->limit_subscriptions && ( wcs_user_has_subscription( $user_id, $product->id, 'on-hold' ) || wcs_user_has_subscription( $user_id, $product->id, 'active' ) ) ) { + if ( 'active' == wcs_get_product_limitation( $product ) && ( wcs_user_has_subscription( $user_id, $product->id, 'on-hold' ) || wcs_user_has_subscription( $user_id, $product->id, 'active' ) ) ) { $has_active_limited_subscription = true; break; } diff --git a/includes/wcs-time-functions.php b/includes/wcs-time-functions.php index 2a8fb6e..4c42891 100644 --- a/includes/wcs-time-functions.php +++ b/includes/wcs-time-functions.php @@ -78,7 +78,7 @@ function wcs_get_subscription_ranges_tlc() { foreach ( array( 'day', 'week', 'month', 'year' ) as $period ) { $subscription_lengths = array( - _x( 'all time', 'Subscription length (eg "$10 per month for _all time_")', 'woocommerce-subscriptions' ), + _x( 'Never expire', 'Subscription length', 'woocommerce-subscriptions' ), ); switch ( $period ) { @@ -214,10 +214,14 @@ function wcs_get_subscription_trial_lengths( $subscription_period = '' ) { */ function wcs_add_time( $number_of_periods, $period, $from_timestamp ) { - if ( 'month' == $period ) { - $next_timestamp = wcs_add_months( $from_timestamp, $number_of_periods ); + if ( $number_of_periods > 0 ) { + if ( 'month' == $period ) { + $next_timestamp = wcs_add_months( $from_timestamp, $number_of_periods ); + } else { + $next_timestamp = wcs_strtotime_dark_knight( "+ {$number_of_periods} {$period}", $from_timestamp ); + } } else { - $next_timestamp = strtotime( "+ {$number_of_periods} {$period}", $from_timestamp ); + $next_timestamp = $from_timestamp; } return $next_timestamp; @@ -239,17 +243,17 @@ function wcs_add_time( $number_of_periods, $period, $from_timestamp ) { */ function wcs_add_months( $from_timestamp, $months_to_add ) { - $first_day_of_month = date( 'Y-m', $from_timestamp ) . '-1'; - $days_in_next_month = date( 't', strtotime( "+ {$months_to_add} month", strtotime( $first_day_of_month ) ) ); + $first_day_of_month = gmdate( 'Y-m', $from_timestamp ) . '-1'; + $days_in_next_month = gmdate( 't', wcs_strtotime_dark_knight( "+ {$months_to_add} month", wcs_date_to_time( $first_day_of_month ) ) ); // Payment is on the last day of the month OR number of days in next billing month is less than the the day of this month (i.e. current billing date is 30th January, next billing date can't be 30th February) - if ( date( 'd m Y', $from_timestamp ) === date( 't m Y', $from_timestamp ) || date( 'd', $from_timestamp ) > $days_in_next_month ) { + if ( gmdate( 'd m Y', $from_timestamp ) === gmdate( 't m Y', $from_timestamp ) || gmdate( 'd', $from_timestamp ) > $days_in_next_month ) { for ( $i = 1; $i <= $months_to_add; $i++ ) { - $next_month = strtotime( '+ 3 days', $from_timestamp ); // Add 3 days to make sure we get to the next month, even when it's the 29th day of a month with 31 days - $next_timestamp = $from_timestamp = strtotime( date( 'Y-m-t H:i:s', $next_month ) ); // NB the "t" to get last day of next month + $next_month = wcs_add_time( 3, 'days', $from_timestamp ); // Add 3 days to make sure we get to the next month, even when it's the 29th day of a month with 31 days + $next_timestamp = $from_timestamp = wcs_date_to_time( gmdate( 'Y-m-t H:i:s', $next_month ) ); // NB the "t" to get last day of next month } } else { // Safe to just add a month - $next_timestamp = strtotime( "+ {$months_to_add} month", $from_timestamp ); + $next_timestamp = wcs_strtotime_dark_knight( "+ {$months_to_add} month", $from_timestamp ); } return $next_timestamp; @@ -389,13 +393,13 @@ function wcs_estimate_period_between( $last_date, $second_date, $interval = 1 ) $interval = 1; } - $last_timestamp = strtotime( $last_date ); - $second_timestamp = strtotime( $second_date ); + $last_timestamp = wcs_date_to_time( $last_date ); + $second_timestamp = wcs_date_to_time( $second_date ); $earlier_timestamp = min( $last_timestamp, $second_timestamp ); $later_timestamp = max( $last_timestamp, $second_timestamp ); - $days_in_month = date( 't', $earlier_timestamp ); + $days_in_month = gmdate( 't', $earlier_timestamp ); $difference = absint( $last_timestamp - $second_timestamp ); $period_in_seconds = round( $difference / $interval ); $possible_periods = array(); @@ -609,13 +613,69 @@ function wcs_is_datetime_mysql_format( $time ) { $match = preg_match( '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $time ); // parses time, returns false for invalid dates - $valid_time = strtotime( $time ); + $valid_time = wcs_date_to_time( $time ); } // magic number -2209078800 is strtotime( '1900-01-00 00:00:00' ). Needed to achieve parity with strptime return ( $match && false !== $valid_time && -2209078800 <= $valid_time ) ? true : false; } +/** + * Convert a date string into a timestamp without ever adding or deducting time. + * + * strtotime() would be handy for this purpose, but alas, if other code running on the server + * is calling date_default_timezone_set() to change the timezone, strtotime() will assume the + * date is in that timezone unless the timezone is specific on the string (which it isn't for + * any MySQL formatted date) and attempt to convert it to UTC time by adding or deducting the + * GMT/UTC offset for that timezone, so for example, when 3rd party code has set the servers + * timezone using date_default_timezone_set( 'America/Los_Angeles' ) doing something like + * gmdate( "Y-m-d H:i:s", strtotime( gmdate( "Y-m-d H:i:s" ) ) ) will actually add 7 hours to + * the date even though it is a date in UTC timezone because the timezone wasn't specificed. + * + * This makes sure the date is never converted. + * + * @param string $date_string A date string formatted in MySQl or similar format that will map correctly when instantiating an instance of DateTime() + * @return int Unix timestamp representation of the timestamp passed in without any changes for timezones + */ +function wcs_date_to_time( $date_string ) { + + if ( 0 == $date_string ) { + return 0; + } + + $date_obj = new DateTime( $date_string, new DateTimeZone( 'UTC' ) ); + + return $date_obj->format( 'U' ); +} + +/** + * A wrapper for strtotime() designed to stand up against those who want to watch the WordPress burn. + * + * One day WordPress will require Harvey Dent (aka PHP 5.3) then we can use DateTime::add() instead, + * but for now, this ensures when using strtotime() to add time to a timestamp, there are no additional + * changes for server specific timezone additions or deductions. + * + * @param string $time_string A string representation of a date in any format that can be parsed by strtotime() + * @return int Unix timestamp representation of the timestamp passed in without any changes for timezones + */ +function wcs_strtotime_dark_knight( $time_string, $from_timestamp = null ) { + + $original_timezone = date_default_timezone_get(); + + // this should be UTC anyway as WordPress sets it to that, but some plugins and l33t h4xors just want to watch the world burn and set it to something else + date_default_timezone_set( 'UTC' ); + + if ( null === $from_timestamp ) { + $next_timestamp = strtotime( $time_string ); + } else { + $next_timestamp = strtotime( $time_string, $from_timestamp ); + } + + date_default_timezone_set( $original_timezone ); + + return $next_timestamp; +} + /** * Find the average number of days for a given billing period and interval. * @@ -642,3 +702,59 @@ function wcs_get_days_in_cycle( $period, $interval ) { return apply_filters( 'wcs_get_days_in_cycle', $days_in_cycle, $period, $interval ); } + +/** + * Get an instance of the site's timezone. + * + * @return DateTimeZone Timezone object for the timezone the site is using. + */ +function wcs_get_sites_timezone() { + + if ( class_exists( 'ActionScheduler_TimezoneHelper' ) ) { + + // Use Action Scheduler's version when possible as it caches the data + $local_timezone = ActionScheduler_TimezoneHelper::get_local_timezone(); + + } else { + + $tzstring = get_option( 'timezone_string' ); + + if ( empty( $tzstring ) ) { + + $gmt_offset = get_option( 'gmt_offset' ); + + if ( 0 == $gmt_offset ) { + + $tzstring = 'UTC'; + + } else { + + $gmt_offset *= HOUR_IN_SECONDS; + $tzstring = timezone_name_from_abbr( '', $gmt_offset ); + + if ( false === $tzstring ) { + + $is_dst = date( 'I' ); + + foreach ( timezone_abbreviations_list() as $abbr ) { + + foreach ( $abbr as $city ) { + if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { + $tzstring = $city['timezone_id']; + break 2; + } + } + } + } + + if ( false === $tzstring ) { + $tzstring = 'UTC'; + } + } + } + + $local_timezone = new DateTimeZone( $tzstring ); + } + + return $local_timezone; +} diff --git a/includes/wcs-user-functions.php b/includes/wcs-user-functions.php index c915661..5576906 100644 --- a/includes/wcs-user-functions.php +++ b/includes/wcs-user-functions.php @@ -235,7 +235,7 @@ function wcs_can_user_put_subscription_on_hold( $subscription, $user = '' ) { if ( $user->ID == $subscription->get_user_id() ) { // Make sure subscription suspension count hasn't been reached - $suspension_count = $subscription->suspension_count; + $suspension_count = intval( $subscription->suspension_count ); $allowed_suspensions = get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions', 0 ); if ( 'unlimited' === $allowed_suspensions || $allowed_suspensions > $suspension_count ) { // 0 not > anything so prevents a customer ever being able to suspend @@ -345,8 +345,21 @@ function wcs_user_has_capability( $allcaps, $caps, $args ) { $allcaps['subscribe_again'] = true; } break; + case 'pay_for_order' : + $user_id = $args[1]; + $order = wc_get_order( $args[2] ); + + if ( $order && wcs_order_contains_subscription( $order, 'any' ) ) { + + if ( $user_id === $order->get_user_id() ) { + $allcaps['pay_for_order'] = true; + } else { + unset( $allcaps['pay_for_order'] ); + } + } + break; } } return $allcaps; } -add_filter( 'user_has_cap', 'wcs_user_has_capability', 10, 3 ); +add_filter( 'user_has_cap', 'wcs_user_has_capability', 15, 3 ); diff --git a/languages/woocommerce-subscriptions.pot b/languages/woocommerce-subscriptions.pot index ecafd58..74be202 100644 --- a/languages/woocommerce-subscriptions.pot +++ b/languages/woocommerce-subscriptions.pot @@ -2,10 +2,10 @@ # This file is distributed under the same license as the WooCommerce Subscriptions package. msgid "" msgstr "" -"Project-Id-Version: WooCommerce Subscriptions 2.0.20\n" +"Project-Id-Version: WooCommerce Subscriptions 2.1.0\n" "Report-Msgid-Bugs-To: " "https://github.com/Prospress/woocommerce-subscriptions/issues\n" -"POT-Creation-Date: 2016-09-23 23:32:44+00:00\n" +"POT-Creation-Date: 2016-11-12 07:04:14+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -15,69 +15,74 @@ msgstr "" "X-Generator: grunt-wp-i18n 0.5.4\n" "Language: en_US\n" +#: includes/admin/class-wc-subscriptions-admin.php:124 +msgid "Simple subscription" +msgstr "" + #: includes/admin/class-wc-subscriptions-admin.php:125 -msgid "Simple Subscription" +msgid "Variable subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:126 -#: woocommerce-subscriptions.php:601 -msgid "Variable Subscription" +#: includes/admin/class-wc-subscriptions-admin.php:143 +msgid "Choose the subscription price, billing interval and period." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:151 -#: templates/admin/deprecated/html-variation-price.php:20 -#: templates/admin/deprecated/html-variation-price.php:30 -#: templates/admin/html-variation-price.php:45 +#: includes/admin/class-wc-subscriptions-admin.php:156 +#: templates/admin/html-variation-price.php:44 #. translators: placeholder is a currency symbol / code -msgid "Subscription Price (%s)" +msgid "Subscription price (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:164 -#: templates/admin/deprecated/html-variation-price.php:46 -msgid "Subscription Periods" +#: includes/admin/class-wc-subscriptions-admin.php:159 +msgid "Subscription interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:173 -#: includes/admin/meta-boxes/views/html-subscription-schedule.php:32 -#: templates/admin/deprecated/html-variation-price.php:57 -msgid "Billing Period" +#: includes/admin/class-wc-subscriptions-admin.php:165 +#: includes/admin/class-wc-subscriptions-admin.php:298 +msgid "Subscription period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:184 -#: includes/admin/class-wc-subscriptions-admin.php:325 -#: templates/admin/deprecated/html-variation-price.php:69 -msgid "Subscription Length" +#: includes/admin/class-wc-subscriptions-admin.php:179 +#: includes/admin/class-wc-subscriptions-admin.php:299 +#: templates/admin/html-variation-price.php:66 +msgid "Subscription length" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:194 -#: templates/admin/deprecated/html-variation-price.php:85 +#: includes/admin/class-wc-subscriptions-admin.php:182 +msgid "" +"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." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:191 +#: templates/admin/html-variation-price.php:20 #. translators: %s is a currency symbol / code -msgid "Sign-up Fee (%s)" +msgid "Sign-up fee (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:196 +#: includes/admin/class-wc-subscriptions-admin.php:193 msgid "" "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." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:209 -#: templates/admin/deprecated/html-variation-price.php:97 -#: templates/admin/deprecated/html-variation-price.php:104 -msgid "Free Trial" +#: includes/admin/class-wc-subscriptions-admin.php:204 +#: templates/admin/html-variation-price.php:25 +msgid "Free trial" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:216 +#: includes/admin/class-wc-subscriptions-admin.php:207 #: templates/admin/deprecated/html-variation-price.php:115 msgid "Subscription Trial Period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:246 -msgid "One Time Shipping" +#: includes/admin/class-wc-subscriptions-admin.php:239 +msgid "One time shipping" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:247 +#: includes/admin/class-wc-subscriptions-admin.php:240 msgid "" "Shipping for subscription products is normally charged on the initial order " "and all renewal orders. Enable this to only charge shipping once on the " @@ -85,80 +90,53 @@ msgid "" "not have a free trial or a synced renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:269 -msgid "Limit Subscription" +#: includes/admin/class-wc-subscriptions-admin.php:295 +msgid "Subscription pricing" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:271 -#. translators: placeholders are opening and closing link tags -msgid "" -"Only allow a customer to have one subscription to this product. %sLearn " -"more%s." +#: includes/admin/class-wc-subscriptions-admin.php:296 +msgid "Subscription sign-up fee" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:273 -msgid "Do not limit" +#: includes/admin/class-wc-subscriptions-admin.php:297 +msgid "Subscription billing interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:274 -msgid "Limit to one active subscription" +#: includes/admin/class-wc-subscriptions-admin.php:300 +msgid "Free trial length" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:275 -msgid "Limit to one of any status" +#: includes/admin/class-wc-subscriptions-admin.php:301 +msgid "Free trial period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:321 -msgid "Subscription Pricing" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:322 -msgid "Subscription Sign-up Fee" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:323 -msgid "Subscription Billing Interval" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:324 -msgid "Subscription Period" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:326 -msgid "Free Trial Length" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:327 -msgid "Free Trial Period" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:650 +#: includes/admin/class-wc-subscriptions-admin.php:624 msgid "" "Unable to change subscription status to \"%s\". Please assign a customer to " "the subscription to activate it." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:692 +#: includes/admin/class-wc-subscriptions-admin.php:666 msgid "" "Trashing this order will also trash the subscriptions purchased with the " "order." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:705 +#: includes/admin/class-wc-subscriptions-admin.php:679 msgid "Enter the new period, either day, week, month or year:" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:706 +#: includes/admin/class-wc-subscriptions-admin.php:680 msgid "Enter a new length (e.g. 5):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:707 +#: includes/admin/class-wc-subscriptions-admin.php:681 msgid "" "Enter a new interval as a single number (e.g. to charge every 2nd month, " "enter 2):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:712 +#: includes/admin/class-wc-subscriptions-admin.php:686 msgid "" "You are about to trash one or more orders which contain a subscription.\n" "\n" @@ -166,7 +144,7 @@ msgid "" "orders." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:725 +#: includes/admin/class-wc-subscriptions-admin.php:699 msgid "" "WARNING: Bad things are about to happen!\n" "\n" @@ -178,13 +156,13 @@ msgid "" "gateway." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:726 +#: includes/admin/class-wc-subscriptions-admin.php:700 msgid "" "You are deleting a subscription item. You will also need to manually cancel " "and trash the subscription on the Manage Subscriptions screen." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:733 +#: includes/admin/class-wc-subscriptions-admin.php:707 msgid "" "Warning: Deleting a user will also delete the user's subscriptions. The " "user's orders will remain but be reassigned to the 'Guest' user.\n" @@ -193,78 +171,59 @@ msgid "" "subscriptions?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:796 -msgid "Active Subscriber?" +#: includes/admin/class-wc-subscriptions-admin.php:770 +msgid "Active subscriber?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:839 +#: includes/admin/class-wc-subscriptions-admin.php:813 msgid "Manage Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:843 -#: woocommerce-subscriptions.php:209 +#: includes/admin/class-wc-subscriptions-admin.php:817 +#: woocommerce-subscriptions.php:214 msgid "Search Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:863 -#: includes/admin/class-wc-subscriptions-admin.php:959 -#: includes/class-wcs-query.php:90 includes/class-wcs-query.php:110 -#: includes/class-wcs-query.php:112 woocommerce-subscriptions.php:200 -#: woocommerce-subscriptions.php:213 +#: includes/admin/class-wc-subscriptions-admin.php:837 +#: includes/admin/class-wc-subscriptions-admin.php:933 +#: includes/admin/class-wcs-admin-reports.php:55 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:654 +#: includes/class-wcs-query.php:93 includes/class-wcs-query.php:113 +#: includes/class-wcs-query.php:115 woocommerce-subscriptions.php:205 +#: woocommerce-subscriptions.php:218 msgid "Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1006 -#. 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 -msgid "" -"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." -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1009 -#. translators: placeholder is name of a gateway -msgid "The %s gateway can process automatic subscription payments." -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1012 -#. 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") -msgid "The %1$s & %2$s gateways can process automatic subscription payments." -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1018 +#: includes/admin/class-wc-subscriptions-admin.php:973 msgid "Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1025 +#: includes/admin/class-wc-subscriptions-admin.php:980 msgid "Add to Cart Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1026 +#: includes/admin/class-wc-subscriptions-admin.php:981 msgid "" "A product displays a button with the text \"Add to Cart\". By default, a " "subscription changes this to \"Sign Up Now\". You can customise the button " "text for subscriptions here." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1030 -#: includes/admin/class-wc-subscriptions-admin.php:1041 +#: includes/admin/class-wc-subscriptions-admin.php:985 +#: includes/admin/class-wc-subscriptions-admin.php:996 #: includes/class-wc-product-subscription-variation.php:75 -#: includes/class-wc-product-subscription.php:115 -#: includes/class-wc-product-variable-subscription.php:101 -#: includes/class-wc-subscriptions-product.php:130 -#: woocommerce-subscriptions.php:456 woocommerce-subscriptions.php:1110 +#: includes/class-wc-product-subscription.php:122 +#: includes/class-wc-product-variable-subscription.php:108 +#: includes/class-wc-subscriptions-product.php:127 +#: woocommerce-subscriptions.php:461 msgid "Sign Up Now" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1036 +#: includes/admin/class-wc-subscriptions-admin.php:991 msgid "Place Order Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1037 +#: includes/admin/class-wc-subscriptions-admin.php:992 msgid "" "Use this field to customise the text displayed on the checkout button when " "an order contains a subscription. Normally the checkout submission button " @@ -272,11 +231,11 @@ msgid "" "changed to \"Sign Up Now\"." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1049 +#: includes/admin/class-wc-subscriptions-admin.php:1004 msgid "Roles" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1052 +#: includes/admin/class-wc-subscriptions-admin.php:1007 #. translators: placeholders are tags msgid "" "Choose the default roles to assign to active and inactive subscribers. For " @@ -285,46 +244,46 @@ msgid "" "allocated these roles to prevent locking out administrators." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1057 +#: includes/admin/class-wc-subscriptions-admin.php:1012 msgid "Subscriber Default Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1058 +#: includes/admin/class-wc-subscriptions-admin.php:1013 msgid "" "When a subscription is activated, either manually or after a successful " "purchase, new users will be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1069 +#: includes/admin/class-wc-subscriptions-admin.php:1024 msgid "Inactive Subscriber Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1070 +#: includes/admin/class-wc-subscriptions-admin.php:1025 msgid "" "If a subscriber's subscription is manually cancelled or expires, she will " "be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1090 +#: includes/admin/class-wc-subscriptions-admin.php:1045 msgid "Manual Renewal Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1091 +#: includes/admin/class-wc-subscriptions-admin.php:1046 msgid "Accept Manual Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1096 +#: includes/admin/class-wc-subscriptions-admin.php:1051 #. translators: placeholders are opening and closing link tags msgid "" "With manual renewals, a customer's subscription is put on-hold until they " "login and pay to renew it. %sLearn more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1102 +#: includes/admin/class-wc-subscriptions-admin.php:1057 msgid "Turn off Automatic Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1107 +#: includes/admin/class-wc-subscriptions-admin.php:1062 #. translators: placeholders are opening and closing link tags msgid "" "If you never want a customer to be automatically charged for a subscription " @@ -332,11 +291,11 @@ msgid "" "more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1122 +#: includes/admin/class-wc-subscriptions-admin.php:1077 msgid "Customer Suspensions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1129 +#: includes/admin/class-wc-subscriptions-admin.php:1084 msgid "" "Set a maximum number of times a customer can suspend their account for each " "billing period. For example, for a value of 3 and a subscription billed " @@ -346,28 +305,28 @@ msgid "" "Set this to 0 to turn off the customer suspension feature completely." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1133 +#: includes/admin/class-wc-subscriptions-admin.php:1088 msgid "Mixed Checkout" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1134 +#: includes/admin/class-wc-subscriptions-admin.php:1089 msgid "Allow subscriptions and products to be purchased simultaneously." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1138 +#: includes/admin/class-wc-subscriptions-admin.php:1093 msgid "Allow subscriptions and products to be purchased in a single transaction." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1142 -#: includes/upgrades/templates/wcs-about.php:108 +#: includes/admin/class-wc-subscriptions-admin.php:1097 +#: includes/upgrades/templates/wcs-about-2-0.php:108 msgid "Drip Downloadable Content" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1143 +#: includes/admin/class-wc-subscriptions-admin.php:1098 msgid "Enable dripping for downloadable content on subscription products." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1147 +#: includes/admin/class-wc-subscriptions-admin.php:1102 msgid "" "Enabling this grants access to new downloadable files added to a product " "only after the next renewal is processed.%sBy default, access to new " @@ -375,18 +334,87 @@ msgid "" "customer that has an active subscription with that product." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1153 -msgid "Payment Gateways" +#: includes/admin/class-wc-subscriptions-admin.php:1138 +#. translators: $1-$2: opening and closing tags, $3-$4: opening and +#. closing tags +msgid "" +"%1$sWooCommerce Subscriptions Installed%2$s – %3$sYou're ready to " +"start selling subscriptions!%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1161 +#: includes/admin/class-wc-subscriptions-admin.php:1143 +msgid "Add a Subscription Product" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1144 +#: includes/upgrades/templates/wcs-about-2-0.php:35 +#: includes/upgrades/templates/wcs-about.php:34 +#: woocommerce-subscriptions.php:944 +msgid "Settings" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1230 +#. translators: placeholder is a number +msgid "We can't find a subscription with ID #%d. Perhaps it was deleted?" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1263 +#: includes/admin/class-wc-subscriptions-admin.php:1268 +#. translators: placeholders are opening link tag, ID of sub, and closing link +#. tag +msgid "Showing orders for %sSubscription %s%s" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1292 +#. translators: number of 1$: days, 2$: weeks, 3$: months, 4$: years +msgid "The trial period can not exceed: %1s, %2s, %3s or %4s." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1297 +#. translators: placeholder is a time period (e.g. "4 weeks") +msgid "The trial period can not exceed %s." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1353 +#: includes/admin/class-wc-subscriptions-admin.php:1406 +msgid "Yes" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1353 +msgid "No" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1389 +msgid "Automatic Recurring Payments" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1406 +msgid "" +"Supports automatic renewal payments with the WooCommerce Subscriptions " +"extension." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1458 +#. translators: $1-2: opening and closing tags of a link that takes to Woo +#. marketplace / Stripe product page +msgid "" +"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." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1463 +msgid "Recurring Payments" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1471 #. translators: placeholders are opening and closing link tags msgid "" -"Other payment gateways can be used to process %smanual subscription renewal " -"payments%s only." +"Payment gateways which don't support automatic recurring payments can be " +"used to process %smanual subscription renewal payments%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1168 +#: includes/admin/class-wc-subscriptions-admin.php:1478 #. translators: $1-$2: opening and closing tags. Link to documents->payment #. gateways, 3$-4$: opening and closing tags. Link to woothemes extensions shop #. page @@ -395,97 +423,38 @@ msgid "" "the official %3$sWooCommerce Marketplace%4$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1205 -#. translators: $1-$2: opening and closing tags, $3-$4: opening and -#. closing tags -msgid "" -"%1$sWooCommerce Subscriptions Installed%2$s – %3$sYou're ready to " -"start selling subscriptions!%4$s" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1210 -msgid "Add a Subscription Product" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1211 -#: includes/upgrades/templates/wcs-about.php:35 -#: woocommerce-subscriptions.php:960 -msgid "Settings" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1297 -#. translators: placeholder is a number -msgid "We can't find a subscription with ID #%d. Perhaps it was deleted?" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1330 -#: includes/admin/class-wc-subscriptions-admin.php:1335 -#. translators: placeholders are opening link tag, ID of sub, and closing link -#. tag -msgid "Showing orders for %sSubscription %s%s" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1359 -#. translators: number of 1$: days, 2$: weeks, 3$: months, 4$: years -msgid "The trial period can not exceed: %1s, %2s, %3s or %4s." -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1364 -#. translators: placeholder is a time period (e.g. "4 weeks") -msgid "The trial period can not exceed %s." -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1420 -#: includes/admin/class-wc-subscriptions-admin.php:1473 -msgid "Yes" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1420 -msgid "No" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1456 -msgid "Automatic Recurring Payments" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:1473 -msgid "" -"Supports automatic renewal payments with the WooCommerce Subscriptions " -"extension." -msgstr "" - -#: includes/admin/class-wcs-admin-meta-boxes.php:60 -#: includes/admin/class-wcs-admin-meta-boxes.php:64 +#: includes/admin/class-wcs-admin-meta-boxes.php:62 +#: includes/admin/class-wcs-admin-meta-boxes.php:66 #: templates/myaccount/related-orders.php:15 msgid "Related Orders" msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:103 +#: includes/admin/class-wcs-admin-meta-boxes.php:105 msgid "Please enter a start date in the past." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:104 +#: includes/admin/class-wcs-admin-meta-boxes.php:106 msgid "Please enter a date at least one hour into the future." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:105 +#: includes/admin/class-wcs-admin-meta-boxes.php:107 msgid "Please enter a date after the trial end." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:106 -#: includes/admin/class-wcs-admin-meta-boxes.php:107 +#: includes/admin/class-wcs-admin-meta-boxes.php:108 +#: includes/admin/class-wcs-admin-meta-boxes.php:109 msgid "Please enter a date after the start date." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:108 +#: includes/admin/class-wcs-admin-meta-boxes.php:110 msgid "Please enter a date before the next payment." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:109 +#: includes/admin/class-wcs-admin-meta-boxes.php:111 msgid "Please enter a date after the next payment." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:110 +#: includes/admin/class-wcs-admin-meta-boxes.php:112 msgid "" "Are you sure you want to process a renewal?\n" "\n" @@ -493,34 +462,46 @@ msgid "" "are enabled)." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:130 +#: includes/admin/class-wcs-admin-meta-boxes.php:121 +msgid "" +"Are you sure you want to retry payment for this renewal order?\n" +"\n" +"This will attempt to charge the customer and send renewal order emails (if " +"emails are enabled)." +msgstr "" + +#: includes/admin/class-wcs-admin-meta-boxes.php:140 msgid "Process renewal" msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:133 +#: includes/admin/class-wcs-admin-meta-boxes.php:143 msgid "Create pending renewal order" msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:147 +#: includes/admin/class-wcs-admin-meta-boxes.php:146 +msgid "Retry Renewal Payment" +msgstr "" + +#: includes/admin/class-wcs-admin-meta-boxes.php:160 msgid "Process renewal order action requested by admin." msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:166 +#: includes/admin/class-wcs-admin-meta-boxes.php:179 msgid "Create pending renewal order requested by admin action." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:114 +#: includes/admin/class-wcs-admin-post-types.php:193 msgid "Search for a product…" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:287 +#: includes/admin/class-wcs-admin-post-types.php:366 #. translators: placeholder is the number of subscriptions updated msgid "%s subscription status changed." msgid_plural "%s subscription statuses changed." msgstr[0] "" msgstr[1] "" -#: includes/admin/class-wcs-admin-post-types.php:294 +#: includes/admin/class-wcs-admin-post-types.php:373 #. translators: 1$: is the number of subscriptions not updated, 2$: is the #. error message msgid "%1$s subscription could not be updated: %2$s" @@ -528,7 +509,7 @@ msgid_plural "%1$s subscriptions could not be updated: %2$s" msgstr[0] "" msgstr[1] "" -#: includes/admin/class-wcs-admin-post-types.php:316 +#: includes/admin/class-wcs-admin-post-types.php:397 #: includes/admin/meta-boxes/views/html-related-orders-table.php:20 #: templates/myaccount/my-subscriptions.php:26 #: templates/myaccount/my-subscriptions.php:40 @@ -540,168 +521,193 @@ msgstr[1] "" msgid "Status" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:317 +#: includes/admin/class-wcs-admin-post-types.php:398 #: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:61 #: templates/emails/cancelled-subscription.php:26 +#: templates/emails/expired-subscription.php:26 +#: templates/emails/on-hold-subscription.php:26 #: templates/emails/subscription-info.php:18 #: templates/myaccount/my-subscriptions.php:25 #: templates/myaccount/related-subscriptions.php:20 -#: woocommerce-subscriptions.php:201 +#: woocommerce-subscriptions.php:206 msgid "Subscription" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:318 +#: includes/admin/class-wcs-admin-post-types.php:399 msgid "Items" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:319 +#: includes/admin/class-wcs-admin-post-types.php:400 msgid "Total" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:320 +#: includes/admin/class-wcs-admin-post-types.php:401 msgid "Start Date" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:321 +#: includes/admin/class-wcs-admin-post-types.php:402 msgid "Trial End" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:322 +#: includes/admin/class-wcs-admin-post-types.php:403 msgid "Next Payment" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:323 +#: includes/admin/class-wcs-admin-post-types.php:404 msgid "Last Payment" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:324 +#: includes/admin/class-wcs-admin-post-types.php:405 msgid "End Date" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:365 +#: includes/admin/class-wcs-admin-post-types.php:446 #: includes/wcs-user-functions.php:272 msgid "Reactivate" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:366 +#: includes/admin/class-wcs-admin-post-types.php:447 #: includes/wcs-user-functions.php:267 msgid "Suspend" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:368 -#: includes/admin/class-wcs-admin-post-types.php:383 +#: includes/admin/class-wcs-admin-post-types.php:449 +#: includes/admin/class-wcs-admin-post-types.php:464 msgid "Trash" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:369 -#: includes/admin/class-wcs-admin-post-types.php:387 +#: includes/admin/class-wcs-admin-post-types.php:450 +#: includes/admin/class-wcs-admin-post-types.php:468 msgid "Delete Permanently" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:381 -#: includes/class-wc-subscriptions-product.php:811 +#: includes/admin/class-wcs-admin-post-types.php:462 +#: includes/class-wc-subscriptions-product.php:797 msgid "Restore this item from the Trash" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:381 -#: includes/class-wc-subscriptions-product.php:812 +#: includes/admin/class-wcs-admin-post-types.php:462 +#: includes/class-wc-subscriptions-product.php:798 msgid "Restore" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:383 +#: includes/admin/class-wcs-admin-post-types.php:464 msgid "Move this item to the Trash" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:387 +#: includes/admin/class-wcs-admin-post-types.php:468 msgid "Delete this item permanently" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:393 +#: includes/admin/class-wcs-admin-post-types.php:474 msgid "Cancel Now" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:426 -#: templates/emails/plain/admin-new-renewal-order.php:54 -#: templates/emails/plain/customer-completed-renewal-order.php:52 -#: templates/emails/plain/customer-processing-renewal-order.php:51 +#: includes/admin/class-wcs-admin-post-types.php:507 #. translators: placeholder is customer's billing email msgid "Email: %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:431 -#: templates/emails/plain/admin-new-renewal-order.php:59 -#: templates/emails/plain/customer-completed-renewal-order.php:58 -#: templates/emails/plain/customer-processing-renewal-order.php:57 +#: includes/admin/class-wcs-admin-post-types.php:512 #. translators: placeholder is customer's billing phone number msgid "Tel: %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:463 +#: includes/admin/class-wcs-admin-post-types.php:544 msgid "Show more details" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:507 +#: includes/admin/class-wcs-admin-post-types.php:587 msgid "%d item" msgid_plural "%d items" msgstr[0] "" msgstr[1] "" -#: includes/admin/class-wcs-admin-post-types.php:542 +#: includes/admin/class-wcs-admin-post-types.php:631 #: templates/myaccount/my-subscriptions.php:48 #. translators: placeholder is the display name of a payment gateway a #. subscription was paid by msgid "Via %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:553 +#: includes/admin/class-wcs-admin-post-types.php:642 msgid "Y/m/d g:i:s A" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:556 +#: includes/admin/class-wcs-admin-post-types.php:645 msgid "" "This date should be treated as an estimate only. The payment gateway for " "this subscription controls when payments are processed." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:829 -#: includes/admin/class-wcs-admin-post-types.php:832 -#: includes/admin/class-wcs-admin-post-types.php:835 +#: includes/admin/class-wcs-admin-post-types.php:919 +#: includes/admin/class-wcs-admin-post-types.php:922 +#: includes/admin/class-wcs-admin-post-types.php:925 msgid "Subscription updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:830 +#: includes/admin/class-wcs-admin-post-types.php:920 msgid "Custom field updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:831 +#: includes/admin/class-wcs-admin-post-types.php:921 msgid "Custom field deleted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:836 +#: includes/admin/class-wcs-admin-post-types.php:926 msgid "Subscription saved." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:837 +#: includes/admin/class-wcs-admin-post-types.php:927 msgid "Subscription submitted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:839 +#: includes/admin/class-wcs-admin-post-types.php:929 #. translators: php date string msgid "Subscription scheduled for: %1$s." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:840 +#: includes/admin/class-wcs-admin-post-types.php:930 msgid "Subscription draft updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:878 +#: includes/admin/class-wcs-admin-post-types.php:968 msgid "Any Payment Method" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:879 +#: includes/admin/class-wcs-admin-post-types.php:969 msgid "None" msgstr "" +#: includes/admin/class-wcs-admin-reports.php:58 +msgid "Subscription Events by Date" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:64 +msgid "Upcoming Recurring Revenue" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:70 +msgid "Retention Rate" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:76 +msgid "Subscriptions by Product" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:82 +msgid "Subscriptions by Customer" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:92 +msgid "Failed Payment Retries" +msgstr "" + +#: includes/admin/class-wcs-admin-reports.php:132 +#: includes/admin/reports/class-wcs-report-cache-manager.php:257 +msgid "WooCommerce" +msgstr "" + #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:54 msgid "Customer:" msgstr "" @@ -715,7 +721,7 @@ msgid "Search for a customer…" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:80 -msgid "Subscription Status:" +msgid "Subscription status:" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:98 @@ -762,20 +768,8 @@ msgstr "" msgid "Error updating some information: %s" msgstr "" -#: includes/admin/meta-boxes/views/html-related-orders-row.php:34 -#: includes/class-wc-subscription.php:681 -#: includes/class-wc-subscriptions-manager.php:2328 -#. translators: placeholder is human time diff (e.g. "3 weeks") -msgid "In %s" -msgstr "" - -#: includes/admin/meta-boxes/views/html-related-orders-row.php:37 -#: includes/class-wc-subscription.php:684 -#. translators: placeholder is human time diff (e.g. "3 weeks") -msgid "%s ago" -msgstr "" - -#: includes/admin/meta-boxes/views/html-related-orders-row.php:43 +#: includes/admin/meta-boxes/views/html-related-orders-row.php:29 +#: includes/admin/meta-boxes/views/html-retries-table.php:47 msgid "Unpublished" msgstr "" @@ -789,16 +783,70 @@ msgid "Relationship" msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-table.php:19 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:515 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:173 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:197 #: templates/myaccount/related-orders.php:23 #: templates/myaccount/related-orders.php:41 msgid "Date" msgstr "" +#: includes/admin/meta-boxes/views/html-retries-table.php:17 +msgid "Retry Date" +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:19 +msgid "Retry Status" +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:20 +msgid "" +"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." +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:23 +msgid "Status of Order" +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:24 +msgid "" +"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." +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:27 +msgid "Status of Subscription" +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:28 +msgid "" +"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." +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:31 +msgid "Email" +msgstr "" + +#: includes/admin/meta-boxes/views/html-retries-table.php:32 +msgid "" +"The email sent to the customer when the renewal payment or payment retry " +"failed to notify them that the payment would be retried." +msgstr "" + #: includes/admin/meta-boxes/views/html-subscription-schedule.php:22 #: includes/admin/meta-boxes/views/html-subscription-schedule.php:41 msgid "Recurring:" msgstr "" +#: includes/admin/meta-boxes/views/html-subscription-schedule.php:32 +#: templates/admin/deprecated/html-variation-price.php:57 +msgid "Billing Period" +msgstr "" + #: includes/admin/meta-boxes/views/html-subscription-schedule.php:60 msgid "Timezone:" msgstr "" @@ -807,122 +855,621 @@ msgstr "" msgid "Error: unable to find timezone of your browser." msgstr "" -#: includes/api/class-wc-api-subscriptions.php:102 wcs-functions.php:168 -msgid "Invalid subscription status given." +#: includes/admin/reports/class-wcs-report-cache-manager.php:260 +msgid "" +"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." msgstr "" -#: includes/api/class-wc-api-subscriptions.php:124 -msgid "You do not have permission to read the subscriptions count" +#: includes/admin/reports/class-wcs-report-dashboard.php:78 +msgid "%s signup subscription signups this month" +msgid_plural "%s signups subscription signups this month" +msgstr[0] "" +msgstr[1] "" + +#: includes/admin/reports/class-wcs-report-dashboard.php:83 +msgid "%s renewal subscription renewals this month" +msgid_plural "%s renewals subscription renewals this month" +msgstr[0] "" +msgstr[1] "" + +#: includes/admin/reports/class-wcs-report-retention-rate.php:226 +msgid "Unended Subscription Count" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:173 -msgid "You do not have permission to create subscriptions" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:22 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:95 +msgid "Customer" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:239 -msgid "The requested subscription cannot be edited." +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:23 +msgid "Customers" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:304 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:32 +msgid "No customers found." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:43 +msgid "Customer Totals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:44 +msgid "Total Subscribers" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:45 +msgid "Active Subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:46 +msgid "Total Subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:47 +msgid "Total Subscription Orders" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48 +msgid "Average Lifetime Value" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96 +msgid "Active Subscriptions %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96 +msgid "" +"The number of subscriptions this customer has with a status of active or " +"pending cancellation." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:97 +msgid "Total Subscriptions %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:97 +msgid "" +"The number of subscriptions this customer has with a status other than " +"pending or trashed." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:98 +msgid "Total Subscription Orders %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:98 +msgid "" +"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)." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:99 +msgid "Lifetime Value from Subscriptions %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:99 +msgid "The total value of this customer's sign-up, switch and renewal orders." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:20 +msgid "Product" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:21 +msgid "Products" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:30 +msgid "No products found." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:84 +msgid "Subscription Product" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:85 +msgid "Subscription Count %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:85 +msgid "" +"The number of subscriptions that include this product as a line item and " +"have a status other than pending or trashed." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:86 +msgid "Average Recurring Line Total %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:86 +msgid "The average line total for this product on each subscription." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:87 +msgid "Average Lifetime Value %s" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:87 +msgid "The average line total on all orders for this product line item." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-by-product.php:246 +msgid "subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:364 +msgid "%s signup revenue in this period" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:365 +msgid "" +"The sum of all subscription parent orders, including other items, fees, tax " +"and shipping." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:371 +msgid "%s renewal revenue in this period" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:372 +msgid "The sum of all renewal orders including tax and shipping." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:378 +msgid "%s resubscribe revenue in this period" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:379 +msgid "The sum of all resubscribe orders including tax and shipping." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:385 +msgid "%s new subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:386 +msgid "" +"The number of subscriptions created during this period, either by being " +"manually created, imported or a customer placing an order." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:392 +msgid "%s subscription signups" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:393 +msgid "" +"The number of subscription parent orders created during this period. This " +"represents the new subscriptions created by customers placing an order via " +"checkout." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:399 +msgid "%s subscription resubscribes" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:400 +msgid "The number of resubscribe orders processed during this period." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:406 +msgid "%s subscription renewals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:407 +msgid "The number of renewal orders processed during this period." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:413 +msgid "%s subscription switches" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:414 +msgid "" +"The number of subscriptions upgraded, downgraded or cross-graded during " +"this period." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:420 +msgid "%s subscription cancellations" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:421 +msgid "" +"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." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:427 +msgid "%s subscriptions ended" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:428 +msgid "" +"The number of subscriptions which have either expired or reached the end of " +"the prepaid term if it was previously cancelled." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:434 +msgid "%s current subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:435 +msgid "" +"The number of subscriptions during this period with an end date in the " +"future and a status other than pending." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:451 +msgid "%s net subscription gain" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:453 +msgid "%s net subscription loss" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:458 +msgid "Change in subscriptions between the start and end of the period." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:472 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:137 +msgid "Year" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:473 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:138 +msgid "Last Month" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:474 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139 +msgid "This Month" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:475 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140 +msgid "Last 7 Days" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:519 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:177 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:201 +msgid "Export CSV" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:576 +msgid "Switched subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:592 +msgid "New Subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:608 +msgid "Subscriptions signups" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:623 +msgid "Number of resubscribes" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:638 +msgid "Number of renewals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:670 +msgid "Subscriptions Ended" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:686 +msgid "Cancellations" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:701 +msgid "Signup Totals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:721 +msgid "Resubscribe Totals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:741 +msgid "Renewal Totals" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:95 +msgid "%s renewal revenue recovered" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:96 +msgid "" +"The total amount of revenue, including tax and shipping, recovered with the " +"failed payment retry system for renewal orders with a failed payment." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:102 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:86 +msgid "%s renewal orders" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:103 +msgid "" +"The number of renewal orders which had a failed payment use the retry " +"system." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:108 +msgid "%s retry attempts succeeded" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:109 +msgid "" +"The number of renewal payment retries for this period which were able to " +"process the payment which had previously failed one or more times." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:115 +msgid "%s retry attempts failed" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:116 +msgid "" +"The number of renewal payment retries for this period which did not result " +"in a successful payment." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:122 +msgid "%s retry attempts pending" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:123 +msgid "The number of renewal payment retries not yet processed." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:225 +msgid "Successful retries" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:241 +msgid "Failed retries" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:257 +msgid "Pending retries" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:273 +msgid "Recovered Renewal Revenue" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:81 +msgid "%s renewal income in this period" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:91 +msgid "%s average renewal amount" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:167 +msgid "Next 12 Months" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:168 +msgid "Next 30 Days" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:169 +msgid "Next Month" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:170 +msgid "Next 7 Days" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:235 +msgid "Renewals count" +msgstr "" + +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:244 +msgid "Renewals amount" +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:163 +msgid "Invalid subscription id." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:232 +msgid "Cannot create subscription: %s." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:275 +msgid "Updating subscription dates errored with message: %s" +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:294 +#: includes/api/legacy/class-wc-api-subscriptions.php:304 msgid "" "Gateway does not support admin changing the payment method on a " "Subscription." msgstr "" -#: includes/api/class-wc-api-subscriptions.php:342 +#: includes/api/class-wc-rest-subscriptions-controller.php:327 +#. translators: 1$: gateway id, 2$: error message +msgid "" +"Subscription payment method could not be set to %1$s with error message: " +"%2$s" +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:341 +msgid "The number of billing periods between subscription renewals." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:346 +msgid "Billing period for the subscription." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:352 +msgid "Subscription payment details." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:357 +msgid "Payment gateway ID." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:364 +msgid "The subscription's start date." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:369 +msgid "The subscription's trial date" +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:374 +msgid "The subscription's next payment date." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:379 +msgid "The subscription's end date." +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:102 wcs-functions.php:170 +msgid "Invalid subscription status given." +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:124 +msgid "You do not have permission to read the subscriptions count" +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:173 +msgid "You do not have permission to create subscriptions" +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:239 +msgid "The requested subscription cannot be edited." +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:342 #. translators: 1$: gateway id, 2$: error message msgid "" "Subscription payment method could not be set to %1$s and has been set to " "manual with error message: %2$s" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:377 wcs-functions.php:142 +#: includes/api/legacy/class-wc-api-subscriptions.php:377 wcs-functions.php:144 msgid "" "Invalid subscription billing interval given. Must be an integer greater " "than 0." msgstr "" -#: includes/api/class-wc-api-subscriptions.php:389 wcs-functions.php:137 +#: includes/api/legacy/class-wc-api-subscriptions.php:389 wcs-functions.php:139 msgid "Invalid subscription billing period given." msgstr "" -#: includes/class-wc-subscription.php:309 -#: includes/class-wc-subscription.php:400 +#: includes/class-wc-subscription.php:305 msgid "Unable to change subscription status to \"%s\"." msgstr "" -#: includes/class-wc-subscription.php:382 +#: includes/class-wc-subscription.php:401 #. translators: $1 note why the status changes (if any), $2: old status, $3: #. new status msgid "%1$s Status changed from %2$s to %3$s." msgstr "" -#: includes/class-wc-subscription.php:691 +#: includes/class-wc-subscription.php:409 +msgid "Unable to change subscription status to \"%s\". Exception: %s" +msgstr "" + +#: includes/class-wc-subscription.php:697 +#: includes/class-wc-subscriptions-manager.php:2212 +#: includes/wcs-formatting-functions.php:228 +#. translators: placeholder is human time diff (e.g. "3 weeks") +msgid "In %s" +msgstr "" + +#: includes/class-wc-subscription.php:700 +#: includes/wcs-formatting-functions.php:231 +#. translators: placeholder is human time diff (e.g. "3 weeks") +msgid "%s ago" +msgstr "" + +#: includes/class-wc-subscription.php:707 msgid "Not yet ended" msgstr "" -#: includes/class-wc-subscription.php:734 +#: includes/class-wc-subscription.php:710 +msgid "Not cancelled" +msgstr "" + +#: includes/class-wc-subscription.php:753 msgid "Invalid format. First parameter needs to be an array." msgstr "" -#: includes/class-wc-subscription.php:738 +#: includes/class-wc-subscription.php:757 msgid "Invalid data. First parameter was empty when passed to update_dates()." msgstr "" -#: includes/class-wc-subscription.php:745 +#: includes/class-wc-subscription.php:764 msgid "" "Invalid data. First parameter has a date that is not in the registered date " "types." msgstr "" -#: includes/class-wc-subscription.php:793 +#: includes/class-wc-subscription.php:812 +msgid "The %s date must occur after the cancellation date." +msgstr "" + +#: includes/class-wc-subscription.php:817 msgid "The %s date must occur after the last payment date." msgstr "" -#: includes/class-wc-subscription.php:797 +#: includes/class-wc-subscription.php:821 msgid "The %s date must occur after the next payment date." msgstr "" -#: includes/class-wc-subscription.php:802 +#: includes/class-wc-subscription.php:826 msgid "The %s date must occur after the trial end date." msgstr "" -#: includes/class-wc-subscription.php:806 +#: includes/class-wc-subscription.php:830 msgid "The %s date must occur after the start date." msgstr "" -#: includes/class-wc-subscription.php:861 +#: includes/class-wc-subscription.php:883 msgid "The start date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:864 +#: includes/class-wc-subscription.php:886 msgid "" "The last payment date of a subscription can not be deleted. You must delete " "the order." msgstr "" -#: includes/class-wc-subscription.php:1267 +#: includes/class-wc-subscription.php:1293 msgid "Sign-up complete." msgstr "" -#: includes/class-wc-subscription.php:1269 +#: includes/class-wc-subscription.php:1295 msgid "Payment received." msgstr "" -#: includes/class-wc-subscription.php:1300 +#: includes/class-wc-subscription.php:1326 msgid "Payment failed." msgstr "" -#: includes/class-wc-subscription.php:1304 +#: includes/class-wc-subscription.php:1330 msgid "Subscription Cancelled: maximum number of failed payments reached." msgstr "" -#: includes/class-wc-subscription.php:1499 +#: includes/class-wc-subscription.php:1525 #: includes/class-wcs-change-payment-method-admin.php:155 msgid "Manual Renewal" msgstr "" -#: includes/class-wc-subscription.php:1564 +#: includes/class-wc-subscription.php:1590 msgid "Payment method meta must be an array." msgstr "" @@ -942,28 +1489,26 @@ msgstr "" msgid "Update the %1$s used for %2$sall%3$s of my active subscriptions" msgstr "" -#: includes/class-wc-subscriptions-cart.php:836 +#: includes/class-wc-subscriptions-cart.php:832 msgid "Please enter a valid postcode/ZIP." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1007 +#: includes/class-wc-subscriptions-cart.php:1003 msgid "" "That subscription product can not be added to your cart as it already " "contains a subscription renewal." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1092 +#: includes/class-wc-subscriptions-cart.php:1088 msgid "Invalid recurring shipping method." msgstr "" -#: includes/class-wc-subscriptions-cart.php:2060 +#: includes/class-wc-subscriptions-cart.php:1865 msgid "now" msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:126 -#: templates/emails/plain/admin-new-switch-order.php:57 -#: templates/emails/plain/cancelled-subscription.php:20 -#: templates/emails/plain/customer-completed-switch-order.php:52 +#: templates/emails/plain/email-order-details.php:19 #. translators: placeholder is the subscription order number wrapped in #. tags msgid "Subscription Number: %s" @@ -1002,7 +1547,8 @@ msgid "Invalid Subscription." msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:197 -#: includes/class-wcs-cart-resubscribe.php:58 +#: includes/class-wcs-cart-resubscribe.php:76 +#: includes/class-wcs-cart-resubscribe.php:127 #: includes/class-wcs-user-change-status-handler.php:103 msgid "That doesn't appear to be one of your subscriptions." msgstr "" @@ -1098,7 +1644,7 @@ msgid "" msgstr "" #: includes/class-wc-subscriptions-manager.php:141 -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:183 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:201 msgid "Subscription doesn't exist in scheduled action: %d" msgstr "" @@ -1156,50 +1702,66 @@ msgstr "" msgid "Change" msgstr "" -#: includes/class-wc-subscriptions-manager.php:2210 +#: includes/class-wc-subscriptions-manager.php:2094 #. translators: placeholder is subscription ID msgid "Failed sign-up for subscription %s." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2301 +#: includes/class-wc-subscriptions-manager.php:2185 msgid "Invalid security token, please reload the page and try again." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2305 +#: includes/class-wc-subscriptions-manager.php:2189 msgid "Only store managers can edit payment dates." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2309 +#: includes/class-wc-subscriptions-manager.php:2193 msgid "Please enter all date fields." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2334 +#: includes/class-wc-subscriptions-manager.php:2218 msgid "Date Changed" msgstr "" -#: includes/class-wc-subscriptions-order.php:362 +#: includes/class-wc-subscriptions-order.php:366 msgid "Your subscription will be activated when payment clears." msgid_plural "Your subscriptions will be activated when payment clears." msgstr[0] "" msgstr[1] "" -#: includes/class-wc-subscriptions-order.php:365 +#: includes/class-wc-subscriptions-order.php:375 #. translators: placeholders are opening and closing link tags msgid "View the status of your subscription in %syour account%s." msgid_plural "View the status of your subscriptions in %syour account%s." msgstr[0] "" msgstr[1] "" -#: includes/class-wc-subscriptions-order.php:635 -msgid "Show all types" +#: includes/class-wc-subscriptions-order.php:423 +msgid "Subscription Relationship" msgstr "" -#: includes/class-wc-subscriptions-order.php:874 +#: includes/class-wc-subscriptions-order.php:443 +msgid "Renewal Order" +msgstr "" + +#: includes/class-wc-subscriptions-order.php:445 +msgid "Resubscribe Order" +msgstr "" + +#: includes/class-wc-subscriptions-order.php:447 +msgid "Parent Order" +msgstr "" + +#: includes/class-wc-subscriptions-order.php:685 +msgid "All orders types" +msgstr "" + +#: includes/class-wc-subscriptions-order.php:952 #. translators: $1: opening link tag, $2: order number, $3: closing link tag msgid "Subscription cancelled for refunded order %1$s#%2$s%3$s." msgstr "" -#: includes/class-wc-subscriptions-product.php:351 +#: includes/class-wc-subscriptions-product.php:348 #: includes/wcs-formatting-functions.php:102 #: includes/wcs-formatting-functions.php:186 #. translators: 1$: recurring amount string, 2$: day of the week (e.g. "$10 @@ -1207,34 +1769,34 @@ msgstr "" msgid "%1$s every %2$s" msgstr "" -#: includes/class-wc-subscriptions-product.php:354 +#: includes/class-wc-subscriptions-product.php:351 #: includes/wcs-formatting-functions.php:111 #. translators: 1$: recurring amount string, 2$: period, 3$: day of the week #. (e.g. "$10 every 2nd week on Wednesday") msgid "%1$s every %2$s on %3$s" msgstr "" -#: includes/class-wc-subscriptions-product.php:361 +#: includes/class-wc-subscriptions-product.php:358 #: includes/wcs-formatting-functions.php:129 #. translators: placeholder is recurring amount msgid "%s on the last day of each month" msgstr "" -#: includes/class-wc-subscriptions-product.php:364 +#: includes/class-wc-subscriptions-product.php:361 #: includes/wcs-formatting-functions.php:132 #. translators: 1$: recurring amount, 2$: day of the month (e.g. "23rd") (e.g. #. "$5 every 23rd of each month") msgid "%1$s on the %2$s of each month" msgstr "" -#: includes/class-wc-subscriptions-product.php:369 +#: includes/class-wc-subscriptions-product.php:366 #: includes/wcs-formatting-functions.php:148 #. translators: 1$: recurring amount, 2$: interval (e.g. "3rd") (e.g. "$10 on #. the last day of every 3rd month") msgid "%1$s on the last day of every %2$s month" msgstr "" -#: includes/class-wc-subscriptions-product.php:372 +#: includes/class-wc-subscriptions-product.php:369 #: includes/wcs-formatting-functions.php:151 #. translators: 1$: on the, 2$: day of every, 3$: #. month (e.g. "$10 on the 23rd day of every 2nd month") @@ -1243,7 +1805,7 @@ msgstr "" msgid "%1$s on the %2$s day of every %3$s month" msgstr "" -#: includes/class-wc-subscriptions-product.php:379 +#: includes/class-wc-subscriptions-product.php:376 #: includes/wcs-formatting-functions.php:164 #. translators: 1$: on, 2$: , 3$: each year (e.g. "$15 on #. March 15th each year") @@ -1252,14 +1814,14 @@ msgstr "" msgid "%1$s on %2$s %3$s each year" msgstr "" -#: includes/class-wc-subscriptions-product.php:382 +#: includes/class-wc-subscriptions-product.php:379 #: includes/wcs-formatting-functions.php:173 #. translators: 1$: recurring amount, 2$: month (e.g. "March"), 3$: day of the #. month (e.g. "23rd") (e.g. "$15 on March 15th every 3rd year") msgid "%1$s on %2$s %3$s every %4$s year" msgstr "" -#: includes/class-wc-subscriptions-product.php:388 +#: includes/class-wc-subscriptions-product.php:385 #: includes/wcs-formatting-functions.php:184 #. translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or #. "3 months") (e.g. "$15 / month" or "$15 every 2nd month") @@ -1268,25 +1830,25 @@ msgid_plural " %1$s every %2$s" msgstr[0] "" msgstr[1] "" -#: includes/class-wc-subscriptions-product.php:394 +#: includes/class-wc-subscriptions-product.php:391 #. translators: billing period (e.g. "every week") msgid "every %s" msgstr "" -#: includes/class-wc-subscriptions-product.php:400 +#: includes/class-wc-subscriptions-product.php:397 #: includes/wcs-formatting-functions.php:194 #. translators: 1$: subscription string (e.g. "$10 up front then $5 on March #. 23rd every 3rd year"), 2$: length (e.g. "4 years") msgid "%1$s for %2$s" msgstr "" -#: includes/class-wc-subscriptions-product.php:406 +#: includes/class-wc-subscriptions-product.php:403 #. translators: 1$: subscription string (e.g. "$15 on March 15th every 3 years #. for 6 years"), 2$: trial length (e.g.: "with 4 months free trial") msgid "%1$s with %2$s free trial" msgstr "" -#: includes/class-wc-subscriptions-product.php:411 +#: includes/class-wc-subscriptions-product.php:408 #. translators: 1$: subscription string (e.g. "$15 on March 15th every 3 years #. for 6 years with 2 months free trial"), 2$: signup fee price (e.g. "and a #. $30 sign-up fee") @@ -1302,18 +1864,18 @@ msgstr "" msgid "Subscription renewal orders cannot be cancelled." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:155 +#: includes/class-wc-subscriptions-switcher.php:154 msgid "" "You have a subscription to this product. Choosing a new subscription will " "replace your existing subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:157 +#: includes/class-wc-subscriptions-switcher.php:156 msgid "Choose a new subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:179 -#: includes/class-wc-subscriptions-switcher.php:857 +#: includes/class-wc-subscriptions-switcher.php:178 +#: includes/class-wc-subscriptions-switcher.php:902 msgid "" "Your cart contained an invalid subscription switch request. It has been " "removed." @@ -1329,7 +1891,7 @@ msgid "" "customer. You can not purchase the product again." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:232 +#: includes/class-wc-subscriptions-switcher.php:229 #. translators: 1$: is the "You have already subscribed to this product" #. notice, 2$-4$: opening/closing link tags, 3$: an order number msgid "" @@ -1337,107 +1899,122 @@ msgid "" "subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:318 +#: includes/class-wc-subscriptions-switcher.php:308 msgid "Switching" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:321 +#: includes/class-wc-subscriptions-switcher.php:311 #. translators: placeholders are opening and closing link tags msgid "" "Allow subscribers to switch (upgrade or downgrade) between different " "subscriptions. %sLearn more%s." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:326 +#: includes/class-wc-subscriptions-switcher.php:316 msgid "Allow Switching" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:327 +#: includes/class-wc-subscriptions-switcher.php:317 msgid "" "Allow subscribers to switch between subscriptions combined in a grouped " "product, different variations of a Variable subscription or don't allow " "switching." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:343 +#: includes/class-wc-subscriptions-switcher.php:333 msgid "Prorate Recurring Payment" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:344 +#: includes/class-wc-subscriptions-switcher.php:334 msgid "" "When switching to a subscription with a different recurring payment or " "billing period, should the price paid for the existing billing period be " "prorated when switching to the new subscription?" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:361 +#: includes/class-wc-subscriptions-switcher.php:351 msgid "Prorate Sign up Fee" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:362 +#: includes/class-wc-subscriptions-switcher.php:352 msgid "" "When switching to a subscription with a sign up fee, you can require the " "customer pay only the gap between the existing subscription's sign up fee " "and the new subscription's sign up fee (if any)." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:377 +#: includes/class-wc-subscriptions-switcher.php:367 msgid "Prorate Subscription Length" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:378 +#: includes/class-wc-subscriptions-switcher.php:368 msgid "" "When switching to a subscription with a length, you can take into account " "the payments already completed by the customer when determining how many " "payments the subscriber needs to make for the new subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:393 +#: includes/class-wc-subscriptions-switcher.php:383 msgid "Switch Button Text" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:394 +#: includes/class-wc-subscriptions-switcher.php:384 msgid "" "Customise the text displayed on the button next to the subscription on the " "subscriber's account page. The default is \"Switch Subscription\", but you " "may wish to change this to \"Upgrade\" or \"Change Subscription\"." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:398 -#: includes/class-wc-subscriptions-switcher.php:424 -#: includes/class-wc-subscriptions-switcher.php:1878 +#: includes/class-wc-subscriptions-switcher.php:388 +#: includes/class-wc-subscriptions-switcher.php:414 +#: includes/class-wc-subscriptions-switcher.php:2112 msgid "Upgrade or Downgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:810 +#: includes/class-wc-subscriptions-switcher.php:765 +msgid "Switch order cancelled due to a new switch order being created #%s." +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:855 msgid "Switch Order" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:825 +#: includes/class-wc-subscriptions-switcher.php:870 msgid "Switched Subscription" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:925 +#: includes/class-wc-subscriptions-switcher.php:969 +msgid "You can only switch to a subscription product." +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:975 msgid "We can not find your old subscription item." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:947 +#: includes/class-wc-subscriptions-switcher.php:997 msgid "You can not switch to the same subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:994 +#: includes/class-wc-subscriptions-switcher.php:1044 msgid "" "You can not switch this subscription. It appears you do not own the " "subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1029 +#: includes/class-wc-subscriptions-switcher.php:1079 msgid "There was an error locating the switch details." msgstr "" +#: includes/class-wc-subscriptions-switcher.php:1814 +msgid "The original subscription item being switched cannot be found." +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:1867 +msgid "Failed to update the subscription shipping method." +msgstr "" + #: includes/class-wc-subscriptions-synchroniser.php:47 -#: templates/admin/deprecated/html-variation-synchronisation.php:30 -msgid "Synchronise Renewals" +msgid "Synchronise renewals" msgstr "" #: includes/class-wc-subscriptions-synchroniser.php:48 @@ -1464,35 +2041,39 @@ msgid "" "year, charge a prorated amount for the subscription at the time of sign up." msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:607 +#: includes/class-wc-subscriptions-synchroniser.php:237 +msgid "Month for Synchronisation" +msgstr "" + +#: includes/class-wc-subscriptions-synchroniser.php:603 msgid "Do not synchronise" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:615 +#: includes/class-wc-subscriptions-synchroniser.php:611 #. translators: placeholder is a day of the week msgid "%s each week" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:621 +#: includes/class-wc-subscriptions-synchroniser.php:617 #. translators: placeholder is a number of day with language specific suffix #. applied (e.g. "1st", "3rd", "5th", etc...) msgid "%s day of the month" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:623 +#: includes/class-wc-subscriptions-synchroniser.php:619 msgid "Last day of the month" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:671 +#: includes/class-wc-subscriptions-synchroniser.php:667 msgid "Today!" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:678 +#: includes/class-wc-subscriptions-synchroniser.php:674 #. translators: placeholder is a date msgid "First payment prorated. Next payment: %s" msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:681 +#: includes/class-wc-subscriptions-synchroniser.php:677 #. translators: placeholder is a date msgid "First payment: %s" msgstr "" @@ -1509,23 +2090,28 @@ msgstr "" msgid "View and manage subscriptions" msgstr "" -#: includes/class-wcs-cart-initial-payment.php:48 +#: includes/class-wcs-cart-initial-payment.php:56 +#: includes/class-wcs-cart-renewal.php:121 msgid "That doesn't appear to be your order." msgstr "" -#: includes/class-wcs-cart-renewal.php:162 +#: includes/class-wcs-cart-renewal.php:149 +msgid "Complete checkout to renew your subscription." +msgstr "" + +#: includes/class-wcs-cart-renewal.php:188 #. translators: placeholder is an item name msgid "" "The %s product has been deleted and can no longer be renewed. Please choose " "a new product or contact us for assistance." msgstr "" -#: includes/class-wcs-cart-renewal.php:191 +#: includes/class-wcs-cart-renewal.php:221 #. translators: %s is subscription's number msgid "Subscription #%s has not been added to the cart." msgstr "" -#: includes/class-wcs-cart-renewal.php:317 +#: includes/class-wcs-cart-renewal.php:347 msgid "" "We couldn't find the original subscription for an item in your cart. The " "item was removed." @@ -1535,7 +2121,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:324 +#: includes/class-wcs-cart-renewal.php:354 msgid "" "We couldn't find the original renewal order for an item in your cart. The " "item was removed." @@ -1545,33 +2131,60 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:540 +#: includes/class-wcs-cart-renewal.php:613 msgid "All linked subscription items have been removed from the cart." msgstr "" -#: includes/class-wcs-cart-resubscribe.php:50 +#: includes/class-wcs-cart-resubscribe.php:68 msgid "There was an error with your request to resubscribe. Please try again." msgstr "" -#: includes/class-wcs-cart-resubscribe.php:54 +#: includes/class-wcs-cart-resubscribe.php:72 msgid "That subscription does not exist. Has it been deleted?" msgstr "" -#: includes/class-wcs-cart-resubscribe.php:62 +#: includes/class-wcs-cart-resubscribe.php:80 msgid "" "You can not resubscribe to that subscription. Please contact us if you need " "assistance." msgstr "" -#: includes/class-wcs-cart-resubscribe.php:71 -#: includes/class-wcs-cart-resubscribe.php:88 +#: includes/class-wcs-cart-resubscribe.php:89 +#: includes/class-wcs-cart-resubscribe.php:117 msgid "Complete checkout to resubscribe." msgstr "" +#: includes/class-wcs-cart-resubscribe.php:310 +msgid "Customer resubscribed in order #%s" +msgstr "" + #: includes/class-wcs-change-payment-method-admin.php:113 msgid "Please choose a valid payment gateway to change to." msgstr "" +#: includes/class-wcs-limiter.php:46 +msgid "Limit subscription" +msgstr "" + +#: includes/class-wcs-limiter.php:48 +#. translators: placeholders are opening and closing link tags +msgid "" +"Only allow a customer to have one subscription to this product. %sLearn " +"more%s." +msgstr "" + +#: includes/class-wcs-limiter.php:50 +msgid "Do not limit" +msgstr "" + +#: includes/class-wcs-limiter.php:51 +msgid "Limit to one active subscription" +msgstr "" + +#: includes/class-wcs-limiter.php:52 +msgid "Limit to one of any status" +msgstr "" + #: includes/class-wcs-remove-item.php:106 msgid "Your request to undo your previous action was unsuccessful." msgstr "" @@ -1624,18 +2237,22 @@ msgid "" "assistance." msgstr "" -#: includes/class-wcs-webhooks.php:83 +#: includes/class-wcs-webhooks.php:90 msgid " Subscription Created" msgstr "" -#: includes/class-wcs-webhooks.php:84 +#: includes/class-wcs-webhooks.php:91 msgid " Subscription Updated" msgstr "" -#: includes/class-wcs-webhooks.php:85 +#: includes/class-wcs-webhooks.php:92 msgid " Subscription Deleted" msgstr "" +#: includes/class-wcs-webhooks.php:93 +msgid " Subscription Switched" +msgstr "" + #: includes/emails/class-wcs-email-cancelled-subscription.php:27 msgid "Cancelled Subscription" msgstr "" @@ -1650,31 +2267,41 @@ msgstr "" msgid "Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:122 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:176 +#: includes/emails/class-wcs-email-cancelled-subscription.php:128 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:183 +#: includes/emails/class-wcs-email-expired-subscription.php:126 +#: includes/emails/class-wcs-email-on-hold-subscription.php:126 msgid "Enable this email notification" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:129 +#: includes/emails/class-wcs-email-cancelled-subscription.php:135 +#: includes/emails/class-wcs-email-expired-subscription.php:133 +#: includes/emails/class-wcs-email-on-hold-subscription.php:133 #. translators: placeholder is admin email msgid "" "Enter recipients (comma separated) for this email. Defaults to " "%s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:136 +#: includes/emails/class-wcs-email-cancelled-subscription.php:142 +#: includes/emails/class-wcs-email-expired-subscription.php:140 +#: includes/emails/class-wcs-email-on-hold-subscription.php:140 msgid "" "This controls the email subject line. Leave blank to use the default " "subject: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:143 +#: includes/emails/class-wcs-email-cancelled-subscription.php:149 +#: includes/emails/class-wcs-email-expired-subscription.php:147 +#: includes/emails/class-wcs-email-on-hold-subscription.php:147 msgid "" "This controls the main heading contained within the email notification. " "Leave blank to use the default heading: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:150 +#: includes/emails/class-wcs-email-cancelled-subscription.php:156 +#: includes/emails/class-wcs-email-expired-subscription.php:154 +#: includes/emails/class-wcs-email-on-hold-subscription.php:154 msgid "Choose which format of email to send." msgstr "" @@ -1717,6 +2344,27 @@ msgid "" "download your files" msgstr "" +#: includes/emails/class-wcs-email-customer-payment-retry.php:27 +msgid "Customer Payment Retry" +msgstr "" + +#: includes/emails/class-wcs-email-customer-payment-retry.php:28 +msgid "" +"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." +msgstr "" + +#: includes/emails/class-wcs-email-customer-payment-retry.php:35 +msgid "Automatic payment failed for {order_number}, we will retry {retry_time}" +msgstr "" + +#: includes/emails/class-wcs-email-customer-payment-retry.php:36 +msgid "Automatic payment failed for order {order_number}" +msgstr "" + #: includes/emails/class-wcs-email-customer-processing-renewal-order.php:24 msgid "Processing Renewal order" msgstr "" @@ -1736,26 +2384,44 @@ msgstr "" msgid "Your {blogname} renewal order receipt from {order_date}" msgstr "" -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:27 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:31 msgid "Customer Renewal Invoice" msgstr "" -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:28 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:32 msgid "" "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." +"automatic recurring payment failed for the initial attempt and all " +"automatic retries (if any). The email contains renewal order information " +"and payment links." msgstr "" -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:35 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:39 msgid "Invoice for renewal order {order_number} from {order_date}" msgstr "" -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:36 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:40 msgid "Invoice for renewal order {order_number}" msgstr "" +#: includes/emails/class-wcs-email-expired-subscription.php:27 +msgid "Expired Subscription" +msgstr "" + +#: includes/emails/class-wcs-email-expired-subscription.php:28 +msgid "Expired Subscription emails are sent when a customer's subscription expires." +msgstr "" + +#: includes/emails/class-wcs-email-expired-subscription.php:30 +msgid "Subscription Expired" +msgstr "" + +#: includes/emails/class-wcs-email-expired-subscription.php:59 +#: includes/emails/class-wcs-email-on-hold-subscription.php:59 +msgid "Subscription argument passed in is not an object." +msgstr "" + #: includes/emails/class-wcs-email-new-renewal-order.php:22 msgid "New Renewal Order" msgstr "" @@ -1789,6 +2455,41 @@ msgstr "" msgid "[{blogname}] Subscription Switched ({order_number}) - {order_date}" msgstr "" +#: includes/emails/class-wcs-email-on-hold-subscription.php:27 +msgid "Suspended Subscription" +msgstr "" + +#: includes/emails/class-wcs-email-on-hold-subscription.php:28 +msgid "" +"Suspended Subscription emails are sent when a customer manually suspends " +"their subscription." +msgstr "" + +#: includes/emails/class-wcs-email-on-hold-subscription.php:30 +msgid "Subscription Suspended" +msgstr "" + +#: includes/emails/class-wcs-email-payment-retry.php:26 +msgid "Payment Retry" +msgstr "" + +#: includes/emails/class-wcs-email-payment-retry.php:27 +msgid "" +"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." +msgstr "" + +#: includes/emails/class-wcs-email-payment-retry.php:29 +msgid "Automatic renewal payment failed" +msgstr "" + +#: includes/emails/class-wcs-email-payment-retry.php:30 +msgid "" +"[{site_title}] Automatic payment failed for {order_number}, retry scheduled " +"to run {retry_time}" +msgstr "" + #: includes/gateways/class-wc-subscriptions-payment-gateways.php:126 msgid "" "Sorry, it seems there are no available payment methods which support " @@ -1796,35 +2497,35 @@ msgid "" "alternate arrangements." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:199 +#: includes/gateways/paypal/class-wcs-paypal.php:204 msgid "Unable to find order for PayPal billing agreement." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:256 +#: includes/gateways/paypal/class-wcs-paypal.php:261 msgid "An error occurred, please try again or try an alternate form of payment." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:359 +#: includes/gateways/paypal/class-wcs-paypal.php:368 #. translators: placeholders are PayPal API error code and PayPal API error #. message msgid "PayPal API error: (%d) %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:364 +#: includes/gateways/paypal/class-wcs-paypal.php:373 #. translators: placeholder is PayPal transaction status message msgid "PayPal Transaction Held: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:376 +#: includes/gateways/paypal/class-wcs-paypal.php:385 #. translators: placeholder is PayPal transaction status message msgid "PayPal payment declined: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:380 +#: includes/gateways/paypal/class-wcs-paypal.php:389 msgid "PayPal payment approved (ID: %s)" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:433 +#: includes/gateways/paypal/class-wcs-paypal.php:442 msgid "" "Are you sure you want to change the payment method from PayPal standard?\n" "\n" @@ -1866,7 +2567,7 @@ msgstr "" #: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:151 #. translators: placeholders are link opening and closing tags. 1$-2$: to -#. gateway settings, 3$-4$: support docs on woothemes.com +#. gateway settings, 3$-4$: support docs on woocommerce.com msgid "" "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." @@ -1880,7 +2581,24 @@ msgid "" "subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:243 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:178 +msgid "" +"%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" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:186 +msgid "Ignore this error (not recommended!)" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:186 +msgid "Open up a ticket now!" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:265 msgid "PayPal Subscription ID:" msgstr "" @@ -1897,7 +2615,7 @@ msgstr "" msgid "SKU: %s" msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php:120 +#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php:119 #. translators: placeholder is localised datetime msgid "expected clearing date %s" msgstr "" @@ -1943,13 +2661,112 @@ msgstr "" msgid "Subscription reactivated with PayPal" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:247 +#: includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php:113 +msgid "PayPal API error - credentials are incorrect." +msgstr "" + +#: includes/payment-retry/class-wcs-retry-admin.php:46 +msgid "Automatic Failed Payment Retries" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-admin.php:97 +msgid "%d Pending Payment Retry" +msgid_plural "%d Pending Payment Retries" +msgstr[0] "" +msgstr[1] "" + +#: includes/payment-retry/class-wcs-retry-admin.php:100 +msgid "%d Processing Payment Retry" +msgid_plural "%d Processing Payment Retries" +msgstr[0] "" +msgstr[1] "" + +#: includes/payment-retry/class-wcs-retry-admin.php:103 +msgid "%d Failed Payment Retry" +msgid_plural "%d Failed Payment Retries" +msgstr[0] "" +msgstr[1] "" + +#: includes/payment-retry/class-wcs-retry-admin.php:106 +msgid "%d Successful Payment Retry" +msgid_plural "%d Successful Payment Retries" +msgstr[0] "" +msgstr[1] "" + +#: includes/payment-retry/class-wcs-retry-admin.php:109 +msgid "%d Cancelled Payment Retry" +msgid_plural "%d Cancelled Payment Retries" +msgstr[0] "" +msgstr[1] "" + +#: includes/payment-retry/class-wcs-retry-admin.php:133 +msgid "Retry Failed Payments" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-admin.php:134 +msgid "Enable automatic retry of failed recurring payments" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-admin.php:138 +msgid "" +"Attempt to recover recurring revenue that would otherwise be lost due to " +"payment methods being declined only temporarily. %sLearn more%s." +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:23 +msgid "" +"Payment retry posts store details about the automatic retry of failed " +"renewal payments." +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:34 +msgid "Renewal Payment Retry" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:36 +msgid "Add" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:37 +msgid "Add New Retry" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:38 +msgid "Edit" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:39 +msgid "Edit Retry" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:40 +msgid "New Retry" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:41 +#: includes/payment-retry/class-wcs-retry-post-store.php:42 +msgid "View Retry" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:43 +msgid "Search Renewal Payment Retries" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:44 +msgid "No retries found" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:45 +msgid "No retries found in trash" +msgstr "" + +#: includes/upgrades/class-wc-subscriptions-upgrader.php:276 #. translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 & #. 1.5") msgid "Database updated to version %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:270 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:299 #. translators: 1$: number of action scheduler hooks upgraded, 2$: #. "{execution_time}", will be replaced on front end with actual time msgid "" @@ -1957,79 +2774,83 @@ msgid "" "seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:286 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:315 #. translators: 1$: number of subscriptions upgraded, 2$: "{execution_time}", #. will be replaced on front end with actual time it took msgid "Migrated %1$s subscriptions to the new structure (in %2$s seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:299 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:328 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag msgid "" "Unable to upgrade subscriptions.
Error: %1$s
Please refresh the " "page and try again. If problem persists, %2$scontact support%3$s." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:554 -msgid "Welcome to WooCommerce Subscriptions 2.0" +#: includes/upgrades/class-wc-subscriptions-upgrader.php:583 +msgid "Welcome to WooCommerce Subscriptions 2.1" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:554 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:583 msgid "About WooCommerce Subscriptions" msgstr "" -#: includes/upgrades/templates/wcs-about.php:20 +#: includes/upgrades/templates/wcs-about-2-0.php:20 msgid "Welcome to Subscriptions 2.0" msgstr "" -#: includes/upgrades/templates/wcs-about.php:23 +#: includes/upgrades/templates/wcs-about-2-0.php:23 +#: includes/upgrades/templates/wcs-about.php:22 msgid "Thank you for updating to the latest version of WooCommerce Subscriptions." msgstr "" -#: includes/upgrades/templates/wcs-about.php:24 +#: includes/upgrades/templates/wcs-about-2-0.php:24 msgid "" "Version 2.0 has been in development for more than a year. We've reinvented " "the extension to take into account 3 years of feedback from store managers." msgstr "" -#: includes/upgrades/templates/wcs-about.php:25 +#: includes/upgrades/templates/wcs-about-2-0.php:25 +#: includes/upgrades/templates/wcs-about.php:24 msgid "We hope you enjoy it!" msgstr "" -#: includes/upgrades/templates/wcs-about.php:31 +#: includes/upgrades/templates/wcs-about-2-0.php:31 +#: includes/upgrades/templates/wcs-about.php:30 #. translators: placeholder is version number msgid "Version %s" msgstr "" -#: includes/upgrades/templates/wcs-about.php:42 +#: includes/upgrades/templates/wcs-about-2-0.php:42 +#: includes/upgrades/templates/wcs-about.php:41 msgid "Check Out What's New" msgstr "" -#: includes/upgrades/templates/wcs-about.php:51 +#: includes/upgrades/templates/wcs-about-2-0.php:51 msgid "Multiple Subscriptions" msgstr "" -#: includes/upgrades/templates/wcs-about.php:52 +#: includes/upgrades/templates/wcs-about-2-0.php:52 msgid "It's now easier for your customers to buy more subscriptions!" msgstr "" -#: includes/upgrades/templates/wcs-about.php:53 +#: includes/upgrades/templates/wcs-about-2-0.php:53 msgid "" "Customers can now purchase different subscription products in one " "transaction. The products can bill on any schedule and have any combination " "of sign-up fees and/or free trials." msgstr "" -#: includes/upgrades/templates/wcs-about.php:56 +#: includes/upgrades/templates/wcs-about-2-0.php:56 #. translators: placeholders are opening and closing link tags msgid "Learn more about the new %smultiple subscriptions%s feature." msgstr "" -#: includes/upgrades/templates/wcs-about.php:68 +#: includes/upgrades/templates/wcs-about-2-0.php:68 msgid "New Add/Edit Subscription Screen" msgstr "" -#: includes/upgrades/templates/wcs-about.php:69 +#: includes/upgrades/templates/wcs-about-2-0.php:69 msgid "" "Subscriptions v2.0 introduces a new administration interface to add or edit " "a subscription. You can make all the familiar changes, like modifying " @@ -2038,7 +2859,7 @@ msgid "" "adding a product line item." msgstr "" -#: includes/upgrades/templates/wcs-about.php:72 +#: includes/upgrades/templates/wcs-about-2-0.php:72 #. translators: placeholders are opening and closing tags msgid "" "The new interface is also built on the existing %sEdit Order%s screen. If " @@ -2046,7 +2867,7 @@ msgid "" "subscription." msgstr "" -#: includes/upgrades/templates/wcs-about.php:76 +#: includes/upgrades/templates/wcs-about-2-0.php:76 #. translators: placeholers are link tags: 1$-2$ new subscription page, 3$-4$: #. docs on woothemes msgid "" @@ -2054,11 +2875,11 @@ msgid "" "interface." msgstr "" -#: includes/upgrades/templates/wcs-about.php:87 +#: includes/upgrades/templates/wcs-about-2-0.php:87 msgid "New View Subscription Page" msgstr "" -#: includes/upgrades/templates/wcs-about.php:91 +#: includes/upgrades/templates/wcs-about-2-0.php:91 #. translators: placeholders are opening and closing tags msgid "" "Your customers can now view the full details of a subscription, including " @@ -2066,19 +2887,19 @@ msgid "" "orders, from a special %sMy Account > View Subscription%s page." msgstr "" -#: includes/upgrades/templates/wcs-about.php:93 +#: includes/upgrades/templates/wcs-about-2-0.php:93 msgid "" "This new page is also where the customer can suspend or cancel their " "subscription, change payment method, change shipping address or " "upgrade/downgrade an item." msgstr "" -#: includes/upgrades/templates/wcs-about.php:97 +#: includes/upgrades/templates/wcs-about-2-0.php:97 #. translators: placeholders are opening and closing link tags msgid "Learn more about the new %sView Subscription page%s." msgstr "" -#: includes/upgrades/templates/wcs-about.php:111 +#: includes/upgrades/templates/wcs-about-2-0.php:111 #. translators: placeholders are for opening and closing link () tags msgid "" "By default, adding new files to an existing subscription product will " @@ -2087,14 +2908,24 @@ msgid "" "subscribers with access to new files only after the next renewal payment." msgstr "" -#: includes/upgrades/templates/wcs-about.php:115 -#: includes/upgrades/templates/wcs-about.php:128 -#: includes/upgrades/templates/wcs-about.php:141 +#: includes/upgrades/templates/wcs-about-2-0.php:115 +#: includes/upgrades/templates/wcs-about-2-0.php:128 +#: includes/upgrades/templates/wcs-about-2-0.php:141 +#: includes/upgrades/templates/wcs-about.php:120 +#: includes/upgrades/templates/wcs-about.php:131 +#: includes/upgrades/templates/wcs-about.php:142 +#: includes/upgrades/templates/wcs-about.php:170 +#: includes/upgrades/templates/wcs-about.php:191 #. translators: placeholders are for opening and closing link () tags +#. translators: placeholders are for opening and closing link () tags +#. translators: placeholders are opening and closing anchor tags linking to +#. documentation +#. translators: placeholders are opening and closing anchor tags linking to +#. documentation msgid "%sLearn more »%s" msgstr "" -#: includes/upgrades/templates/wcs-about.php:124 +#: includes/upgrades/templates/wcs-about-2-0.php:124 #. translators: placeholders are opening and closing tags msgid "" "For a store manager to change a subscription from automatic to manual " @@ -2104,11 +2935,11 @@ msgid "" "Subscription%s interface." msgstr "" -#: includes/upgrades/templates/wcs-about.php:134 +#: includes/upgrades/templates/wcs-about-2-0.php:134 msgid "Change Trial and End Dates" msgstr "" -#: includes/upgrades/templates/wcs-about.php:137 +#: includes/upgrades/templates/wcs-about-2-0.php:137 #. translators: placeholders are opening and closing tags msgid "" "It was already possible to change a subscription's next payment date, but " @@ -2117,52 +2948,53 @@ msgid "" "of these dates from the %sEdit Subscription%s screen." msgstr "" -#: includes/upgrades/templates/wcs-about.php:150 +#: includes/upgrades/templates/wcs-about-2-0.php:150 msgid "And much more..." msgstr "" -#: includes/upgrades/templates/wcs-about.php:157 +#: includes/upgrades/templates/wcs-about-2-0.php:157 +#: includes/upgrades/templates/wcs-about.php:151 msgid "Peek Under the Hood for Developers" msgstr "" -#: includes/upgrades/templates/wcs-about.php:158 +#: includes/upgrades/templates/wcs-about-2-0.php:158 msgid "" "Subscriptions 2.0 introduces a new architecture built on the WooCommerce " "Custom Order Types API." msgstr "" -#: includes/upgrades/templates/wcs-about.php:164 +#: includes/upgrades/templates/wcs-about-2-0.php:164 #. translators: placeholders are opening and closing code tags msgid "New %sshop_subscription%s Post Type" msgstr "" -#: includes/upgrades/templates/wcs-about.php:166 +#: includes/upgrades/templates/wcs-about-2-0.php:166 msgid "" "By making a subscription a Custom Order Type, a subscription is also now a " "custom post type. This makes it faster to query subscriptions and it uses a " "database schema that is as scalable as WordPress posts and pages." msgstr "" -#: includes/upgrades/templates/wcs-about.php:169 +#: includes/upgrades/templates/wcs-about-2-0.php:169 #. translators: placeholders are opening and closing tags msgid "" "Developers can also now use all the familiar WordPress functions, like " "%sget_posts()%s, to query or modify subscription data." msgstr "" -#: includes/upgrades/templates/wcs-about.php:175 +#: includes/upgrades/templates/wcs-about-2-0.php:175 #. translators: placeholders are opening and closing tags msgid "New %sWC_Subscription%s Object" msgstr "" -#: includes/upgrades/templates/wcs-about.php:177 +#: includes/upgrades/templates/wcs-about-2-0.php:177 msgid "" "Subscriptions 2.0 introduces a new object for working with a subscription " "at the application level. The cumbersome APIs for retrieving or modifying a " "subscription's data are gone!" msgstr "" -#: includes/upgrades/templates/wcs-about.php:180 +#: includes/upgrades/templates/wcs-about-2-0.php:180 #. translators: all placeholders are opening and closing tags, no need #. to order them msgid "" @@ -2171,18 +3003,18 @@ msgid "" "%s$subscription->get_total()%s." msgstr "" -#: includes/upgrades/templates/wcs-about.php:184 +#: includes/upgrades/templates/wcs-about-2-0.php:184 msgid "REST API Endpoints" msgstr "" -#: includes/upgrades/templates/wcs-about.php:185 +#: includes/upgrades/templates/wcs-about-2-0.php:185 msgid "" "We didn't just improve interfaces for humans, we also improved them for " "computers. Your applications can now create, read, update or delete " "subscriptions via RESTful API endpoints." msgstr "" -#: includes/upgrades/templates/wcs-about.php:188 +#: includes/upgrades/templates/wcs-about-2-0.php:188 #. translators: all placeholders are opening and closing tags, no need #. to order them msgid "" @@ -2191,10 +3023,283 @@ msgid "" "subscription? Get %s/wc-api/v2/subscriptions//%s." msgstr "" -#: includes/upgrades/templates/wcs-about.php:194 +#: includes/upgrades/templates/wcs-about-2-0.php:194 msgid "Go to WooCommerce Subscriptions Settings" msgstr "" +#: includes/upgrades/templates/wcs-about.php:19 +msgid "Welcome to Subscriptions 2.1!" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:23 +msgid "" +"Version 2.1 introduces some great new features requested by store managers " +"just like you (and possibly even by %syou%s)." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:49 +msgid "Subscription Reports" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:50 +msgid "" +"How many customers stay subscribed for more than 6 months? What is the " +"average lifetime value of your subscribers? How much renewal revenue will " +"your store earn next month?" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:51 +msgid "These are important questions for any subscription commerce business." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:52 +msgid "" +"Prior to Subscriptions 2.1, they were not easy to answer. Subscriptions 2.1 " +"introduces new reports to answer these questions, and many more." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:54 +msgid "View Reports" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:67 +msgid "Automatic Failed Payment Retry" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:68 +msgid "" +"Failed recurring payments can now be retried automatically. This helps " +"recover revenue that would otherwise be lost due to payment methods being " +"declined only temporarily." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:69 +msgid "" +"By default, Subscriptions will retry the payment 5 times over 7 days. The " +"rules that control the retry system can be modified to customise:" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:71 +msgid "the total number of retry attempts" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:72 +msgid "how long to wait between retry attempts" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:73 +msgid "emails sent to the customer and store manager" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:74 +msgid "the status applied to the renewal order and subscription" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:76 +msgid "" +"The retry system is disabled by default. To enable it, visit the " +"Subscriptions settings administration screen." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:78 +msgid "Enable Automatic Retry" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:90 +msgid "New Subscription Emails" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:91 +msgid "Subscriptions 2.1 also introduces a number of new emails to notify you when:" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:93 +msgid "a customer suspends a subscription" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:94 +msgid "an automatic payment fails" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:95 +msgid "a subscription expires" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:97 +msgid "" +"These emails can be enabled, disabled and customised under the " +"%sWooCommerce > Settings > Emails%s administration screen." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:99 +msgid "View Email Settings" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:108 +msgid "But wait, there's more!" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:109 +msgid "" +"That's not all we've working on for the last 12 months when it comes to " +"Subscriptions. We've also released free mini-extensions to help you get the " +"most from your subscription store." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:115 +msgid "Subscription Gifting" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:116 +msgid "" +"What happens when a customer wants to purchase a subscription product for " +"someone else?" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:117 +msgid "" +"The free Gifting extension makes it possible for one person to purchase a " +"subscription product for someone else. It then shares control of the " +"subscription between the purchaser and recipient, allowing both to manage " +"the subscription over its lifecycle." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:127 +msgid "" +"Import subscriptions to WooCommerce via CSV, or export your subscriptions " +"from WooCommerce to a CSV with the WooCommerce Subscriptions " +"Importer/Exporter extension." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:128 +msgid "" +"This free extension makes it possible to migrate subscribers from 3rd party " +"systems to WooCommerce. It also makes it possible to export your " +"subscription data for analysis in spreadsheet tools or 3rd party apps." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:137 +msgid "Subscribe All the Things" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:138 +msgid "Want your customers to be able to subscribe to non-subscription products?" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:139 +msgid "" +"With WooCommerce Subscribe All the Things, they can! This experimental " +"extension is exploring how to convert any product, including Product " +"Bundles and Composite Products, into a subscription product. It also offers " +"customers a way to subscribe to a cart of non-subscription products." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:157 +#. translators: placeholders are opening and closing tags +msgid "Customise Retry Rules" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:159 +msgid "" +"The best part about the new automatic retry system is that the retry rules " +"are completely customisable." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:162 +#. translators: all placeholders are opening and closing tags, no need +#. to order them +msgid "" +"With the %s'wcs_default_retry_rules'%s filter, you can define a set of " +"default rules to apply to all failed payments in your store." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:166 +#. translators: all placeholders are opening and closing tags, no need +#. to order them +msgid "" +"To apply a specific rule based on certain conditions, like high value " +"orders or an infrequent renewal schedule, you can use the retry specific " +"%s'wcs_get_retry_rule'%s filter. This provides the ID of the renewal order " +"for the failed payment, which can be used to find information about the " +"products, subscription and totals to which the failed payment relates." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:174 +msgid "WP REST API Endpoints" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:177 +#. translators: $1: opening tag linking to WC API docs, $2: closing +#. tag, $3: opening tag linking to WP API docs, $4: closing tag +msgid "" +"WooCommerce 2.6 added support for %1$sREST API%2$s endpoints built on " +"WordPress core's %3$sREST API%4$s infrastructure." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:179 +msgid "Subscriptions 2.1 adds support for subscription data to this infrastructure." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:180 +msgid "" +"Your applications can now create, read, update or delete subscriptions via " +"RESTful API endpoints with the same design as the latest version of " +"WooCommerce's REST API endpoints." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:183 +#. translators: all placeholders are opening and closing tags, no need +#. to order them +msgid "" +"Want to list all the subscriptions on a site? Get " +"%s/wp-json/wc/v1/subscriptions%s." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:187 +#. translators: all placeholders are opening and closing tags, no need +#. to order them +msgid "" +"Want the details of a specific subscription? Get " +"%s/wp-json/wc/v1/subscriptions//%s." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:197 +#. translators: placeholders are opening and closing code tags +msgid "Honour Renewal Order Data" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:199 +msgid "" +"In previous versions of Subscriptions, the subscription total was passed to " +"payment gateways as the amount to charge for automatic renewal payments. " +"This made it unnecessarily complicated to add one-time fees or discounts to " +"a renewal." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:202 +#. translators: placeholders are opening and closing tags +msgid "" +"Subscriptions 2.1 now passes the renewal order's total, making it possible " +"to add a fee or discount to the renewal order with simple one-liners like " +"%s$order->add_fee()%s or %s$order->add_coupon()%s." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:206 +#. translators: placeholders are opening and closing tags +msgid "" +"Subscriptions also now uses the renewal order to setup the cart for " +"%smanual renewals%s, making it easier to add products or discounts to a " +"single renewal paid manually." +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:212 +msgid "See the full guide to What's New in Subscriptions version 2.1 »" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:213 +msgid "Go to WooCommerce Subscriptions Settings »" +msgstr "" + #: includes/upgrades/templates/wcs-upgrade-in-progress.php:24 msgid "WooCommerce Subscriptions Update in Progress" msgstr "" @@ -2313,21 +3418,21 @@ msgstr "" msgid "There was an error with the update. Please refresh the page and try again." msgstr "" -#: includes/wcs-cart-functions.php:73 +#: includes/wcs-cart-functions.php:75 msgid "Shipping via %s" msgstr "" -#: includes/wcs-cart-functions.php:91 +#: includes/wcs-cart-functions.php:95 msgid "Shipping" msgid_plural "Shipping %d" msgstr[0] "" msgstr[1] "" -#: includes/wcs-cart-functions.php:220 +#: includes/wcs-cart-functions.php:224 msgid "Free shipping coupon" msgstr "" -#: includes/wcs-cart-functions.php:323 +#: includes/wcs-cart-functions.php:327 #. translators: placeholder is a date msgid "First renewal: %s" msgstr "" @@ -2437,27 +3542,27 @@ msgstr "" msgid "MM" msgstr "" -#: includes/wcs-order-functions.php:267 +#: includes/wcs-order-functions.php:298 msgid "Subscription Renewal Order – %s" msgstr "" -#: includes/wcs-order-functions.php:270 +#: includes/wcs-order-functions.php:301 msgid "Resubscribe Order – %s" msgstr "" -#: includes/wcs-order-functions.php:289 +#: includes/wcs-order-functions.php:320 msgid "$type passed to the function was not a string." msgstr "" -#: includes/wcs-order-functions.php:294 +#: includes/wcs-order-functions.php:325 msgid "\"%s\" is not a valid new order type." msgstr "" -#: includes/wcs-order-functions.php:396 +#: includes/wcs-order-functions.php:512 msgid "Invalid data. No valid subscription / order was passed in." msgstr "" -#: includes/wcs-order-functions.php:400 +#: includes/wcs-order-functions.php:516 msgid "Invalid data. No valid item id was passed in." msgstr "" @@ -2486,56 +3591,71 @@ msgstr[0] "" msgstr[1] "" #: includes/wcs-user-functions.php:279 -#: templates/single-product/add-to-cart/subscription.php:42 -#: templates/single-product/add-to-cart/variable-subscription.php:30 +#: templates/single-product/add-to-cart/subscription.php:41 +#: templates/single-product/add-to-cart/variable-subscription.php:29 msgid "Resubscribe" msgstr "" -#: templates/admin/html-variation-price.php:20 -msgid "Sign-up Fee: (%s)" +#: templates/admin/deprecated/html-variation-price.php:20 +#: templates/admin/deprecated/html-variation-price.php:30 +#. translators: placeholder is a currency symbol / code +msgid "Subscription Price (%s)" msgstr "" -#: templates/admin/html-variation-price.php:25 -msgid "Free Trial:" +#: templates/admin/deprecated/html-variation-price.php:46 +msgid "Subscription Periods" msgstr "" -#: templates/admin/html-variation-price.php:32 -msgid "Subscription Trial Period:" +#: templates/admin/deprecated/html-variation-price.php:69 +msgid "Subscription Length" msgstr "" -#: templates/admin/html-variation-price.php:50 -msgid "Billing Interval:" +#: templates/admin/deprecated/html-variation-price.php:85 +msgid "Sign-up Fee (%s)" msgstr "" -#: templates/admin/html-variation-price.php:57 -msgid "Billing Period:" +#: templates/admin/deprecated/html-variation-price.php:97 +#: templates/admin/deprecated/html-variation-price.php:104 +msgid "Free Trial" msgstr "" -#: templates/admin/html-variation-price.php:66 -msgid "Subscription Length:" +#: templates/admin/deprecated/html-variation-synchronisation.php:30 +msgid "Synchronise Renewals" msgstr "" -#: templates/admin/post-types/writepanels/order-shipping-html.php:8 +#: templates/admin/deprecated/order-shipping-html.php:8 msgid "Label" msgstr "" -#: templates/admin/post-types/writepanels/order-shipping-html.php:13 +#: templates/admin/deprecated/order-shipping-html.php:13 msgid "Shipping Method" msgstr "" -#: templates/admin/post-types/writepanels/order-shipping-html.php:34 -#: templates/admin/post-types/writepanels/order-shipping-html.php:36 +#: templates/admin/deprecated/order-shipping-html.php:34 +#: templates/admin/deprecated/order-shipping-html.php:36 msgid "Other" msgstr "" -#: templates/admin/post-types/writepanels/order-tax-html.php:17 +#: templates/admin/deprecated/order-tax-html.php:17 msgid "Recurring Sales Tax:" msgstr "" -#: templates/admin/post-types/writepanels/order-tax-html.php:21 +#: templates/admin/deprecated/order-tax-html.php:21 msgid "Shipping Tax:" msgstr "" +#: templates/admin/html-variation-price.php:31 +msgid "Subscription trial period:" +msgstr "" + +#: templates/admin/html-variation-price.php:49 +msgid "Billing interval:" +msgstr "" + +#: templates/admin/html-variation-price.php:56 +msgid "Billing Period:" +msgstr "" + #: templates/cart/cart-recurring-shipping.php:19 msgid "Recurring shipping options can be selected on checkout." msgstr "" @@ -2569,55 +3689,32 @@ msgstr "" msgid "Recurring Total" msgstr "" -#: templates/emails/admin-new-renewal-order.php:65 -#: templates/emails/customer-processing-renewal-order.php:65 -#: templates/emails/plain/admin-new-renewal-order.php:50 -#: templates/myaccount/view-subscription.php:243 -msgid "Customer details" -msgstr "" - -#: templates/emails/admin-new-renewal-order.php:71 -#: templates/emails/customer-processing-renewal-order.php:71 -#. translators: $1: opening tag, $2: closing tag, $3: billing -#. email -msgid "%1$sEmail:%2$s %3$s" -msgstr "" - -#: templates/emails/admin-new-renewal-order.php:79 -#: templates/emails/customer-processing-renewal-order.php:79 -#. translators: $1: opening tag, $2: closing tag, $3: billing -#. phone -msgid "%1$sTel:%2$s %3$s" -msgstr "" - #: templates/emails/admin-new-switch-order.php:24 msgid "Switch Order Details" msgstr "" -#: templates/emails/admin-new-switch-order.php:29 -#: templates/emails/customer-completed-switch-order.php:30 -#: templates/emails/customer-renewal-invoice.php:35 -#. translators: placeholder is the order's number -msgid "Order: %s" -msgstr "" - -#: templates/emails/admin-new-switch-order.php:67 -#: templates/emails/customer-completed-switch-order.php:66 +#: templates/emails/admin-new-switch-order.php:30 +#: templates/emails/customer-completed-switch-order.php:28 msgid "New Subscription Details" msgstr "" -#: templates/emails/admin-new-switch-order.php:71 -#: templates/emails/customer-completed-switch-order.php:70 -msgid "Subscription %s" +#: templates/emails/admin-payment-retry.php:28 +#: templates/emails/plain/admin-payment-retry.php:21 +msgid "The renewal order is as follows:" msgstr "" #: templates/emails/cancelled-subscription.php:19 #: templates/emails/plain/cancelled-subscription.php:16 -#. translators: $1: customer's billing first name, $2: customer's billing last -#. name +#. translators: $1: customer's billing first name and last name msgid "" -"A subscription belonging to %1$s %2$s has been cancelled. Their " -"subscription's details are as follows:" +"A subscription belonging to %1$s has been cancelled. Their subscription's " +"details are as follows:" +msgstr "" + +#: templates/emails/cancelled-subscription.php:46 +#: templates/emails/expired-subscription.php:46 +#: templates/emails/on-hold-subscription.php:46 +msgid "-" msgstr "" #: templates/emails/customer-completed-renewal-order.php:20 @@ -2636,10 +3733,6 @@ msgid "" "new order and subscription details are shown below for your reference:" msgstr "" -#: templates/emails/customer-completed-switch-order.php:26 -msgid "Order Details" -msgstr "" - #: templates/emails/customer-processing-renewal-order.php:17 #: templates/emails/plain/customer-processing-renewal-order.php:15 msgid "" @@ -2652,53 +3745,62 @@ msgstr "" msgid "Pay Now »" msgstr "" -#: templates/emails/plain/admin-new-renewal-order.php:24 -#: templates/emails/plain/admin-new-switch-order.php:24 -#: templates/emails/plain/customer-completed-renewal-order.php:22 -#: templates/emails/plain/customer-completed-switch-order.php:22 -#: templates/emails/plain/customer-processing-renewal-order.php:21 -#: templates/emails/plain/customer-renewal-invoice.php:27 -msgid "Order number: %s" +#: templates/emails/expired-subscription.php:19 +#: templates/emails/plain/expired-subscription.php:16 +#. translators: $1: customer's billing first name and last name +msgid "" +"A subscription belonging to %1$s has expired. Their subscription's details " +"are as follows:" msgstr "" -#: templates/emails/plain/admin-new-renewal-order.php:25 -#: templates/emails/plain/customer-completed-renewal-order.php:23 -#: templates/emails/plain/customer-completed-switch-order.php:23 -#: templates/emails/plain/customer-processing-renewal-order.php:22 -#: templates/emails/plain/customer-renewal-invoice.php:28 -msgid "Order date: %s" +#: templates/emails/on-hold-subscription.php:19 +#: templates/emails/plain/on-hold-subscription.php:16 +#. translators: $1: customer's billing first name and last name +msgid "" +"A subscription belonging to %1$s has been suspended by the user. Their " +"subscription's details are as follows:" msgstr "" -#: templates/emails/plain/admin-new-switch-order.php:47 -#. translators: placeholder is edit post link for the order -msgid "View order: %s" -msgstr "" - -#: templates/emails/plain/cancelled-subscription.php:22 +#: templates/emails/plain/cancelled-subscription.php:32 +#: templates/emails/plain/expired-subscription.php:32 +#: templates/emails/plain/on-hold-subscription.php:32 #. translators: placeholder is last time subscription was paid msgid "Last Payment: %s" msgstr "" -#: templates/emails/plain/cancelled-subscription.php:28 +#: templates/emails/plain/cancelled-subscription.php:39 #. translators: placeholder is localised date string msgid "End of Prepaid Term: %s" msgstr "" -#: templates/emails/plain/customer-completed-renewal-order.php:48 -#: templates/emails/plain/customer-processing-renewal-order.php:47 -msgid "Your details" -msgstr "" - -#: templates/emails/plain/customer-completed-switch-order.php:45 +#: templates/emails/plain/customer-completed-switch-order.php:23 #. translators: placeholder is order's view url msgid "View your order: %s" msgstr "" -#: templates/emails/plain/customer-completed-switch-order.php:70 +#: templates/emails/plain/customer-completed-switch-order.php:34 #. translators: placeholder is subscription's view url msgid "View your subscription: %s" msgstr "" +#: templates/emails/plain/email-order-details.php:16 +msgid "Order number: %s" +msgstr "" + +#: templates/emails/plain/email-order-details.php:17 +msgid "Order date: %s" +msgstr "" + +#: templates/emails/plain/expired-subscription.php:39 +#. translators: placeholder is localised date string +msgid "End Date: %s" +msgstr "" + +#: templates/emails/plain/on-hold-subscription.php:36 +#. translators: placeholder is localised date string +msgid "Date Suspended: %s" +msgstr "" + #: templates/emails/plain/subscription-info.php:16 #: templates/emails/subscription-info.php:14 msgid "Subscription Information:" @@ -2753,24 +3855,12 @@ msgstr "" msgid "Subscription Totals" msgstr "" -#: templates/myaccount/view-subscription.php:110 +#: templates/myaccount/view-subscription.php:109 msgid "Are you sure you want remove this item from your subscription?" msgstr "" -#: templates/myaccount/view-subscription.php:224 -msgid "Refunded:" -msgstr "" - -#: templates/myaccount/view-subscription.php:271 wcs-functions.php:254 -msgid "Billing Address" -msgstr "" - -#: templates/myaccount/view-subscription.php:290 wcs-functions.php:253 -msgid "Shipping Address" -msgstr "" - -#: templates/single-product/add-to-cart/subscription.php:45 -#: templates/single-product/add-to-cart/variable-subscription.php:33 +#: templates/single-product/add-to-cart/subscription.php:43 +#: templates/single-product/add-to-cart/variable-subscription.php:31 msgid "You have an active subscription to this product already." msgstr "" @@ -2778,91 +3868,99 @@ msgstr "" msgid "This product is currently out of stock and unavailable." msgstr "" -#: templates/single-product/add-to-cart/variable-subscription.php:45 +#: templates/single-product/add-to-cart/variable-subscription.php:43 msgid "Clear selection" msgstr "" -#: wcs-functions.php:226 +#: wcs-functions.php:228 msgid "Can not get status name. Status is not a string." msgstr "" -#: wcs-functions.php:249 +#: wcs-functions.php:251 msgid "Can not get address type display name. Address type is not a string." msgstr "" -#: wcs-functions.php:290 +#: wcs-functions.php:255 +msgid "Shipping Address" +msgstr "" + +#: wcs-functions.php:256 +msgid "Billing Address" +msgstr "" + +#: wcs-functions.php:314 msgid "Date type is not a string." msgstr "" -#: wcs-functions.php:292 +#: wcs-functions.php:316 msgid "Date type can not be an empty string." msgstr "" -#: woocommerce-subscriptions.php:215 +#: woocommerce-subscriptions.php:220 msgid "This is where subscriptions are stored." msgstr "" -#: woocommerce-subscriptions.php:259 +#: woocommerce-subscriptions.php:264 msgid "No Subscriptions found" msgstr "" -#: woocommerce-subscriptions.php:261 +#: woocommerce-subscriptions.php:266 msgid "" "Subscriptions will appear here for you to view and manage once purchased by " "a customer." msgstr "" -#: woocommerce-subscriptions.php:263 +#: woocommerce-subscriptions.php:268 #. translators: placeholders are opening and closing link tags msgid "%sLearn more about managing subscriptions »%s" msgstr "" -#: woocommerce-subscriptions.php:265 +#: woocommerce-subscriptions.php:270 #. translators: placeholders are opening and closing link tags msgid "%sAdd a subscription product »%s" msgstr "" -#: woocommerce-subscriptions.php:379 +#: woocommerce-subscriptions.php:384 msgid "" "A subscription renewal has been removed from your cart. Multiple " "subscriptions can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:385 +#: woocommerce-subscriptions.php:390 msgid "" "A subscription has been removed from your cart. Due to payment gateway " "restrictions, different subscription products can not be purchased at the " "same time." msgstr "" -#: woocommerce-subscriptions.php:391 +#: woocommerce-subscriptions.php:396 msgid "" "A subscription has been removed from your cart. Products and subscriptions " "can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:525 woocommerce-subscriptions.php:542 +#: woocommerce-subscriptions.php:530 woocommerce-subscriptions.php:547 #. translators: placeholder is a number, this is for the teens #. translators: placeholder is a number, numbers ending in 4-9, 0 msgid "%sth" msgstr "" -#: woocommerce-subscriptions.php:530 +#: woocommerce-subscriptions.php:535 #. translators: placeholder is a number, numbers ending in 1 msgid "%sst" msgstr "" -#: woocommerce-subscriptions.php:534 +#: woocommerce-subscriptions.php:539 #. translators: placeholder is a number, numbers ending in 2 msgid "%snd" msgstr "" -#: woocommerce-subscriptions.php:538 +#: woocommerce-subscriptions.php:543 #. translators: placeholder is a number, numbers ending in 3 msgid "%srd" msgstr "" -#: woocommerce-subscriptions.php:568 +#: woocommerce-subscriptions.php:573 #. translators: 1$-2$: opening and closing tags, 3$-4$: link tags, #. takes to woocommerce plugin on wp.org, 5$-6$: opening and closing link tags, #. leads to plugins.php in admin @@ -2872,26 +3970,20 @@ msgid "" "%5$sinstall & activate WooCommerce »%6$s" msgstr "" -#: woocommerce-subscriptions.php:575 +#: woocommerce-subscriptions.php:580 #. translators: 1$-2$: opening and closing tags, 3$-4$: opening and #. closing link tags, leads to plugin admin msgid "" "%1$sWooCommerce Subscriptions is inactive.%2$s This version of " -"Subscriptions requires WooCommerce 2.3 or newer. Please %3$supdate " -"WooCommerce to version 2.3 or newer »%4$s" +"Subscriptions requires WooCommerce 2.4 or newer. Please %3$supdate " +"WooCommerce to version 2.4 or newer »%4$s" msgstr "" -#: woocommerce-subscriptions.php:746 -#. translators: 1$-2$: opening and closing tags, 3$-4$: opening and -#. closing link tags, leads to plugin admin -msgid "" -"%1$sYou have an out-of-date version of WooCommerce installed%2$s. " -"WooCommerce Subscriptions no longer supports versions of WooCommerce prior " -"to 2.3. Please %3$supgrade WooCommerce to version 2.3 or newer%4$s to avoid " -"issues." +#: woocommerce-subscriptions.php:606 +msgid "Variable Subscription" msgstr "" -#: woocommerce-subscriptions.php:781 +#: woocommerce-subscriptions.php:765 #. translators: 1$-2$: opening and closing tags, 3$-4$: opening and #. closing link tags. Leads to duplicate site article on docs msgid "" @@ -2901,19 +3993,19 @@ msgid "" "environment. %3$sLearn more »%4$s." msgstr "" -#: woocommerce-subscriptions.php:783 +#: woocommerce-subscriptions.php:767 msgid "Quit nagging me (but don't enable automatic payments)" msgstr "" -#: woocommerce-subscriptions.php:784 +#: woocommerce-subscriptions.php:768 msgid "Enable automatic payments" msgstr "" -#: woocommerce-subscriptions.php:962 +#: woocommerce-subscriptions.php:946 msgid "Support" msgstr "" -#: woocommerce-subscriptions.php:1063 +#: woocommerce-subscriptions.php:1047 #. translators: placeholders are opening and closing tags. Leads to docs on #. version 2 msgid "" @@ -2924,14 +4016,14 @@ msgid "" "2.0 »%s" msgstr "" -#: woocommerce-subscriptions.php:1078 +#: woocommerce-subscriptions.php:1062 msgid "" "Warning! You are running version %s of WooCommerce Subscriptions plugin " "code but your database has been upgraded to Subscriptions version 2.0. This " "will cause major problems on your store." msgstr "" -#: woocommerce-subscriptions.php:1079 +#: woocommerce-subscriptions.php:1063 msgid "" "Please upgrade the WooCommerce Subscriptions plugin to version 2.0 or newer " "immediately. If you need assistance, after upgrading to Subscriptions v2.0, " @@ -2943,7 +4035,7 @@ msgid "WooCommerce Subscriptions" msgstr "" #. Plugin URI of the plugin/theme -msgid "http://www.woothemes.com/products/woocommerce-subscriptions/" +msgid "http://www.woocommerce.com/products/woocommerce-subscriptions/" msgstr "" #. Description of the plugin/theme @@ -2960,34 +4052,31 @@ msgstr "" msgid "http://prospress.com/" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:152 -#: includes/admin/class-wc-subscriptions-admin.php:195 -#: templates/admin/deprecated/html-variation-price.php:31 -#: templates/admin/deprecated/html-variation-price.php:86 -#: templates/admin/html-variation-price.php:21 -#: templates/admin/html-variation-price.php:48 -msgctxt "example price" -msgid "e.g. 9.90" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:175 -msgctxt "for in \"Every month _for_ 12 months\"" -msgid "for" -msgstr "" - -#: includes/admin/class-wc-subscriptions-admin.php:219 -#: templates/admin/deprecated/html-variation-price.php:118 -#: templates/admin/html-variation-price.php:27 +#: includes/admin/class-wc-subscriptions-admin.php:145 #. translators: placeholder is trial period validation message if passed an #. invalid value (e.g. "Trial period can not exceed 4 weeks") -msgctxt "Trial period dropdown's description in pricing fields" +msgctxt "Trial period field tooltip on Edit Product administration screen" msgid "" "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" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:751 +#: includes/admin/class-wc-subscriptions-admin.php:158 +msgctxt "example price" +msgid "e.g. 5.90" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:192 +#: templates/admin/deprecated/html-variation-price.php:31 +#: templates/admin/deprecated/html-variation-price.php:86 +#: templates/admin/html-variation-price.php:21 +#: templates/admin/html-variation-price.php:47 +msgctxt "example price" +msgid "e.g. 9.90" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:725 #. translators: placeholders are for HTML tags. They are 1$: "

", 2$: #. "

", 3$: "

", 4$: "", 5$: "", 6$: "", 7$: "", 8$: #. "

" @@ -2998,7 +4087,7 @@ msgid "" "%6$sVariable subscription%7$s.%8$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:753 +#: includes/admin/class-wc-subscriptions-admin.php:727 #. translators: placeholders are for HTML tags. They are 1$: "

", 2$: #. "

", 3$: "

", 4$: "

" msgctxt "used in admin pointer script params in javascript as price pointer content" @@ -3008,74 +4097,74 @@ msgid "" "sign-up fee and free trial.%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1083 +#: includes/admin/class-wc-subscriptions-admin.php:1038 msgctxt "option section heading" msgid "Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1115 +#: includes/admin/class-wc-subscriptions-admin.php:1070 msgctxt "options section heading" msgid "Miscellaneous" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1123 +#: includes/admin/class-wc-subscriptions-admin.php:1078 msgctxt "there's a number immediately in front of this text" msgid "suspensions per billing period." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1390 +#: includes/admin/class-wc-subscriptions-admin.php:1323 msgctxt "in [subscriptions] shortcode" msgid "No subscriptions found." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1399 +#: includes/admin/class-wc-subscriptions-admin.php:1332 #. translators: order number msgctxt "in [subscriptions] shortcode" msgid "Subscription %s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1419 +#: includes/admin/class-wc-subscriptions-admin.php:1352 msgctxt "label that indicates whether debugging is turned on for the plugin" msgid "WCS_DEBUG" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1425 +#: includes/admin/class-wc-subscriptions-admin.php:1358 msgctxt "Live or Staging, Label on WooCommerce -> System Status page" msgid "Subscriptions Mode" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1426 +#: includes/admin/class-wc-subscriptions-admin.php:1359 msgctxt "refers to staging site" msgid "Staging" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1426 +#: includes/admin/class-wc-subscriptions-admin.php:1359 msgctxt "refers to live site" msgid "Live" msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:54 +#: includes/admin/class-wcs-admin-meta-boxes.php:56 msgctxt "meta box title" msgid "Subscription Data" msgstr "" -#: includes/admin/class-wcs-admin-meta-boxes.php:56 +#: includes/admin/class-wcs-admin-meta-boxes.php:58 msgctxt "meta box title" msgid "Billing Schedule" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:154 +#: includes/admin/class-wcs-admin-post-types.php:233 msgctxt "an action on a subscription" msgid "Activate" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:155 +#: includes/admin/class-wcs-admin-post-types.php:234 msgctxt "an action on a subscription" msgid "Put on-hold" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:156 -#: includes/admin/class-wcs-admin-post-types.php:367 +#: includes/admin/class-wcs-admin-post-types.php:235 +#: includes/admin/class-wcs-admin-post-types.php:448 #: includes/class-wc-subscriptions-manager.php:1762 #: includes/wcs-user-functions.php:288 #: templates/myaccount/related-orders.php:66 @@ -3083,35 +4172,35 @@ msgctxt "an action on a subscription" msgid "Cancel" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:231 +#: includes/admin/class-wcs-admin-post-types.php:310 msgctxt "Used in order note. Reason why status changed." msgid "Subscription status changed by bulk edit:" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:325 +#: includes/admin/class-wcs-admin-post-types.php:406 msgctxt "number of orders linked to a subscription" msgid "Orders" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:421 +#: includes/admin/class-wcs-admin-post-types.php:502 msgctxt "meaning billing address" msgid "Billing:" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:459 +#: includes/admin/class-wcs-admin-post-types.php:540 #. translators: $1: is opening link, $2: is subscription order number, $3: is #. closing link tag, $4: is user's name msgctxt "Subscription title on admin table. (e.g.: #211 for John Doe)" msgid "%1$s#%2$s%3$s for %4$s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:834 +#: includes/admin/class-wcs-admin-post-types.php:924 #. translators: placeholder is previous post title msgctxt "used in post updated messages" msgid "Subscription restored to revision from %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:839 +#: includes/admin/class-wcs-admin-post-types.php:929 msgctxt "used in \"Subscription scheduled for \"" msgid "M j, Y @ G:i" msgstr "" @@ -3142,6 +4231,14 @@ msgctxt "edit subscription header" msgid "Subscription #%s details" msgstr "" +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:126 +#: includes/class-wcs-change-payment-method-admin.php:52 +msgctxt "" +"The gateway ID displayed on the Edit Subscriptions screen when editing " +"payment method." +msgid "Gateway ID: [%s]" +msgstr "" + #: includes/admin/meta-boxes/views/html-related-orders-row.php:15 #: includes/class-wc-subscriptions-renewal-order.php:137 #: templates/myaccount/my-subscriptions.php:37 @@ -3151,12 +4248,13 @@ msgctxt "hash before order number" msgid "#%s" msgstr "" -#: includes/class-wcs-query.php:87 +#: includes/class-wcs-query.php:90 msgctxt "hash before order number" msgid "Subscription #%s" msgstr "" -#: includes/admin/meta-boxes/views/html-related-orders-row.php:29 +#: includes/admin/meta-boxes/views/html-related-orders-row.php:26 +#: includes/admin/meta-boxes/views/html-retries-table.php:44 #. translators: php date format msgctxt "post date" msgid "Y/m/d g:i:s A" @@ -3171,19 +4269,26 @@ msgctxt "table heading" msgid "Total" msgstr "" -#: templates/emails/cancelled-subscription.php:28 wcs-functions.php:275 +#: includes/class-wcs-retry-manager.php:95 +msgctxt "table heading" +msgid "Renewal Payment Retry" +msgstr "" + +#: templates/emails/cancelled-subscription.php:28 +#: templates/emails/expired-subscription.php:28 +#: templates/emails/on-hold-subscription.php:28 wcs-functions.php:277 msgctxt "table heading" msgid "Last Payment" msgstr "" #: templates/emails/subscription-info.php:19 -#: templates/myaccount/view-subscription.php:36 wcs-functions.php:272 +#: templates/myaccount/view-subscription.php:36 wcs-functions.php:274 msgctxt "table heading" msgid "Start Date" msgstr "" #: templates/emails/subscription-info.php:20 -#: templates/myaccount/view-subscription.php:42 wcs-functions.php:276 +#: templates/myaccount/view-subscription.php:42 wcs-functions.php:279 msgctxt "table heading" msgid "End Date" msgstr "" @@ -3196,33 +4301,53 @@ msgstr "" #: templates/myaccount/my-subscriptions.php:27 #: templates/myaccount/my-subscriptions.php:43 #: templates/myaccount/related-subscriptions.php:22 -#: templates/myaccount/related-subscriptions.php:38 wcs-functions.php:274 +#: templates/myaccount/related-subscriptions.php:38 wcs-functions.php:276 msgctxt "table heading" msgid "Next Payment" msgstr "" -#: wcs-functions.php:273 +#: wcs-functions.php:275 msgctxt "table heading" msgid "Trial End" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:267 +#: wcs-functions.php:278 +msgctxt "table heading" +msgid "Cancelled Date" +msgstr "" + +#: includes/admin/reports/class-wcs-report-retention-rate.php:156 +msgctxt "X axis label on retention rate graph" +msgid "Number of days after sign-up" +msgstr "" + +#: includes/admin/reports/class-wcs-report-retention-rate.php:159 +msgctxt "X axis label on retention rate graph" +msgid "Number of weeks after sign-up" +msgstr "" + +#: includes/admin/reports/class-wcs-report-retention-rate.php:162 +msgctxt "X axis label on retention rate graph" +msgid "Number of months after sign-up" +msgstr "" + +#: includes/api/legacy/class-wc-api-subscriptions.php:267 #. translators: placeholder is error message msgctxt "API error message when editing the order failed" msgid "Edit subscription failed with error: %s" msgstr "" -#: includes/api/class-wc-api-subscriptions.php:605 +#: includes/api/legacy/class-wc-api-subscriptions.php:605 msgctxt "API response confirming order note deleted from a subscription" msgid "Permanently deleted subscription note" msgstr "" -#: includes/class-wc-subscription.php:696 +#: includes/class-wc-subscription.php:715 msgctxt "original denotes there is no date to display" msgid "-" msgstr "" -#: includes/class-wc-subscription.php:752 +#: includes/class-wc-subscription.php:771 #. translators: placeholder is date type (e.g. "end", "next_payment"...) msgctxt "appears in an error message if date is wrong format" msgid "Invalid %s date. The date must be of the format: \"Y-m-d H:i:s\"." @@ -3252,22 +4377,27 @@ msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment due:" msgstr "" -#: includes/class-wc-subscriptions-manager.php:973 wcs-functions.php:205 +#: includes/class-wcs-retry-manager.php:250 +msgctxt "used in order note as reason for why subscription status changed" +msgid "Subscription renewal payment retry:" +msgstr "" + +#: includes/class-wc-subscriptions-manager.php:973 wcs-functions.php:207 msgctxt "Subscription status" msgid "Active" msgstr "" -#: includes/class-wc-subscriptions-manager.php:976 wcs-functions.php:207 +#: includes/class-wc-subscriptions-manager.php:976 wcs-functions.php:209 msgctxt "Subscription status" msgid "Cancelled" msgstr "" -#: includes/class-wc-subscriptions-manager.php:979 wcs-functions.php:209 +#: includes/class-wc-subscriptions-manager.php:979 wcs-functions.php:211 msgctxt "Subscription status" msgid "Expired" msgstr "" -#: includes/class-wc-subscriptions-manager.php:982 wcs-functions.php:204 +#: includes/class-wc-subscriptions-manager.php:982 wcs-functions.php:206 msgctxt "Subscription status" msgid "Pending" msgstr "" @@ -3282,17 +4412,17 @@ msgctxt "Subscription status" msgid "On-hold" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:2019 wcs-functions.php:208 +#: includes/class-wc-subscriptions-switcher.php:2253 wcs-functions.php:210 msgctxt "Subscription status" msgid "Switched" msgstr "" -#: wcs-functions.php:206 +#: wcs-functions.php:208 msgctxt "Subscription status" msgid "On hold" msgstr "" -#: wcs-functions.php:210 +#: wcs-functions.php:212 msgctxt "Subscription status" msgid "Pending Cancellation" msgstr "" @@ -3304,127 +4434,134 @@ msgctxt "used in a select box" msgid "%1$s-%2$s" msgstr "" -#: includes/class-wc-subscriptions-order.php:648 +#: includes/class-wc-subscriptions-order.php:688 msgctxt "An order type" msgid "Original" msgstr "" -#: includes/class-wc-subscriptions-order.php:651 +#: includes/class-wc-subscriptions-order.php:689 msgctxt "An order type" -msgid "Renewal" +msgid "Subscription Parent" msgstr "" -#: includes/class-wc-subscriptions-renewal-order.php:242 -#. translators: 1$: blog name, 2$: order number -msgctxt "used in new renewal order email, deprecated" -msgid "[%1$s] New Subscription Renewal Order (%2$s)" +#: includes/class-wc-subscriptions-order.php:690 +msgctxt "An order type" +msgid "Subscription Renewal" msgstr "" -#: includes/class-wc-subscriptions-renewal-order.php:264 -#: includes/class-wc-subscriptions-renewal-order.php:288 -#. translators: placeholder is blog name -msgctxt "used as email subject for renewal order notification email to customer" -msgid "[%s] Subscription Renewal Order" +#: includes/class-wc-subscriptions-order.php:691 +msgctxt "An order type" +msgid "Subscription Resubscribe" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:334 -#: includes/class-wc-subscriptions-switcher.php:351 -#: includes/class-wc-subscriptions-switcher.php:385 +#: includes/class-wc-subscriptions-order.php:692 +msgctxt "An order type" +msgid "Subscription Switch" +msgstr "" + +#: includes/class-wc-subscriptions-order.php:693 +msgctxt "An order type" +msgid "Non-subscription" +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:324 +#: includes/class-wc-subscriptions-switcher.php:341 +#: includes/class-wc-subscriptions-switcher.php:375 #: includes/class-wc-subscriptions-synchroniser.php:172 msgctxt "when to allow a setting" msgid "Never" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:335 +#: includes/class-wc-subscriptions-switcher.php:325 msgctxt "when to allow switching" msgid "Between Subscription Variations" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:336 +#: includes/class-wc-subscriptions-switcher.php:326 msgctxt "when to allow switching" msgid "Between Grouped Subscriptions" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:337 +#: includes/class-wc-subscriptions-switcher.php:327 msgctxt "when to allow switching" msgid "Between Both Variations & Grouped Subscriptions" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:352 +#: includes/class-wc-subscriptions-switcher.php:342 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades of Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:353 +#: includes/class-wc-subscriptions-switcher.php:343 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades of All Subscription Products" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:354 +#: includes/class-wc-subscriptions-switcher.php:344 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades & Downgrades of Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:355 +#: includes/class-wc-subscriptions-switcher.php:345 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades & Downgrades of All Subscription Products" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:369 +#: includes/class-wc-subscriptions-switcher.php:359 msgctxt "when to prorate signup fee when switching" msgid "Never (do not charge a sign up fee)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:370 +#: includes/class-wc-subscriptions-switcher.php:360 msgctxt "when to prorate signup fee when switching" msgid "Never (charge the full sign up fee)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:371 +#: includes/class-wc-subscriptions-switcher.php:361 msgctxt "when to prorate signup fee when switching" msgid "Always" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:386 +#: includes/class-wc-subscriptions-switcher.php:376 #: includes/class-wc-subscriptions-synchroniser.php:173 msgctxt "when to prorate first payment / subscription length" msgid "For Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:387 +#: includes/class-wc-subscriptions-switcher.php:377 #: includes/class-wc-subscriptions-synchroniser.php:174 msgctxt "when to prorate first payment / subscription length" msgid "For All Subscription Products" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:715 -#. translators: 1$: old item name, 2$: new item name when switching -msgctxt "used in order notes" -msgid "Customer switched from: %1$s to %2$s." -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:1624 +#: includes/class-wc-subscriptions-switcher.php:1688 msgctxt "a switch order" msgid "Downgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1627 +#: includes/class-wc-subscriptions-switcher.php:1691 msgctxt "a switch order" msgid "Upgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1630 +#: includes/class-wc-subscriptions-switcher.php:1694 msgctxt "a switch order" msgid "Crossgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1635 +#: includes/class-wc-subscriptions-switcher.php:1699 #. translators: %1: product subtotal, %2: HTML span tag, %3: direction #. (upgrade, downgrade, crossgrade), %4: closing HTML span tag msgctxt "product subtotal string" msgid "%1$s %2$s(%3$s)%4$s" msgstr "" +#: includes/class-wc-subscriptions-switcher.php:1825 +#. translators: 1$: old item, 2$: new item when switching +msgctxt "used in order notes" +msgid "Customer switched from: %1$s to %2$s." +msgstr "" + #: includes/class-wc-subscriptions-synchroniser.php:50 #. translators: placeholder is a year (e.g. "2016") msgctxt "used in subscription product edit screen" @@ -3443,14 +4580,14 @@ msgid "" "For example, the first day of the month. %sLearn more%s." msgstr "" -#: includes/class-wc-subscriptions-synchroniser.php:236 +#: includes/class-wc-subscriptions-synchroniser.php:235 #: templates/admin/deprecated/html-variation-synchronisation.php:36 -#: templates/admin/html-variation-synchronisation.php:32 +#: templates/admin/html-variation-synchronisation.php:34 msgctxt "input field placeholder for day field for annual subscriptions" msgid "Day" msgstr "" -#: includes/class-wcs-cart-renewal.php:569 +#: includes/class-wcs-cart-renewal.php:642 msgctxt "" "Used in WooCommerce by removed item notification: \"_All linked " "subscription items were_ removed. Undo?\" Filter for item title." @@ -3481,6 +4618,11 @@ msgctxt "used in order note" msgid "IPN subscription payment %s." msgstr "" +#: includes/class-wcs-retry-manager.php:182 +msgctxt "used in order note as reason for why status changed" +msgid "Retry rule applied:" +msgstr "" + #: includes/class-wcs-user-change-status-handler.php:56 msgctxt "order note left on subscription after user action" msgid "Subscription reactivated by the subscriber from their account page." @@ -3518,45 +4660,61 @@ msgctxt "default email subject for cancelled emails sent to the admin" msgid "[%s] Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:120 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:174 +#: includes/emails/class-wcs-email-cancelled-subscription.php:126 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:181 +#: includes/emails/class-wcs-email-expired-subscription.php:124 +#: includes/emails/class-wcs-email-on-hold-subscription.php:124 msgctxt "an email notification" msgid "Enable/Disable" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:126 +#: includes/emails/class-wcs-email-cancelled-subscription.php:132 +#: includes/emails/class-wcs-email-expired-subscription.php:130 +#: includes/emails/class-wcs-email-on-hold-subscription.php:130 msgctxt "of an email" msgid "Recipient(s)" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:134 +#: includes/emails/class-wcs-email-cancelled-subscription.php:140 +#: includes/emails/class-wcs-email-expired-subscription.php:138 +#: includes/emails/class-wcs-email-on-hold-subscription.php:138 msgctxt "of an email" msgid "Subject" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:141 +#: includes/emails/class-wcs-email-cancelled-subscription.php:147 +#: includes/emails/class-wcs-email-expired-subscription.php:145 +#: includes/emails/class-wcs-email-on-hold-subscription.php:145 msgctxt "" "Name the setting that controls the main heading contained within the email " "notification" msgid "Email Heading" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:148 +#: includes/emails/class-wcs-email-cancelled-subscription.php:154 +#: includes/emails/class-wcs-email-expired-subscription.php:152 +#: includes/emails/class-wcs-email-on-hold-subscription.php:152 msgctxt "text, html or multipart" msgid "Email type" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:154 +#: includes/emails/class-wcs-email-cancelled-subscription.php:160 +#: includes/emails/class-wcs-email-expired-subscription.php:158 +#: includes/emails/class-wcs-email-on-hold-subscription.php:158 msgctxt "email type" msgid "Plain text" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:155 +#: includes/emails/class-wcs-email-cancelled-subscription.php:161 +#: includes/emails/class-wcs-email-expired-subscription.php:159 +#: includes/emails/class-wcs-email-on-hold-subscription.php:159 msgctxt "email type" msgid "HTML" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:156 +#: includes/emails/class-wcs-email-cancelled-subscription.php:162 +#: includes/emails/class-wcs-email-expired-subscription.php:160 +#: includes/emails/class-wcs-email-on-hold-subscription.php:160 msgctxt "email type" msgid "Multipart" msgstr "" @@ -3587,7 +4745,21 @@ msgid "" "files" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:335 +#: includes/emails/class-wcs-email-expired-subscription.php:32 +#. translators: placeholder is {blogname}, a variable that will be substituted +#. when email is sent out +msgctxt "default email subject for expired emails sent to the admin" +msgid "[%s] Subscription Expired" +msgstr "" + +#: includes/emails/class-wcs-email-on-hold-subscription.php:32 +#. translators: placeholder is {blogname}, a variable that will be substituted +#. when email is sent out +msgctxt "default email subject for suspended emails sent to the admin" +msgid "[%s] Subscription Suspended" +msgstr "" + +#: includes/gateways/paypal/class-wcs-paypal.php:344 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:208 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:316 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:327 @@ -3602,7 +4774,7 @@ msgctxt "" msgid "#" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:547 +#: includes/gateways/paypal/class-wcs-paypal.php:556 msgctxt "" "used in User Agent data sent to PayPal to help identify where a payment " "came from" @@ -3621,21 +4793,19 @@ msgctxt "data sent to paypal" msgid "%1$s subscription event triggered at %2$s" msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:105 +#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:136 msgctxt "used in api error message if there is no severity code from PayPal" msgid "Error" msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:107 +#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:138 msgctxt "used in api error message if there is no long message" msgid "Unknown error" msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:119 -#: templates/admin/post-types/writepanels/order-shipping-html.php:14 -#: templates/admin/post-types/writepanels/order-tax-html.php:9 -#: templates/myaccount/view-subscription.php:276 -#: templates/myaccount/view-subscription.php:295 +#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php:150 +#: templates/admin/deprecated/order-shipping-html.php:14 +#: templates/admin/deprecated/order-tax-html.php:9 msgctxt "no information about something" msgid "N/A" msgstr "" @@ -3655,21 +4825,31 @@ msgctxt "item name sent to paypal" msgid "Subscription %1$s (Order %2$s) - %3$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:258 +#: includes/payment-retry/class-wcs-retry-post-store.php:33 +msgctxt "Post type name" +msgid "Renewal Payment Retries" +msgstr "" + +#: includes/payment-retry/class-wcs-retry-post-store.php:35 +msgctxt "Admin menu name" +msgid "Renewal Payment Retries" +msgstr "" + +#: includes/upgrades/class-wc-subscriptions-upgrader.php:287 #. translators: placeholder is number of upgraded subscriptions msgctxt "used in the subscriptions upgrader" msgid "Marked %s subscription products as \"sold individually\"." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:289 -#: includes/upgrades/class-wc-subscriptions-upgrader.php:339 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:318 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:368 #. translators: placeholder is "{time_left}", will be replaced on front end #. with actual time msgctxt "Message that gets sent to front end." msgid "Estimated time left (minutes:seconds): %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:318 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:347 #. translators: placeholder is the number of subscriptions repaired msgctxt "Repair message that gets sent to front end." msgid "" @@ -3677,7 +4857,7 @@ msgid "" "customer notes." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:324 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:353 #. translators: placeholder is number of subscriptions that were checked and #. did not need repairs. There's a space at the beginning! msgctxt "Repair message that gets sent to front end." @@ -3686,14 +4866,14 @@ msgid_plural "%d other subscriptions were checked and did not need any repairs." msgstr[0] "" msgstr[1] "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:328 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:357 #. translators: placeholder is "{execution_time}", which will be replaced on #. front end with actual time msgctxt "Repair message that gets sent to front end." msgid "(in %s seconds)" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:331 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:360 #. translators: $1: "Repaired x subs with incorrect dates...", $2: "X others #. were checked and no repair needed", $3: "(in X seconds)". Ordering for RTL #. languages. @@ -3701,7 +4881,7 @@ msgctxt "The assembled repair message that gets sent to front end." msgid "%1$s%2$s %3$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:350 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:379 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag msgctxt "Error message that gets sent to front end when upgrading Subscriptions" msgid "" @@ -3709,30 +4889,53 @@ msgid "" "and try again. If problem persists, %2$scontact support%3$s." msgstr "" -#: includes/upgrades/templates/wcs-about.php:36 -#: woocommerce-subscriptions.php:961 +#: includes/upgrades/templates/wcs-about-2-0.php:36 +#: woocommerce-subscriptions.php:945 msgctxt "short for documents" msgid "Docs" msgstr "" -#: includes/upgrades/templates/wcs-about.php:121 +#: includes/upgrades/templates/wcs-about.php:35 +msgctxt "short for documents" +msgid "Documentation" +msgstr "" + +#: includes/upgrades/templates/wcs-about-2-0.php:121 msgctxt "h3 on the About Subscriptions page for this new feature" msgid "Change Payment Method" msgstr "" +#: includes/upgrades/templates/wcs-about.php:126 +msgctxt "h3 on the About Subscriptions page for this new feature" +msgid "Import/Export Subscriptions" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:55 +msgctxt "learn more link to subscription reports documentation" +msgid "Learn More" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:79 +msgctxt "learn more link to failed payment retry documentation" +msgid "Learn More" +msgstr "" + +#: includes/upgrades/templates/wcs-about.php:100 +msgctxt "learn more link to subscription emails documentation" +msgid "Learn More" +msgstr "" + #: includes/upgrades/templates/wcs-upgrade.php:45 msgctxt "text on submit button" msgid "Update Database" msgstr "" -#: includes/wcs-cart-functions.php:180 +#: includes/wcs-cart-functions.php:184 msgctxt "shipping method price" msgid "Free" msgstr "" -#: includes/wcs-cart-functions.php:255 -#: templates/myaccount/view-subscription.php:201 -#: templates/myaccount/view-subscription.php:206 +#: includes/wcs-cart-functions.php:259 #. translators: placeholder is price string, denotes tax included in cart/order #. total msgctxt "includes tax" @@ -3758,7 +4961,7 @@ msgctxt "" msgid "Invalid data. Type of copy is not a string." msgstr "" -#: includes/wcs-order-functions.php:263 wcs-functions.php:151 +#: includes/wcs-order-functions.php:294 wcs-functions.php:153 #. translators: Order date parsed by strftime msgctxt "Used in subscription post title. \"Subscription renewal order - \"" msgid "%b %d, %Y @ %I:%M %p" @@ -3801,8 +5004,8 @@ msgstr[0] "" msgstr[1] "" #: includes/wcs-time-functions.php:81 -msgctxt "Subscription length (eg \"$10 per month for _all time_\")" -msgid "all time" +msgctxt "Subscription length" +msgid "Never expire" msgstr "" #: includes/wcs-time-functions.php:86 @@ -3890,31 +5093,34 @@ msgctxt "example number of days / weeks / months" msgid "e.g. 3" msgstr "" +#: templates/admin/deprecated/html-variation-price.php:118 +#: templates/admin/html-variation-price.php:27 +#. translators: placeholder is trial period validation message if passed an +#. invalid value (e.g. "Trial period can not exceed 4 weeks") +msgctxt "Trial period dropdown's description in pricing fields" +msgid "" +"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" +msgstr "" + +#: templates/admin/html-variation-price.php:67 +msgctxt "Subscription Length dropdown's description in pricing fields" +msgid "" +"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." +msgstr "" + #: templates/checkout/form-change-payment-method.php:20 -#: templates/emails/admin-new-renewal-order.php:35 -#: templates/emails/admin-new-switch-order.php:38 -#: templates/emails/admin-new-switch-order.php:76 -#: templates/emails/cancelled-subscription.php:54 -#: templates/emails/customer-completed-renewal-order.php:36 -#: templates/emails/customer-completed-switch-order.php:36 -#: templates/emails/customer-completed-switch-order.php:75 -#: templates/emails/customer-processing-renewal-order.php:31 -#: templates/emails/customer-renewal-invoice.php:42 +#: templates/emails/email-order-details.php:33 #: templates/myaccount/view-subscription.php:95 msgctxt "table headings in notification email" msgid "Product" msgstr "" #: templates/checkout/form-change-payment-method.php:21 -#: templates/emails/admin-new-renewal-order.php:36 -#: templates/emails/admin-new-switch-order.php:39 -#: templates/emails/admin-new-switch-order.php:77 -#: templates/emails/cancelled-subscription.php:55 -#: templates/emails/customer-completed-renewal-order.php:37 -#: templates/emails/customer-completed-switch-order.php:37 -#: templates/emails/customer-completed-switch-order.php:76 -#: templates/emails/customer-processing-renewal-order.php:32 -#: templates/emails/customer-renewal-invoice.php:43 +#: templates/emails/email-order-details.php:34 msgctxt "table headings in notification email" msgid "Quantity" msgstr "" @@ -3924,16 +5130,10 @@ msgctxt "table headings in notification email" msgid "Totals" msgstr "" -#: templates/emails/admin-new-renewal-order.php:37 -#: templates/emails/admin-new-switch-order.php:40 -#: templates/emails/admin-new-switch-order.php:78 #: templates/emails/cancelled-subscription.php:27 -#: templates/emails/cancelled-subscription.php:56 -#: templates/emails/customer-completed-renewal-order.php:38 -#: templates/emails/customer-completed-switch-order.php:38 -#: templates/emails/customer-completed-switch-order.php:77 -#: templates/emails/customer-processing-renewal-order.php:33 -#: templates/emails/customer-renewal-invoice.php:44 +#: templates/emails/email-order-details.php:35 +#: templates/emails/expired-subscription.php:27 +#: templates/emails/on-hold-subscription.php:27 msgctxt "table headings in notification email" msgid "Price" msgstr "" @@ -3943,43 +5143,73 @@ msgctxt "table headings in notification email" msgid "End of Prepaid Term" msgstr "" +#: templates/emails/expired-subscription.php:29 +msgctxt "table headings in notification email" +msgid "End Date" +msgstr "" + +#: templates/emails/on-hold-subscription.php:29 +msgctxt "table headings in notification email" +msgid "Date Suspended" +msgstr "" + #: templates/checkout/form-change-payment-method.php:55 msgctxt "text on button on checkout page" msgid "Change Payment Method" msgstr "" -#: templates/emails/admin-new-renewal-order.php:19 +#: templates/emails/admin-new-renewal-order.php:16 #: templates/emails/plain/admin-new-renewal-order.php:16 -#. translators: $1: customer's billing first name, $2: customer's billing last -#. name +#. translators: $1: customer's billing first name and last name msgctxt "Used in admin email: new renewal order" msgid "" -"You have received a subscription renewal order from %1$s %2$s. Their order " -"is as follows:" -msgstr "" - -#: templates/emails/admin-new-renewal-order.php:28 -#: templates/emails/customer-completed-renewal-order.php:29 -#: templates/emails/customer-processing-renewal-order.php:24 -#. translators: $1: order's order number, $2: date of order in
tags $3: order's order number +#. $4: date of order in tags $2: subscription's order +#. number +msgctxt "Used in email notification" +msgid "Subscription %1$s#%2$s%3$s" +msgstr "" + +#: templates/emails/plain/cancelled-subscription.php:44 +#: templates/emails/plain/expired-subscription.php:44 +#: templates/emails/plain/on-hold-subscription.php:40 #: templates/emails/plain/subscription-info.php:21 -#. translators: placeholder is edit post link for the subscription #. translators: placeholder is either view or edit url for the subscription msgctxt "in plain emails for subscription information" msgid "View Subscription: %s" @@ -4040,12 +5297,22 @@ msgctxt "in plain emails for subscription information" msgid "Price: %s" msgstr "" +#: templates/emails/plain/email-order-details.php:17 +msgctxt "date format for order date in notification emails" +msgid "jS F Y" +msgstr "" + #: templates/emails/plain/subscription-info.php:25 #: templates/emails/subscription-info.php:29 msgctxt "Used as end date for an indefinite subscription" msgid "When Cancelled" msgstr "" +#: templates/emails/subscription-info.php:27 +msgctxt "subscription number in email table. (eg: #106)" +msgid "#%s" +msgstr "" + #: templates/myaccount/my-subscriptions.php:54 #: templates/myaccount/related-orders.php:47 #: templates/myaccount/related-subscriptions.php:41 @@ -4085,125 +5352,89 @@ msgctxt "date on subscription updates list. Will be localized" msgid "l jS \\o\\f F Y, h:ia" msgstr "" -#: templates/myaccount/view-subscription.php:136 -#. translators: %1$s is the number of the file (only in plural!), %2$s: the -#. name of the file -msgctxt "Used as link text in view-subscription template" -msgid "Download file: %2$s" -msgid_plural "Download file %1$s: %2$s" -msgstr[0] "" -msgstr[1] "" - -#: templates/myaccount/view-subscription.php:233 -msgctxt "customer note" -msgid "Note:" -msgstr "" - -#: templates/myaccount/view-subscription.php:249 -#. translators: there is markup here, hence can't use Email: %s -msgctxt "heading in customer details on subscription detail page" -msgid "Email" -msgstr "" - -#: templates/myaccount/view-subscription.php:254 -#. translators: there is markup here, hence can't use Email: %s -msgctxt "heading in customer details on subscription detail page" -msgid "Tel" -msgstr "" - -#: templates/myaccount/view-subscription.php:249 -msgctxt "Used in data attribute for a td tag, escaped." -msgid "Email" -msgstr "" - -#: templates/myaccount/view-subscription.php:254 -msgctxt "Used in data attribute for a td tag, escaped." -msgid "Telephone" -msgstr "" - -#: wcs-functions.php:125 +#: wcs-functions.php:127 msgctxt "Error message while creating a subscription" msgid "Invalid date. The date must be a string and of the format: \"Y-m-d H:i:s\"." msgstr "" -#: wcs-functions.php:127 +#: wcs-functions.php:129 msgctxt "Error message while creating a subscription" msgid "Subscription start date must be before current day." msgstr "" -#: wcs-functions.php:132 +#: wcs-functions.php:134 msgctxt "Error message while creating a subscription" msgid "Invalid subscription customer_id." msgstr "" -#: wcs-functions.php:153 +#: wcs-functions.php:155 #. translators: placeholder is order date parsed by strftime msgctxt "The post title for the new subscription" msgid "Subscription – %s" msgstr "" -#: woocommerce-subscriptions.php:202 +#: woocommerce-subscriptions.php:207 msgctxt "custom post type setting" msgid "Add Subscription" msgstr "" -#: woocommerce-subscriptions.php:203 +#: woocommerce-subscriptions.php:208 msgctxt "custom post type setting" msgid "Add New Subscription" msgstr "" -#: woocommerce-subscriptions.php:204 +#: woocommerce-subscriptions.php:209 msgctxt "custom post type setting" msgid "Edit" msgstr "" -#: woocommerce-subscriptions.php:205 +#: woocommerce-subscriptions.php:210 msgctxt "custom post type setting" msgid "Edit Subscription" msgstr "" -#: woocommerce-subscriptions.php:206 +#: woocommerce-subscriptions.php:211 msgctxt "custom post type setting" msgid "New Subscription" msgstr "" -#: woocommerce-subscriptions.php:207 woocommerce-subscriptions.php:208 +#: woocommerce-subscriptions.php:212 woocommerce-subscriptions.php:213 msgctxt "custom post type setting" msgid "View Subscription" msgstr "" -#: woocommerce-subscriptions.php:211 +#: woocommerce-subscriptions.php:216 msgctxt "custom post type setting" msgid "No Subscriptions found in trash" msgstr "" -#: woocommerce-subscriptions.php:212 +#: woocommerce-subscriptions.php:217 msgctxt "custom post type setting" msgid "Parent Subscriptions" msgstr "" -#: woocommerce-subscriptions.php:279 +#: woocommerce-subscriptions.php:284 msgctxt "post status label including post count" msgid "Active (%s)" msgid_plural "Active (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:280 +#: woocommerce-subscriptions.php:285 msgctxt "post status label including post count" msgid "Switched (%s)" msgid_plural "Switched (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:281 +#: woocommerce-subscriptions.php:286 msgctxt "post status label including post count" msgid "Expired (%s)" msgid_plural "Expired (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:282 +#: woocommerce-subscriptions.php:287 msgctxt "post status label including post count" msgid "Pending Cancellation (%s)" msgid_plural "Pending Cancellation (%s)" diff --git a/readme.txt b/readme.txt index b42e5a6..e5d7b5d 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: prospress, javorszky, jconroy, mattallan, thenbrent Tags: woocommerce, subscriptions, ecommerce, e-commerce, commerce, wordpress ecommerce Requires at least: 4.0 -Tested up to: 4.3 +Tested up to: 4.5 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html -WC requires at least: 2.3 -WC tested up to: 2.4 \ No newline at end of file +WC requires at least: 2.4 +WC tested up to: 2.6 \ No newline at end of file diff --git a/templates/admin/post-types/writepanels/order-shipping-html.php b/templates/admin/deprecated/order-shipping-html.php similarity index 100% rename from templates/admin/post-types/writepanels/order-shipping-html.php rename to templates/admin/deprecated/order-shipping-html.php diff --git a/templates/admin/post-types/writepanels/order-tax-html.php b/templates/admin/deprecated/order-tax-html.php similarity index 100% rename from templates/admin/post-types/writepanels/order-tax-html.php rename to templates/admin/deprecated/order-tax-html.php diff --git a/templates/admin/html-variation-price.php b/templates/admin/html-variation-price.php index b08c5f6..aa6cb72 100644 --- a/templates/admin/html-variation-price.php +++ b/templates/admin/html-variation-price.php @@ -1,6 +1,6 @@ I ?>