"),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_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 ) ) : ?>
-
@@ -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 '
diff --git a/includes/admin/reports/class-wcs-report-cache-manager.php b/includes/admin/reports/class-wcs-report-cache-manager.php
new file mode 100644
index 0000000..3d03bee
--- /dev/null
+++ b/includes/admin/reports/class-wcs-report-cache-manager.php
@@ -0,0 +1,264 @@
+ report classes to determine which reports need to be updated on certain events.
+ *
+ * The index for each report's class is specified as its used later to determine when to schedule the report and we want
+ * it to be consistently at the same time, regardless of the hook which triggered the cache update. The indexes are based
+ * on the order of the reports in the menu on the WooCommerce > Reports > Subscriptions screen, which is why the indexes
+ * are not sequential (because not all reports need caching).
+ *
+ */
+ private $update_events_and_classes = array(
+ 'woocommerce_subscriptions_reports_schedule_cache_updates' => array( // a custom hook that can be called to schedule a full cache update, used by WC_Subscriptions_Upgrader
+ 0 => 'WC_Report_Subscription_Events_By_Date',
+ 1 => 'WC_Report_Upcoming_Recurring_Revenue',
+ 3 => 'WC_Report_Subscription_By_Product',
+ 4 => 'WC_Report_Subscription_By_Customer',
+ ),
+ 'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments
+ 0 => 'WC_Report_Subscription_Events_By_Date',
+ 4 => 'WC_Report_Subscription_By_Customer',
+ ),
+ 'woocommerce_subscriptions_switch_completed' => array(
+ 0 => 'WC_Report_Subscription_Events_By_Date',
+ ),
+ 'woocommerce_subscription_status_changed' => array(
+ 0 => 'WC_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience
+ 4 => 'WC_Report_Subscription_By_Customer',
+ ),
+ 'woocommerce_subscription_status_active' => array(
+ 1 => 'WC_Report_Upcoming_Recurring_Revenue',
+ ),
+ 'woocommerce_order_add_product' => array(
+ 3 => 'WC_Report_Subscription_By_Product',
+ ),
+ 'woocommerce_order_edit_product' => array(
+ 3 => 'WC_Report_Subscription_By_Product',
+ ),
+ );
+
+ /**
+ * Record of all the report calsses to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events.
+ */
+ private $reports_to_update = array();
+
+ /**
+ * The hook name to use for our WP-Cron entry for updating report cache.
+ */
+ private $cron_hook = 'wcs_report_update_cache';
+
+ /**
+ * The hook name to use for our WP-Cron entry for updating report cache.
+ */
+ protected $use_large_site_cache;
+
+ /**
+ * Attach callbacks to manage cache updates
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function __construct() {
+
+ add_action( $this->cron_hook, array( $this, 'update_cache' ), 10, 1 );
+
+ foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
+ add_action( $event_hook, array( $this, 'set_reports_to_update' ), 10 );
+ }
+
+ add_action( 'shutdown', array( $this, 'schedule_cache_updates' ), 10 );
+
+ // Notify store owners that report data can be out-of-date
+ add_action( 'admin_notices', array( $this, 'admin_notices' ), 0 );
+ }
+
+ /**
+ * Check if the given hook has reports associated with it, and if so, add them to our $this->reports_to_update
+ * property so we know to schedule an event to update their cache at the end of the request.
+ *
+ * This function is attached as a callback on the events in the $update_events_and_classes property.
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function set_reports_to_update() {
+ if ( isset( $this->update_events_and_classes[ current_filter() ] ) ) {
+ $this->reports_to_update = array_unique( array_merge( $this->reports_to_update, $this->update_events_and_classes[ current_filter() ] ) );
+ }
+ }
+
+ /**
+ * At the end of the request, schedule cache updates for any events that occured during this request.
+ *
+ * For large sites, cache updates are run only once per day to avoid overloading the DB where the queries are very resource intensive
+ * (as reported during beta testing in https://github.com/Prospress/woocommerce-subscriptions/issues/1732). We do this at 4am in the
+ * site's timezone, which helps avoid running the queries during busy periods and also runs them after all the renewals for synchronised
+ * subscriptions should have finished for the day (which begins at 3am and rarely takes more than 1 hours of processing to get through
+ * an entire queue).
+ *
+ * This function is attached as a callback on 'shutdown' and will schedule cache updates for any reports found to need updates by
+ * @see $this->set_reports_to_update().
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function schedule_cache_updates() {
+
+ if ( ! empty( $this->reports_to_update ) ) {
+
+ // On large sites, we want to run the cache update once at 4am in the site's timezone
+ if ( $this->use_large_site_cache() ) {
+
+ $four_am_site_time = new DateTime( '4 am', wcs_get_sites_timezone() );
+
+ // Convert to a UTC timestamp for scheduling
+ $cache_update_timestamp = $four_am_site_time->format( 'U' );
+
+ // PHP doesn't support a "next 4am" time format equivalent, so we need to manually handle getting 4am from earlier today (which will always happen when this is run after 4am and before midnight in the site's timezone)
+ if ( $cache_update_timestamp <= gmdate( 'U' ) ) {
+ $cache_update_timestamp += DAY_IN_SECONDS;
+ }
+
+ // Schedule one update event for each class to avoid updating cache more than once for the same class for different events
+ foreach ( $this->reports_to_update as $index => $report_class ) {
+
+ $cron_args = array( 'report_class' => $report_class );
+
+ if ( false === wp_next_scheduled( $this->cron_hook, $cron_args ) ) {
+ // Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
+ wp_schedule_single_event( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
+ }
+ }
+ } else { // Otherwise, run it 10 minutes after the last cache invalidating event
+
+ // Schedule one update event for each class to avoid updating cache more than once for the same class for different events
+ foreach ( $this->reports_to_update as $index => $report_class ) {
+
+ $cron_args = array( 'report_class' => $report_class );
+
+ if ( false !== ( $next_scheduled = wp_next_scheduled( $this->cron_hook, $cron_args ) ) ) {
+ wp_unschedule_event( $next_scheduled, $this->cron_hook, $cron_args );
+ }
+
+ // Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
+ wp_schedule_single_event( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the cache data for a given report, as specified with $report_class, by call it's get_data() method.
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function update_cache( $report_class ) {
+
+ // Validate the report class
+ $valid_report_class = false;
+
+ foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
+ if ( in_array( $report_class, $report_classes ) ) {
+ $valid_report_class = true;
+ break;
+ }
+ }
+
+ if ( false === $valid_report_class ) {
+ return;
+ }
+
+ // Load report class dependencies
+ require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
+ require_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
+
+ $report_name = strtolower( str_replace( '_', '-', str_replace( 'WC_Report_', '', $report_class ) ) );
+ $report_path = WCS_Admin_Reports::initialize_reports_path( '', $report_name, $report_class );
+
+ require_once( $report_path );
+
+ $reflector = new ReflectionMethod( $report_class, 'get_data' );
+
+ // Some report classes extend WP_List_Table which has a constructor using methods not available on WP-Cron (and unable to be loaded with a __doing_it_wrong() notice), so they have a static get_data() method and do not need to be instantiated
+ if ( $reflector->isStatic() ) {
+
+ call_user_func( array( $report_class, 'get_data' ), array( 'no_cache' => true ) );
+
+ } else {
+
+ $report = new $report_class();
+
+ // Classes with a non-static get_data() method can be displayed for different time series, so we need to update the cache for each of those ranges
+ foreach ( array( 'year', 'last_month', 'month', '7day' ) as $range ) {
+ $report->calculate_current_range( $range );
+ $report->get_data( array( 'no_cache' => true ) );
+ }
+ }
+ }
+
+ /**
+ * Boolean flag to check whether to use a the large site cache method or not, which is determined based on the number of
+ * subscriptions and orders on the site (using arbitrary counts).
+ *
+ * @since 2.1
+ * @return bool
+ */
+ protected function use_large_site_cache() {
+
+ if ( null === $this->use_large_site_cache ) {
+
+ if ( false == get_option( 'wcs_report_use_large_site_cache' ) ) {
+
+ $subscription_counts = (array) wp_count_posts( 'shop_subscription' );
+ $order_counts = (array) wp_count_posts( 'shop_order' );
+
+ if ( array_sum( $subscription_counts ) > 3000 || array_sum( $order_counts ) > 25000 ) {
+
+ update_option( 'wcs_report_use_large_site_cache', 'true', false );
+
+ $this->use_large_site_cache = true;
+ } else {
+ $this->use_large_site_cache = false;
+ }
+ } else {
+ $this->use_large_site_cache = true;
+ }
+ }
+
+ return apply_filters( 'wcs_report_use_large_site_cache', $this->use_large_site_cache );
+ }
+
+ /**
+ * Make it clear to store owners that data for some reports can be out-of-date.
+ *
+ * @since 2.1
+ */
+ public function admin_notices() {
+
+ $screen = get_current_screen();
+ $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce-subscriptions' ) );
+
+ if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] && ( ! isset( $_GET['report'] ) || in_array( $_GET['report'], array( 'subscription_events_by_date', 'upcoming_recurring_revenue', 'subscription_by_product', 'subscription_by_customer' ) ) ) && $this->use_large_site_cache() ) {
+ wcs_add_admin_notice( __( 'Please note: data for this report is cached. The data displayed may be out of date by up to 24 hours. The cache is updated each morning at 4am in your site\'s timezone.', 'woocommerce-subscriptions' ) );
+ }
+ }
+}
+return new WCS_Report_Cache_Manager();
diff --git a/includes/admin/reports/class-wcs-report-dashboard.php b/includes/admin/reports/class-wcs-report-dashboard.php
new file mode 100644
index 0000000..2f004a7
--- /dev/null
+++ b/includes/admin/reports/class-wcs-report-dashboard.php
@@ -0,0 +1,100 @@
+prepare(
+ "SELECT COUNT(DISTINCT wcsubs.ID) AS count
+ FROM {$wpdb->posts} AS wcsubs
+ INNER JOIN {$wpdb->posts} AS wcorder
+ ON wcsubs.post_parent = wcorder.ID
+ WHERE wcorder.post_type IN ( 'shop_order' )
+ AND wcsubs.post_type IN ( 'shop_subscription' )
+ AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
+ AND wcorder.post_date >= '%s'
+ AND wcorder.post_date < '%s'",
+ date( 'Y-m-01', current_time( 'timestamp' ) ),
+ date( 'Y-m-d H:i:s', current_time( 'timestamp' ) )
+ );
+
+ $signup_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) );
+
+ $query = $wpdb->prepare(
+ "SELECT COUNT(DISTINCT wcorder.ID) AS count
+ FROM {$wpdb->posts} AS wcorder
+ INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
+ ON (
+ wcorder.id = meta__subscription_renewal.post_id
+ AND
+ meta__subscription_renewal.meta_key = '_subscription_renewal'
+ )
+ WHERE wcorder.post_type IN ( 'shop_order' )
+ AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
+ AND wcorder.post_date >= '%s'
+ AND wcorder.post_date < '%s'",
+ date( 'Y-m-01', current_time( 'timestamp' ) ),
+ date( 'Y-m-d H:i:s', current_time( 'timestamp' ) )
+ );
+
+ $renewal_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) );
+
+ ?>
+
+ report_data ) ) {
+ $this->query_report_data();
+ }
+ return $this->report_data;
+ }
+
+ /**
+ * Get the number of periods each subscription has between sign-up and end.
+ *
+ * This function uses a new "living" and "age" terminology to refer to the time between when a subscription
+ * is created and when it ends (i.e. expires or is cancelled). The function can't use "active" because the
+ * subscription may not have been active all of that time. Instead, it may have been on-hold for part of it.
+ *
+ * @since 2.1
+ * @return null
+ */
+ private function query_report_data() {
+ global $wpdb;
+
+ $this->report_data = new stdClass;
+
+ // First, let's find the age of the longest living subscription in days
+ $oldest_subscription_age_in_days = $wpdb->get_var( $wpdb->prepare(
+ "SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
+ FROM {$wpdb->prefix}posts posts
+ LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
+ WHERE posts.post_type = 'shop_subscription'
+ AND postmeta.meta_key = %s
+ AND postmeta.meta_value <> '0'
+ ORDER BY age_in_days DESC
+ LIMIT 1",
+ wcs_get_date_meta_key( 'end' )
+ ) );
+
+ // Now determine what interval to use based on that length
+ if ( $oldest_subscription_age_in_days > 365 ) {
+ $this->report_data->interval_period = 'month';
+ } elseif ( $oldest_subscription_age_in_days > 182 ) {
+ $this->report_data->interval_period = 'week';
+ } else {
+ $this->report_data->interval_period = 'day';
+ }
+
+ // Use the number of days in the chosen interval period to determine how many periods between each start/end date
+ $days_in_interval_period = wcs_get_days_in_cycle( $this->report_data->interval_period, 1 );
+
+ // Find the number of these periods in the longest living subscription
+ $oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period );
+
+ // Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription)
+ $base_query = $wpdb->prepare(
+ "SELECT
+ IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
+ COUNT(posts.ID) as count
+ FROM {$wpdb->prefix}posts posts
+ LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
+ ON posts.ID = cancelled_date.post_id
+ AND cancelled_date.meta_key = %s
+ AND cancelled_date.meta_value <> '0'
+ LEFT JOIN {$wpdb->prefix}postmeta end_date
+ ON posts.ID = end_date.post_id
+ AND end_date.meta_key = %s
+ WHERE posts.post_type = 'shop_subscription'
+ AND posts.post_status NOT IN( 'wc-pending', 'trash' )
+ GROUP BY periods_active
+ ORDER BY periods_active ASC",
+ $days_in_interval_period,
+ ( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subsription
+ wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime
+ wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscritions that have expired
+ );
+
+ $subscription_ages = $wpdb->get_results( $base_query, OBJECT_K );
+
+ $this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
+ $this->report_data->living_subscriptions = array();
+
+ // At day zero, no subscriptions have ended
+ $this->report_data->living_subscriptions[0] = $this->report_data->total_subscriptions;
+
+ // Fill out the report data to provide a smooth curve
+ for ( $i = 0; $i <= $oldest_subscription_age; $i++ ) {
+
+ // We want to push the the array keys ahead by one to make sure out the 0 index represents the total subscriptions
+ $periods_after_sign_up = $i + 1;
+
+ // Only reduce the number of living subscriptions when we have a new number for a given period as that indicates a new set of subscriptions have ended
+ if ( isset( $subscription_ages[ $i ] ) ) {
+ $this->report_data->living_subscriptions[ $periods_after_sign_up ] = $this->report_data->living_subscriptions[ $i ] - $subscription_ages[ $i ]->count;
+ $this->report_data->unended_subscriptions -= $subscription_ages[ $i ]->count;
+ } else {
+ $this->report_data->living_subscriptions[ $periods_after_sign_up ] = $this->report_data->living_subscriptions[ $i ];
+ }
+ }
+ }
+
+ /**
+ * Output the report
+ *
+ * Use a custom report as we don't need the date filters provided by the WooCommerce html-report-by-date.php template.
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function output_report() {
+ include( plugin_dir_path( WC_Subscriptions::$plugin_file ) . '/includes/admin/views/html-report-by-period.php' );
+ }
+
+ /**
+ * Output the HTML and JavaScript to plot the chart
+ *
+ * @since 2.1
+ * @return null
+ */
+ public function get_main_chart() {
+
+ $this->get_report_data();
+
+ $data_to_plot = array();
+
+ foreach ( $this->report_data->living_subscriptions as $periods_since_sign_up => $living_subscription_count ) {
+ $data_to_plot[] = array(
+ absint( $periods_since_sign_up ),
+ absint( $living_subscription_count ),
+ );
+ }
+
+ switch ( $this->report_data->interval_period ) {
+ case 'day':
+ $x_axes_label = _x( 'Number of days after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
+ break;
+ case 'week':
+ $x_axes_label = _x( 'Number of weeks after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
+ break;
+ case 'month':
+ $x_axes_label = _x( 'Number of months after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
+ break;
+ }
+
+ ?>
+
+
+
+
+ __( 'Customer', 'woocommerce-subscriptions' ),
+ 'plural' => __( 'Customers', 'woocommerce-subscriptions' ),
+ 'ajax' => false,
+ ) );
+ }
+
+ /**
+ * No subscription products found text.
+ */
+ public function no_items() {
+ esc_html_e( 'No customers found.', 'woocommerce-subscriptions' );
+ }
+
+ /**
+ * Output the report.
+ */
+ public function output_report() {
+
+ $this->prepare_items();
+ echo '
diff --git a/includes/api/class-wc-rest-subscription-notes-controller.php b/includes/api/class-wc-rest-subscription-notes-controller.php
new file mode 100644
index 0000000..446c9aa
--- /dev/null
+++ b/includes/api/class-wc-rest-subscription-notes-controller.php
@@ -0,0 +1,36 @@
+/notes endpoint.
+ *
+ * @author Prospress
+ * @since 2.1
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+/**
+ * REST API Subscription Notes controller class.
+ *
+ * @package WooCommerce_Subscriptions/API
+ * @extends WC_REST_Order_Notes_Controller
+ */
+class WC_REST_Subscription_Notes_Controller extends WC_REST_Order_Notes_Controller {
+
+ /**
+ * Route base.
+ *
+ * @var string
+ */
+ protected $rest_base = 'subscriptions/(?P[\d]+)/notes';
+
+ /**
+ * Post type.
+ *
+ * @var string
+ */
+ protected $post_type = 'shop_subscription';
+
+}
diff --git a/includes/api/class-wc-rest-subscriptions-controller.php b/includes/api/class-wc-rest-subscriptions-controller.php
new file mode 100644
index 0000000..d0c936f
--- /dev/null
+++ b/includes/api/class-wc-rest-subscriptions-controller.php
@@ -0,0 +1,412 @@
+namespace, '/' . $this->rest_base . '/(?P[\d]+)/orders', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_subscription_orders' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ 'args' => $this->get_collection_params(),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ ) );
+
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/statuses', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_statuses' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ ) );
+ }
+
+ /**
+ * Filter WC_REST_Orders_Controller::get_item response for subscription post types
+ *
+ * @since 2.1
+ * @param WP_REST_Response $response
+ * @param WP_POST $post
+ * @param WP_REST_Request $request
+ */
+ public function filter_get_subscription_response( $response, $post, $request ) {
+
+ if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) {
+ $subscription = wcs_get_subscription( $post->ID );
+
+ $response->data['billing_period'] = $subscription->billing_period;
+ $response->data['billing_interval'] = $subscription->billing_interval;
+ $response->data['start_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'start' ) );
+ $response->data['trial_end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'trial_end' ) );
+ $response->data['next_payment_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'next_payment' ) );
+ $response->data['end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'end_date' ) );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Sets the order_total value on the subscription after WC_REST_Orders_Controller::create_order
+ * calls calculate_totals(). This allows store admins to create a recurring payment via the api
+ * without needing to attach a product to the subscription.
+ *
+ * @since 2.1
+ * @param WP_REST_Request $request
+ */
+ protected function create_order( $request ) {
+ $post_id = parent::create_order( $request );
+
+ if ( is_wp_error( $post_id ) ) {
+ return $post_id;
+ }
+
+ if ( isset( $request['order_total'] ) ) {
+ update_post_meta( $post_id, '_order_total', wc_format_decimal( $request['order_total'], get_option( 'woocommerce_price_num_decimals' ) ) );
+ }
+
+ return $post_id;
+ }
+
+ /**
+ * Overrides WC_REST_Orders_Controller::update_order to update subscription specific meta
+ * calls parent::update_order to update the rest.
+ *
+ * @since 2.1
+ * @param WP_REST_Request $request
+ * @param WP_POST $post
+ */
+ protected function update_order( $request, $post ) {
+ try {
+ $post_id = parent::update_order( $request, $post );
+
+ if ( is_wp_error( $post_id ) ) {
+ return $post_id;
+ }
+
+ $subscription = wcs_get_subscription( $post_id );
+ $this->update_schedule( $subscription, $request );
+
+ if ( empty( $request['payment_details']['method_id'] ) && ! empty( $request['payment_method'] ) ) {
+ $request['payment_details']['method_id'] = $request['payment_method'];
+ }
+
+ $this->update_payment_method( $subscription, $request['payment_details'], true );
+
+ return $post_id;
+ } catch ( WC_REST_Exception $e ) {
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
+ } catch ( Exception $e ) {
+ return new WP_Error( 'woocommerce_rest_cannot_update_subscription', $e->getMessage(), array( 'status' => 400 ) );
+ }
+ }
+
+ /**
+ * Get subscription orders
+ *
+ * @since 2.1
+ * @param WP_REST_Request $request
+ * @return WP_Error|WP_REST_Response $response
+ */
+ public function get_subscription_orders( $request ) {
+ $id = (int) $request['id'];
+
+ if ( empty( $id ) || ! wcs_is_subscription( $id ) ) {
+ return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
+ }
+
+ $this->post_type = 'shop_order';
+ $subscription = wcs_get_subscription( $id );
+ $subscription_orders = $subscription->get_related_orders();
+
+ $orders = array();
+
+ foreach ( $subscription_orders as $order_id ) {
+ $post = get_post( $order_id );
+ if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
+ continue;
+ }
+
+ $response = $this->prepare_item_for_response( $post, $request );
+
+ foreach ( array( 'parent', 'renewal', 'switch' ) as $order_type ) {
+ if ( wcs_order_contains_subscription( $order_id, $order_type ) ) {
+ $response->data['order_type'] = $order_type . '_order';
+ break;
+ }
+ }
+
+ $orders[] = $this->prepare_response_for_collection( $response );
+ }
+
+ $response = rest_ensure_response( $orders );
+ $response->header( 'X-WP-Total', count( $orders ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return apply_filters( 'wcs_rest_subscription_orders_response', $response, $request );
+ }
+
+ /**
+ * Get subscription statuses
+ *
+ * @since 2.1
+ */
+ public function get_statuses() {
+ return rest_ensure_response( wcs_get_subscription_statuses() );
+ }
+
+ /**
+ * Overrides WC_REST_Orders_Controller::get_order_statuses() so that subscription statuses are
+ * validated correctly in WC_REST_Orders_Controller::get_collection_params()
+ *
+ * @since 2.1
+ */
+ protected function get_order_statuses() {
+ $subscription_statuses = array();
+
+ foreach ( array_keys( wcs_get_subscription_statuses() ) as $status ) {
+ $subscription_statuses[] = str_replace( 'wc-', '', $status );
+ }
+ return $subscription_statuses;
+ }
+
+ /**
+ * Create WC_Subscription object.
+ *
+ * @since 2.1
+ * @param array $args subscription args.
+ * @return WC_Subscription
+ */
+ protected function create_base_order( $args ) {
+ $subscription = wcs_create_subscription( $args );
+
+ if ( is_wp_error( $subscription ) ) {
+ throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_subscription', sprintf( __( 'Cannot create subscription: %s.', 'woocommerce-subscriptions' ), implode( ', ', $subscription->get_error_messages() ) ), 400 );
+ }
+
+ $this->update_schedule( $subscription, $args );
+
+ if ( empty( $args['payment_details']['method_id'] ) && ! empty( $args['payment_method'] ) ) {
+ $args['payment_details']['method_id'] = $args['payment_method'];
+ }
+
+ $this->update_payment_method( $subscription, $args['payment_details'] );
+
+ return $subscription;
+ }
+
+ /**
+ * Update or set the subscription schedule with the request data
+ *
+ * @since 2.1
+ * @param WC_Subscription $subscription
+ * @param array $data
+ */
+ public function update_schedule( $subscription, $data ) {
+ if ( isset( $data['billing_interval'] ) ) {
+ update_post_meta( $subscription->id, '_billing_interval', absint( $data['billing_interval'] ) );
+ }
+
+ if ( ! empty( $data['billing_period'] ) ) {
+ update_post_meta( $subscription->id, '_billing_period', $data['billing_period'] );
+ }
+
+ try {
+ $dates_to_update = array();
+
+ foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
+ if ( isset( $data[ $date_type . '_date' ] ) ) {
+ $dates_to_update[ $date_type ] = $data[ $date_type . '_date' ];
+ }
+ }
+
+ if ( ! empty( $dates_to_update ) ) {
+ $subscription->update_dates( $dates_to_update );
+ }
+ } catch ( Exception $e ) {
+ throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_subscription_dates', sprintf( __( 'Updating subscription dates errored with message: %s', 'woocommerce-subscriptions' ), $e->getMessage() ), 400 );
+ }
+ }
+
+ /**
+ * Validate and update payment method on a subscription
+ *
+ * @since 2.1
+ * @param WC_Subscription $subscription
+ * @param array $data
+ * @param bool $updating
+ */
+ public function update_payment_method( $subscription, $data, $updating = false ) {
+ $payment_gateways = WC()->payment_gateways->get_available_payment_gateways();
+ $payment_method = ( ! empty( $data['method_id'] ) ) ? $data['method_id'] : 'manual';
+ $payment_gateway = ( ! empty( $payment_gateways[ $payment_method ] ) ) ? $payment_gateways[ $payment_method ] : '';
+
+ try {
+ if ( $updating && ! array_key_exists( $payment_method, WCS_Change_Payment_Method_Admin::get_valid_payment_methods( $subscription ) ) ) {
+ throw new Exception( __( 'Gateway does not support admin changing the payment method on a Subscription.', 'woocommerce-subscriptions' ) );
+ }
+
+ $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
+
+ if ( ! empty( $payment_gateway ) && isset( $payment_method_meta[ $payment_gateway->id ] ) ) {
+ $payment_method_meta = $payment_method_meta[ $payment_gateway->id ];
+
+ if ( ! empty( $payment_method_meta ) ) {
+
+ foreach ( $payment_method_meta as $meta_table => &$meta ) {
+ if ( ! is_array( $meta ) ) {
+ continue;
+ }
+
+ foreach ( $meta as $meta_key => &$meta_data ) {
+
+ if ( isset( $data[ $meta_table ][ $meta_key ] ) ) {
+ $meta_data['value'] = $data[ $meta_table ][ $meta_key ];
+ }
+ }
+ }
+ }
+ }
+
+ if ( empty( $subscription->payment_gateway ) ) {
+ $subscription->payment_gateway = $payment_gateway;
+ }
+
+ $subscription->set_payment_method( $payment_gateway, $payment_method_meta );
+
+ } catch ( Exception $e ) {
+ // translators: 1$: gateway id, 2$: error message
+ throw new WC_REST_Exception( 'woocommerce_rest_invalid_payment_data', sprintf( __( 'Subscription payment method could not be set to %1$s with error message: %2$s', 'woocommerce-subscriptions' ), $payment_method, $e->getMessage() ), 400 );
+ }
+ }
+
+ /**
+ * Adds additional item schema information for subscription requests
+ *
+ * @since 2.1
+ */
+ public function get_item_schema() {
+ $schema = parent::get_item_schema();
+
+ $subscriptions_schema = array(
+ 'billing_interval' => array(
+ 'description' => __( 'The number of billing periods between subscription renewals.', 'woocommerce-subscriptions' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'billing_period' => array(
+ 'description' => __( 'Billing period for the subscription.', 'woocommerce-subscriptions' ),
+ 'type' => 'string',
+ 'enum' => array_keys( wcs_get_subscription_period_strings() ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'payment_details' => array(
+ 'description' => __( 'Subscription payment details.', 'woocommerce-subscriptions' ),
+ 'type' => 'array',
+ 'context' => array( 'edit' ),
+ 'properties' => array(
+ 'method_id' => array(
+ 'description' => __( 'Payment gateway ID.', 'woocommerce-subscriptions' ),
+ 'type' => 'string',
+ 'context' => array( 'edit' ),
+ ),
+ ),
+ ),
+ 'start_date' => array(
+ 'description' => __( "The subscription's start date.", 'woocommerce-subscriptions' ),
+ 'type' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'trial_date' => array(
+ 'description' => __( "The subscription's trial date", 'woocommerce-subscriptions' ),
+ 'type' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'next_payment_date' => array(
+ 'description' => __( "The subscription's next payment date.", 'woocommerce-subscriptions' ),
+ 'type' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'end_date' => array(
+ 'description' => __( "The subscription's end date.", 'woocommerce-subscriptions' ),
+ 'type' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ );
+
+ $schema['properties'] += $subscriptions_schema;
+ return $schema;
+ }
+
+ /**
+ * Prepare subscription data for create.
+ *
+ * @since 2.1
+ * @param stdClass $data
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass
+ */
+ public function prepare_subscription_args( $data, $request ) {
+ $data->billing_interval = $request['billing_interval'];
+ $data->billing_period = $request['billing_period'];
+
+ foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
+ if ( ! empty( $request[ $date_type . '_date' ] ) ) {
+ $data->{$date_type . '_date'} = $request[ $date_type . '_date' ];
+ }
+ }
+
+ $data->payment_details = ! empty( $request['payment_details'] ) ? $request['payment_details'] : '';
+ $data->payment_method = ! empty( $request['payment_method'] ) ? $request['payment_method'] : '';
+
+ return $data;
+ }
+}
diff --git a/includes/api/class-wc-api-subscriptions-customers.php b/includes/api/legacy/class-wc-api-subscriptions-customers.php
similarity index 100%
rename from includes/api/class-wc-api-subscriptions-customers.php
rename to includes/api/legacy/class-wc-api-subscriptions-customers.php
diff --git a/includes/api/class-wc-api-subscriptions.php b/includes/api/legacy/class-wc-api-subscriptions.php
similarity index 100%
rename from includes/api/class-wc-api-subscriptions.php
rename to includes/api/legacy/class-wc-api-subscriptions.php
diff --git a/includes/class-wc-product-subscription-variation.php b/includes/class-wc-product-subscription-variation.php
index 9f7d4cd..b80ead3 100644
--- a/includes/class-wc-product-subscription-variation.php
+++ b/includes/class-wc-product-subscription-variation.php
@@ -142,19 +142,7 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
*/
function is_purchasable() {
- $purchasable = $this->parent->is_purchasable();
-
- // if we have a limited subscription product, make sure the customer doesn't already have another variation for the same variable product in their cart, but only if we're not on the order received or PayPal return pages (we can't use is_order_received_page() to check that becuase get_cart_from_session() is called before the query vars are setup)
- if ( 'no' != $this->parent->limit_subscriptions && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
-
- foreach ( WC()->cart->cart_contents as $cart_item ) { // can't use WC()->cart->get_cart() because it will trigger an infinite loop when this is called within WC_Cart::get_cart_from_session()
-
- if ( $this->id == $cart_item['data']->id && $this->variation_id != $cart_item['data']->variation_id ) {
- $purchasable = false;
- break;
- }
- }
- }
+ $purchasable = WCS_Limiter::is_purchasable( $this->parent->is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_variation_is_purchasable', $purchasable, $this );
}
diff --git a/includes/class-wc-product-subscription.php b/includes/class-wc-product-subscription.php
index b619183..67cc2da 100644
--- a/includes/class-wc-product-subscription.php
+++ b/includes/class-wc-product-subscription.php
@@ -77,14 +77,21 @@ class WC_Product_Subscription extends WC_Product_Simple {
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
- if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
- $this->limit_subscriptions = 'no';
- } elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
- $this->limit_subscriptions = 'any';
- } else {
- $this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
- }
+ }
+ /**
+ * Auto-load in-accessible properties on demand.
+ *
+ * @param mixed $key
+ * @return mixed
+ */
+ public function __get( $key ) {
+ if ( 'limit_subscriptions' === $key ) {
+ _deprecated_argument( 'WC_Product_Subscription->limit_subscriptions', '2.1', 'Use wcs_get_product_limitation directly' );
+ return wcs_get_product_limitation( $this );
+ } else {
+ return parent::__get( $key );
+ }
}
/**
@@ -182,12 +189,7 @@ class WC_Product_Subscription extends WC_Product_Simple {
* @return bool
*/
function is_purchasable() {
-
- $purchasable = parent::is_purchasable();
-
- if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
- $purchasable = false;
- }
+ $purchasable = WCS_Limiter::is_purchasable( parent::is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}
diff --git a/includes/class-wc-product-variable-subscription.php b/includes/class-wc-product-variable-subscription.php
index ab8f740..2e5f380 100644
--- a/includes/class-wc-product-variable-subscription.php
+++ b/includes/class-wc-product-variable-subscription.php
@@ -78,17 +78,24 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable {
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
- if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
- $this->limit_subscriptions = 'no';
- } elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
- $this->limit_subscriptions = 'any';
- } else {
- $this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
- }
-
add_filter( 'woocommerce_add_to_cart_handler', array( &$this, 'add_to_cart_handler' ), 10, 2 );
}
+ /**
+ * Auto-load in-accessible properties on demand.
+ *
+ * @param mixed $key
+ * @return mixed
+ */
+ public function __get( $key ) {
+ if ( 'limit_subscriptions' === $key ) {
+ _deprecated_argument( 'WC_Product_Subscription->limit_subscriptions', '2.1', 'Use wcs_get_product_limitation directly' );
+ return wcs_get_product_limitation( $this );
+ } else {
+ return parent::__get( $key );
+ }
+ }
+
/**
* Get the add to cart button text for the single page
*
@@ -594,12 +601,7 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable {
* @return bool
*/
function is_purchasable() {
-
- $purchasable = parent::is_purchasable();
-
- if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
- $purchasable = false;
- }
+ $purchasable = WCS_Limiter::is_purchasable( parent::is_purchasable(), $this );
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}
diff --git a/includes/class-wc-subscription.php b/includes/class-wc-subscription.php
index 634603f..9dabe2b 100644
--- a/includes/class-wc-subscription.php
+++ b/includes/class-wc-subscription.php
@@ -20,6 +20,9 @@ class WC_Subscription extends WC_Order {
/** @public string Order type */
public $order_type = 'shop_subscription';
+ /** @private int Stores get_completed_payment_count when used multiple times in payment_complete() */
+ private $cached_completed_payment_count = false;
+
/**
* Initialize the subscription object.
*
@@ -155,13 +158,6 @@ class WC_Subscription extends WC_Order {
'value' => $this->id,
'type' => 'numeric',
),
- array(
- 'key' => '_subscription_switch',
- 'compare' => '=',
- 'value' => $this->id,
- 'type' => 'numeric',
- ),
- 'relation' => 'OR',
),
) );
@@ -332,14 +328,17 @@ class WC_Subscription extends WC_Order {
$end_date = $this->calculate_date( 'end_of_prepaid_term' );
- // If there is no future payment and no expiration date set, the customer has no prepaid term (this shouldn't be possible as only active subscriptions can be set to pending cancellation and an active subscription always has either an end date or next payment)
- if ( 0 == $end_date ) {
- $end_date = current_time( 'mysql', true );
+ // If there is no future payment and no expiration date set, or the end date is before now, the customer has no prepaid term (this shouldn't be possible as only active subscriptions can be set to pending cancellation and an active subscription always has either an end date or next payment), so set the end date and cancellation date to now
+ if ( 0 == $end_date || wcs_date_to_time( $end_date ) < current_time( 'timestamp', true ) ) {
+ $cancelled_date = $end_date = current_time( 'mysql', true );
+ } else {
+ // the cancellation date is now, and the end date is the end of prepaid term date
+ $cancelled_date = current_time( 'mysql', true );
}
$this->delete_date( 'trial_end' );
$this->delete_date( 'next_payment' );
- $this->update_dates( array( 'end' => $end_date ) );
+ $this->update_dates( array( 'cancelled' => $cancelled_date, 'end' => $end_date ) );
break;
case 'completed' : // core WC order status mapped internally to avoid exceptions
@@ -373,14 +372,21 @@ class WC_Subscription extends WC_Order {
case 'expired' :
$this->delete_date( 'trial_end' );
$this->delete_date( 'next_payment' );
- $this->update_dates( array( 'end' => current_time( 'mysql', true ) ) );
+
+ $dates_to_update = array(
+ 'end' => current_time( 'mysql', true ),
+ );
+
+ // Also set the cancelled date to now if it wasn't set previously (when the status was changed to pending-cancellation)
+ if ( 'cancelled' === $new_status && 0 == $this->get_date( 'cancelled' ) ) {
+ $dates_to_update['cancelled'] = $dates_to_update['end'];
+ }
+
+ $this->update_dates( $dates_to_update );
wcs_maybe_make_user_inactive( $this->customer_user );
break;
}
- // translators: $1 note why the status changes (if any), $2: old status, $3: new status
- $this->add_order_note( trim( sprintf( __( '%1$s Status changed from %2$s to %3$s.', 'woocommerce-subscriptions' ), $note, wcs_get_subscription_status_name( $old_status ), wcs_get_subscription_status_name( $new_status ) ) ), 0, $manual );
-
// dynamic hooks for convenience
do_action( 'woocommerce_subscription_status_' . $new_status, $this );
do_action( 'woocommerce_subscription_status_' . $old_status . '_to_' . $new_status, $this );
@@ -391,13 +397,16 @@ class WC_Subscription extends WC_Order {
// Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes
do_action( 'woocommerce_subscription_status_changed', $this->id, $old_status, $new_status );
+ // translators: $1 note why the status changes (if any), $2: old status, $3: new status
+ $this->add_order_note( trim( sprintf( __( '%1$s Status changed from %2$s to %3$s.', 'woocommerce-subscriptions' ), $note, wcs_get_subscription_status_name( $old_status ), wcs_get_subscription_status_name( $new_status ) ) ), 0, $manual );
+
} catch ( Exception $e ) {
// Make sure the old status is restored
wp_update_post( array( 'ID' => $this->id, 'post_status' => $old_status_key ) );
$this->post_status = $old_status_key;
- $this->add_order_note( sprintf( __( 'Unable to change subscription status to "%s".', 'woocommerce-subscriptions' ), $new_status ) );
+ $this->add_order_note( sprintf( __( 'Unable to change subscription status to "%s". Exception: %s', 'woocommerce-subscriptions' ), $new_status, $e->getMessage() ) );
do_action( 'woocommerce_subscription_unable_to_update_status', $this, $new_status, $old_status );
@@ -497,55 +506,67 @@ class WC_Subscription extends WC_Order {
*/
public function get_completed_payment_count() {
- $completed_payment_count = ( false !== $this->order && ( isset( $this->order->paid_date ) || $this->order->has_status( $this->get_paid_order_statuses() ) ) ) ? 1 : 0;
+ // If not cached, calculate the completed payment count otherwise return the cached version
+ if ( false === $this->cached_completed_payment_count ) {
- // not all gateways will call $order->payment_complete() so we need to find renewal orders with a paid status rather than just a _paid_date
- $paid_status_renewal_orders = get_posts( array(
- 'posts_per_page' => -1,
- 'post_status' => $this->get_paid_order_statuses(),
- 'post_type' => 'shop_order',
- 'fields' => 'ids',
- 'orderby' => 'date',
- 'order' => 'desc',
- 'meta_query' => array(
- array(
- 'key' => '_subscription_renewal',
- 'compare' => '=',
- 'value' => $this->id,
- 'type' => 'numeric',
- ),
- ),
- ) );
+ $completed_payment_count = ( false !== $this->order && ( isset( $this->order->paid_date ) || $this->order->has_status( $this->get_paid_order_statuses() ) ) ) ? 1 : 0;
- // because some stores may be using custom order status plugins, we also can't rely on order status to find paid orders, so also check for a _paid_date
- $paid_date_renewal_orders = get_posts( array(
- 'posts_per_page' => -1,
- 'post_status' => 'any',
- 'post_type' => 'shop_order',
- 'fields' => 'ids',
- 'orderby' => 'date',
- 'order' => 'desc',
- 'meta_query' => array(
- array(
- 'key' => '_subscription_renewal',
- 'compare' => '=',
- 'value' => $this->id,
- 'type' => 'numeric',
- ),
- array(
- 'key' => '_paid_date',
- 'compare' => 'EXISTS',
- ),
- ),
- ) );
+ // Get all renewal orders - for large sites its more efficient to find the two different sets of renewal orders below using post__in than complicated meta queries
+ $renewal_orders = get_posts( array(
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ 'post_type' => 'shop_order',
+ 'fields' => 'ids',
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'meta_key' => '_subscription_renewal',
+ 'meta_compare' => '=',
+ 'meta_type' => 'numeric',
+ 'meta_value' => $this->id,
+ 'update_post_term_cache' => false,
+ ) );
- $paid_renewal_orders = array_unique( array_merge( $paid_date_renewal_orders, $paid_status_renewal_orders ) );
+ if ( ! empty( $renewal_orders ) ) {
- if ( ! empty( $paid_renewal_orders ) ) {
- $completed_payment_count += count( $paid_renewal_orders );
+ // Not all gateways will call $order->payment_complete() so we need to find renewal orders with a paid status rather than just a _paid_date
+ $paid_status_renewal_orders = get_posts( array(
+ 'posts_per_page' => -1,
+ 'post_status' => $this->get_paid_order_statuses(),
+ 'post_type' => 'shop_order',
+ 'fields' => 'ids',
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'post__in' => $renewal_orders,
+ ) );
+
+ // Some stores may be using custom order status plugins, we also can't rely on order status to find paid orders, so also check for a _paid_date
+ $paid_date_renewal_orders = get_posts( array(
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ 'post_type' => 'shop_order',
+ 'fields' => 'ids',
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'post__in' => $renewal_orders,
+ 'meta_key' => '_paid_date',
+ 'meta_compare' => 'EXISTS',
+ 'update_post_term_cache' => false,
+ ) );
+
+ $paid_renewal_orders = array_unique( array_merge( $paid_date_renewal_orders, $paid_status_renewal_orders ) );
+
+ if ( ! empty( $paid_renewal_orders ) ) {
+ $completed_payment_count += count( $paid_renewal_orders );
+ }
+ }
+ } else {
+ $completed_payment_count = $this->cached_completed_payment_count;
}
- return apply_filters( 'woocommerce_subscription_payment_completed_count', $completed_payment_count, $this );
+ // Store the completed payment count to avoid hitting the database again
+ $this->cached_completed_payment_count = apply_filters( 'woocommerce_subscription_payment_completed_count', $completed_payment_count, $this );
+
+ return $this->cached_completed_payment_count;
}
/**
@@ -627,16 +648,11 @@ class WC_Subscription extends WC_Order {
case 'start' :
$this->schedule->{$date_type} = ( '0000-00-00 00:00:00' != $this->post->post_date_gmt ) ? $this->post->post_date_gmt : get_gmt_from_date( $this->post->post_date ); // why not always use post_date_gmt? Because when a post is first created via the Add Subscription screen, it has a post_date but not a post_date_gmt value yet
break;
- case 'next_payment' :
- case 'trial_end' :
- case 'end' :
- $this->schedule->{$date_type} = get_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), true );
- break;
case 'last_payment' :
$this->schedule->{$date_type} = $this->get_last_payment_date();
break;
default :
- $this->schedule->{$date_type} = 0;
+ $this->schedule->{$date_type} = get_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), true );
break;
}
@@ -690,6 +706,9 @@ class WC_Subscription extends WC_Order {
case 'end' :
$date_to_display = __( 'Not yet ended', 'woocommerce-subscriptions' );
break;
+ case 'cancelled' :
+ $date_to_display = __( 'Not cancelled', 'woocommerce-subscriptions' );
+ break;
case 'next_payment' :
case 'trial_end' :
default :
@@ -712,7 +731,7 @@ class WC_Subscription extends WC_Order {
$datetime = $this->get_date( $date_type, $timezone );
if ( 0 !== $datetime ) {
- $datetime = strtotime( $datetime );
+ $datetime = wcs_date_to_time( $datetime );
}
return $datetime;
@@ -764,7 +783,7 @@ class WC_Subscription extends WC_Order {
$datetime = get_gmt_from_date( $datetime );
}
- $timestamps[ $date_type ] = strtotime( $datetime );
+ $timestamps[ $date_type ] = wcs_date_to_time( $datetime );
}
}
@@ -789,6 +808,11 @@ class WC_Subscription extends WC_Order {
foreach ( $timestamps as $date_type => $datetime ) {
switch ( $date_type ) {
case 'end' :
+ if ( array_key_exists( 'cancelled', $timestamps ) && $datetime < $timestamps['cancelled'] ) {
+ $messages[] = sprintf( __( 'The %s date must occur after the cancellation date.', 'woocommerce-subscriptions' ), $date_type );
+ }
+
+ case 'cancelled' :
if ( array_key_exists( 'last_payment', $timestamps ) && $datetime < $timestamps['last_payment'] ) {
$messages[] = sprintf( __( 'The %s date must occur after the last payment date.', 'woocommerce-subscriptions' ), $date_type );
}
@@ -815,18 +839,13 @@ class WC_Subscription extends WC_Order {
$is_updated = false;
foreach ( $timestamps as $date_type => $timestamp ) {
- $datetime = date( 'Y-m-d H:i:s', $timestamp );
+ $datetime = gmdate( 'Y-m-d H:i:s', $timestamp );
if ( $datetime == $this->get_date( $date_type ) ) {
continue;
}
switch ( $date_type ) {
- case 'next_payment' :
- case 'trial_end' :
- case 'end' :
- $is_updated = update_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), $datetime );
- break;
case 'start' :
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_date = %s, post_date_gmt = %s WHERE ID = %s", get_date_from_gmt( $datetime ), $datetime, $this->id ) ); // Don't use wp_update_post() to avoid infinite loops here
$is_updated = true;
@@ -835,6 +854,9 @@ class WC_Subscription extends WC_Order {
$this->update_last_payment_date( $datetime );
$is_updated = true;
break;
+ default :
+ $is_updated = update_post_meta( $this->id, wcs_get_date_meta_key( $date_type ), $datetime );
+ break;
}
if ( $is_updated ) {
@@ -888,6 +910,7 @@ class WC_Subscription extends WC_Order {
}
break;
case 'trial_end' :
+ $this->cached_completed_payment_count = false;
if ( $this->get_completed_payment_count() < 2 && ! $this->has_status( wcs_get_subscription_ended_statuses() ) && ( $this->has_status( 'pending' ) || $this->payment_method_supports( 'subscription_date_changes' ) ) ) {
$can_date_be_updated = true;
} else {
@@ -1003,12 +1026,12 @@ class WC_Subscription extends WC_Order {
}
// If the subscription has an end date and the next billing period comes after that, return 0
- if ( 0 != $end_time && ( $next_payment_timestamp + 120 ) > $end_time ) {
+ if ( 0 != $end_time && ( $next_payment_timestamp + 23 * HOUR_IN_SECONDS ) > $end_time ) {
$next_payment_timestamp = 0;
}
if ( $next_payment_timestamp > 0 ) {
- $next_payment_date = date( 'Y-m-d H:i:s', $next_payment_timestamp );
+ $next_payment_date = gmdate( 'Y-m-d H:i:s', $next_payment_timestamp );
}
return $next_payment_date;
@@ -1249,6 +1272,9 @@ class WC_Subscription extends WC_Order {
*/
public function payment_complete( $transaction_id = '' ) {
+ // Clear the cached completed payment count
+ $this->cached_completed_payment_count = false;
+
// Make sure the last order's status is updated
$last_order = $this->get_last_order( 'all', 'any' );
@@ -1276,7 +1302,7 @@ class WC_Subscription extends WC_Order {
do_action( 'woocommerce_subscription_payment_complete', $this );
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
- do_action( 'woocommerce_subscription_renewal_payment_complete', $this );
+ do_action( 'woocommerce_subscription_renewal_payment_complete', $this, $last_order );
}
}
@@ -1309,7 +1335,7 @@ class WC_Subscription extends WC_Order {
do_action( 'woocommerce_subscription_payment_failed', $this, $new_status );
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
- do_action( 'woocommerce_subscription_renewal_payment_failed', $this );
+ do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $last_order );
}
}
@@ -1449,7 +1475,7 @@ class WC_Subscription extends WC_Order {
public function get_last_order( $return_fields = 'ids', $order_types = array( 'parent', 'renewal' ) ) {
$return_fields = ( 'ids' == $return_fields ) ? $return_fields : 'all';
- $order_types = ( 'any' == $order_types ) ? array( 'parent', 'renewal', 'switch' ) : $order_types;
+ $order_types = ( 'any' == $order_types ) ? array( 'parent', 'renewal', 'switch' ) : (array) $order_types;
$related_orders = array();
foreach ( $order_types as $order_type ) {
diff --git a/includes/class-wc-subscriptions-cart.php b/includes/class-wc-subscriptions-cart.php
index 7bc54f7..4c5d4db 100644
--- a/includes/class-wc-subscriptions-cart.php
+++ b/includes/class-wc-subscriptions-cart.php
@@ -91,7 +91,7 @@ class WC_Subscriptions_Cart {
add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' );
add_action( 'woocommerce_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' );
- add_action( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 3 );
+ add_action( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 6 );
add_filter( 'woocommerce_cart_needs_shipping', __CLASS__ . '::cart_needs_shipping', 11, 1 );
@@ -310,10 +310,6 @@ class WC_Subscriptions_Cart {
$total = max( 0, round( WC()->cart->cart_contents_total + WC()->cart->tax_total + WC()->cart->shipping_tax_total + WC()->cart->shipping_total + WC()->cart->fee_total, WC()->cart->dp ) );
- if ( isset( WC()->cart->discount_total ) && 0 !== WC()->cart->discount_total ) { // WC < 2.3, deduct deprecated after tax discount total
- $total = max( 0, round( $total - WC()->cart->discount_total, WC()->cart->dp ) );
- }
-
if ( ! self::charge_shipping_up_front() ) {
$total = max( 0, $total - WC()->cart->shipping_tax_total - WC()->cart->shipping_total );
WC()->cart->shipping_taxes = array();
@@ -948,7 +944,7 @@ class WC_Subscriptions_Cart {
$cart_key = '';
$product = $cart_item['data'];
- $product_id = ! empty( $product->variation_id ) ? $product->variation_id : $product->id;
+ $product_id = wcs_get_canonical_product_id( $product );
$renewal_time = ! empty( $renewal_time ) ? $renewal_time : WC_Subscriptions_Product::get_first_renewal_payment_time( $product_id );
$interval = WC_Subscriptions_Product::get_interval( $product );
$period = WC_Subscriptions_Product::get_period( $product );
@@ -957,7 +953,7 @@ class WC_Subscriptions_Cart {
$trial_length = WC_Subscriptions_Product::get_trial_length( $product );
if ( $renewal_time > 0 ) {
- $cart_key .= date( 'Y_m_d_', $renewal_time );
+ $cart_key .= gmdate( 'Y_m_d_', $renewal_time );
}
// First start with the billing interval and period
@@ -996,13 +992,13 @@ class WC_Subscriptions_Cart {
}
/**
- * Don't allow other subscriptions to be added to the cart while it contains a renewal
+ * Don't allow new subscription products to be added to the cart if it contains a subscription renewal already.
*
* @since 2.0
*/
- public static function check_valid_add_to_cart( $is_valid, $product, $quantity ) {
+ public static function check_valid_add_to_cart( $is_valid, $product_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) {
- if ( $is_valid && wcs_cart_contains_renewal() && WC_Subscriptions_Product::is_subscription( $product ) ) {
+ if ( $is_valid && ! isset( $item_data['subscription_renewal'] ) && wcs_cart_contains_renewal() && WC_Subscriptions_Product::is_subscription( $product_id ) ) {
wc_add_notice( __( 'That subscription product can not be added to your cart as it already contains a subscription renewal.', 'woocommerce-subscriptions' ), 'error' );
$is_valid = false;
@@ -1206,197 +1202,6 @@ class WC_Subscriptions_Cart {
/* Deprecated */
- /**
- * Returns the formatted subscription price string for an item
- *
- * @since 1.0
- */
- public static function get_cart_item_price_html( $price_string, $cart_item ) {
-
- _deprecated_function( __METHOD__, '1.2' );
-
- return $price_string;
- }
-
- /**
- * Returns either the total if prices include tax because this doesn't include tax, or the
- * subtotal if prices don't includes tax, because this doesn't include tax.
- *
- * @return string formatted price
- *
- * @since 1.0
- */
- public static function get_cart_contents_total( $cart_contents_total ) {
-
- _deprecated_function( __METHOD__, '1.2' );
-
- return $cart_contents_total;
- }
-
- /**
- * Calculate totals for the sign-up fees in the cart, based on @see WC_Cart::calculate_totals()
- *
- * @since 1.0
- */
- public static function calculate_sign_up_fee_totals() {
- _deprecated_function( __METHOD__, '1.2' );
- }
-
- /**
- * Function to apply discounts to a product and get the discounted price (before tax is applied)
- *
- * @param mixed $values
- * @param mixed $price
- * @param bool $add_totals (default: false)
- * @return float price
- * @since 1.0
- */
- public static function get_discounted_price( $values, $price, $add_totals = false ) {
-
- _deprecated_function( __METHOD__, '1.2' );
-
- return $price;
- }
-
- /**
- * Function to apply product discounts after tax
- *
- * @param mixed $values
- * @param mixed $price
- * @since 1.0
- */
- public static function apply_product_discounts_after_tax( $values, $price ) {
- _deprecated_function( __METHOD__, '1.2' );
- }
-
- /**
- * Function to apply cart discounts after tax
- *
- * @since 1.0
- */
- public static function apply_cart_discounts_after_tax() {
- _deprecated_function( __METHOD__, '1.2' );
- }
-
- /**
- * Get tax row amounts with or without compound taxes includes
- *
- * @return float price
- */
- public static function get_sign_up_taxes_total( $compound = true ) {
- _deprecated_function( __METHOD__, '1.2' );
- return 0;
- }
-
- public static function get_sign_up_fee_fields() {
- _deprecated_function( __METHOD__, '1.2' );
-
- return array(
- 'cart_contents_sign_up_fee_total',
- 'cart_contents_sign_up_fee_count',
- 'sign_up_fee_total',
- 'sign_up_fee_subtotal',
- 'sign_up_fee_subtotal_ex_tax',
- 'sign_up_fee_tax_total',
- 'sign_up_fee_taxes',
- 'sign_up_fee_discount_cart',
- 'sign_up_fee_discount_total',
- );
- }
-
- /**
- * Returns the subtotal for a cart item including the subscription period and duration details
- *
- * @since 1.0
- */
- public static function get_product_subtotal( $product_subtotal, $product ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_product_subtotal( $product_subtotal, $product )' );
- return self::get_formatted_product_subtotal( $product_subtotal, $product );
- }
-
- /**
- * Returns a string with the cart discount and subscription period.
- *
- * @deprecated 1.2
- * @since 1.0
- */
- public static function get_discounts_before_tax( $discount, $cart ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_discounts_before_tax( $discount )' );
- return self::get_formatted_discounts_before_tax( $discount );
- }
-
- /**
- * Gets the order discount amount - these are applied after tax
- *
- * @deprecated 1.2
- * @since 1.0
- */
- public static function get_discounts_after_tax( $discount, $cart ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_discounts_after_tax( $discount )' );
- return self::get_formatted_discounts_after_tax( $discount );
- }
-
- /**
- * Includes the sign-up fee subtotal in the subtotal displayed in the cart.
- *
- * @deprecated 1.2
- * @since 1.0
- */
- public static function get_cart_subtotal( $cart_subtotal, $compound, $cart ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_cart_subtotal( $cart_subtotal, $compound, $cart )' );
- return self::get_formatted_cart_subtotal( $cart_subtotal, $compound, $cart );
- }
-
- /**
- * Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it.
- *
- * @deprecated 1.2
- * @since 1.0
- */
- public static function get_total( $total ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_total( $total )' );
- return self::get_formatted_total( $total );
- }
-
- /**
- * Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it.
- *
- * @deprecated 1.2
- * @since 1.0
- */
- public static function get_total_ex_tax( $total_ex_tax ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ .'::get_formatted_total_ex_tax( $total_ex_tax )' );
- return self::get_formatted_total_ex_tax( $total_ex_tax );
- }
-
- /**
- * Displays each cart tax in a subscription string and calculates the sign-up fee taxes (if any)
- * to display in the string.
- *
- * @since 1.2
- */
- public static function get_formatted_taxes( $formatted_taxes, $cart ) {
- _deprecated_function( __METHOD__, '1.4.9', __CLASS__ .'::get_recurring_tax_totals( $total_ex_tax )' );
-
- if ( self::cart_contains_subscription() ) {
-
- $recurring_taxes = self::get_recurring_taxes();
-
- foreach ( $formatted_taxes as $tax_id => $tax_amount ) {
- $formatted_taxes[ $tax_id ] = self::get_cart_subscription_string( $tax_amount, $recurring_taxes[ $tax_id ] );
- }
-
- // Add any recurring tax not already handled - when a subscription has a free trial and a sign-up fee, we get a recurring shipping tax with no initial shipping tax
- foreach ( $recurring_taxes as $tax_id => $tax_amount ) {
- if ( ! array_key_exists( $tax_id, $formatted_taxes ) ) {
- $formatted_taxes[ $tax_id ] = self::get_cart_subscription_string( '', $tax_amount );
- }
- }
- }
-
- return $formatted_taxes;
- }
-
/**
* Checks the cart to see if it contains a subscription product renewal.
*
diff --git a/includes/class-wc-subscriptions-coupon.php b/includes/class-wc-subscriptions-coupon.php
index a8c6b2c..443c934 100644
--- a/includes/class-wc-subscriptions-coupon.php
+++ b/includes/class-wc-subscriptions-coupon.php
@@ -632,30 +632,6 @@ class WC_Subscriptions_Coupon {
return $cart;
}
- /**
- * Determines if cart contains a recurring fee discount code
- *
- * Does not check if the code is valid, etc
- *
- * @since 1.2
- */
- public static function cart_contains_recurring_discount() {
- _deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "recurring_fee" )' );
- return self::cart_contains_discount( 'recurring_fee' );
- }
-
- /**
- * Determines if cart contains a sign up fee discount code
- *
- * Does not check if the code is valid, etc
- *
- * @since 1.2
- */
- public static function cart_contains_sign_up_discount() {
- _deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "sign_up_fee" )' );
- return self::cart_contains_discount( 'sign_up_fee' );
- }
-
/**
* Restores discount coupons which had been removed for special subscription calculations.
*
diff --git a/includes/class-wc-subscriptions-email.php b/includes/class-wc-subscriptions-email.php
index da12ba1..daacd84 100644
--- a/includes/class-wc-subscriptions-email.php
+++ b/includes/class-wc-subscriptions-email.php
@@ -7,7 +7,7 @@
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Email
* @category Class
- * @author Brent Shepherd
+ * @author Prospress
*/
class WC_Subscriptions_Email {
@@ -26,6 +26,7 @@ class WC_Subscriptions_Email {
add_filter( 'woocommerce_resend_order_emails_available', __CLASS__ . '::renewal_order_emails_available', -1 ); // run before other plugins so we don't remove their emails
+ add_action( 'woocommerce_subscriptions_email_order_details', __CLASS__ . '::order_details', 10, 4 );
}
/**
@@ -42,6 +43,8 @@ class WC_Subscriptions_Email {
require_once( 'emails/class-wcs-email-customer-completed-switch-order.php' );
require_once( 'emails/class-wcs-email-customer-renewal-invoice.php' );
require_once( 'emails/class-wcs-email-cancelled-subscription.php' );
+ require_once( 'emails/class-wcs-email-expired-subscription.php' );
+ require_once( 'emails/class-wcs-email-on-hold-subscription.php' );
$email_classes['WCS_Email_New_Renewal_Order'] = new WCS_Email_New_Renewal_Order();
$email_classes['WCS_Email_New_Switch_Order'] = new WCS_Email_New_Switch_Order();
@@ -50,6 +53,8 @@ class WC_Subscriptions_Email {
$email_classes['WCS_Email_Completed_Switch_Order'] = new WCS_Email_Completed_Switch_Order();
$email_classes['WCS_Email_Customer_Renewal_Invoice'] = new WCS_Email_Customer_Renewal_Invoice();
$email_classes['WCS_Email_Cancelled_Subscription'] = new WCS_Email_Cancelled_Subscription();
+ $email_classes['WCS_Email_Expired_Subscription'] = new WCS_Email_Expired_Subscription();
+ $email_classes['WCS_Email_On_Hold_Subscription'] = new WCS_Email_On_Hold_Subscription();
return $email_classes;
}
@@ -67,6 +72,8 @@ class WC_Subscriptions_Email {
}
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::send_cancelled_email', 10, 2 );
+ add_action( 'woocommerce_subscription_status_expired', __CLASS__ . '::send_expired_email', 10, 2 );
+ add_action( 'woocommerce_customer_changed_subscription_to_on-hold', __CLASS__ . '::send_on_hold_email', 10, 2 );
$order_email_actions = array(
'woocommerce_order_status_pending_to_processing',
@@ -102,6 +109,30 @@ class WC_Subscriptions_Email {
}
}
+ /**
+ * Init the mailer and call for the expired email notification hook.
+ *
+ * @param $subscription WC Subscription
+ * @since 2.1
+ */
+ public static function send_expired_email( $subscription ) {
+ WC()->mailer();
+
+ do_action( 'expired_subscription_notification', $subscription );
+ }
+
+ /**
+ * Init the mailer and call for the suspended email notification hook.
+ *
+ * @param $subscription WC Subscription
+ * @since 2.1
+ */
+ public static function send_on_hold_email( $subscription ) {
+ WC()->mailer();
+
+ do_action( 'on-hold_subscription_notification', $subscription );
+ }
+
/**
* Init the mailer and call the notifications for the renewal orders.
*
@@ -139,7 +170,7 @@ class WC_Subscriptions_Email {
*/
public static function maybe_reattach_woocommerce_email( $order_id ) {
if ( wcs_order_contains_renewal( $order_id ) || wcs_order_contains_switch( $order_id ) ) {
- add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
+ add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ), 10, 10 );
}
}
@@ -231,6 +262,44 @@ class WC_Subscriptions_Email {
return $items_table;
}
+ /**
+ * Show the order details table
+ *
+ * @param WC_Order $order
+ * @param bool $sent_to_admin Whether the email is sent to admin - defaults to false
+ * @param bool $plain_text Whether the email should use plain text templates - defaults to false
+ * @param WC_Email $email
+ * @since 2.1
+ */
+ public static function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) {
+
+ $order_items_table_args = array(
+ 'show_download_links' => ( $sent_to_admin ) ? false : $order->is_download_permitted(),
+ 'show_sku' => $sent_to_admin,
+ 'show_purchase_note' => ( $sent_to_admin ) ? false : $order->has_status( apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ) ),
+ 'show_image' => '',
+ 'image_size' => '',
+ 'plain_text' => $plain_text,
+ );
+
+ $template_path = ( $plain_text ) ? 'emails/plain/email-order-details.php' : 'emails/email-order-details.php';
+ $order_type = ( wcs_is_subscription( $order ) ) ? 'subscription' : 'order';
+
+ wc_get_template(
+ $template_path,
+ array(
+ 'order' => $order,
+ 'sent_to_admin' => $sent_to_admin,
+ 'plain_text' => $plain_text,
+ 'email' => $email,
+ 'order_type' => $order_type,
+ 'order_items_table_args' => $order_items_table_args,
+ ),
+ '',
+ plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/'
+ );
+ }
+
/**
* Init the mailer and call the notifications for the current filter.
*
diff --git a/includes/class-wc-subscriptions-manager.php b/includes/class-wc-subscriptions-manager.php
index b4a3684..c61aa8f 100644
--- a/includes/class-wc-subscriptions-manager.php
+++ b/includes/class-wc-subscriptions-manager.php
@@ -105,7 +105,7 @@ class WC_Subscriptions_Manager {
}
}
- if ( 0 == $subscription->get_total() ) {
+ if ( 0 == $renewal_order->get_total() ) {
$renewal_order->payment_complete();
@@ -434,7 +434,7 @@ class WC_Subscriptions_Manager {
$billing_interval = WC_Subscriptions_Product::get_interval( $product_id );
// Support passing timestamps
- $args['start_date'] = is_numeric( $args['start_date'] ) ? date( 'Y-m-d H:i:s', $args['start_date'] ) : $args['start_date'];
+ $args['start_date'] = is_numeric( $args['start_date'] ) ? gmdate( 'Y-m-d H:i:s', $args['start_date'] ) : $args['start_date'];
$product = wc_get_product( $product_id );
@@ -494,7 +494,7 @@ class WC_Subscriptions_Manager {
// Adding a new subscription so set the expiry date/time from the order date
if ( ! empty( $args['expiry_date'] ) ) {
if ( is_numeric( $args['expiry_date'] ) ) {
- $args['expiry_date'] = date( 'Y-m-d H:i:s', $args['expiry_date'] );
+ $args['expiry_date'] = gmdate( 'Y-m-d H:i:s', $args['expiry_date'] );
}
$expiration = $args['expiry_date'];
@@ -1171,7 +1171,7 @@ class WC_Subscriptions_Manager {
public static function set_expiration_date( $subscription_key, $user_id = '', $expiration_date = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "end" => $expiration_date ) )' );
if ( is_int( $expiration_date ) ) {
- $expiration_date = date( 'Y-m-d H:i:s', $expiration_date );
+ $expiration_date = gmdate( 'Y-m-d H:i:s', $expiration_date );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
return apply_filters( 'woocommerce_subscriptions_set_expiration_date', $subscription->update_dates( array( 'end' => $expiration_date ) ), $subscription->get_date( 'end' ), $subscription_key, $user_id );
@@ -1232,7 +1232,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "next_payment" => $next_payment ) )' );
if ( is_int( $next_payment ) ) {
- $next_payment = date( 'Y-m-d H:i:s', $next_payment );
+ $next_payment = gmdate( 'Y-m-d H:i:s', $next_payment );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
@@ -1284,7 +1284,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::calculate_date( "next_payment" )' );
$subscription = wcs_get_subscription_from_key( $subscription_key );
$next_payment = $subscription->calculate_date( 'next_payment' );
- return ( 'mysql' == $type ) ? $next_payment : strtotime( $next_payment );
+ return ( 'mysql' == $type ) ? $next_payment : wcs_date_to_time( $next_payment );
}
/**
@@ -1314,7 +1314,7 @@ class WC_Subscriptions_Manager {
public static function set_trial_expiration_date( $subscription_key, $user_id = '', $trial_expiration_date = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "trial_end" => $expiration_date ) )' );
if ( is_int( $trial_expiration_date ) ) {
- $trial_expiration_date = date( 'Y-m-d H:i:s', $trial_expiration_date );
+ $trial_expiration_date = gmdate( 'Y-m-d H:i:s', $trial_expiration_date );
}
$subscription = wcs_get_subscription_from_key( $subscription_key );
return apply_filters( 'woocommerce_subscriptions_set_trial_expiration_date', $subscription->update_dates( array( 'trial_end' => $trial_expiration_date ) ), $subscription->get_date( 'trial_end' ), $subscription_key, $user_id );
@@ -1333,7 +1333,7 @@ class WC_Subscriptions_Manager {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::calculate_date( "trial_end" )' );
$subscription = wcs_get_subscription_from_key( $subscription_key );
$trial_end = $subscription->calculate_date( 'trial_end' );
- $trial_end = ( 'mysql' == $type ) ? $trial_end : strtotime( $trial_end );
+ $trial_end = ( 'mysql' == $type ) ? $trial_end : wcs_date_to_time( $trial_end );
return apply_filters( 'woocommerce_subscription_calculated_trial_expiration_date' , $trial_end, $subscription_key, $user_id );
}
@@ -1601,14 +1601,14 @@ class WC_Subscriptions_Manager {
public static function update_next_payment_date( $new_payment_date, $subscription_key, $user_id = '', $timezone = 'server' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::update_dates( array( "next_payment" => $new_payment_date ) )' );
- $new_payment_timestamp = ( is_numeric( $new_payment_date ) ) ? $new_payment_date : strtotime( $new_payment_date );
+ $new_payment_timestamp = ( is_numeric( $new_payment_date ) ) ? $new_payment_date : wcs_date_to_time( $new_payment_date );
// The date needs to be converted to GMT/UTC
if ( 'server' != $timezone ) {
$new_payment_timestamp = $new_payment_timestamp - ( get_option( 'gmt_offset' ) * 3600 );
}
- $new_payment_date = date( 'Y-m-d H:i:s', $new_payment_timestamp );
+ $new_payment_date = gmdate( 'Y-m-d H:i:s', $new_payment_timestamp );
$subscription = wcs_get_subscription_from_key( $subscription_key );
@@ -1860,122 +1860,6 @@ class WC_Subscriptions_Manager {
/* Deprecated Functions */
- /**
- * @deprecated 1.1
- * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
- * @since 1.0
- */
- public static function can_subscription_be_cancelled( $subscription_key, $user_id = '' ) {
- _deprecated_function( __METHOD__, '1.1', __CLASS__ . '::can_subscription_be_changed_to( "cancelled", $subscription_key, $user_id )' );
- $subscription_can_be_cancelled = self::can_subscription_be_changed_to( 'cancelled', $subscription_key, $user_id );
-
- return apply_filters( 'woocommerce_subscription_can_be_cancelled', $subscription_can_be_cancelled, $subscription, $order );
- }
-
- /**
- * @deprecated 1.1
- * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
- * @since 1.0
- */
- public static function get_users_cancellation_link( $subscription_key ) {
- _deprecated_function( __METHOD__, '1.1', __CLASS__ . '::get_users_cancellation_link( $subscription_key, "cancel" )' );
- return apply_filters( 'woocommerce_subscriptions_users_cancellation_link', self::get_users_change_status_link( $subscription_key, 'cancel' ), $subscription_key );
- }
-
- /**
- * @deprecated 1.1
- * @since 1.0
- */
- public static function maybe_cancel_users_subscription() {
- _deprecated_function( __METHOD__, '1.1', __CLASS__ . '::maybe_change_users_subscription()' );
- self::maybe_change_users_subscription();
- }
-
- /**
- * @deprecated 1.1
- * @param int $user_id The ID of the user who owns the subscriptions.
- * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
- * @since 1.0
- */
- public static function get_failed_payment_count( $user_id, $subscription_key ) {
- _deprecated_function( __METHOD__, '1.1', __CLASS__ . '::get_subscriptions_failed_payment_count( $subscription_key, $user_id )' );
- return self::get_subscriptions_failed_payment_count( $subscription_key, $user_id );
- }
-
- /**
- * Deprecated in favour of a more correctly named @see maybe_reschedule_subscription_payment()
- *
- * @deprecated 1.1.5
- * @since 1.0
- */
- public static function reschedule_subscription_payment( $user_id, $subscription_key ) {
- _deprecated_function( __METHOD__, '1.1.5', __CLASS__ . '::maybe_reschedule_subscription_payment( $user_id, $subscription_key )' );
- self::maybe_reschedule_subscription_payment( $user_id, $subscription_key );
- }
-
-
- /**
- * Suspended a single subscription on a users account by placing it in the "suspended" status.
- *
- * Subscriptions version 1.2 replaced the "suspended" status with the "on-hold" status to match WooCommerce core.
- *
- * @param int $user_id The id of the user whose subscription should be suspended.
- * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
- * @deprecated 1.2
- * @since 1.0
- */
- public static function suspend_subscription( $user_id, $subscription_key ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ . '::put_subscription_on_hold( $user_id, $subscription_key )' );
- self::put_subscription_on_hold( $user_id, $subscription_key );
- }
-
-
- /**
- * Suspended all the subscription products in an order.
- *
- * Subscriptions version 1.2 replaced the "suspended" status with the "on-hold" status to match WooCommerce core.
- *
- * @param WC_Order|int $order The order or ID of the order for which subscriptions should be marked as activated.
- * @deprecated 1.2
- * @since 1.0
- */
- public static function suspend_subscriptions_for_order( $order ) {
- _deprecated_function( __METHOD__, '1.2', __CLASS__ . '::put_subscription_on_hold_for_order( $order )' );
- self::put_subscription_on_hold_for_order( $order );
- }
-
-
- /**
- * Gets a specific subscription for a user, as specified by $subscription_key
- *
- * Subscriptions version 1.4 moved subscription details out of user meta and into item meta, meaning it can be accessed
- * efficiently without a user ID.
- *
- * @param int $user_id (optional) The id of the user whose subscriptions you want. Defaults to the currently logged in user.
- * @param string $subscription_key A subscription key of the form created by @see self::subscription_key()
- * @deprecated 1.4
- * @since 1.0
- */
- public static function get_users_subscription( $user_id = 0, $subscription_key ) {
- _deprecated_function( __METHOD__, '1.4', __CLASS__ . '::get_subscription( $subscription_key )' );
- return apply_filters( 'woocommerce_users_subscription', self::get_subscription( $subscription_key ), $user_id, $subscription_key );
- }
-
-
- /**
- * Removed a specific subscription for a user, as specified by $subscription_key, but as subscriptions are no longer stored
- * against a user and are instead stored against the order, this is no longer required (changing the user on the order effectively
- * performs the same thing without requiring the subscription to have any changes).
- *
- * @param int $user_id (optional) The id of the user whose subscriptions you want. Defaults to the currently logged in user.
- * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
- * @deprecated 1.4
- * @since 1.0
- */
- public static function remove_users_subscription( $user_id, $subscription_key ) {
- _deprecated_function( __METHOD__, '1.4' );
- }
-
/**
* When a scheduled subscription payment hook is fired, automatically process the subscription payment
* if the amount is for $0 (and therefore, there is no payment to be processed by a gateway, and likely
@@ -2310,7 +2194,7 @@ class WC_Subscriptions_Manager {
} else {
- $new_payment_date = sprintf( '%s-%s-%s %s', (int) $_POST['wcs_year'], zeroise( (int) $_POST['wcs_month'], 2 ), zeroise( (int) $_POST['wcs_day'], 2 ), date( 'H:i:s', current_time( 'timestamp' ) ) );
+ $new_payment_date = sprintf( '%s-%s-%s %s', (int) $_POST['wcs_year'], zeroise( (int) $_POST['wcs_month'], 2 ), zeroise( (int) $_POST['wcs_day'], 2 ), gmdate( 'H:i:s', current_time( 'timestamp' ) ) );
$new_payment_timestamp = self::update_next_payment_date( $new_payment_date, $_POST['wcs_subscription_key'], self::get_user_id_from_subscription_key( $_POST['wcs_subscription_key'] ), 'user' );
if ( is_wp_error( $new_payment_timestamp ) ) {
diff --git a/includes/class-wc-subscriptions-order.php b/includes/class-wc-subscriptions-order.php
index 20a9638..c419918 100644
--- a/includes/class-wc-subscriptions-order.php
+++ b/includes/class-wc-subscriptions-order.php
@@ -36,8 +36,13 @@ class WC_Subscriptions_Order {
add_action( 'woocommerce_thankyou', __CLASS__ . '::subscription_thank_you' );
add_action( 'manage_shop_order_posts_custom_column', __CLASS__ . '::add_contains_subscription_hidden_field', 10, 1 );
+
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::contains_subscription_hidden_field', 10, 1 );
+ // Add column that indicates whether an order is parent or renewal for a subscription
+ add_filter( 'manage_edit-shop_order_columns', __CLASS__ . '::add_contains_subscription_column' );
+ add_action( 'manage_shop_order_posts_custom_column', __CLASS__ . '::add_contains_subscription_column_content', 10, 1 );
+
// Record initial payment against the subscription & set start date based on that payment
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_record_subscription_payment', 9, 3 );
@@ -357,12 +362,17 @@ class WC_Subscriptions_Order {
if ( wcs_order_contains_subscription( $order_id, 'any' ) ) {
- $subscription_count = count( wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) ) );
+ $subscription_count = count( wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) ) );
+ $thank_you_message = '
' . _n( 'Your subscription will be activated when payment clears.', 'Your subscriptions will be activated when payment clears.', $subscription_count, 'woocommerce-subscriptions' ) . '
' . _n( 'Your subscription will be activated when payment clears.', 'Your subscriptions will be activated when payment clears.', $subscription_count, 'woocommerce-subscriptions' ) . '
';
+ // Post WC 2.6 link directly to the My Account subscriptions endpoint
+ if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
+ $my_account_subscriptions_url = wc_get_endpoint_url( 'subscriptions', '', wc_get_page_permalink( 'myaccount' ) );
+ }
// translators: placeholders are opening and closing link tags
- $thank_you_message .= '
' . sprintf( _n( 'View the status of your subscription in %syour account%s.', 'View the status of your subscriptions in %syour account%s.', $subscription_count, 'woocommerce-subscriptions' ), '', '' ) . '
';
+ $thank_you_message .= '
' . sprintf( _n( 'View the status of your subscription in %syour account%s.', 'View the status of your subscriptions in %syour account%s.', $subscription_count, 'woocommerce-subscriptions' ), '', '' ) . '
';
echo wp_kses( apply_filters( 'woocommerce_subscriptions_thank_you_message', $thank_you_message, $order_id ), array( 'a' => array( 'href' => array(), 'title' => array() ), 'p' => array(), 'em' => array(), 'strong' => array() ) );
}
@@ -401,6 +411,46 @@ class WC_Subscriptions_Order {
echo '';
}
+ /**
+ * 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 '
';
@@ -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' ),
+ '
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__( '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__( '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__( 'Developers can also now use all the familiar WordPress functions, like %sget_posts()%s, to query or modify subscription data.', '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' ), '', '', '', '' ); ?>
+
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__( '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__( '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__( '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, 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-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$: "