From a60db3815d46c6fa6aeb2e9a3ae3f167b3386b70 Mon Sep 17 00:00:00 2001 From: Prospress Inc Date: Thu, 12 Sep 2019 11:49:11 +0200 Subject: [PATCH] 2.6.1 --- assets/css/admin.css | 36 +- assets/css/dashboard.css | 46 +- assets/css/modal.css | 136 ++ assets/css/view-subscription.css | 25 + assets/js/admin/admin.js | 38 +- assets/js/frontend/view-subscription.js | 31 + assets/js/modal.js | 114 ++ changelog.txt | 51 + composer.json | 16 - composer.lock | 1252 ------------ .../abstract-wcs-background-repairer.php | 180 ++ .../admin/class-wc-subscriptions-admin.php | 122 +- includes/admin/class-wcs-admin-post-types.php | 10 +- .../admin/class-wcs-admin-system-status.php | 2 +- .../class-wcs-meta-box-related-orders.php | 123 +- .../views/html-unknown-related-orders-row.php | 25 + .../reports/class-wcs-report-dashboard.php | 10 +- ...ss-wcs-report-subscription-by-customer.php | 8 +- ...wcs-report-subscription-events-by-date.php | 152 +- ...class-wc-rest-subscriptions-controller.php | 200 ++ .../class-wc-order-item-pending-switch.php | 17 + ...lass-wc-product-subscription-variation.php | 2 +- includes/class-wc-product-subscription.php | 2 +- ...class-wc-product-variable-subscription.php | 2 +- ...ubscription-item-coupon-pending-switch.php | 24 + ...c-subscription-item-fee-pending-switch.php | 24 + ...lass-wc-subscription-line-item-removed.php | 24 + ...ass-wc-subscription-line-item-switched.php | 24 + includes/class-wc-subscription.php | 184 +- includes/class-wc-subscriptions-addresses.php | 2 +- .../class-wc-subscriptions-cart-validator.php | 140 ++ includes/class-wc-subscriptions-cart.php | 57 +- ...c-subscriptions-change-payment-gateway.php | 12 +- includes/class-wc-subscriptions-checkout.php | 30 +- includes/class-wc-subscriptions-coupon.php | 8 +- includes/class-wc-subscriptions-manager.php | 11 +- includes/class-wc-subscriptions-order.php | 2 +- includes/class-wc-subscriptions-product.php | 61 +- includes/class-wc-subscriptions-switcher.php | 969 ++++++---- .../class-wc-subscriptions-synchroniser.php | 2 +- includes/class-wcs-action-scheduler.php | 36 +- includes/class-wcs-add-cart-item.php | 73 + includes/class-wcs-autoloader.php | 1 + includes/class-wcs-cart-renewal.php | 45 +- includes/class-wcs-cart-resubscribe.php | 4 +- .../class-wcs-custom-order-item-manager.php | 101 + includes/class-wcs-dependent-hook-manager.php | 88 + includes/class-wcs-modal.php | 246 +++ ...class-wcs-my-account-auto-renew-toggle.php | 5 +- includes/class-wcs-object-sorter.php | 75 + .../class-wcs-renewal-cart-stock-manager.php | 201 ++ includes/class-wcs-staging.php | 27 + includes/class-wcs-switch-cart-item.php | 413 ++++ .../class-wcs-switch-totals-calculator.php | 510 +++++ includes/class-wcs-template-loader.php | 26 + .../class-wcs-related-order-store-cpt.php | 7 +- .../class-wcs-cart-early-renewal.php | 36 +- .../class-wcs-early-renewal-manager.php | 83 +- .../class-wcs-early-renewal-modal-handler.php | 159 ++ .../wcs-early-renewal-functions.php | 27 + ...class-wcs-email-cancelled-subscription.php | 30 +- ...lass-wcs-email-completed-renewal-order.php | 30 +- ...class-wcs-email-completed-switch-order.php | 34 +- ...class-wcs-email-customer-payment-retry.php | 34 +- ...ass-wcs-email-customer-renewal-invoice.php | 30 +- .../class-wcs-email-expired-subscription.php | 30 +- .../class-wcs-email-new-renewal-order.php | 30 +- .../class-wcs-email-new-switch-order.php | 34 +- .../class-wcs-email-on-hold-subscription.php | 30 +- .../emails/class-wcs-email-payment-retry.php | 26 +- ...ass-wcs-email-processing-renewal-order.php | 30 +- ...lass-wc-subscriptions-payment-gateways.php | 10 +- .../includes/admin/class-wcs-paypal-admin.php | 34 +- .../class-wcs-paypal-standard-ipn-handler.php | 4 +- .../class-wcs-paypal-standard-request.php | 4 +- .../includes/class-wcs-paypal-supports.php | 32 + .../includes/templates/admin-notices.php | 3 + .../class-wc-subscriptions-upgrader.php | 8 +- ...ss-wcs-repair-line-item-has-trial-meta.php | 113 ++ .../class-wcs-upgrade-notice-manager.php | 30 +- .../templates/update-welcome-notice.php | 4 +- includes/wcs-compatibility-functions.php | 33 + includes/wcs-deprecated-functions.php | 32 +- includes/wcs-helper-functions.php | 20 + includes/wcs-limit-functions.php | 2 +- includes/wcs-order-functions.php | 65 +- includes/wcs-renewal-functions.php | 25 + includes/wcs-resubscribe-functions.php | 2 +- languages/woocommerce-subscriptions.pot | 1673 +++++++++-------- templates/admin/status.php | 8 +- templates/cart/cart-recurring-shipping.php | 7 +- .../checkout/form-change-payment-method.php | 97 +- templates/checkout/recurring-totals.php | 10 +- templates/emails/admin-new-renewal-order.php | 20 +- templates/emails/admin-new-switch-order.php | 42 +- templates/emails/admin-payment-retry.php | 37 +- templates/emails/cancelled-subscription.php | 27 +- .../customer-completed-renewal-order.php | 30 +- .../customer-completed-switch-order.php | 43 +- templates/emails/customer-payment-retry.php | 36 +- .../customer-processing-renewal-order.php | 26 +- templates/emails/customer-renewal-invoice.php | 44 +- templates/emails/email-order-details.php | 58 +- templates/emails/expired-subscription.php | 28 +- templates/emails/on-hold-subscription.php | 27 +- .../emails/plain/admin-new-renewal-order.php | 10 +- .../emails/plain/admin-new-switch-order.php | 11 +- .../emails/plain/admin-payment-retry.php | 10 +- .../emails/plain/cancelled-subscription.php | 10 +- .../customer-completed-renewal-order.php | 17 +- .../plain/customer-completed-switch-order.php | 15 +- .../emails/plain/customer-payment-retry.php | 18 +- .../customer-processing-renewal-order.php | 15 +- .../emails/plain/customer-renewal-invoice.php | 21 +- .../emails/plain/expired-subscription.php | 8 + .../emails/plain/on-hold-subscription.php | 8 + templates/emails/plain/subscription-info.php | 4 +- templates/emails/subscription-info.php | 10 +- .../html-early-renewal-modal-content.php | 38 + templates/html-modal.php | 35 + templates/myaccount/my-subscriptions.php | 36 +- templates/myaccount/related-orders.php | 36 +- templates/myaccount/related-subscriptions.php | 35 +- templates/myaccount/subscription-details.php | 151 +- .../myaccount/subscription-totals-table.php | 99 + templates/myaccount/subscription-totals.php | 109 +- templates/myaccount/view-subscription.php | 2 +- .../add-to-cart/subscription.php | 48 +- .../add-to-cart/variable-subscription.php | 18 +- wcs-functions.php | 107 +- woocommerce-subscriptions.php | 41 +- 131 files changed, 6719 insertions(+), 3502 deletions(-) create mode 100755 assets/css/modal.css create mode 100755 assets/js/modal.js delete mode 100755 composer.json delete mode 100755 composer.lock create mode 100755 includes/abstracts/abstract-wcs-background-repairer.php create mode 100755 includes/admin/meta-boxes/views/html-unknown-related-orders-row.php create mode 100755 includes/class-wc-subscription-item-coupon-pending-switch.php create mode 100755 includes/class-wc-subscription-item-fee-pending-switch.php create mode 100755 includes/class-wc-subscription-line-item-removed.php create mode 100755 includes/class-wc-subscription-line-item-switched.php create mode 100755 includes/class-wc-subscriptions-cart-validator.php create mode 100755 includes/class-wcs-add-cart-item.php create mode 100755 includes/class-wcs-custom-order-item-manager.php create mode 100755 includes/class-wcs-dependent-hook-manager.php create mode 100755 includes/class-wcs-modal.php create mode 100755 includes/class-wcs-object-sorter.php create mode 100755 includes/class-wcs-renewal-cart-stock-manager.php create mode 100755 includes/class-wcs-switch-cart-item.php create mode 100755 includes/class-wcs-switch-totals-calculator.php create mode 100755 includes/early-renewal/class-wcs-early-renewal-modal-handler.php create mode 100755 includes/upgrades/class-wcs-repair-line-item-has-trial-meta.php create mode 100755 templates/html-early-renewal-modal-content.php create mode 100755 templates/html-modal.php create mode 100755 templates/myaccount/subscription-totals-table.php diff --git a/assets/css/admin.css b/assets/css/admin.css index 90f9e19..9e4ac60 100755 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -488,6 +488,16 @@ a.close-subscriptions-search { .woocommerce_subscriptions_related_orders table tbody tr:last-child td { border-bottom: none; } +.wcs-unknown-order-link { + vertical-align: middle; + font-size: 1.2em; +} +.wcs-unknown-order-info-wrapper { + display: inline; +} +.wcs-unknown-order-info-wrapper .woocommerce-help-tip { + color: inherit; +} /* WooCommerce Orders admin table */ table.wp-list-table .column-subscription_relationship { @@ -523,8 +533,8 @@ table.wp-list-table .subscription_resubscribe_order:after { color: #999; } table.wp-list-table .subscription_renewal_order:after { - font-family: WooCommerce; - content: "\e031"; + font-family: Dashicons; + content: "\f321"; } table.wp-list-table .payment_retry:after { font-family: WooCommerce; @@ -635,3 +645,25 @@ body.post-type-shop_subscription .add-items .button.refund-items { span.product-type.variable-subscription:before { content: "\e003" !important; } + +/* Settings Page */ +.wcs_setting_switching_options { + margin-top: 6px; +} +.wcs_setting_switching_options label { + display: block; + width: 400px; + margin-bottom: 1em; +} + +/* Reports Page */ +.woocommerce-reports-wide .postbox .chart-legend li a { + text-decoration: none; +} +.woocommerce-reports-wide .postbox .chart-legend li a .woocommerce-subscriptions-count:after { + margin-left: 1.5%; + font-size: 60%; + font-weight: normal; + font-family: dashicons; + content: "\f504"; +} diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 947c130..1b43b92 100755 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -2,40 +2,56 @@ Subscriptions 2.1 Dashboard Stats ------------------------------------------------------------------------------*/ #woocommerce_dashboard_status .wc_status_list li.signup-count a:before { - content: "\e02c"; - color: #5da5da; + font-family: WooCommerce; + content: '\e014'; + color: #59cf8a; } #woocommerce_dashboard_status .wc_status_list li.renewal-count a:before { - content: "\e02c"; - color: #f29ec4; + font-family: Dashicons; + content: "\f321"; + color: #f29ec4; } #woocommerce_dashboard_status .wc_status_list li.cancel-count { - width: 100%; + width: 100%; } #woocommerce_dashboard_status .wc_status_list li.cancel-count a:before { - content: "\e02c"; - color: #aa0000; + font-family: WooCommerce; + content: "\e033"; + color: #aa0000; } #woocommerce_dashboard_status .wc_status_list li.signup-revenue a:before { - font-family: Dashicons; - content: '\f185'; - color: #5da5da; + font-family: Dashicons; + content: '\f185'; + color: #59cf8a; } #woocommerce_dashboard_status .wc_status_list li.renewal-revenue a:before { - font-family: Dashicons; - content: '\f185'; - color: #f29ec4; + font-family: Dashicons; + content: '\f185'; + color: #f29ec4; } #woocommerce_dashboard_status .wc_status_list li.signup-count { - border-right: 1px solid #ececec; + border-right: 1px solid #ececec; } #woocommerce_dashboard_status .wc_status_list li.renewal-count { - border-right: 1px solid #ececec; + border-right: 1px solid #ececec; +} + +@media (max-width: 1685px) and (min-width: 1485px), +(max-width: 2193px) and (min-width: 1936px), +(max-width: 1179px) and (min-width: 1052px), +(max-width: 960px) { + #woocommerce_dashboard_status .wc_status_list li.renewal-count, + #woocommerce_dashboard_status .wc_status_list li.renewal-revenue, + #woocommerce_dashboard_status .wc_status_list li.signup-count, + #woocommerce_dashboard_status .wc_status_list li.signup-revenue, + #woocommerce_dashboard_status .wc_status_list li.cancel-count { + width: 100% + } } diff --git a/assets/css/modal.css b/assets/css/modal.css new file mode 100755 index 0000000..db576d7 --- /dev/null +++ b/assets/css/modal.css @@ -0,0 +1,136 @@ +body.wcs-modal-open { + overflow: hidden; +} + +.wcs-modal { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + height: 0vh; + background-color: transparent; + overflow: hidden; + transition: background-color 0.25s ease; + z-index: 1000; +} + +.wcs-modal.open { + position: fixed; + width: 100%; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + transition: background-color 0.25s; +} + +.wcs-modal.open > .content-wrapper { + transform: scale(1); + min-width: 30%; + max-width: 80%; +} + +.wcs-modal .content-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + margin: 0; + padding: 2em; + background-color: white; + border-radius: 0.3em; + transform: scale(0); + transition: transform 0.25s; + transition-delay: 0.15s; +} + +.wcs-modal .content-wrapper .close { + position: absolute; + top: 0px; + right: 0px; + z-index: 50; +} + +.wcs-modal .content-wrapper .modal-header { + position: relative; + display: block; + height: 5%; + align-items: center; + justify-content: space-between; + width: 100%; + margin: 0; +} + +.modal-header > h2 { + font-size: 1.5em; +} + +.wcs-modal .content-wrapper .content { + position: relative; + min-width: 100%; + height: 90%; + font-size: 0.875rem; +} + +.wcs-modal .content-wrapper .content p { + line-height: 1.75; +} + +.wcs-modal .content-wrapper .modal-footer { + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + margin: 0; +} + +.wcs-modal .content-wrapper .modal-footer .action { + position: relative; + margin-left: 0.625rem; +} + +.wcs-modal footer > a:not( :first-child ) { + margin-left: 0.8em; +} + + +/* + * Mobile Display Styles + */ +@media only screen and (max-width:414px) { + .wcs-modal.open > .content-wrapper { + max-width: none; + width: 100%; + height: 100%; + padding: 0.8em; + border-radius: 0; + } + + .wcs-modal.open > .content-wrapper > .content { + width: 100%; + height: 75%; /* WooCommerce has a nav at the bottom of mobile displays so we need to account for it */ + } + + .wcs-modal.open .order_details { + font-size: 0.85em; + } +} + +@media only screen and (max-width:320px) { + .wcs-modal.open .content-wrapper .modal-header { + height: 7%; + } + + .wcs-modal.open > .content-wrapper > .content { + width: 100%; + height: 65%; /* WooCommerce has a nav at the bottom of mobile displays so we need to account for it */ + } +} + +@media only screen and (max-width:768px) { + .wcs-modal.open > .content-wrapper { + min-width: 60%; + } +} diff --git a/assets/css/view-subscription.css b/assets/css/view-subscription.css index a8e8fe0..8f11982 100755 --- a/assets/css/view-subscription.css +++ b/assets/css/view-subscription.css @@ -56,3 +56,28 @@ .subscription-auto-renew-toggle--hidden { display: none; } +.subscription-auto-renew-toggle-disabled-note { + margin-left: 1em; +} + +/** + * Early renewal Modal +**/ +.wcs_early_renew_modal_totals_table { + overflow: scroll; + height: 80%; + margin-bottom: 1em; +} + +.wcs_early_renew_modal_note { + position: sticky; + bottom: 0px; + min-width: 100%; + width: 0; +} + +#early_renewal_modal_submit { + width: 100%; + font-size:1.4em; + text-align: center; +} diff --git a/assets/js/admin/admin.js b/assets/js/admin/admin.js index 68bf856..a670a15 100755 --- a/assets/js/admin/admin.js +++ b/assets/js/admin/admin.js @@ -42,7 +42,7 @@ jQuery(document).ready(function($){ $('.hide_if_variable').hide(); $('.show_if_variable-subscription').show(); $('.hide_if_variable-subscription').hide(); - $( 'input#_manage_stock' ).change(); + $.showOrHideStockFields(); // Make the sale price row full width $('.sale_price_dates_fields').prev('.form-row').addClass('form-row-full').removeClass('form-row-last'); @@ -53,13 +53,20 @@ jQuery(document).ready(function($){ $( '.show_if_variable-subscription' ).hide(); $( '.show_if_variable' ).show(); $( '.hide_if_variable' ).hide(); - $( 'input#_manage_stock' ).change(); + $.showOrHideStockFields(); } // Restore the sale price row width to half $('.sale_price_dates_fields').prev('.form-row').removeClass('form-row-full').addClass('form-row-last'); } }, + showOrHideStockFields : function(){ + if ( $( 'input#_manage_stock' ).is( ':checked' ) ) { + $( 'div.stock_fields' ).show(); + } else { + $( 'div.stock_fields' ).hide(); + } + }, setSubscriptionLengths: function(){ $('[name^="_subscription_length"], [name^="variable_subscription_length"]').each(function(){ var $lengthElement = $(this), @@ -554,30 +561,33 @@ jQuery(document).ready(function($){ return data; }); - var $allowSwitching = $( document.getElementById( 'woocommerce_subscriptions_allow_switching' ) ); - var $syncRenewals = $( document.getElementById( 'woocommerce_subscriptions_sync_payments' ) ); + var $allowSwitching = $( document.getElementById( 'woocommerce_subscriptions_allow_switching' ) ), + $syncRenewals = $( document.getElementById( 'woocommerce_subscriptions_sync_payments' ) ); // We're on the Subscriptions settings page if ( $allowSwitching.length > 0 ) { - var allowSwitchingVal = $allowSwitching.val(), - $switchSettingsRows = $allowSwitching.parents( 'tr' ).siblings( 'tr' ), - $prorateFirstRenewal = $( document.getElementById( 'woocommerce_subscriptions_prorate_synced_payments' ) ), - $syncRows = $syncRenewals.parents( 'tr' ).siblings( 'tr' ), - $daysNoFeeRow = $( document.getElementById( 'woocommerce_subscriptions_days_no_fee' ) ).parents( 'tr' ), + var allowSwitchingEnabled = $allowSwitching.find( 'input:checked' ).length, + $switchSettingsRows = $allowSwitching.parents( 'tr' ).siblings( 'tr' ), + $prorateFirstRenewal = $( document.getElementById( 'woocommerce_subscriptions_prorate_synced_payments' ) ), + $syncRows = $syncRenewals.parents( 'tr' ).siblings( 'tr' ), + $daysNoFeeRow = $( document.getElementById( 'woocommerce_subscriptions_days_no_fee' ) ).parents( 'tr' ), $suspensionExtensionRow = $( '#woocommerce_subscriptions_recoup_suspension' ).parents( 'tr' ); // No animation for initial hiding when switching is disabled. - if ( 'no' === allowSwitchingVal ) { + if ( 0 === allowSwitchingEnabled ) { $switchSettingsRows.hide(); } - $allowSwitching.on( 'change', function() { - if ( 'no' === $( this ).val() ) { + $allowSwitching.find( 'input' ).on( 'change', function() { + + var isEnabled = $allowSwitching.find( 'input:checked' ).length; + + if ( 0 === isEnabled ) { $switchSettingsRows.fadeOut(); - } else if ( 'no' === allowSwitchingVal ) { // switching was previously disabled, so settings will be hidden + } else if ( 0 === allowSwitchingEnabled ) { // switching was previously disabled, so settings will be hidden $switchSettingsRows.fadeIn(); } - allowSwitchingVal = $( this ).val(); + allowSwitchingEnabled = isEnabled; } ); // Show/hide suspension extension setting diff --git a/assets/js/frontend/view-subscription.js b/assets/js/frontend/view-subscription.js index c78db8f..5a0fa04 100755 --- a/assets/js/frontend/view-subscription.js +++ b/assets/js/frontend/view-subscription.js @@ -1,10 +1,15 @@ jQuery( document ).ready( function( $ ) { + // Auto Renewal Toggle var $toggleContainer = $( '.wcs-auto-renew-toggle' ); var $toggle = $( '.subscription-auto-renew-toggle', $toggleContainer ); var $icon = $toggle.find( 'i' ); var txtColor = null; var $paymentMethod = $( '.subscription-payment-method' ); + // Early Renewal + var $early_renewal_modal_submit = $( '#early_renewal_modal_submit' ); + var $early_renewal_modal_content = $( '.wcs-modal > .content-wrapper' ); + function getTxtColor() { if ( !txtColor && ( $icon && $icon.length ) ) { txtColor = getComputedStyle( $icon[0] ).color; @@ -33,6 +38,11 @@ jQuery( document ).ready( function( $ ) { // Remove focus from the toggle element. $toggle.blur(); + // Ignore the request if the toggle is disabled. + if ( $toggle.hasClass( 'subscription-auto-renew-toggle--disabled' ) ) { + return; + } + var ajaxHandler = function( action ) { var data = { subscription_id: WCSViewSubscription.subscription_id, @@ -53,6 +63,9 @@ jQuery( document ).ready( function( $ ) { $paymentMethod.html( result.payment_method ).fadeIn(); }); } + if ( undefined !== result.is_manual ) { + $paymentMethod.data( 'is_manual', result.is_manual ); + } }, error: function( jqxhr, status, exception ) { alert( 'Exception:', exception ); @@ -100,8 +113,26 @@ jQuery( document ).ready( function( $ ) { $toggleContainer.unblock(); } + function blockEarlyRenewalModal() { + $early_renewal_modal_content.block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + } + + // Don't display the modal for manual subscriptions, they will need to renew via the checkout. + function shouldShowEarlyRenewalModal() { + return $paymentMethod.data( 'is_manual' ) === 'no'; + }; + $toggle.on( 'click', onToggle ); maybeApplyColor(); displayToggle(); + + $early_renewal_modal_submit.on( 'click', blockEarlyRenewalModal ); + $( document ).on( 'wcs_show_modal', shouldShowEarlyRenewalModal ); }); diff --git a/assets/js/modal.js b/assets/js/modal.js new file mode 100755 index 0000000..68e9932 --- /dev/null +++ b/assets/js/modal.js @@ -0,0 +1,114 @@ +jQuery( document ).ready( function( $ ) { + const modals = $( '.wcs-modal' ); + + // Resize all open modals on window resize. + $( window ).on( 'resize', resizeModals ); + + // Initialize modals + $( modals ).each( function() { + trigger = $( this ).data( 'modal-trigger' ); + $( trigger ).click( { modal: this }, show_modal ); + }); + + /** + * Displays the modal linked to a click event. + * + * Attaches all close callbacks and resizes to fit. + * + * @param {JQuery event} event + */ + function show_modal( event ) { + const modal = $( event.data.modal ); + + if ( ! should_show_modal( modal ) ) { + return; + } + + // Prevent the trigger element event being triggered. + event.preventDefault(); + + const contentWrapper = modal.find( '.content-wrapper' ); + const close = modal.find( '.close' ); + + modal.focus(); + modal.addClass( 'open' ); + + resizeModal( modal ); + + $( document.body ).toggleClass( 'wcs-modal-open', true ); + + // Attach callbacks to handle closing the modal. + close.on( 'click', () => close_modal( modal ) ); + modal.on( 'click', () => close_modal( modal ) ); + contentWrapper.on( 'click', ( e ) => e.stopPropagation() ); + + // Close the modal if the escape key is pressed. + modal.on( 'keyup', function( e ) { + if ( 27 === e.keyCode ) { + close_modal( modal ) + } + } ); + } + + /** + * Closes a modal and resets any forced height styles. + * + * @param {JQuery Object} modal + */ + function close_modal( modal ) { + modal.removeClass( 'open' ); + $( modal ).find( '.content-wrapper' ).css( 'height', '' ); + + if ( 0 === modals.filter( '.open' ).length ) { + $( document.body ).removeClass( 'wcs-modal-open' ); + } + } + + /** + * Determines if a modal should be displayed. + * + * A custom trigger is called to allow third-parties to filter whether the modal should be displayed or not. + * + * @param {JQuery Object} modal + */ + function should_show_modal( modal ) { + // Allow third-parties to filter whether the modal should be displayed. + var event = jQuery.Event( 'wcs_show_modal' ); + event.modal = modal; + + $( document ).trigger( event ); + + // Fallback to true (show modal) if the result is undefined. + return undefined === event.result ? true : event.result; + } + + /** + * Resize all open modals to fit the display. + */ + function resizeModals() { + $( modals ).each( function() { + if ( ! $( this ).hasClass( 'open' ) ) { + return; + } + + resizeModal( this ); + }); + } + + /** + * Resize a modal to fit the display. + * + * @param {JQuery Object} modal + */ + function resizeModal( modal ) { + var modal_container = $( modal ).find( '.content-wrapper' ); + + // On smaller displays the height is already forced to be 100% in CSS. We just clear any height we might set previously. + if ( $( window ).width() <= 414 ) { + modal_container.css( 'height', '' ); + } else if ( modal_container.height() > $( window ).height() ) { + // Force the container height to trigger scroll etc if it doesn't fit on the screen. + modal_container.css( 'height', '90%' ); + } + } +}); diff --git a/changelog.txt b/changelog.txt index bc626d7..e777146 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,56 @@ *** WooCommerce Subscriptions Changelog *** +2019.09.04 - version 2.6.1 +* Fix a bug that would lead to switch log entries not including all information. PR#3441 +* Fix fatal errors that would occur on the admin edit order screen on staging sites. PR#3443 +* Performance: Sort subscription related order IDs on the application layer with rsort() instead of MySQL orderby clause. PR#3442 + +2019.09.02 - version 2.6.0 +* New: New option to allow customers with automatically renewing subscriptions to renew early via a modal rather than going through the checkout. PR#3293 +* New: Link subscription report counts in the 'by date' report to order and subscription listing page. PR#3318 +* New: Add different message and link to documentation for store managers when no payment methods available on checkout. PR#3340 +* New: Add a note and a tooltip explaining locked manual subscriptions on staging sites. PR#3327 +* New: Use the switching flow to enable items to be added to existing subscriptions. PR#3394 +* New: Log switch debug information when a switch order is processed. PR#3424 +* Tweak: Use sentence case for table and section headings in customer facing templates. PR#3392,#3407,#3412,#3432 +* Tweak: Make improvements to the subscription admin dashboard reports. Fixes misalignment issues on certain display sizes. PR#3401 +* Tweak: Update the PayPal admin notices to be more correct based on if Standard is enabled or not. PR#3393 +* Tweak: Display PayPal type specific features in the System Status and feature tooltip. PR#3411 +* Tweak: Display un-loadable orders in the subscriptions related order table. PR#3342 +* Tweak: Always pass product instances to WC_Subscriptions_Product methods. PR#3396 +* Tweak: Update the ended subscriptions report legend key for more grammatically correct option. PR#3415 +* Tweak: Disable the auto-renewal toggle on staging sites. PR#3387 +* Tweak: [REST API] Include 'removed line items' in subscription response. PR#3198 +* Tweak: Replace self::class with php __CLASS__ constant in report classes. PR# +* Tweak: Transform "Allow Switching" admin settings into to a multi-checkbox option. PR#3373 +* Fix: Refactor the WC_Subscriptions_Switcher::calculate_prorated_totals() function and fix a number of switching issues in the process. PR#3250 +* Fix: Keep the trial end when switching between two products with matching trial periods when the subscription is still on trial. PR#3409 +* Fix: Use the last order's paid date in switching calculations when determining the number of days consumed in the current cycle. PR#3420 +* Fix: Ignore the WP_SITEURL global on multisites when determining the site URL for staging sites. PR#3397 +* Fix: Repair subscription line items with missing `_has_trial` line item meta. PR#3239 +* Fix: Only retrieve subscriptions with the product as a line item - exclude switched and removed products while using `wcs_get_subscriptions_for_product()`. PR#3386 +* Fix: Don't display tax values in recurring cart section when taxes aren't enabled under certain circumstances. PR#3408 +* Fix: Trigger the `woocommerce_before\after_add_to_cart_quantity` actions on the subscription single product pages. PR#3388 +* Fix: [WC 3.7] Include the "additional content" in subscription-related emails. #3416 +* Fix: [WC 3.7] Remove uses of deprecated $order->get_used_coupons(). PR#3421 +* Fix: Copy or replace fees from cart to subscription while switching. PR#3184 +* Fix: Save subscription after setting payment meta via wcs_set_payment_meta(). Fixes issues after updating the payment meta before payment retry. PR#3425 +* Fix: Don't allow users to partially pay for renewal orders if some products are out of stock. This was previously fixed by broke in Subscriptions 2.1. PR#3436 +* Fix: Add filter which can be turned on to allow out of stock manual renewals to pass cart and checkout validations. PR#3435 +* Fix: [WC Services] Fixed compatibility bug which caused coupons to be removed when automatic tax rates enabled with WC Services. PR#3376 +* Fix: Remove uninitiated database transaction rollback in WC_Subscriptions_Switcher::process_checkout. PR#3307 +* Fix: Validate cart contents after login when mixed checkout is disabled. PR#3151 +* Fix: Fix issue which led to the 'save changes' button always being active on product variations tab. PR#3357 +* Fix: Fix division by zero warning in subscriptions by customer report. PR#3371 +* Fix: Use site time while adding a period to the first payment date so that last day of month is right. PR#3368 +* Performance: Limit the "product has a subscription" query to just a single result to be more performant. PR#3389 +* Dev: Deprecate get_completed_payment_count() in favor of get_payment_count(). PR#2971 +* Dev: [WC 3.7] Only use deprecated woocommerce_before_cart_item_quantity_zero hooks on WC pre 3.7. PR#3377 +* Dev: Introduce WCS_Dependent_Hook_Manager class to assist in attaching callbacks on specific WC versions. PR#3377 +* Dev: Add BEM classes to templates and make general code improvements. PR#3135 +* Dev: Always use get_date_types_to_schedule() to schedule dates. PR#3091 +* Dev: Deprecate old and unused WC_Subscriptions_Manager::process_subscription_payments_on_order() and WC_Subscriptions_Manager::process_subscription_payment_failure_on_order() functions. PR#3378 + 2019.07.04 - version 2.5.7 * Fix: Check for any free shipping which has its requirements met - not just the first one. PR#3329 * Fix: Fix un-purchasability issues with limited subscriptions in manual renewal carts. PR#3358 diff --git a/composer.json b/composer.json deleted file mode 100755 index 49b575c..0000000 --- a/composer.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "prospress/woocommerce-subscriptions", - "description": "Sell products and services with recurring payments in your WooCommerce Store.", - "homepage": "http://www.woocommerce.com/products/woocommerce-subscriptions/", - "type": "wordpress-plugin", - "license": "GPL-2.0+", - "require": { - "composer/installers": "~1.2" - }, - "config": { - "vendor-dir": "includes/libraries" - }, - "require-dev": { - "phpunit/phpunit": "^4.5" - } -} diff --git a/composer.lock b/composer.lock deleted file mode 100755 index 51054e4..0000000 --- a/composer.lock +++ /dev/null @@ -1,1252 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "021c140a843bd5b969f18f6e06902332", - "packages": [ - { - "name": "composer/installers", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/installers.git", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "replace": { - "roundcube/plugin-installer": "*", - "shama/baton": "*" - }, - "require-dev": { - "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "4.1.*" - }, - "type": "composer-plugin", - "extra": { - "class": "Composer\\Installers\\Plugin", - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Installers\\": "src/Composer/Installers" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kyle Robinson Young", - "email": "kyle@dontkry.com", - "homepage": "https://github.com/shama" - } - ], - "description": "A multi-framework Composer library installer", - "homepage": "https://composer.github.io/installers/", - "keywords": [ - "Craft", - "Dolibarr", - "Eliasis", - "Hurad", - "ImageCMS", - "Kanboard", - "Lan Management System", - "MODX Evo", - "Mautic", - "Maya", - "OXID", - "Plentymarkets", - "Porto", - "RadPHP", - "SMF", - "Thelia", - "WolfCMS", - "agl", - "aimeos", - "annotatecms", - "attogram", - "bitrix", - "cakephp", - "chef", - "cockpit", - "codeigniter", - "concrete5", - "croogo", - "dokuwiki", - "drupal", - "eZ Platform", - "elgg", - "expressionengine", - "fuelphp", - "grav", - "installer", - "itop", - "joomla", - "kohana", - "laravel", - "lavalite", - "lithium", - "magento", - "mako", - "mediawiki", - "modulework", - "moodle", - "osclass", - "phpbb", - "piwik", - "ppi", - "puppet", - "reindex", - "roundcube", - "shopware", - "silverstripe", - "sydes", - "symfony", - "typo3", - "wordpress", - "yawik", - "zend", - "zikula" - ], - "time": "2017-08-09T07:53:48+00:00" - } - ], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14T21:17:01+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2015-12-27T11:43:31+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-06-03T08:32:36+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.7.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2017-03-02T20:05:34+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2015-10-06T15:47:00+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2016-10-03T07:40:28+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.11", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-02-27T10:12:30+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "4.8.36", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" - }, - "suggest": { - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.8.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2017-06-21T08:07:12+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2015-10-02T06:51:40+00:00" - }, - { - "name": "sebastian/comparator", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2017-01-29T09:50:25+00:00" - }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, - { - "name": "sebastian/environment", - "version": "1.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-08-18T05:49:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-06-17T09:04:28+00:00" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12T03:26:01+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03T07:41:43+00:00" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" - }, - { - "name": "symfony/yaml", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2017-07-23T12:43:26+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23T20:04:58+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} diff --git a/includes/abstracts/abstract-wcs-background-repairer.php b/includes/abstracts/abstract-wcs-background-repairer.php new file mode 100755 index 0000000..06155dd --- /dev/null +++ b/includes/abstracts/abstract-wcs-background-repairer.php @@ -0,0 +1,180 @@ +repair_hook, array( $this, 'repair_item' ) ); + } + + /** + * Schedules the @see $this->scheduled_hook action to run in + * @see $this->time_limit seconds (60 seconds by default). + * + * Sets the page to 1. + * + * @since 2.6.0 + */ + public function schedule_repair() { + $this->set_page( 1 ); + parent::schedule_repair(); + } + + /** + * Gets a batch of items which need to be repaired. + * + * @since 2.6.0 + * @return array An array of items which need to be repaired. + */ + protected function get_items_to_update() { + $items_to_repair = array(); + + // Check if there are items from the last request that we should process first. + $unprocessed_items = $this->get_unprocessed_items(); + + if ( ! empty( $unprocessed_items ) ) { + $items_to_repair = $unprocessed_items; + $this->clear_unprocessed_items_cache(); + } elseif ( $page = $this->get_page() ) { + $items_to_repair = $this->get_items_to_repair( $page ); + $this->set_page( $page + 1 ); + } + + // Store the items as array keys for more performant un-setting. + $this->items_to_repair = array_flip( $items_to_repair ); + + return $items_to_repair; + } + + /** + * Runs the update and save any items which didn't get processed. + * + * @since 2.6.0 + */ + public function run_update() { + parent::run_update(); + + // After running the update, save any items which haven't processed so we can handle them in the next request. + $this->save_unprocessed_items(); + } + + /** + * Schedules the repair event for this item. + * + * @since 2.6.0 + */ + protected function update_item( $item ) { + // Schedule the individual repair actions to run in 1 hr to give us the best chance at scheduling all the actions before they start running and clogging up the queue. + as_schedule_single_action( gmdate( 'U' ) + HOUR_IN_SECONDS, $this->repair_hook, array( 'repair_object' => $item ) ); + unset( $this->items_to_repair[ $item ] ); + } + + /** + * Gets the current page number. + * + * @since 2.6.0 + * @return int + */ + protected function get_page() { + return absint( get_option( "{$this->repair_hook}_page", 0 ) ); + } + + /** + * Sets the current page number. + * + * @since 2.6.0 + * @param int $page. + */ + protected function set_page( $page ) { + update_option( "{$this->repair_hook}_page", (string) $page ); + } + + /** + * Gets items from the last request which weren't processed. + * + * @since 2.6.0 + * @return array + */ + protected function get_unprocessed_items() { + return get_option( "{$this->repair_hook}_unprocessed", array() ); + } + + /** + * Saves any items which haven't been handled. + * + * @since 2.6.0 + */ + protected function save_unprocessed_items() { + if ( ! empty( $this->items_to_repair ) ) { + // The items_to_repair array will have been flipped by get_items_to_update() so flip them back before storing. + update_option( "{$this->repair_hook}_unprocessed", array_flip( $this->items_to_repair ) ); + } + } + + /** + * Deletes any items stored in the unprocessed cache stored in an option. + * + * @since 2.6.0 + */ + protected function clear_unprocessed_items_cache() { + delete_option( "{$this->repair_hook}_unprocessed" ); + } + + /** + * Unschedules the instance's hook in Action Scheduler and deletes the page counter. + * + * This function is called when there are no longer any items to update. + * + * @since 2.6.0 + */ + protected function unschedule_background_updates() { + parent::unschedule_background_updates(); + delete_option( "{$this->repair_hook}_page" ); + } + + /** + * Repairs an item. + */ + abstract protected function repair_item( $item ); + + /** + * Get a batch of items which need to be repaired. + * + * @param int $page The page number to return results from. + * @return array The items to repair. Each item must be a string or int. + */ + abstract protected function get_items_to_repair( $page ); +} diff --git a/includes/admin/class-wc-subscriptions-admin.php b/includes/admin/class-wc-subscriptions-admin.php index 0060855..693ba0c 100755 --- a/includes/admin/class-wc-subscriptions-admin.php +++ b/includes/admin/class-wc-subscriptions-admin.php @@ -108,7 +108,11 @@ class WC_Subscriptions_Admin { add_action( 'woocommerce_admin_field_informational', __CLASS__ . '::add_informational_admin_field' ); - add_filter( 'posts_where', __CLASS__ . '::filter_orders' ); + add_filter( 'posts_where', array( __CLASS__, 'filter_orders' ) ); + + add_filter( 'posts_where', array( __CLASS__, 'filter_orders_from_list' ) ); + + add_filter( 'posts_where', array( __CLASS__, 'filter_subscriptions_from_list' ) ); add_filter( 'posts_where', array( __CLASS__, 'filter_paid_subscription_orders_for_user' ) ); @@ -821,7 +825,7 @@ class WC_Subscriptions_Admin { 'bulkEditIntervalhMessage' => __( 'Enter a new interval as a single number (e.g. to charge every 2nd month, enter 2):', 'woocommerce-subscriptions' ), 'bulkDeleteOptionLabel' => __( 'Delete all variations without a subscription', 'woocommerce-subscriptions' ), 'oneTimeShippingCheckNonce' => wp_create_nonce( 'one_time_shipping' ), - 'productHasSubscriptions' => wcs_get_subscriptions_for_product( $post->ID ) ? 'yes' : 'no', + 'productHasSubscriptions' => wcs_get_subscriptions_for_product( $post->ID, 'ids', array( 'limit' => 1 ) ) ? 'yes' : 'no', 'productTypeWarning' => __( 'Product type can not be changed because this product is associated with active subscriptions', 'woocommerce-subscriptions' ), ); } elseif ( 'edit-shop_order' == $screen->id ) { @@ -898,7 +902,7 @@ class WC_Subscriptions_Admin { delete_transient( WC_Subscriptions::$activation_transient ); } - if ( $is_woocommerce_screen || $is_activation_screen || 'edit-product' == $screen->id ) { + if ( $is_woocommerce_screen || $is_activation_screen || 'edit-product' == $screen->id || ( isset( $_GET['page'], $_GET['tab'] ) && 'wc-reports' === $_GET['page'] && 'subscriptions' === $_GET['tab'] ) ) { wp_enqueue_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), WC_Subscriptions::$version ); wp_enqueue_style( 'woocommerce_subscriptions_admin', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/css/admin.css', array( 'woocommerce_admin_styles' ), WC_Subscriptions::$version ); } @@ -1054,6 +1058,27 @@ class WC_Subscriptions_Admin { self::$option_prefix . '_order_button_text' => '', ); + // Add the $_POST[ 'woocommerce_subscriptions_allow_switching' ] value + if ( isset( $_POST[ self::$option_prefix . '_allow_switching_variable' ] ) || isset( $_POST[ self::$option_prefix . '_allow_switching_grouped' ] ) ) { + + $value = array(); + + if ( ! empty( $_POST[ self::$option_prefix . '_allow_switching_variable' ] ) ) { + $value[] = 'variable'; + unset( $_POST[ self::$option_prefix . '_allow_switching_variable' ] ); + } + + if ( ! empty( $_POST[ self::$option_prefix . '_allow_switching_grouped' ] ) ) { + $value[] = 'grouped'; + unset( $_POST[ self::$option_prefix . '_allow_switching_grouped' ] ); + } + + $_POST[ self::$option_prefix . '_allow_switching' ] = implode( '_', $value ); + + } else { + $_POST[ self::$option_prefix . '_allow_switching' ] = 'no'; + } + foreach ( $settings as $setting ) { if ( ! isset( $setting['id'], $setting['default'], $defaults_to_find[ $setting['id'] ], $_POST[ $setting['id'] ] ) ) { continue; @@ -1072,6 +1097,22 @@ class WC_Subscriptions_Admin { } } + // Add extra switching options, if any. + $extra_switching_options = (array) apply_filters( 'woocommerce_subscriptions_allow_switching_options', array() ); + + foreach ( $extra_switching_options as $option ) { + + if ( empty( $option['id'] ) || empty( $option['label'] ) ) { + continue; + } + + // Add to $settings to be natively saved. + $settings[] = array( + 'id' => WC_Subscriptions_Admin::$option_prefix . '_allow_switching_' . $option['id'], + 'type' => 'checkbox', // This will sanitize value to yes/no. + ); + } + woocommerce_update_options( $settings ); } @@ -1144,26 +1185,26 @@ class WC_Subscriptions_Admin { array( 'name' => __( 'Add to Cart Button Text', 'woocommerce-subscriptions' ), - 'desc' => __( '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.', 'woocommerce-subscriptions' ), + 'desc' => __( '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.', 'woocommerce-subscriptions' ), 'tip' => '', 'id' => self::$option_prefix . '_add_to_cart_button_text', 'css' => 'min-width:150px;', - 'default' => __( 'Sign Up Now', 'woocommerce-subscriptions' ), + 'default' => __( 'Sign up now', 'woocommerce-subscriptions' ), 'type' => 'text', 'desc_tip' => true, - 'placeholder' => __( 'Sign Up Now', 'woocommerce-subscriptions' ), + 'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ), ), array( 'name' => __( 'Place Order Button Text', 'woocommerce-subscriptions' ), - 'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription. Normally the checkout submission button displays "Place Order". When the cart contains a subscription, this is changed to "Sign Up Now".', 'woocommerce-subscriptions' ), + 'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription. Normally the checkout submission button displays "Place order". When the cart contains a subscription, this is changed to "Sign up now".', 'woocommerce-subscriptions' ), 'tip' => '', 'id' => self::$option_prefix . '_order_button_text', 'css' => 'min-width:150px;', - 'default' => __( 'Sign Up Now', 'woocommerce-subscriptions' ), + 'default' => __( 'Sign up now', 'woocommerce-subscriptions' ), 'type' => 'text', 'desc_tip' => true, - 'placeholder' => __( 'Sign Up Now', 'woocommerce-subscriptions' ), + 'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ), ), array( 'type' => 'sectionend', 'id' => self::$option_prefix . '_button_text' ), @@ -1385,7 +1426,6 @@ class WC_Subscriptions_Admin { * Filter the "Orders" list to show only orders associated with a specific subscription. * * @param string $where - * @param string $request * @return string * @since 2.0 */ @@ -1414,6 +1454,66 @@ class WC_Subscriptions_Admin { return $where; } + /** + * Filters the Admin orders table results based on a list of IDs returned by a report query. + * + * @param string $where The query WHERE clause. + * @return string $where + * @since 2.6.0 + */ + public static function filter_orders_from_list( $where ) { + global $typenow, $wpdb; + + if ( ! is_admin() || 'shop_order' !== $typenow || ! isset( $_GET['_orders_list_key'], $_GET['_report'] ) ) { + return $where; + } + + if ( ! empty( $_GET['_orders_list_key'] ) && ! empty( $_GET['_report'] ) ) { + $cache = get_transient( $_GET['_report'] ); + $results = $cache[ $_GET['_orders_list_key'] ]; + $order_ids = explode( ',', implode( ',', wp_list_pluck( $results, 'order_ids', true ) ) ); + + // $format = '%d, %d, %d, %d, %d, [...]' + $format = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID IN ($format)", $order_ids ); + } else { + // No orders in list. So, give invalid 'where' clause so as to make the query return 0 items. + $where .= " AND {$wpdb->posts}.ID = 0"; + } + + return $where; + } + + /** + * Filters the Admin subscriptions table results based on a list of IDs returned by a report query. + * + * @param string $where The query WHERE clause. + * @return string + * @since 2.6.0 + */ + public static function filter_subscriptions_from_list( $where ) { + global $typenow, $wpdb; + + if ( ! is_admin() || 'shop_subscription' !== $typenow || ! isset( $_GET['_subscriptions_list_key'], $_GET['_report'] ) ) { + return $where; + } + + if ( ! empty( $_GET['_subscriptions_list_key'] ) && ! empty( $_GET['_report'] ) ) { + $cache = get_transient( $_GET['_report'] ); + $results = $cache[ $_GET['_subscriptions_list_key'] ]; + $subscription_ids = explode( ',', implode( ',', wp_list_pluck( $results, 'subscription_ids', true ) ) ); + + // $format = '%d, %d, %d, %d, %d, [...]' + $format = implode( ', ', array_fill( 0, count( $subscription_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID IN ($format)", $subscription_ids ); + } else { + // No subscriptions in list. So, give invalid 'where' clause so as to make the query return 0 items. + $where .= " AND {$wpdb->posts}.ID = 0"; + } + + return $where; + } + /** * Filter the "Orders" list to show only paid subscription orders for a particular user * @@ -1446,7 +1546,7 @@ class WC_Subscriptions_Admin { $where .= " AND {$wpdb->posts}.ID = 0"; } else { // Orders with paid status - $where .= sprintf( " AND {$wpdb->posts}.post_status IN ( 'wc-processing', 'wc-completed' )" ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_status IN ( 'wc-processing', 'wc-completed' )" ); $where .= sprintf( " AND {$wpdb->posts}.ID IN (%s)", implode( ',', array_unique( $users_subscription_orders ) ) ); } diff --git a/includes/admin/class-wcs-admin-post-types.php b/includes/admin/class-wcs-admin-post-types.php index 43f3c97..f15bd7d 100755 --- a/includes/admin/class-wcs-admin-post-types.php +++ b/includes/admin/class-wcs-admin-post-types.php @@ -596,9 +596,15 @@ class WCS_Admin_Post_Types { case 'recurring_total' : $column_content .= esc_html( strip_tags( $the_subscription->get_formatted_order_total() ) ); - + $column_content .= ''; // translators: placeholder is the display name of a payment gateway a subscription was paid by - $column_content .= '' . esc_html( sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $the_subscription->get_payment_method_to_display() ) ) . ''; + $column_content .= esc_html( sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $the_subscription->get_payment_method_to_display() ) ); + + if ( WC_Subscriptions::is_duplicate_site() && $the_subscription->has_payment_gateway() && ! $the_subscription->get_requires_manual_renewal() ) { + $column_content .= WCS_Staging::get_payment_method_tooltip( $the_subscription ); + } + + $column_content .= ''; break; case 'start_date': diff --git a/includes/admin/class-wcs-admin-system-status.php b/includes/admin/class-wcs-admin-system-status.php index c866533..1e90675 100755 --- a/includes/admin/class-wcs-admin-system-status.php +++ b/includes/admin/class-wcs-admin-system-status.php @@ -318,7 +318,7 @@ class WCS_Admin_System_Status { $debug_data[ 'wcs_' . $gateway_id . '_feature_support' ] = array( 'name' => $gateway->method_title, 'label' => $gateway->method_title, - 'data' => $gateway->supports, + 'data' => (array) apply_filters( 'woocommerce_subscriptions_payment_gateway_features_list', $gateway->supports, $gateway ), ); if ( 'paypal' === $gateway_id ) { diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php b/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php index 2edd545..2639317 100755 --- a/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php @@ -45,77 +45,92 @@ class WCS_Meta_Box_Related_Orders { * @since 2.0 */ public static function output_rows( $post ) { + $orders_to_display = array(); + $subscriptions = array(); + $initial_subscriptions = array(); + $orders_by_type = array(); + $unknown_orders = array(); // Orders which couldn't be loaded. - $subscriptions = array(); - $orders = array(); - $is_subscription_screen = wcs_is_subscription( $post->ID ); - - // On the subscription page, just show related orders - if ( $is_subscription_screen ) { + // If this is a subscriptions screen, + if ( wcs_is_subscription( $post->ID ) ) { $this_subscription = wcs_get_subscription( $post->ID ); $subscriptions[] = $this_subscription; - } elseif ( wcs_order_contains_subscription( $post->ID, array( 'parent', 'renewal' ) ) ) { - $subscriptions = wcs_get_subscriptions_for_order( $post->ID, array( 'order_type' => array( 'parent', 'renewal' ) ) ); - } - // First, display all the subscriptions - foreach ( $subscriptions as $subscription ) { - wcs_set_objects_property( $subscription, 'relationship', __( 'Subscription', 'woocommerce-subscriptions' ), 'set_prop_only' ); - $orders[] = $subscription; - } - - //Resubscribed - $initial_subscriptions = array(); - - if ( $is_subscription_screen ) { - - $initial_subscriptions = wcs_get_subscriptions_for_resubscribe_order( $this_subscription ); - - $resubscribe_order_ids = WCS_Related_Order_Store::instance()->get_related_order_ids( $this_subscription, 'resubscribe' ); - - foreach ( $resubscribe_order_ids as $order_id ) { - $order = wc_get_order( $order_id ); - $relation = wcs_is_subscription( $order ) ? _x( 'Resubscribed Subscription', 'relation to order', 'woocommerce-subscriptions' ) : _x( 'Resubscribe Order', 'relation to order', 'woocommerce-subscriptions' ); - wcs_set_objects_property( $order, 'relationship', $relation, 'set_prop_only' ); - $orders[] = $order; - } - } else if ( wcs_order_contains_subscription( $post->ID, array( 'resubscribe' ) ) ) { + // Resubscribed subscriptions and orders. + $initial_subscriptions = wcs_get_subscriptions_for_resubscribe_order( $this_subscription ); + $orders_by_type['resubscribe'] = WCS_Related_Order_Store::instance()->get_related_order_ids( $this_subscription, 'resubscribe' ); + } else { + $subscriptions = wcs_get_subscriptions_for_order( $post->ID, array( 'order_type' => array( 'parent', 'renewal' ) ) ); $initial_subscriptions = wcs_get_subscriptions_for_order( $post->ID, array( 'order_type' => array( 'resubscribe' ) ) ); } - foreach ( $initial_subscriptions as $subscription ) { - wcs_set_objects_property( $subscription, 'relationship', _x( 'Initial Subscription', 'relation to order', 'woocommerce-subscriptions' ), 'set_prop_only' ); - $orders[] = $subscription; + foreach ( $subscriptions as $subscription ) { + // If we're on a single subscription or renewal order's page, display the parent orders + if ( 1 == count( $subscriptions ) && $subscription->get_parent_id() ) { + $orders_by_type['parent'][] = $subscription->get_parent_id(); + } + + // Finally, display the renewal orders + $orders_by_type['renewal'] = $subscription->get_related_orders( 'ids', 'renewal' ); + + // Build the array of subscriptions and orders to display. + $subscription->update_meta_data( '_relationship', _x( 'Subscription', 'relation to order', 'woocommerce-subscriptions' ) ); + $orders_to_display[] = $subscription; } - // Now, if we're on a single subscription or renewal order's page, display the parent orders - if ( 1 == count( $subscriptions ) ) { - foreach ( $subscriptions as $subscription ) { - if ( $subscription->get_parent_id() ) { - $order = $subscription->get_parent(); - wcs_set_objects_property( $order, 'relationship', _x( 'Parent Order', 'relation to order', 'woocommerce-subscriptions' ), 'set_prop_only' ); - $orders[] = $order; + foreach ( $initial_subscriptions as $subscription ) { + $subscription->update_meta_data( '_relationship', _x( 'Initial Subscription', 'relation to order', 'woocommerce-subscriptions' ) ); + $orders_to_display[] = $subscription; + } + + // Assign all order and subscription relationships and filter out non-objects. + foreach ( $orders_by_type as $order_type => $orders ) { + foreach ( $orders as $order_id ) { + $order = wc_get_order( $order_id ); + + switch ( $order_type ) { + case 'renewal': + $relation = _x( 'Renewal Order', 'relation to order', 'woocommerce-subscriptions' ); + break; + case 'parent': + $relation = _x( 'Parent Order', 'relation to order', 'woocommerce-subscriptions' ); + break; + case 'resubscribe': + $relation = wcs_is_subscription( $order ) ? _x( 'Resubscribed Subscription', 'relation to order', 'woocommerce-subscriptions' ) : _x( 'Resubscribe Order', 'relation to order', 'woocommerce-subscriptions' ); + break; + default: + $relation = _x( 'Unknown Order Type', 'relation to order', 'woocommerce-subscriptions' ); + break; + } + + if ( $order ) { + $order->update_meta_data( '_relationship', $relation ); + $orders_to_display[] = $order; + } else { + $unknown_orders[] = array( + 'order_id' => $order_id, + 'relation' => $relation, + ); } } } - // Finally, display the renewal orders - foreach ( $subscriptions as $subscription ) { + $orders_to_display = apply_filters( 'woocommerce_subscriptions_admin_related_orders_to_display', $orders_to_display, $subscriptions, $post ); - foreach ( $subscription->get_related_orders( 'all', 'renewal' ) as $order ) { - wcs_set_objects_property( $order, 'relationship', _x( 'Renewal Order', 'relation to order', 'woocommerce-subscriptions' ), 'set_prop_only' ); - $orders[] = $order; - } - } - - $orders = apply_filters( 'woocommerce_subscriptions_admin_related_orders_to_display', $orders, $subscriptions, $post ); - - foreach ( $orders as $order ) { - - if ( wcs_get_objects_property( $order, 'id' ) == $post->ID ) { + foreach ( $orders_to_display as $order ) { + // Skip the order being viewed. + if ( $order->get_id() === (int) $post->ID ) { continue; } + include( dirname( __FILE__ ) . '/views/html-related-orders-row.php' ); } + + foreach ( $unknown_orders as $order_and_relationship ) { + $order_id = $order_and_relationship['order_id']; + $relationship = $order_and_relationship['relation']; + + include( dirname( __FILE__ ) . '/views/html-unknown-related-orders-row.php' ); + } } } diff --git a/includes/admin/meta-boxes/views/html-unknown-related-orders-row.php b/includes/admin/meta-boxes/views/html-unknown-related-orders-row.php new file mode 100755 index 0000000..9aa0fac --- /dev/null +++ b/includes/admin/meta-boxes/views/html-unknown-related-orders-row.php @@ -0,0 +1,25 @@ + + + + +
+ ' ) ); ?> +
+ + + — + — + — + diff --git a/includes/admin/reports/class-wcs-report-dashboard.php b/includes/admin/reports/class-wcs-report-dashboard.php index 1882983..71d2767 100755 --- a/includes/admin/reports/class-wcs-report-dashboard.php +++ b/includes/admin/reports/class-wcs-report-dashboard.php @@ -50,7 +50,7 @@ class WCS_Report_Dashboard { $report_data = new stdClass; - $cached_results = get_transient( strtolower( self::class ) ); + $cached_results = get_transient( strtolower( __CLASS__ ) ); // Subscription signups this month $query = $wpdb->prepare( @@ -103,7 +103,7 @@ class WCS_Report_Dashboard { if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) ); - set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS ); } $report_data->signup_revenue = $cached_results[ $query_hash ]; @@ -131,7 +131,7 @@ class WCS_Report_Dashboard { if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) ); - set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS ); } $report_data->renewal_count = $cached_results[ $query_hash ]; @@ -165,7 +165,7 @@ class WCS_Report_Dashboard { if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) ); - set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS ); } $report_data->renewal_revenue = $cached_results[ $query_hash ]; @@ -188,7 +188,7 @@ class WCS_Report_Dashboard { if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) ); - set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS ); } $report_data->cancel_count = $cached_results[ $query_hash ]; diff --git a/includes/admin/reports/class-wcs-report-subscription-by-customer.php b/includes/admin/reports/class-wcs-report-subscription-by-customer.php index 2182700..ea30a25 100755 --- a/includes/admin/reports/class-wcs-report-subscription-by-customer.php +++ b/includes/admin/reports/class-wcs-report-subscription-by-customer.php @@ -45,7 +45,9 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table { echo ' ' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->active_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '
'; echo ' ' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . wcs_help_tip( __( 'The total number of sign-up, switch and renewal orders placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) . '
'; - echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ': ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '

'; + echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ': '; + echo wp_kses_post( wc_price( $this->totals->total_customers > 0 ? ( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) : 0 ) ); + echo wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '

'; echo ''; $this->display(); echo ''; @@ -212,11 +214,11 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table { COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total, COUNT(DISTINCT parent_order.ID) as initial_order_count, - SUM(CASE + COALESCE(SUM(CASE WHEN subscription_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1 ELSE 0 - END) AS active_subscriptions + END), 0) AS active_subscriptions FROM {$wpdb->posts} subscription_posts INNER JOIN {$wpdb->postmeta} customer_ids ON customer_ids.post_id = subscription_posts.ID diff --git a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php index d159800..348db22 100755 --- a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php +++ b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php @@ -16,6 +16,24 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { private $report_data; + private $generating_report; + + /** + * Sets the query hash for saving the results to enable listing later. + * + * @since 2.6.0 + * @param array $query The report query clause array. + * @return array $query + */ + public function set_query_hash( $query ) { + + if ( in_array( $this->generating_report, array( 'new_subscriptions', 'renewals', 'resubscribes', 'switches' ) ) ) { + $this->report_data->{$this->generating_report . '_query_hash'} = md5( 'get_results' . implode( ' ', $query ) ); + } + + return $query; + } + /** * Get report data * @return array @@ -51,7 +69,11 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data = new stdClass; - $this->report_data->new_subscriptions = (array) $this->get_order_report_data( + add_filter( 'woocommerce_reports_get_order_report_query', array( $this, 'set_query_hash' ) ); + + $this->generating_report = 'new_subscriptions'; + + $this->report_data->new_subscriptions_data = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( @@ -60,6 +82,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'name' => 'count', 'distinct' => true, ), + 'id' => array( + 'type' => 'post_data', + 'function' => 'GROUP_CONCAT', + 'name' => 'subscription_ids', + 'distinct' => true, + ), 'post_date' => array( 'type' => 'post_data', 'function' => '', @@ -83,6 +111,8 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { ) ); + $this->generating_report = 'renewals'; + $this->report_data->renewal_data = (array) $this->get_order_report_data( array( 'data' => array( @@ -92,6 +122,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'name' => 'count', 'distinct' => true, ), + 'id' => array( + 'type' => 'post_data', + 'function' => 'GROUP_CONCAT', + 'name' => 'order_ids', + 'distinct' => true, + ), 'post_date' => array( 'type' => 'post_data', 'function' => '', @@ -126,6 +162,8 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { ) ); + $this->generating_report = 'resubscribes'; + $this->report_data->resubscribe_data = (array) $this->get_order_report_data( array( 'data' => array( @@ -135,6 +173,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'name' => 'count', 'distinct' => true, ), + 'id' => array( + 'type' => 'post_data', + 'function' => 'GROUP_CONCAT', + 'name' => 'order_ids', + 'distinct' => true, + ), 'post_date' => array( 'type' => 'post_data', 'function' => '', @@ -169,7 +213,9 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { ) ); - $this->report_data->switch_counts = (array) $this->get_order_report_data( + $this->generating_report = 'switches'; + + $this->report_data->switch_data = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( @@ -178,6 +224,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'name' => 'count', 'distinct' => true, ), + 'id' => array( + 'type' => 'post_data', + 'function' => 'GROUP_CONCAT', + 'name' => 'order_ids', + 'distinct' => true, + ), 'post_date' => array( 'type' => 'post_data', 'function' => '', @@ -188,6 +240,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'function' => '', 'name' => 'switch_orders', ), + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'switch_totals', + 'join_type' => 'LEFT', // To avoid issues if there is no switch_total meta + ), ), 'where' => array( 'post_status' => array( @@ -206,6 +264,10 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { ) ); + unset( $this->generating_report ); + + remove_filter( 'woocommerce_reports_get_order_report_query', array( $this, 'set_query_hash' ) ); + $cached_results = get_transient( strtolower( get_class( $this ) ) ); /* @@ -214,11 +276,13 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $query = $wpdb->prepare( "SELECT SUM(subscriptions.count) as count, order_posts.post_date as post_date, - SUM(order_total_post_meta.meta_value) as signup_totals + SUM(order_total_post_meta.meta_value) as signup_totals, + GROUP_CONCAT( DISTINCT subscriptions.ids ) as subscription_ids FROM {$wpdb->posts} AS order_posts INNER JOIN ( SELECT COUNT(DISTINCT(subscription_posts.ID)) as count, - subscription_posts.post_parent as order_id + subscription_posts.post_parent as order_id, + GROUP_CONCAT( subscription_posts.ID ) as ids FROM {$wpdb->posts} as subscription_posts WHERE subscription_posts.post_type = 'shop_subscription' AND subscription_posts.post_date >= %s @@ -251,11 +315,13 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->signup_data = $cached_results[ $query_hash ]; + $this->report_data->signup_orders_query_hash = $query_hash; + /* * Subscribers by date */ $query = $wpdb->prepare( - "SELECT searchdate.Date as date, COUNT( DISTINCT wcsubs.ID) as count + "SELECT searchdate.Date as date, COUNT( DISTINCT wcsubs.ID) as count, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids FROM ( SELECT DATE(last_thousand_days.Date) as Date FROM ( @@ -314,11 +380,14 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->subscriber_counts = $cached_results[ $query_hash ]; + $cached_results[ $query_hash ] = array_slice( $this->report_data->subscriber_counts, -1 ); + $this->report_data->current_subscriptions_query_hash = $query_hash; + /* * Subscription cancellations */ $query = $wpdb->prepare( - "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) as cancel_date + "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) as cancel_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids FROM {$wpdb->posts} as wcsubs JOIN {$wpdb->postmeta} AS wcsmeta_cancel ON wcsubs.ID = wcsmeta_cancel.post_id @@ -342,11 +411,13 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->cancel_counts = $cached_results[ $query_hash ]; + $this->report_data->cancelled_subscriptions_query_hash = $query_hash; + /* * Subscriptions ended */ $query = $wpdb->prepare( - "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_end.meta_value, '+00:00', '{$site_timezone}' ) as end_date + "SELECT COUNT( DISTINCT wcsubs.ID ) as count, CONVERT_TZ( wcsmeta_end.meta_value, '+00:00', '{$site_timezone}' ) as end_date, GROUP_CONCAT( DISTINCT wcsubs.ID ) as subscription_ids FROM {$wpdb->posts} as wcsubs JOIN {$wpdb->postmeta} AS wcsmeta_end ON wcsubs.ID = wcsmeta_end.post_id @@ -370,15 +441,18 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->ended_counts = $cached_results[ $query_hash ]; + $this->report_data->ended_subscriptions_query_hash = $query_hash; + // Total up the query data $this->report_data->signup_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ); $this->report_data->renewal_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ); $this->report_data->resubscribe_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ); - $this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) ); + $this->report_data->switch_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->switch_data, 'switch_totals' ) ); + $this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions_data, 'count' ) ) ); $this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) ); $this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) ); $this->report_data->resubscribe_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'count' ) ) ); - $this->report_data->switch_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->switch_counts, 'count' ) ) ); + $this->report_data->switch_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->switch_data, 'count' ) ) ); $this->report_data->total_subscriptions_cancelled = absint( array_sum( wp_list_pluck( $this->report_data->cancel_counts, 'count' ) ) ); $this->report_data->total_subscriptions_ended = absint( array_sum( wp_list_pluck( $this->report_data->ended_counts, 'count' ) ) ); $this->report_data->total_subscriptions_at_period_end = $this->report_data->subscriber_counts ? absint( end( $this->report_data->subscriber_counts )->count ) : 0; @@ -416,56 +490,71 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { ); $legend[] = array( - 'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '' . $this->report_data->new_subscription_total_count . '' ), + 'title' => sprintf( __( '%s switch revenue in this period', 'woocommerce-subscriptions' ), '' . wc_price( $data->switch_orders_total_amount ) . '' ), + 'placeholder' => __( 'The sum of all switch orders including tax and shipping.', 'woocommerce-subscriptions' ), + 'color' => $this->chart_colours['switch_total'], + 'highlight_series' => 11, + ); + + $legend[] = array( + 'title' => sprintf( __( '%2$s %1$s new subscriptions', 'woocommerce-subscriptions' ), ' ' . $this->report_data->new_subscription_total_count . ' ', + '' ), 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order. This includes orders pending payment.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['new_count'], 'highlight_series' => 1, ); $legend[] = array( - 'title' => sprintf( __( '%s subscription signups', 'woocommerce-subscriptions' ), '' . $this->report_data->signup_orders_total_count . '' ), - 'placeholder' => __( 'The number of subscription parent orders created during this period. This represents the new subscriptions created by customers placing an order via checkout.', 'woocommerce-subscriptions' ), + 'title' => sprintf( __( '%2$s %1$s subscription signups', 'woocommerce-subscriptions' ), ' ' . $this->report_data->signup_orders_total_count . ' ', + '' ), + 'placeholder' => __( 'The number of subscriptions purchased in parent orders created during this period. This represents the new subscriptions created by customers placing an order via checkout.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['signup_count'], 'highlight_series' => 2, ); $legend[] = array( - 'title' => sprintf( __( '%s subscription resubscribes', 'woocommerce-subscriptions' ), '' . $data->resubscribe_orders_total_count . '' ), + 'title' => sprintf( __( '%2$s %1$s subscription resubscribes', 'woocommerce-subscriptions' ), ' ' . $this->report_data->resubscribe_orders_total_count . ' ', + '' ), 'placeholder' => __( 'The number of resubscribe orders processed during this period.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['resubscribe_count'], 'highlight_series' => 3, ); $legend[] = array( - 'title' => sprintf( __( '%s subscription renewals', 'woocommerce-subscriptions' ), '' . $data->renewal_orders_total_count . '' ), + 'title' => sprintf( __( '%2$s %1$s subscription renewals', 'woocommerce-subscriptions' ), ' ' . $this->report_data->renewal_orders_total_count . ' ', + '' ), 'placeholder' => __( 'The number of renewal orders processed during this period.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['renewal_count'], 'highlight_series' => 4, ); $legend[] = array( - 'title' => sprintf( __( '%s subscription switches', 'woocommerce-subscriptions' ), '' . $data->switch_orders_total_count . '' ), + 'title' => sprintf( __( '%2$s %1$s subscription switches', 'woocommerce-subscriptions' ), ' ' . $this->report_data->switch_orders_total_count . ' ', + '' ), 'placeholder' => __( 'The number of subscriptions upgraded, downgraded or cross-graded during this period.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['switch_count'], 'highlight_series' => 0, ); $legend[] = array( - 'title' => sprintf( __( '%s subscription cancellations', 'woocommerce-subscriptions' ), '' . $data->total_subscriptions_cancelled . '' ), + 'title' => sprintf( __( '%2$s %1$s subscription cancellations', 'woocommerce-subscriptions' ), ' ' . $this->report_data->total_subscriptions_cancelled . ' ', + '' ), 'placeholder' => __( 'The number of subscriptions cancelled by the customer or store manager during this period. The pre-paid term may not yet have ended during this period.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['cancel_count'], 'highlight_series' => 7, ); $legend[] = array( - 'title' => sprintf( __( '%s subscriptions ended', 'woocommerce-subscriptions' ), '' . $data->total_subscriptions_ended . '' ), + 'title' => sprintf( __( '%2$s %1$s ended subscriptions', 'woocommerce-subscriptions' ), ' ' . $this->report_data->total_subscriptions_ended . ' ', + '' ), 'placeholder' => __( 'The number of subscriptions which have either expired or reached the end of the prepaid term if it was previously cancelled.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['ended_count'], 'highlight_series' => 6, ); $legend[] = array( - 'title' => sprintf( __( '%s current subscriptions', 'woocommerce-subscriptions' ), '' . $data->total_subscriptions_at_period_end . '' ), + 'title' => sprintf( __( '%2$s %1$s current subscriptions', 'woocommerce-subscriptions' ), ' ' . $this->report_data->total_subscriptions_at_period_end . ' ', + '' ), 'placeholder' => __( 'The number of subscriptions during this period with an end date in the future and a status other than pending.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['subscriber_count'], 'highlight_series' => 5, @@ -513,6 +602,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'signup_total' => '#439ad9', 'renewal_total' => '#b1d4ea', 'resubscribe_total' => '#7ab7e2', + 'switch_total' => '#a7b7f1', 'new_count' => '#9adbb5', 'signup_count' => '#5cc488', 'resubscribe_count' => '#449163', @@ -568,11 +658,12 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $signup_orders_amount = $this->prepare_chart_data( $this->report_data->signup_data, 'post_date', 'signup_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); $renewal_orders_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); $resubscribe_orders_amount = $this->prepare_chart_data( $this->report_data->resubscribe_data, 'post_date', 'resubscribe_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $new_subscriptions_count = $this->prepare_chart_data( $this->report_data->new_subscriptions, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $switch_orders_amount = $this->prepare_chart_data( $this->report_data->switch_data, 'post_date', 'switch_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $new_subscriptions_count = $this->prepare_chart_data( $this->report_data->new_subscriptions_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $signup_orders_count = $this->prepare_chart_data( $this->report_data->signup_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $renewal_orders_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $resubscribe_orders_count = $this->prepare_chart_data( $this->report_data->resubscribe_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $switch_orders_count = $this->prepare_chart_data( $this->report_data->switch_counts, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $switch_orders_count = $this->prepare_chart_data( $this->report_data->switch_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $subscriber_count = $this->prepare_chart_data_daily_average( $this->report_data->subscriber_counts, 'date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $cancel_count = $this->prepare_chart_data( $this->report_data->cancel_counts, 'cancel_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $ended_count = $this->prepare_chart_data( $this->report_data->ended_counts, 'end_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); @@ -582,6 +673,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { 'signup_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $signup_orders_amount ) ), 'renewal_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $renewal_orders_amount ) ), 'resubscribe_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $resubscribe_orders_amount ) ), + 'switch_orders_amount' => array_map( array( $this, 'round_chart_totals' ), array_values( $switch_orders_amount ) ), 'new_subscriptions_count' => array_values( $new_subscriptions_count ), 'signup_orders_count' => array_values( $signup_orders_count ), 'renewal_orders_count' => array_values( $renewal_orders_count ), @@ -791,6 +883,26 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { shadowSize: 0, get_currency_tooltip() ); ?> }, + { + label: "", + data: order_data.switch_orders_amount, + yaxis: 2, + color: 'chart_colours['switch_total'] ); ?>', + points: { + show: true, + radius: 5, + lineWidth: 4, + fillColor: '#fff', + fill: true + }, + lines: { + show: true, + lineWidth: 5, + fill: false + }, + shadowSize: 0, + get_currency_tooltip() ); ?> + }, ]; if ( highlight !== 'undefined' && series[ highlight ] ) { diff --git a/includes/api/class-wc-rest-subscriptions-controller.php b/includes/api/class-wc-rest-subscriptions-controller.php index 189549e..baf223b 100755 --- a/includes/api/class-wc-rest-subscriptions-controller.php +++ b/includes/api/class-wc-rest-subscriptions-controller.php @@ -77,6 +77,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { * @param WP_REST_Request $request */ public function filter_get_subscription_response( $response, $post, $request ) { + $decimal_places = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) { $subscription = wcs_get_subscription( $post->ID ); @@ -97,6 +98,72 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { // v1 API includes some date types in site time, include those dates in UTC as well. $response->data['date_completed_gmt'] = wc_rest_prepare_date_response( $subscription->get_date_completed() ); $response->data['date_paid_gmt'] = wc_rest_prepare_date_response( $subscription->get_date_paid() ); + $response->data['removed_line_items'] = array(); + + // Include removed line items of a subscription + foreach ( $subscription->get_items( 'line_item_removed' ) as $item_id => $item ) { + $product = $item->get_product(); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $subscription->get_item_total( $item, false, false ), $decimal_places ), + 'subtotal' => wc_format_decimal( $subscription->get_line_subtotal( $item, false, false ), $decimal_places ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $decimal_places ), + 'total' => wc_format_decimal( $subscription->get_line_total( $item, false, false ), $decimal_places ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $decimal_places ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $response->data['removed_line_items'][] = $line_item; + } } return $response; @@ -484,6 +551,139 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { 'context' => array( 'view' ), 'readonly' => true, ), + 'removed_line_items' => array( + 'description' => __( 'Removed line items data.', 'woocommerce-subscriptions' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce-subscriptions' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce-subscriptions' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce-subscriptions' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce-subscriptions' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce-subscriptions' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce-subscriptions' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce-subscriptions' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Removed line item meta data.', 'woocommerce-subscriptions' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce-subscriptions' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce-subscriptions' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), ); $schema['properties'] += $subscriptions_schema; diff --git a/includes/class-wc-order-item-pending-switch.php b/includes/class-wc-order-item-pending-switch.php index 6220d7d..9a557cf 100755 --- a/includes/class-wc-order-item-pending-switch.php +++ b/includes/class-wc-order-item-pending-switch.php @@ -1,6 +1,23 @@ is_purchasable() && $this->is_in_stock() ) { - $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) ); + $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) ); } else { $text = parent::add_to_cart_text(); // translated "Read More" } diff --git a/includes/class-wc-product-subscription.php b/includes/class-wc-product-subscription.php index c246145..65681ea 100755 --- a/includes/class-wc-product-subscription.php +++ b/includes/class-wc-product-subscription.php @@ -69,7 +69,7 @@ class WC_Product_Subscription extends WC_Product_Simple { public function add_to_cart_text() { if ( $this->is_purchasable() && $this->is_in_stock() ) { - $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) ); + $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) ); } else { $text = parent::add_to_cart_text(); // translated "Read More" } diff --git a/includes/class-wc-product-variable-subscription.php b/includes/class-wc-product-variable-subscription.php index f1e2ef2..e6463d6 100755 --- a/includes/class-wc-product-variable-subscription.php +++ b/includes/class-wc-product-variable-subscription.php @@ -70,7 +70,7 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable { public function single_add_to_cart_text() { if ( $this->is_purchasable() && $this->is_in_stock() ) { - $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) ); + $text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) ); } else { $text = parent::add_to_cart_text(); // translated "Read More" } diff --git a/includes/class-wc-subscription-item-coupon-pending-switch.php b/includes/class-wc-subscription-item-coupon-pending-switch.php new file mode 100755 index 0000000..6a5dfbf --- /dev/null +++ b/includes/class-wc-subscription-item-coupon-pending-switch.php @@ -0,0 +1,24 @@ +cached_completed_payment_count ) { - - $completed_payment_count = ( ( $parent_order = $this->get_parent() ) && ( null !== wcs_get_objects_property( $parent_order, 'date_paid' ) || $parent_order->has_status( $this->get_paid_order_statuses() ) ) ) ? 1 : 0; - - $paid_renewal_orders = array(); - $renewal_order_ids = $this->get_related_order_ids( 'renewal' ); - - if ( ! empty( $renewal_order_ids ) ) { - - // Looping over the known orders is faster than database queries on large sites - foreach ( $renewal_order_ids as $renewal_order_id ) { - - $renewal_order = wc_get_order( $renewal_order_id ); - - // Not all gateways call $order->payment_complete(), so with WC < 3.0 we need to find renewal orders with a paid date or a paid status. WC 3.0+ takes care of setting the paid date when payment_complete() wasn't called, so isn't needed with WC 3.0 or newer. - if ( $renewal_order && ( null !== wcs_get_objects_property( $renewal_order, 'date_paid' ) || $renewal_order->has_status( $this->get_paid_order_statuses() ) ) ) { - $paid_renewal_orders[] = $renewal_order_id; - } - } - - if ( ! empty( $paid_renewal_orders ) ) { - $completed_payment_count += count( $paid_renewal_orders ); - } - } - } else { - $completed_payment_count = $this->cached_completed_payment_count; + if ( empty( $order_types ) ) { + $order_types = array( 'parent', 'renewal' ); + } elseif ( ! is_array( $order_types ) ) { + $order_types = array( $order_types ); } - // 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 ); + // Replace 'any' to prevent counting orders twice. + $any_key = array_search( 'any', $order_types ); + if ( false !== $any_key ) { + unset( $order_types[ $any_key ] ); + $order_types = array_merge( $order_types, array( 'parent', 'renewal', 'resubscribe', 'switch' ) ); + } - return $this->cached_completed_payment_count; + // Ensure orders are only counted once and parent is counted before renewal for deprecated filter. + $order_types = array_unique( $order_types ); + sort( $order_types ); + + if ( ! is_array( $this->cached_payment_count ) ) { + $this->cached_payment_count = array( + 'completed' => array(), + 'refunded' => array(), + ); + } + + // Keep a tally of the counts of all requested order types + $total_completed_payment_count = $total_refunded_payment_count = 0; + + foreach ( $order_types as $order_type ) { + // If not cached, calculate the payment counts otherwise use the cached version. + if ( ! isset( $this->cached_payment_count['completed'][ $order_type ] ) ) { + $completed_payment_count = $refunded_payment_count = 0; + + // Looping over the known orders is faster than database queries on large sites + foreach ( $this->get_related_orders( 'all', $order_type ) as $related_order ) { + if ( null !== $related_order->get_date_paid() ) { + $completed_payment_count++; + + if ( $related_order->has_status( 'refunded' ) ) { + $refunded_payment_count++; + } + } + } + } else { + $completed_payment_count = $this->cached_payment_count['completed'][ $order_type ]; + $refunded_payment_count = $this->cached_payment_count['refunded'][ $order_type ]; + } + + // Store the payment counts to avoid hitting the database again + $this->cached_payment_count['completed'][ $order_type ] = apply_filters( "woocommerce_subscription_{$order_type}_payment_completed_count", $completed_payment_count, $this, $order_type ); + $this->cached_payment_count['refunded'][ $order_type ] = apply_filters( "woocommerce_subscription_{$order_type}_payment_refunded_count", $refunded_payment_count, $this, $order_type ); + + $total_completed_payment_count += $this->cached_payment_count['completed'][ $order_type ]; + $total_refunded_payment_count += $this->cached_payment_count['refunded'][ $order_type ]; + } + + switch ( $payment_type ) { + case 'completed': + $count = $total_completed_payment_count; + + /** + * Previously the @see WC_Subscription::get_completed_payment_count() function would filter the completed renewal and parent order count as a combined total. + * To remain backwards compatible, we need to apply that filter but only if we're getting the parent and renewal order completed count. + */ + if ( array( 'parent', 'renewal' ) === $order_types ) { + $count = $this->apply_deprecated_completed_payment_count_filter( $count ); + } + break; + case 'refunded': + $count = $total_refunded_payment_count; + break; + case 'net': + $count = $total_completed_payment_count - $total_refunded_payment_count; + break; + default: + $count = 0; + break; + } + + return $count; } /** @@ -1300,8 +1349,11 @@ 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' ) ) ) { + if ( isset( $this->cached_payment_count['completed'] ) ) { + $this->cached_payment_count = null; + } + + if ( $this->get_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 { $can_date_be_updated = false; @@ -1338,7 +1390,7 @@ class WC_Subscription extends WC_Order { $date = $this->calculate_next_payment_date(); break; case 'trial_end' : - if ( $this->get_completed_payment_count() >= 2 ) { + if ( $this->get_payment_count() >= 2 ) { $date = 0; } else { // By default, trial end is the same as the next payment date @@ -1395,7 +1447,7 @@ class WC_Subscription extends WC_Order { } else { // The next payment date is {interval} billing periods from the start date, trial end date or last payment date - if ( 0 !== $next_payment_time && $next_payment_time < gmdate( 'U' ) && ( ( 0 !== $trial_end_time && 1 >= $this->get_completed_payment_count() ) || WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $this ) ) ) { + if ( 0 !== $next_payment_time && $next_payment_time < gmdate( 'U' ) && ( ( 0 !== $trial_end_time && 1 >= $this->get_payment_count() ) || WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $this ) ) ) { $from_timestamp = $next_payment_time; } elseif ( $last_payment_time > $start_time && apply_filters( 'wcs_calculate_next_payment_from_last_payment', true, $this ) ) { $from_timestamp = $last_payment_time; @@ -1649,8 +1701,10 @@ class WC_Subscription extends WC_Order { return; } - // Clear the cached completed payment count, kept here for backward compat even though it's also reset in $this->process_payment_complete() - $this->cached_completed_payment_count = false; + // Clear the cached renewal payment counts, kept here for backward compat even though it's also reset in $this->process_payment_complete() + if ( isset( $this->cached_payment_count['completed'] ) ) { + $this->cached_payment_count = null; + } // Make sure the last order's status is updated $last_order = $this->get_last_order( 'all', 'any' ); @@ -1669,8 +1723,10 @@ class WC_Subscription extends WC_Order { */ public function payment_complete_for_order( $last_order ) { - // Clear the cached completed payment count - $this->cached_completed_payment_count = false; + // Clear the cached renewal payment counts + if ( isset( $this->cached_payment_count['completed'] ) ) { + $this->cached_payment_count = null; + } // Reset suspension count $this->set_suspension_count( 0 ); @@ -2571,4 +2627,40 @@ class WC_Subscription extends WC_Order { return $datetime; } + + /** + * Get the number of payments completed for a subscription + * + * Completed payment include all renewal orders and potentially an initial order (if the + * subscription was created as a result of a purchase from the front end rather than + * manually by the store manager). + * + * @deprecated 2.6.0 + */ + public function get_completed_payment_count() { + wcs_deprecated_function( __METHOD__, '2.6.0', __CLASS__ . '::get_payment_count()' ); + + return $this->get_payment_count(); + } + + /** + * Apply the deprecated 'woocommerce_subscription_payment_completed_count' filter + * to maintain backward compatibility. + * + * @param int $count + * + * @return int + * + * @deprecated 2.6.0 + */ + protected function apply_deprecated_completed_payment_count_filter( $count ) { + $deprecated_filter_hook = 'woocommerce_subscription_payment_completed_count'; + + if ( has_filter( $deprecated_filter_hook ) ) { + wcs_deprecated_hook( $deprecated_filter_hook, '2.6.0', '"woocommerce_subscription_parent_payment_completed_count" and "woocommerce_subscription_renewal_payment_completed_count" to provide the discrete counts summed in the "' . $deprecated_filter_hook . '" filter' ); + $count = apply_filters( $deprecated_filter_hook, $count ); + } + + return $count; + } } diff --git a/includes/class-wc-subscriptions-addresses.php b/includes/class-wc-subscriptions-addresses.php index d03162f..37c8693 100755 --- a/includes/class-wc-subscriptions-addresses.php +++ b/includes/class-wc-subscriptions-addresses.php @@ -44,7 +44,7 @@ class WC_Subscriptions_Addresses { if ( $subscription->needs_shipping_address() && $subscription->has_status( array( 'active', 'on-hold' ) ) ) { $actions['change_address'] = array( 'url' => add_query_arg( array( 'subscription' => $subscription->get_id() ), wc_get_endpoint_url( 'edit-address', 'shipping' ) ), - 'name' => __( 'Change Address', 'woocommerce-subscriptions' ), + 'name' => __( 'Change address', 'woocommerce-subscriptions' ), ); } diff --git a/includes/class-wc-subscriptions-cart-validator.php b/includes/class-wc-subscriptions-cart-validator.php new file mode 100755 index 0000000..0301a82 --- /dev/null +++ b/includes/class-wc-subscriptions-cart-validator.php @@ -0,0 +1,140 @@ +cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data ); + $product = wc_get_product( $product_id ); + + // If the product is sold individually or if the cart doesn't already contain this product, empty the cart. + if ( ( $product && $product->is_sold_individually() ) || ! WC()->cart->find_product_in_cart( $cart_item_id ) ) { + WC()->cart->empty_cart(); + } + } elseif ( $is_subscription && wcs_cart_contains_renewal() && ! $multiple_subscriptions_possible && ! $manual_renewals_enabled ) { + + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); + + wc_add_notice( __( 'A subscription renewal has been removed from your cart. Multiple subscriptions can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); + + } elseif ( $is_subscription && $cart_contains_subscription && ! $multiple_subscriptions_possible && ! $manual_renewals_enabled && ! WC_Subscriptions_Cart::cart_contains_product( $canonical_product_id ) ) { + + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); + + wc_add_notice( __( 'A subscription has been removed from your cart. Due to payment gateway restrictions, different subscription products can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); + + } elseif ( $cart_contains_subscription && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) { + + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); + + wc_add_notice( __( 'A subscription has been removed from your cart. Products and subscriptions can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); + + // Redirect to cart page to remove subscription & notify shopper + if ( WC_Subscriptions::is_woocommerce_pre( '3.0.8' ) ) { + add_filter( 'add_to_cart_fragments', __CLASS__ . '::redirect_ajax_add_to_cart' ); + } else { + add_filter( 'woocommerce_add_to_cart_fragments', __CLASS__ . '::redirect_ajax_add_to_cart' ); + } + } + + return $valid; + } + + /** + * This checks cart items for mixed checkout. + * + * @param $cart WC_Cart the one we got from session + * @return WC_Cart $cart + * + * @since 2.6.0 + */ + public static function validate_cart_contents_for_mixed_checkout( $cart ) { + + // When mixed checkout is enabled + if ( $cart->cart_contents && 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) { + return $cart; + } + + if ( ! WC_Subscriptions_Cart::cart_contains_subscription() && ! wcs_cart_contains_renewal() ) { + return $cart; + } + + foreach ( $cart->cart_contents as $key => $item ) { + $is_subscription = WC_Subscriptions_Product::is_subscription( $item['product_id'] ); + + // If a non-subscription product is found in the cart containing subscriptions ( maybe because of carts merge while logging in ) + if ( ! $is_subscription ) { + + // remove the subscriptions from the cart + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); + + // and add an appropriate notice + wc_add_notice( __( 'Your cart has been emptied of subscription products. Products and subscriptions cannot be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); + + // Redirect to cart page to remove subscription & notify shopper + if ( WC_Subscriptions::is_woocommerce_pre( '3.0.8' ) ) { + add_filter( 'add_to_cart_fragments', array( 'WC_Subscriptions', 'redirect_ajax_add_to_cart' ) ); + } else { + add_filter( 'woocommerce_add_to_cart_fragments', array( 'WC_Subscriptions', 'redirect_ajax_add_to_cart' ) ); + } + break; + } + } + return $cart; + } + + /** + * Don't allow new subscription products to be added to the cart if it contains a subscription renewal already. + * + * @since 2.6.0 + */ + public static function can_add_subscription_product_to_cart( $can_add, $product_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) { + + if ( $can_add && ! 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' ); + $can_add = false; + } + + return $can_add; + } + +} diff --git a/includes/class-wc-subscriptions-cart.php b/includes/class-wc-subscriptions-cart.php index c9a6e6d..863e5a4 100755 --- a/includes/class-wc-subscriptions-cart.php +++ b/includes/class-wc-subscriptions-cart.php @@ -84,13 +84,11 @@ 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_filter( '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 ); // Remove recurring shipping methods stored in the session whenever a subscription product is removed from the cart add_action( 'woocommerce_remove_cart_item', array( __CLASS__, 'maybe_reset_chosen_shipping_methods' ) ); - add_action( 'woocommerce_before_cart_item_quantity_zero', array( __CLASS__, 'maybe_reset_chosen_shipping_methods' ) ); + wcs_add_woocommerce_dependent_action( 'woocommerce_before_cart_item_quantity_zero', array( __CLASS__, 'maybe_reset_chosen_shipping_methods' ), '3.7.0', '<' ); // Massage our shipping methods into the format used by WC core (we can't use normal form elements to do this as WC overrides them) add_action( 'woocommerce_checkout_update_order_review', array( __CLASS__, 'add_shipping_method_post_data' ) ); @@ -888,7 +886,7 @@ class WC_Subscriptions_Cart { } // Skip checks if cart contains subscription switches or automatic payments are disabled. - if ( false !== WC_Subscriptions_Switcher::cart_contains_switches() || 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) { + if ( false !== WC_Subscriptions_Switcher::cart_contains_switches( 'any' ) || 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) { return $needs_payment; } @@ -1043,8 +1041,7 @@ class WC_Subscriptions_Cart { $cart_key = ''; $product = $cart_item['data']; - $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 ); + $renewal_time = ! empty( $renewal_time ) ? $renewal_time : WC_Subscriptions_Product::get_first_renewal_payment_time( $product ); $interval = WC_Subscriptions_Product::get_interval( $product ); $period = WC_Subscriptions_Product::get_period( $product ); $length = WC_Subscriptions_Product::get_length( $product ); @@ -1090,22 +1087,6 @@ class WC_Subscriptions_Cart { return apply_filters( 'woocommerce_subscriptions_recurring_cart_key', $cart_key, $cart_item ); } - /** - * 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_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) { - - 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; - } - - return $is_valid; - } - /** * When calculating shipping for recurring carts, return a revised list of shipping methods that apply to this recurring cart. * @@ -1401,8 +1382,40 @@ class WC_Subscriptions_Cart { WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); } + /** + * Removes all subscription products from the shopping cart. + * + * @since 2.6.0 + */ + public static function remove_subscriptions_from_cart() { + + foreach ( WC()->cart->cart_contents as $cart_item_key => $cart_item ) { + if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { + WC()->cart->set_quantity( $cart_item_key, 0 ); + } + } + } + /* Deprecated */ + /** + * Don't allow new subscription products to be added to the cart if it contains a subscription renewal already. + * + * @deprecated 2.6.0 + * @since 2.0 + */ + public static function check_valid_add_to_cart( $is_valid, $product_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) { + _deprecated_function( __METHOD__, '2.6.0', 'WC_Subscriptions_Cart_Validator::check_valid_add_to_cart' ); + + 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; + } + + return $is_valid; + } + /** * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string. diff --git a/includes/class-wc-subscriptions-change-payment-gateway.php b/includes/class-wc-subscriptions-change-payment-gateway.php index 5eb9a08..2a4286b 100755 --- a/includes/class-wc-subscriptions-change-payment-gateway.php +++ b/includes/class-wc-subscriptions-change-payment-gateway.php @@ -315,9 +315,9 @@ class WC_Subscriptions_Change_Payment_Gateway { if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) { if ( $subscription->has_payment_gateway() && wc_get_payment_gateway_by_order( $subscription )->supports( 'subscriptions' ) ) { - $action_name = _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ); + $action_name = _x( 'Change payment', 'label on button, imperative', 'woocommerce-subscriptions' ); } else { - $action_name = _x( 'Add Payment', 'label on button, imperative', 'woocommerce-subscriptions' ); + $action_name = _x( 'Add payment', 'label on button, imperative', 'woocommerce-subscriptions' ); } $actions['change_payment_method'] = array( @@ -742,9 +742,9 @@ class WC_Subscriptions_Change_Payment_Gateway { } if ( $subscription->has_payment_gateway() ) { - $title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ); + $title = _x( 'Change payment method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ); } else { - $title = _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ); + $title = _x( 'Add payment method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ); } return $title; @@ -779,12 +779,12 @@ class WC_Subscriptions_Change_Payment_Gateway { if ( $subscription->has_payment_gateway() ) { $crumbs[3] = array( - _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), + _x( 'Change payment method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), '', ); } else { $crumbs[3] = array( - _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ), + _x( 'Add payment method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ), '', ); } diff --git a/includes/class-wc-subscriptions-checkout.php b/includes/class-wc-subscriptions-checkout.php index 1e0bd94..7d1af9a 100755 --- a/includes/class-wc-subscriptions-checkout.php +++ b/includes/class-wc-subscriptions-checkout.php @@ -21,25 +21,28 @@ class WC_Subscriptions_Checkout { public static function init() { // We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees - add_action( 'woocommerce_checkout_order_processed', __CLASS__ . '::process_checkout', 100, 2 ); + add_action( 'woocommerce_checkout_order_processed', array( __CLASS__, 'process_checkout' ), 100, 2 ); // Make sure users can register on checkout (before any other hooks before checkout) - add_action( 'woocommerce_before_checkout_form', __CLASS__ . '::make_checkout_registration_possible', -1 ); + add_action( 'woocommerce_before_checkout_form', array( __CLASS__, 'make_checkout_registration_possible' ), -1 ); // Display account fields as required - add_action( 'woocommerce_checkout_fields', __CLASS__ . '::make_checkout_account_fields_required', 10 ); + add_action( 'woocommerce_checkout_fields', array( __CLASS__, 'make_checkout_account_fields_required' ), 10 ); // Restore the settings after switching them for the checkout form - add_action( 'woocommerce_after_checkout_form', __CLASS__ . '::restore_checkout_registration_settings', 100 ); + add_action( 'woocommerce_after_checkout_form', array( __CLASS__, 'restore_checkout_registration_settings' ), 100 ); // Some callbacks need to hooked after WC has loaded. add_action( 'woocommerce_loaded', array( __CLASS__, 'attach_dependant_hooks' ) ); // Force registration during checkout process - add_action( 'woocommerce_before_checkout_process', __CLASS__ . '::force_registration_during_checkout', 10 ); + add_action( 'woocommerce_before_checkout_process', array( __CLASS__, 'force_registration_during_checkout' ), 10 ); // When a line item is added to a subscription on checkout, ensure the backorder data added by WC is removed - add_action( 'woocommerce_checkout_create_order_line_item', __CLASS__ . '::remove_backorder_meta_from_subscription_line_item', 10, 4 ); + add_action( 'woocommerce_checkout_create_order_line_item', array( __CLASS__, 'remove_backorder_meta_from_subscription_line_item' ), 10, 4 ); + + // When a line item is added to a subscription, ensure the __has_trial meta data is added if applicable. + add_action( 'woocommerce_checkout_create_order_line_item', array( __CLASS__, 'maybe_add_free_trial_item_meta' ), 10, 4 ); } /** @@ -326,6 +329,21 @@ class WC_Subscriptions_Checkout { } } + /** + * Set a flag in subscription line item meta if the line item has a free trial. + * + * @param WC_Order_Item_Product $item The item being added to the subscription. + * @param string $cart_item_key The item's cart item key. + * @param array $cart_item The cart item. + * @param WC_Subscription $subscription The subscription the item is being added to. + * @since 2.6.0 + */ + public static function maybe_add_free_trial_item_meta( $item, $cart_item_key, $cart_item, $subscription ) { + if ( wcs_is_subscription( $subscription ) && WC_Subscriptions_Product::get_trial_length( $item->get_product() ) > 0 ) { + $item->update_meta_data( '_has_trial', 'true' ); + } + } + /** * Add a cart item to a subscription. * diff --git a/includes/class-wc-subscriptions-coupon.php b/includes/class-wc-subscriptions-coupon.php index 6d44da5..bcfcd8c 100755 --- a/includes/class-wc-subscriptions-coupon.php +++ b/includes/class-wc-subscriptions-coupon.php @@ -584,7 +584,7 @@ class WC_Subscriptions_Coupon { if ( 'recurring_total' === $calculation_type ) { // Special handling for a single payment coupon. - if ( 1 === self::get_coupon_limit( $coupon_code ) && 0 < $cart->get_coupon_discount_amount( $coupon_code ) ) { + if ( 1 === self::get_coupon_limit( $coupon_code ) && 0 < WC()->cart->get_coupon_discount_amount( $coupon_code ) ) { $cart->remove_coupon( $coupon_code ); } @@ -750,8 +750,8 @@ class WC_Subscriptions_Coupon { */ public static function order_has_limited_recurring_coupon( $order ) { $has_coupon = false; - $coupons = $order->get_used_coupons(); - foreach ( $coupons as $code ) { + + foreach ( wcs_get_used_coupon_codes( $order ) as $code ) { if ( self::coupon_is_limited( $code ) ) { $has_coupon = true; break; @@ -981,7 +981,7 @@ class WC_Subscriptions_Coupon { */ public static function check_coupon_usages( $subscription ) { // If there aren't any coupons, there's nothing to do. - $coupons = $subscription->get_used_coupons(); + $coupons = wcs_get_used_coupon_codes( $subscription ); if ( empty( $coupons ) ) { return; } diff --git a/includes/class-wc-subscriptions-manager.php b/includes/class-wc-subscriptions-manager.php index 9fbeda1..89482a6 100755 --- a/includes/class-wc-subscriptions-manager.php +++ b/includes/class-wc-subscriptions-manager.php @@ -248,7 +248,7 @@ class WC_Subscriptions_Manager { * @since 1.0 */ public static function process_subscription_payments_on_order( $order, $product_id = '' ) { - + wcs_deprecated_function( __METHOD__, '2.6.0' ); $subscriptions = wcs_get_subscriptions_for_order( $order ); if ( ! empty( $subscriptions ) ) { @@ -262,7 +262,7 @@ class WC_Subscriptions_Manager { } /** - * This function should be called whenever a subscription payment has failed. + * This function should be called whenever a subscription payment has failed on a parent order. * * The function is a convenience wrapper for @see self::process_subscription_payment_failure(), so if calling that * function directly, do not call this function also. @@ -271,7 +271,7 @@ class WC_Subscriptions_Manager { * @since 1.0 */ public static function process_subscription_payment_failure_on_order( $order, $product_id = '' ) { - + wcs_deprecated_function( __METHOD__, '2.6.0' ); $subscriptions = wcs_get_subscriptions_for_order( $order ); if ( ! empty( $subscriptions ) ) { @@ -823,6 +823,7 @@ class WC_Subscriptions_Manager { /** @var WC_Subscription[] $subscriptions */ $subscriptions = wcs_get_subscriptions_for_order( $post_id, array( 'subscription_status' => array( 'any', 'trash' ), + 'order_type' => 'parent', ) ); foreach ( $subscriptions as $subscription ) { wp_delete_post( $subscription->get_id() ); @@ -1204,8 +1205,8 @@ class WC_Subscriptions_Manager { * @deprecated 2.0 */ public static function get_subscriptions_completed_payment_count( $subscription_key ) { - _deprecated_function( __METHOD__, '2.0', 'WC_Subscription::get_completed_payment_count()' ); - return apply_filters( 'woocommerce_subscription_completed_payment_count', wcs_get_subscription_from_key( $subscription_key )->get_completed_payment_count(), $subscription_key ); + _deprecated_function( __METHOD__, '2.0', 'WC_Subscription::get_payment_count()' ); + return apply_filters( 'woocommerce_subscription_completed_payment_count', wcs_get_subscription_from_key( $subscription_key )->get_payment_count(), $subscription_key ); } /** diff --git a/includes/class-wc-subscriptions-order.php b/includes/class-wc-subscriptions-order.php index ce4ae37..f7f8d79 100755 --- a/includes/class-wc-subscriptions-order.php +++ b/includes/class-wc-subscriptions-order.php @@ -2106,7 +2106,7 @@ class WC_Subscriptions_Order { foreach ( $subscriptions as $subscription_id => $subscription ) { // No payments have been recorded yet - if ( 0 == $subscription->get_completed_payment_count() ) { + if ( 0 == $subscription->get_payment_count() ) { $subscription->update_dates( array( 'date_created' => current_time( 'mysql', true ) ) ); $subscription->payment_complete(); } diff --git a/includes/class-wc-subscriptions-product.php b/includes/class-wc-subscriptions-product.php index 83160f4..316f56c 100755 --- a/includes/class-wc-subscriptions-product.php +++ b/includes/class-wc-subscriptions-product.php @@ -88,7 +88,7 @@ class WC_Subscriptions_Product { } /** - * Override the WooCommerce "Add to Cart" text with "Sign Up Now". + * Override the WooCommerce "Add to cart" text with "Sign up now". * * @since 1.0 */ @@ -96,7 +96,7 @@ class WC_Subscriptions_Product { global $product; if ( self::is_subscription( $product ) || in_array( $product_type, array( 'subscription', 'subscription-variation' ) ) ) { - $button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) ); + $button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) ); } return $button_text; @@ -524,15 +524,15 @@ class WC_Subscriptions_Product { * Takes a subscription product's ID and returns the date on which the first renewal payment will be processed * based on the subscription's length and calculated from either the $from_date if specified, or the current date/time. * - * @param int $product_id The product/post ID of a subscription product + * @param int|WC_Product $product The product instance or product/post ID of a subscription product. * @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date, or empty (default), which will use today's date/time. * @param string $type The return format for the date, either 'mysql', or 'timezone'. Default 'mysql'. * @param string $timezone The timezone for the returned date, either 'site' for the site's timezone, or 'gmt'. Default, 'site'. * @since 2.0 */ - public static function get_first_renewal_payment_date( $product_id, $from_date = '', $timezone = 'gmt' ) { + public static function get_first_renewal_payment_date( $product, $from_date = '', $timezone = 'gmt' ) { - $first_renewal_timestamp = self::get_first_renewal_payment_time( $product_id, $from_date, $timezone ); + $first_renewal_timestamp = self::get_first_renewal_payment_time( $product, $from_date, $timezone ); if ( $first_renewal_timestamp > 0 ) { $first_renewal_date = gmdate( 'Y-m-d H:i:s', $first_renewal_timestamp ); @@ -540,30 +540,30 @@ class WC_Subscriptions_Product { $first_renewal_date = 0; } - return apply_filters( 'woocommerce_subscriptions_product_first_renewal_payment_date', $first_renewal_date, $product_id, $from_date, $timezone ); + return apply_filters( 'woocommerce_subscriptions_product_first_renewal_payment_date', $first_renewal_date, $product, $from_date, $timezone ); } /** * Takes a subscription product's ID and returns the date on which the first renewal payment will be processed * based on the subscription's length and calculated from either the $from_date if specified, or the current date/time. * - * @param int $product_id The product/post ID of a subscription product + * @param int|WC_Product $product The product instance or product/post ID of a subscription product. * @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date, or empty (default), which will use today's date/time. * @param string $type The return format for the date, either 'mysql', or 'timezone'. Default 'mysql'. * @param string $timezone The timezone for the returned date, either 'site' for the site's timezone, or 'gmt'. Default, 'site'. * @since 2.0 */ - public static function get_first_renewal_payment_time( $product_id, $from_date = '', $timezone = 'gmt' ) { + public static function get_first_renewal_payment_time( $product, $from_date = '', $timezone = 'gmt' ) { - if ( ! self::is_subscription( $product_id ) ) { + if ( ! self::is_subscription( $product ) ) { return 0; } $from_date_param = $from_date; - $billing_interval = self::get_interval( $product_id ); - $billing_length = self::get_length( $product_id ); - $trial_length = self::get_trial_length( $product_id ); + $billing_interval = self::get_interval( $product ); + $billing_length = self::get_length( $product ); + $trial_length = self::get_trial_length( $product ); if ( $billing_interval !== $billing_length || $trial_length > 0 ) { @@ -574,34 +574,37 @@ class WC_Subscriptions_Product { // If the subscription has a free trial period, the first renewal payment date is the same as the expiration of the free trial if ( $trial_length > 0 ) { - $first_renewal_timestamp = wcs_date_to_time( self::get_trial_expiration_date( $product_id, $from_date ) ); + $first_renewal_timestamp = wcs_date_to_time( self::get_trial_expiration_date( $product, $from_date ) ); } else { - $first_renewal_timestamp = wcs_add_time( $billing_interval, self::get_period( $product_id ), wcs_date_to_time( $from_date ) ); + $site_time_offset = (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); - if ( 'site' == $timezone ) { - $first_renewal_timestamp += ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); + // As wcs_add_time() calls wcs_add_months() which checks for last day of month, pass the site time + $first_renewal_timestamp = wcs_add_time( $billing_interval, self::get_period( $product ), wcs_date_to_time( $from_date ) + $site_time_offset ); + + if ( 'site' !== $timezone ) { + $first_renewal_timestamp -= $site_time_offset; } } } else { $first_renewal_timestamp = 0; } - return apply_filters( 'woocommerce_subscriptions_product_first_renewal_payment_time', $first_renewal_timestamp, $product_id, $from_date_param, $timezone ); + return apply_filters( 'woocommerce_subscriptions_product_first_renewal_payment_time', $first_renewal_timestamp, $product, $from_date_param, $timezone ); } /** * Takes a subscription product's ID and returns the date on which the subscription product will expire, * based on the subscription's length and calculated from either the $from_date if specified, or the current date/time. * - * @param mixed $product_id The product/post ID of the subscription + * @param int|WC_Product $product The product instance or product/post ID of a subscription product. * @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date, or empty (default), which will use today's date/time. * @since 1.0 */ - public static function get_expiration_date( $product_id, $from_date = '' ) { + public static function get_expiration_date( $product, $from_date = '' ) { - $subscription_length = self::get_length( $product_id ); + $subscription_length = self::get_length( $product ); if ( $subscription_length > 0 ) { @@ -609,11 +612,11 @@ class WC_Subscriptions_Product { $from_date = gmdate( 'Y-m-d H:i:s' ); } - if ( self::get_trial_length( $product_id ) > 0 ) { - $from_date = self::get_trial_expiration_date( $product_id, $from_date ); + if ( self::get_trial_length( $product ) > 0 ) { + $from_date = self::get_trial_expiration_date( $product, $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 ) ) ); + $expiration_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription_length, self::get_period( $product ), wcs_date_to_time( $from_date ) ) ); } else { @@ -621,7 +624,7 @@ class WC_Subscriptions_Product { } - return apply_filters( 'woocommerce_subscriptions_product_expiration_date', $expiration_date, $product_id, $from_date ); + return apply_filters( 'woocommerce_subscriptions_product_expiration_date', $expiration_date, $product, $from_date ); } /** @@ -629,13 +632,13 @@ class WC_Subscriptions_Product { * based on the subscription's trial length and calculated from either the $from_date if specified, * or the current date/time. * - * @param int $product_id The product/post ID of the subscription + * @param int|WC_Product $product The product instance or product/post ID of a subscription product. * @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date (in UTC timezone), or empty (default), which will use today's date/time (in UTC timezone). * @since 1.0 */ - public static function get_trial_expiration_date( $product_id, $from_date = '' ) { + public static function get_trial_expiration_date( $product, $from_date = '' ) { - $trial_length = self::get_trial_length( $product_id ); + $trial_length = self::get_trial_length( $product ); if ( $trial_length > 0 ) { @@ -643,7 +646,7 @@ class WC_Subscriptions_Product { $from_date = gmdate( 'Y-m-d H:i:s' ); } - $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 ) ) ); + $trial_expiration_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $trial_length, self::get_trial_period( $product ), wcs_date_to_time( $from_date ) ) ); } else { @@ -651,7 +654,7 @@ class WC_Subscriptions_Product { } - return apply_filters( 'woocommerce_subscriptions_product_trial_expiration_date', $trial_expiration_date, $product_id, $from_date ); + return apply_filters( 'woocommerce_subscriptions_product_trial_expiration_date', $trial_expiration_date, $product, $from_date ); } /** diff --git a/includes/class-wc-subscriptions-switcher.php b/includes/class-wc-subscriptions-switcher.php index 17e29d9..1827cc3 100755 --- a/includes/class-wc-subscriptions-switcher.php +++ b/includes/class-wc-subscriptions-switcher.php @@ -10,6 +10,14 @@ */ class WC_Subscriptions_Switcher { + /** + * The last known switch total calculator instance which was calculated. + * + * @since 2.6.1 + * @var WCS_Switch_Totals_Calculator + */ + protected static $switch_totals_calculator; + /** * Bootstraps the class and hooks required actions & filters. * @@ -18,109 +26,118 @@ class WC_Subscriptions_Switcher { public static function init() { // Attach hooks which depend on WooCommerce constants - add_action( 'woocommerce_loaded', __CLASS__ . '::attach_dependant_hooks' ); + add_action( 'woocommerce_loaded', array( __CLASS__, 'attach_dependant_hooks' ) ); // Check if the current request is for switching a subscription and if so, start he switching process - add_action( 'template_redirect', __CLASS__ . '::subscription_switch_handler', 100 ); + add_action( 'template_redirect', array( __CLASS__, 'subscription_switch_handler' ), 100 ); // Pass in the filter switch to the group items - add_filter( 'woocommerce_grouped_product_list_link', __CLASS__ . '::add_switch_query_arg_grouped', 12 ); - add_filter( 'post_type_link', __CLASS__ . '::add_switch_query_arg_post_link', 12, 2 ); + add_filter( 'woocommerce_grouped_product_list_link', array( __CLASS__, 'add_switch_query_arg_grouped' ), 12 ); + add_filter( 'post_type_link', array( __CLASS__, 'add_switch_query_arg_post_link' ), 12, 2 ); // Add the settings to control whether Switching is enabled and how it will behave - add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::add_settings' ); + add_filter( 'woocommerce_subscription_settings', array( __CLASS__, 'add_settings' ) ); + + // Render "wcs_switching_options" field + add_action( 'woocommerce_admin_field_wcs_switching_options', __CLASS__ . '::switching_options_field_html' ); // Add the "Switch" button to the View Subscription table - add_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 ); + add_action( 'woocommerce_order_item_meta_end', array( __CLASS__, 'print_switch_link' ), 10, 3 ); // We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees - add_action( 'woocommerce_checkout_order_processed', __CLASS__ . '::process_checkout', 50, 2 ); + add_action( 'woocommerce_checkout_order_processed', array( __CLASS__, 'process_checkout' ), 50, 2 ); // When creating an order, add meta if it's for switching a subscription - add_action( 'woocommerce_checkout_update_order_meta', __CLASS__ . '::add_order_meta', 10, 2 ); + add_action( 'woocommerce_checkout_update_order_meta', array( __CLASS__, 'add_order_meta' ), 10, 2 ); // Add a renewal orders section to the Related Orders meta box - add_action( 'woocommerce_subscriptions_related_orders_meta_box_rows', __CLASS__ . '::switch_order_meta_box_rows', 10 ); + add_action( 'woocommerce_subscriptions_related_orders_meta_box_rows', array( __CLASS__, 'switch_order_meta_box_rows' ), 10 ); // Don't allow switching to the same product - add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::validate_switch_request', 10, 4 ); + add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'validate_switch_request' ), 10, 4 ); // Record subscription switching in the cart - add_filter( 'woocommerce_add_cart_item_data', __CLASS__ . '::set_switch_details_in_cart', 10, 3 ); + add_filter( 'woocommerce_add_cart_item_data', array( __CLASS__, 'set_switch_details_in_cart' ), 10, 3 ); + + add_action( 'woocommerce_add_to_cart', array( __CLASS__, 'trigger_switch_added_to_cart_hook' ), 15, 6 ); + + // Retain coupons if required + add_action( 'woocommerce_subscriptions_switch_added_to_cart', array( __CLASS__, 'retain_coupons' ), 15, 1 ); // Make sure the 'switch_subscription' cart item data persists - add_filter( 'woocommerce_get_cart_item_from_session', __CLASS__ . '::get_cart_from_session', 10, 3 ); + add_filter( 'woocommerce_get_cart_item_from_session', array( __CLASS__, 'get_cart_from_session' ), 10, 3 ); // Set totals for subscription switch orders (needs to be hooked just before WC_Subscriptions_Cart::calculate_subscription_totals()) - add_action( 'woocommerce_before_calculate_totals', __CLASS__ . '::calculate_prorated_totals', 99, 1 ); + add_action( 'woocommerce_before_calculate_totals', array( __CLASS__, 'calculate_prorated_totals' ), 99, 1 ); // 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 ); + add_filter( 'woocommerce_subscriptions_product_price_string_inclusions', array( __CLASS__, 'customise_product_string_inclusions' ), 12, 2 ); // Don't carry switch meta data to renewal orders - add_filter( 'wcs_renewal_order_meta_query', __CLASS__ . '::remove_renewal_order_meta_query', 10 ); + add_filter( 'wcs_renewal_order_meta_query', array( __CLASS__, 'remove_renewal_order_meta_query' ), 10 ); // Don't carry switch meta data to renewal orders - add_filter( 'woocommerce_subscriptions_recurring_cart_key', __CLASS__ . '::get_recurring_cart_key', 10, 2 ); + add_filter( 'woocommerce_subscriptions_recurring_cart_key', array( __CLASS__, 'get_recurring_cart_key' ), 10, 2 ); // Make sure the first renewal date takes into account any prorated length of time for upgrades/downgrades - add_filter( 'wcs_recurring_cart_next_payment_date', __CLASS__ . '::recurring_cart_next_payment_date', 100, 2 ); + add_filter( 'wcs_recurring_cart_next_payment_date', array( __CLASS__, 'recurring_cart_next_payment_date' ), 100, 2 ); // Make sure the new end date starts from the end of the time that has already paid for - add_filter( 'wcs_recurring_cart_end_date', __CLASS__ . '::recurring_cart_end_date', 100, 3 ); + add_filter( 'wcs_recurring_cart_end_date', array( __CLASS__, 'recurring_cart_end_date' ), 100, 3 ); // Make sure the switch process persists when having to choose product addons - add_action( 'addons_add_to_cart_url', __CLASS__ . '::addons_add_to_cart_url', 10 ); + add_action( 'addons_add_to_cart_url', array( __CLASS__, 'addons_add_to_cart_url' ), 10 ); // Make sure the switch process persists when having to choose product addons - add_filter( 'woocommerce_hidden_order_itemmeta', __CLASS__ . '::hidden_order_itemmeta', 10 ); + add_filter( 'woocommerce_hidden_order_itemmeta', array( __CLASS__, 'hidden_order_itemmeta' ), 10 ); // Add/remove the print switch link filters when printing HTML/plain subscription emails - add_action( 'woocommerce_email_before_subscription_table', __CLASS__ . '::remove_print_switch_link' ); - add_filter( 'woocommerce_email_order_items_table', __CLASS__ . '::add_print_switch_link' ); + add_action( 'woocommerce_email_before_subscription_table', array( __CLASS__, 'remove_print_switch_link' ) ); + add_filter( 'woocommerce_email_order_items_table', array( __CLASS__, 'add_print_switch_link' ) ); // Make sure sign-up fees paid on switch orders are accounted for in an items sign-up fee - add_filter( 'woocommerce_subscription_items_sign_up_fee', __CLASS__ . '::subscription_items_sign_up_fee', 10, 4 ); + add_filter( 'woocommerce_subscription_items_sign_up_fee', array( __CLASS__, 'subscription_items_sign_up_fee' ), 10, 4 ); // 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 ); + add_filter( 'woocommerce_cart_item_subtotal', array( __CLASS__, 'add_cart_item_switch_direction' ), 10, 3 ); // 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 ); + add_action( 'woocommerce_subscriptions_switch_completed', array( __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 ); + add_action( 'woocommerce_subscriptions_switched_item', array( __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 ); + add_action( 'woocommerce_order_status_changed', array( __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 ); + add_filter( 'woocommerce_subscriptions_calculated_total', array( __CLASS__, 'set_force_payment_flag_in_cart' ), 10, 1 ); // Require payment when switching from a $0 / period subscription to a non-zero subscription to process automatic payments - add_filter( 'woocommerce_cart_needs_payment', __CLASS__ . '::cart_needs_payment' , 50, 2 ); + add_filter( 'woocommerce_cart_needs_payment', array( __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_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::maybe_set_payment_method_after_switch' , 10, 1 ); + add_action( 'woocommerce_subscriptions_switch_completed', array( __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 ); + add_filter( 'woocommerce_order_item_quantity', array( __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 ); - add_action( 'woocommerce_subscription_cart_before_grouping', __CLASS__ . '::maybe_unset_free_trial' ); - add_action( 'woocommerce_subscription_cart_after_grouping', __CLASS__ . '::maybe_set_free_trial' ); - add_action( 'wcs_recurring_cart_start_date', __CLASS__ . '::maybe_unset_free_trial', 0, 1 ); - add_action( 'wcs_recurring_cart_end_date', __CLASS__ . '::maybe_set_free_trial', 100, 1 ); - add_filter( 'woocommerce_subscriptions_calculated_total', __CLASS__ . '::maybe_unset_free_trial', 10000, 1 ); - add_action( 'woocommerce_cart_totals_before_shipping', __CLASS__ . '::maybe_set_free_trial' ); - add_action( 'woocommerce_cart_totals_after_shipping', __CLASS__ . '::maybe_unset_free_trial' ); - add_action( 'woocommerce_review_order_before_shipping', __CLASS__ . '::maybe_set_free_trial' ); - add_action( 'woocommerce_review_order_after_shipping', __CLASS__ . '::maybe_unset_free_trial' ); + add_filter( 'woocommerce_before_calculate_totals', array( __CLASS__, 'maybe_set_free_trial' ), 100, 1 ); + add_action( 'woocommerce_subscription_cart_before_grouping', array( __CLASS__, 'maybe_unset_free_trial' ) ); + add_action( 'woocommerce_subscription_cart_after_grouping', array( __CLASS__, 'maybe_set_free_trial' ) ); + add_action( 'wcs_recurring_cart_start_date', array( __CLASS__, 'maybe_unset_free_trial' ), 0, 1 ); + add_action( 'wcs_recurring_cart_end_date', array( __CLASS__, 'maybe_set_free_trial' ), 100, 1 ); + add_filter( 'woocommerce_subscriptions_calculated_total', array( __CLASS__, 'maybe_unset_free_trial' ), 10000, 1 ); + add_action( 'woocommerce_cart_totals_before_shipping', array( __CLASS__, 'maybe_set_free_trial' ) ); + add_action( 'woocommerce_cart_totals_after_shipping', array( __CLASS__, 'maybe_unset_free_trial' ) ); + add_action( 'woocommerce_review_order_before_shipping', array( __CLASS__, 'maybe_set_free_trial' ) ); + add_action( 'woocommerce_review_order_after_shipping', array( __CLASS__, 'maybe_unset_free_trial' ) ); // Grant download permissions after the switch is complete. - add_action( 'woocommerce_grant_product_download_permissions', __CLASS__ . '::delay_granting_download_permissions', 9, 1 ); - add_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::grant_download_permissions', 9, 1 ); + add_action( 'woocommerce_grant_product_download_permissions', array( __CLASS__, 'delay_granting_download_permissions' ), 9, 1 ); + add_action( 'woocommerce_subscriptions_switch_completed', array( __CLASS__, 'grant_download_permissions' ), 9, 1 ); + add_action( 'woocommerce_subscription_checkout_switch_order_processed', array( __CLASS__, 'log_switches' ) ); } /** @@ -133,15 +150,15 @@ class WC_Subscriptions_Switcher { if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { // For order items created as part of a switch, keep a record of the prorated amounts - add_action( 'woocommerce_add_order_item_meta', __CLASS__ . '::add_order_item_meta', 10, 3 ); + add_action( 'woocommerce_add_order_item_meta', array( __CLASS__, 'add_order_item_meta' ), 10, 3 ); // For subscription items created as part of a switch, keep a record of the relationship between the items - add_action( 'woocommerce_add_subscription_item_meta', __CLASS__ . '::set_subscription_item_meta', 50, 3 ); + add_action( 'woocommerce_add_subscription_item_meta', array( __CLASS__, 'set_subscription_item_meta' ), 50, 3 ); } else { // For order items created as part of a switch, keep a record of the prorated amounts - add_action( 'woocommerce_checkout_create_order_line_item', __CLASS__ . '::add_line_item_meta', 10, 4 ); + add_action( 'woocommerce_checkout_create_order_line_item', array( __CLASS__, 'add_line_item_meta' ), 10, 4 ); } } @@ -176,16 +193,34 @@ class WC_Subscriptions_Switcher { wc_add_notice( $switch_message, 'notice' ); } - } elseif ( ( is_cart() || is_checkout() ) && ! is_order_received_page() && false !== ( $switch_items = self::cart_contains_switches() ) ) { + } elseif ( ( is_cart() || is_checkout() ) && ! is_order_received_page() && false !== ( $switch_items = self::cart_contains_switches( 'any' ) ) ) { $removed_item_count = 0; foreach ( $switch_items as $cart_item_key => $switch_item ) { - $subscription = wcs_get_subscription( $switch_item['subscription_id'] ); - $line_item = wcs_get_order_item( $switch_item['item_id'], $subscription ); + $subscription = wcs_get_subscription( $switch_item['subscription_id'] ); + $is_valid_item = is_object( $subscription ); - if ( ! is_object( $subscription ) || empty( $line_item ) || ! self::can_item_be_switched_by_user( $line_item, $subscription ) ) { + if ( $is_valid_item ) { + if ( empty( $switch_item['item_id'] ) ) { + + $item = isset( WC()->cart->cart_contents[ $cart_item_key ] ) ? WC()->cart->cart_contents[ $cart_item_key ] : false; + + if ( empty( $item ) || ! self::can_item_be_added_by_user( $item, $subscription ) ) { + $is_valid_item = false; + } + } else { + + $item = wcs_get_order_item( $switch_item['item_id'], $subscription ); + + if ( empty( $item ) || ! self::can_item_be_switched_by_user( $item, $subscription ) ) { + $is_valid_item = false; + } + } + } + + if ( ! $is_valid_item ) { WC()->cart->remove_cart_item( $cart_item_key ); $removed_item_count++; } @@ -346,20 +381,8 @@ class WC_Subscriptions_Switcher { ), array( - 'name' => __( 'Allow Switching', 'woocommerce-subscriptions' ), - 'desc' => __( 'Allow subscribers to switch between subscriptions combined in a grouped product, different variations of a Variable subscription or don\'t allow switching.', 'woocommerce-subscriptions' ), - 'tip' => '', - 'id' => WC_Subscriptions_Admin::$option_prefix . '_allow_switching', - 'css' => 'min-width:150px;', - 'default' => 'no', - 'type' => 'select', - 'options' => array( - 'no' => _x( 'Never', 'when to allow a setting', 'woocommerce-subscriptions' ), - 'variable' => _x( 'Between Subscription Variations', 'when to allow switching', 'woocommerce-subscriptions' ), - 'grouped' => _x( 'Between Grouped Subscriptions', 'when to allow switching', 'woocommerce-subscriptions' ), - 'variable_grouped' => _x( 'Between Both Variations & Grouped Subscriptions', 'when to allow switching', 'woocommerce-subscriptions' ), - ), - 'desc_tip' => true, + 'type' => 'wcs_switching_options', + 'id' => WC_Subscriptions_Admin::$option_prefix . '_allow_switching', ), array( @@ -429,6 +452,72 @@ class WC_Subscriptions_Switcher { return $settings; } + /** + * Render the wcs_switching_options setting field. + * + * @since 2.6.0 + * @param array $data + */ + public static function switching_options_field_html( $data ) { + + // Calculate current checked options + $allow_switching = get_option( $data['id'], 'no' ); + // Sanity check + if ( ! in_array( $allow_switching, array( 'no', 'variable', 'grouped', 'variable_grouped' ) ) ) { + $allow_switching = 'no'; + } + + $allow_switching_variable_checked = strpos( $allow_switching, 'variable' ) !== false; + $allow_switching_grouped_checked = strpos( $allow_switching, 'grouped' ) !== false; + ?> + + + + + +
+ + + %s', checked( $value, 'yes', false ), esc_attr( $name ), esc_html( $label ) ); + } + ?> +
+ + + has_status( 'active' ) && 0 !== $subscription->get_date( 'last_order_date_created' ) ) { @@ -537,35 +640,104 @@ class WC_Subscriptions_Switcher { } if ( $is_product_switchable && $is_subscription_switchable && $can_subscription_be_updated ) { - $item_can_be_switch = true; + $is_action_allowed = true; } else { - $item_can_be_switch = false; + $is_action_allowed = false; } - return apply_filters( 'woocommerce_subscriptions_can_item_be_switched', $item_can_be_switch, $item, $subscription ); + return $is_action_allowed; } /** - * Check if a given item on a subscription can be switched by a given user. + * Check if a cart item can be added to a subscription. * - * @param array $item An order item on the subscription + * The subscription must be active and use manual renewals or use a payment method which supports cancellation. + * + * @since 2.6.0 + * + * @param array $item A cart to add to a subscription. + * @param WC_Subscription $subscription An instance of WC_Subscription + */ + public static function can_item_be_added( $item, $subscription = null ) { + return apply_filters( 'woocommerce_subscriptions_can_item_be_added', self::is_action_allowed( 'add', $item, $subscription ), $item, $subscription ); + } + + /** + * Check if a given item on a subscription can be switched. + * + * 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 and use manual renewals or use a payment method which supports cancellation. + * + * @param WC_Order_Item_Product $item An order item on the subscription to switch, or cart item to add. * @param WC_Subscription $subscription An instance of WC_Subscription - * @param int $user_id (optional) The ID of a user. Defaults to currently logged in user. * @since 2.0 */ - public static function can_item_be_switched_by_user( $item, $subscription, $user_id = 0 ) { + public static function can_item_be_switched( $item, $subscription = null ) { + return apply_filters( 'woocommerce_subscriptions_can_item_be_switched', self::is_action_allowed( 'switch', $item, $subscription ), $item, $subscription ); + } + + /** + * Checks if a user can perform a cart item "add" or order item "switch" action, given a subscription. + * + * @since 2.6.0 + * + * @param string $action An action to perform with the item ('add' or 'switch'). + * @param WC_Order_Item_Product $item An order item to switch, or cart item to add. + * @param WC_Subscription $subscription An instance of WC_Subscription. + * @param int $user_id (optional) The ID of a user. Defaults to currently logged in user. + */ + protected static function can_user_perform_action( $action, $item, $subscription, $user_id = 0 ) { + + if ( ! in_array( $action, array( 'add', 'switch' ) ) ) { + return false; + } + + if ( 'switch' === $action && false === ( $item instanceof WC_Order_Item_Product ) ) { + return false; + } if ( 0 === $user_id ) { $user_id = get_current_user_id(); } - $item_can_be_switched = false; + $can_user_perform_action = false; - if ( user_can( $user_id, 'switch_shop_subscription', $subscription->get_id() ) && self::can_item_be_switched( $item, $subscription ) ) { - $item_can_be_switched = true; + if ( user_can( $user_id, 'switch_shop_subscription', $subscription->get_id() ) ) { + if ( 'switch' === $action ) { + $can_user_perform_action = self::can_item_be_switched( $item, $subscription ); + } elseif ( 'add' === $action ) { + $can_user_perform_action = self::can_item_be_added( $item, $subscription ); + } } - return apply_filters( 'woocommerce_subscriptions_can_item_be_switched_by_user', $item_can_be_switched, $item, $subscription ); + return $can_user_perform_action; + } + + /** + * Check if a given item can be added to a subscription by a given user. + * + * @since 2.6.0 + * + * @param array $item A cart item to add to a subscription. + * @param WC_Subscription $subscription An instance of WC_Subscription. + * @param int $user_id (optional) The ID of a user. Defaults to currently logged in user. + */ + public static function can_item_be_added_by_user( $item, $subscription, $user_id = 0 ) { + return apply_filters( 'woocommerce_subscriptions_can_item_be_added_by_user', self::can_user_perform_action( 'add', $item, $subscription, $user_id ), $item, $subscription ); + } + + /** + * Check if a given item on a subscription can be switched by a given user. + * + * @param WC_Order_Item_Product $item An order item to switch. + * @param WC_Subscription $subscription An instance of WC_Subscription. + * @param int $user_id (optional) The ID of a user. Defaults to currently logged in user. + * @since 2.0 + */ + public static function can_item_be_switched_by_user( $item, $subscription, $user_id = 0 ) { + return apply_filters( 'woocommerce_subscriptions_can_item_be_switched_by_user', self::can_user_perform_action( 'switch', $item, $subscription, $user_id ), $item, $subscription ); } /** @@ -583,7 +755,7 @@ class WC_Subscriptions_Switcher { // delete all the existing subscription switch links before adding new ones WCS_Related_Order_Store::instance()->delete_relations( $order, 'switch' ); - $switches = self::cart_contains_switches(); + $switches = self::cart_contains_switches( 'any' ); if ( false !== $switches ) { @@ -648,20 +820,19 @@ class WC_Subscriptions_Switcher { */ public static function add_line_item_meta( $order_item, $cart_item_key, $cart_item, $order ) { if ( isset( $cart_item['subscription_switch'] ) ) { - if ( $switches = self::cart_contains_switches() ) { - foreach ( $switches as $switch_item_key => $switch_details ) { - if ( $cart_item_key == $switch_item_key ) { + if ( $switches = self::cart_contains_switches( 'any' ) && isset( $switches[ $cart_item_key ] ) ) { - if ( wcs_is_subscription( $order ) ) { - $order_item->add_meta_data( '_switched_subscription_item_id', $switch_details['item_id'] ); - } else { - $sign_up_fee_prorated = WC()->cart->cart_contents[ $cart_item_key ]['data']->get_meta( 'subscription_sign_up_fee_prorated', true ); - $price_prorated = WC()->cart->cart_contents[ $cart_item_key ]['data']->get_meta( 'subscription_price_prorated', true ); + $switch_details = $switches[ $cart_item_key ]; - $order_item->add_meta_data( '_switched_subscription_sign_up_fee_prorated', empty( $sign_up_fee_prorated ) ? 0 : $sign_up_fee_prorated ); - $order_item->add_meta_data( '_switched_subscription_price_prorated', empty( $price_prorated ) ? 0 : $price_prorated ); - } + if ( wcs_is_subscription( $order ) ) { + if ( ! empty( $switch_details['item_id'] ) ) { + $order_item->add_meta_data( '_switched_subscription_item_id', $switch_details['item_id'] ); } + } else { + $sign_up_fee_prorated = WC()->cart->cart_contents[ $cart_item_key ]['data']->get_meta( 'subscription_sign_up_fee_prorated', true ); + $price_prorated = WC()->cart->cart_contents[ $cart_item_key ]['data']->get_meta( 'subscription_price_prorated', true ); + $order_item->add_meta_data( '_switched_subscription_sign_up_fee_prorated', empty( $sign_up_fee_prorated ) ? 0 : $sign_up_fee_prorated ); + $order_item->add_meta_data( '_switched_subscription_price_prorated', empty( $price_prorated ) ? 0 : $price_prorated ); } } } @@ -714,7 +885,6 @@ class WC_Subscriptions_Switcher { } $order = wc_get_order( $order_id ); - $order_items = $order->get_items(); $switch_order_data = array(); try { @@ -727,10 +897,9 @@ class WC_Subscriptions_Switcher { continue; } - $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); - $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); + $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); - // If we haven't calculated a first payment date, fall back to the recurring cart's next payment date + // 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'] = wcs_date_to_time( $recurring_cart->next_payment_date ); } @@ -740,9 +909,16 @@ class WC_Subscriptions_Switcher { $is_different_length = self::has_different_length( $recurring_cart, $subscription ); $is_single_item_subscription = self::is_single_item_subscription( $subscription ); - $switched_item_data = array( 'remove_line_item' => $cart_item['subscription_switch']['item_id'] ); + $switched_item_data = array(); - // If the item is on the same schedule, we can just add it to the new subscription and remove the old item + if ( ! empty( $cart_item['subscription_switch']['item_id'] ) ) { + $switched_item_data['remove_line_item'] = $cart_item['subscription_switch']['item_id']; + $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); + $switch_item = new WCS_Switch_Cart_Item( $cart_item, $subscription, $existing_item ); + $is_switch_with_matching_trials = $switch_item->is_switch_during_trial() && $switch_item->trial_periods_match(); + } + + // 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 @@ -781,7 +957,7 @@ class WC_Subscriptions_Switcher { $subscription->add_item( $item ); - // The subscription is not saved automatically, we need to call 'save' becaused we added an item + // The subscription is not saved automatically, we need to call 'save' because we added an item $subscription->save(); $item_id = $item->get_id(); } @@ -820,12 +996,68 @@ class WC_Subscriptions_Switcher { $updated_dates['end'] = $recurring_cart->end_date; } + // If the switch should maintain the current trial or delete it. + if ( isset( $is_switch_with_matching_trials ) && $is_switch_with_matching_trials ) { + $updated_dates['trial_end'] = $subscription->get_date( 'trial_end' ); + } else { + $updated_dates['trial_end'] = 0; + } + if ( ! empty( $updated_dates ) ) { $subscription->validate_date_updates( $updated_dates ); $switch_order_data[ $subscription->get_id() ]['dates']['update'] = $updated_dates; } } + // If there are coupons in the cart, mark them for pending addition + $new_coupons = array(); + foreach ( $recurring_cart->get_coupons() as $coupon_code => $coupon ) { + $coupon_item = new WC_Subscription_Item_Coupon_Pending_Switch( $coupon_code ); + $coupon_item->set_props( + array( + 'code' => $coupon_code, + 'discount' => $recurring_cart->get_coupon_discount_amount( $coupon_code ), + 'discount_tax' => $recurring_cart->get_coupon_discount_tax_amount( $coupon_code ), + ) + ); + // Avoid storing used_by - it's not needed and can get large. + $coupon_data = $coupon->get_data(); + unset( $coupon_data['used_by'] ); + $coupon_item->add_meta_data( 'coupon_data', $coupon_data ); + + $coupon_item->save(); + do_action( 'woocommerce_checkout_create_order_coupon_item', $coupon_item, $coupon_code, $coupon, $subscription ); + $subscription->add_item( $coupon_item ); + + $new_coupons[] = $coupon_item->get_id(); + } + $subscription->save(); + $switch_order_data[ $subscription->get_id() ]['coupons'] = $new_coupons; + + // If there are fees in the cart, mark them for pending addition + $new_fee_items = array(); + foreach ( $recurring_cart->get_fees() as $fee_key => $fee ) { + $fee_item = new WC_Subscription_Item_Fee_Pending_Switch(); + $fee_item->set_props( array( + 'name' => $fee->name, + 'tax_status' => $fee->taxable, + 'amount' => $fee->amount, + 'total' => $fee->total, + 'tax' => $fee->tax, + 'tax_class' => $fee->tax_class, + 'tax_data' => $fee->tax_data, + ) + ); + + $fee_item->save(); + do_action( 'woocommerce_checkout_create_order_fee_item', $fee_item, $fee_key, $fee, $subscription ); + $subscription->add_item( $fee_item ); + + $new_fee_items[] = $fee_item->get_id(); + } + $subscription->save(); + $switch_order_data[ $subscription->get_id() ]['fee_items'] = $new_fee_items; + // Add the shipping // Keep a record of the current shipping line items so we can flip any new shipping items to a _pending_switch shipping item. $current_shipping_line_items = array_keys( $subscription->get_shipping_methods() ); @@ -873,11 +1105,12 @@ class WC_Subscriptions_Switcher { } } - wcs_set_objects_property( $order, 'subscription_switch_data', $switch_order_data ); - + if ( ! empty( $switch_order_data ) ) { + wcs_set_objects_property( $order, 'subscription_switch_data', $switch_order_data ); + do_action( 'woocommerce_subscription_checkout_switch_order_processed', $order, $switch_order_data ); + } } catch ( Exception $e ) { - // There was an error updating the subscription, roll back and delete pending order for switch - $wpdb->query( 'ROLLBACK' ); + // There was an error updating the subscription, delete pending switch order. wp_delete_post( $order_id, true ); throw $e; } @@ -982,30 +1215,49 @@ class WC_Subscriptions_Switcher { } /** - * Check if the cart includes any items which are to switch an existing subscription's item. + * Check if the cart includes any items which are to switch an existing subscription's contents. * - * @return bool|array Returns all the items that are for a switching or false if none of the items in the cart are a switch request. + * @return bool|array Returns cart items that modify subscription contents, or false if no such items exist. * @since 2.0 + * @param string $item_action Types of items to include ("any", "switch", or "add"). */ - public static function cart_contains_switches() { - + public static function cart_contains_switches( $item_action = 'switch' ) { $subscription_switches = false; if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { return $subscription_switches; } - if ( isset( WC()->cart ) ) { - // We use WC()->cart->cart_contents instead of WC()->cart->get_cart() to prevent recursion caused when get_cart_from_session() too early is called ref: https://github.com/woocommerce/woocommerce/commit/1f3365f2066b1e9d7e84aca7b1d7e89a6989c213 - foreach ( WC()->cart->cart_contents as $cart_item_key => $cart_item ) { - if ( isset( $cart_item['subscription_switch'] ) ) { - if ( wcs_is_subscription( $cart_item['subscription_switch']['subscription_id'] ) ) { - $subscription_switches[ $cart_item_key ] = $cart_item['subscription_switch']; - } else { - WC()->cart->remove_cart_item( $cart_item_key ); - wc_add_notice( __( 'Your cart contained an invalid subscription switch request. It has been removed.', 'woocommerce-subscriptions' ), 'error' ); - } - } + if ( ! isset( WC()->cart ) ) { + return $subscription_switches; + } + + // We use WC()->cart->cart_contents instead of WC()->cart->get_cart() to prevent recursion caused when get_cart_from_session() is called too early ref: https://github.com/woocommerce/woocommerce/commit/1f3365f2066b1e9d7e84aca7b1d7e89a6989c213 + foreach ( WC()->cart->cart_contents as $cart_item_key => $cart_item ) { + // Use WC()->cart->cart_contents instead of '$cart_item' as the item may have been removed by a parent item that manages it inside this loop. + if ( ! isset( WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch'] ) ) { + continue; + } + + if ( ! wcs_is_subscription( $cart_item['subscription_switch']['subscription_id'] ) ) { + WC()->cart->remove_cart_item( $cart_item_key ); + wc_add_notice( __( 'Your cart contained an invalid subscription switch request. It has been removed.', 'woocommerce-subscriptions' ), 'error' ); + continue; + } + + $is_switch = ! empty( $cart_item['subscription_switch']['item_id'] ); + $include_item = false; + + if ( 'any' === $item_action ) { + $include_item = true; + } elseif ( 'switch' === $item_action && $is_switch ) { + $include_item = true; + } elseif ( 'add' === $item_action && ! $is_switch ) { + $include_item = true; + } + + if ( $include_item ) { + $subscription_switches[ $cart_item_key ] = $cart_item['subscription_switch']; } } @@ -1016,7 +1268,7 @@ class WC_Subscriptions_Switcher { * Check if the cart includes any items which are to switch an existing subscription's item. * * @param int|object Either a product ID (not variation ID) or product object - * @return bool True if the cart contains a switch fora given product, or false if it does not. + * @return bool True if the cart contains a switch for a given product, or false if it does not. * @since 2.0 */ public static function cart_contains_switch_for_product( $product ) { @@ -1049,6 +1301,48 @@ class WC_Subscriptions_Switcher { return in_array( $product_id, $switch_product_ids ); } + /** + * Triggers the woocommerce_subscriptions_switch_added_to_cart action hook when a subscription switch is added to the cart. + * + * @since 2.6.0 + * + * @param string $cart_item_key The new cart item's key. + * @param int $product_id The product added to the cart. + * @param int $quantity The cart item's quantity. + * @param int $variation_id ID of the variation being added to the cart or 0. + * @param array $variation_attributes The variation's attributes, if any. + * @param array $cart_item_data The cart item's custom data. + */ + public static function trigger_switch_added_to_cart_hook( $cart_item_key, $product_id, $quantity, $variation_id, $variation_attributes, $cart_item_data ) { + if ( ! isset( $cart_item_data['subscription_switch'] ) || empty( $cart_item_data['subscription_switch']['item_id'] ) ) { + return; + } + + $subscription = wcs_get_subscription( $cart_item_data['subscription_switch']['subscription_id'] ); + $existing_item = wcs_get_order_item( $cart_item_data['subscription_switch']['item_id'], $subscription ); + $cart_item = WC()->cart->get_cart_item( $cart_item_key ); + + do_action( 'woocommerce_subscriptions_switch_added_to_cart', $subscription, $existing_item, $cart_item_key, $cart_item ); + } + + /** + * When a switch is added to the cart, add coupons which should be retained during switch. + * + * By default subscription coupons are not retained. Use woocommerce_subscriptions_retain_coupon_on_switch + * and return true to copy coupons from the subscription into the cart. + * + * @since 2.6.0 + * @param WC_Subscription $subscription + */ + public static function retain_coupons( $subscription ) { + foreach ( wcs_get_used_coupon_codes( $subscription ) as $coupon_code ) { + $coupon = new WC_Coupon( $coupon_code ); + if ( ! WC()->cart->has_discount( $coupon_code ) && true === apply_filters( 'woocommerce_subscriptions_retain_coupon_on_switch', false, $coupon_code, $coupon, $subscription ) ) { + WC()->cart->add_discount( $coupon_code ); + } + } + } + /** * When a product is added to the cart, check if it is being added to switch a subscription and if so, * make sure it's valid (i.e. not the same subscription). @@ -1275,256 +1569,13 @@ class WC_Subscriptions_Switcher { * Set the subscription prices to be used in calculating totals by @see WC_Subscriptions_Cart::calculate_subscription_totals() * * @since 2.0 + * @param WC_Cart The cart object which totals are being calculated. */ public static function calculate_prorated_totals( $cart ) { - - if ( false === self::cart_contains_switches() ) { - return; + if ( self::cart_contains_switches( 'any' ) ) { + self::$switch_totals_calculator = new WCS_Switch_Totals_Calculator( $cart ); + self::$switch_totals_calculator->calculate_prorated_totals(); } - - // Maybe charge an initial amount to account for upgrading from a cheaper subscription - $apportion_recurring_price = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'no' ); - $apportion_sign_up_fee = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'no' ); - $apportion_length = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'no' ); - - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - if ( ! isset( $cart_item['subscription_switch']['subscription_id'] ) ) { - continue; - } - - $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); - $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); - - if ( empty( $existing_item ) ) { - WC()->cart->remove_cart_item( $cart_item_key ); - continue; - } - - $product_in_cart = $cart_item['data']; - $product_id = wcs_get_canonical_product_id( $cart_item ); - $product = wc_get_product( $product_id ); - $is_virtual_product = $product->is_virtual(); - - // 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 = wcs_date_to_time( WC_Subscriptions_Product::get_expiration_date( $product_id, $subscription->get_date( 'last_order_date_created' ) ) ); - - // Add any extra sign up fees required to switch to the new subscription - if ( 'yes' == $apportion_sign_up_fee ) { - - // With WC 3.0, make sure we get a fresh copy of the product's meta to avoid prorating an already prorated sign-up fee - if ( is_callable( array( $product, 'read_meta_data' ) ) ) { - $product->read_meta_data( true ); - } - - // Because product add-ons etc. don't apply to sign-up fees, it's safe to use the product's sign-up fee value rather than the cart item's - $sign_up_fee_due = WC_Subscriptions_Product::get_sign_up_fee( $product ); - $sign_up_fee_paid = $subscription->get_items_sign_up_fee( $existing_item, get_option( 'woocommerce_prices_include_tax' ) === 'yes' ? 'inclusive_of_tax' : 'exclusive_of_tax' ); - - // Make sure total prorated sign-up fee is prorated across total amount of sign-up fee so that customer doesn't get extra discounts - if ( $cart_item['quantity'] > $existing_item['qty'] ) { - $sign_up_fee_paid = ( $sign_up_fee_paid * $existing_item['qty'] ) / $cart_item['quantity']; - } - - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee', max( $sign_up_fee_due - $sign_up_fee_paid, 0 ), 'set_prop_only' ); - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee_prorated', WC_Subscriptions_Product::get_sign_up_fee( WC()->cart->cart_contents[ $cart_item_key ]['data'] ), 'set_prop_only' ); - - } elseif ( 'no' == $apportion_sign_up_fee ) { // $0 the initial sign-up fee - - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee', 0, 'set_prop_only' ); - - } - - // Get the current subscription's last payment date - $last_order_time_created = $subscription->get_time( 'last_order_date_created' ); - $days_since_last_payment = floor( ( gmdate( 'U' ) - $last_order_time_created ) / ( 60 * 60 * 24 ) ); - - // Get the current subscription's next payment date - $next_payment_timestamp = $cart_item['subscription_switch']['next_payment_timestamp']; - $days_until_next_payment = ceil( ( $next_payment_timestamp - gmdate( 'U' ) ) / ( 60 * 60 * 24 ) ); - - // If the subscription contains a synced product and the next payment is actually the first payment, determine the days in the "old" cycle from the subscription object - if ( WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription->get_id() ) && WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product, 'timestamp', $subscription->get_date( 'start' ) ) == $next_payment_timestamp ) { - $days_in_old_cycle = wcs_get_days_in_cycle( $subscription->get_billing_period(), $subscription->get_billing_interval() ); - } else { - // Find the number of days between the two - $days_in_old_cycle = $days_until_next_payment + $days_since_last_payment; - } - - $days_in_old_cycle = apply_filters( 'wcs_switch_proration_days_in_old_cycle', $days_in_old_cycle, $subscription, $cart_item ); - - // Find the actual recurring amount charged for the old subscription (we need to use the '_recurring_line_total' meta here rather than '_subscription_recurring_amount' because we want the recurring amount to include extra from extensions, like Product Add-ons etc.) - $old_recurring_total = $existing_item['line_total']; - - // Use previous parent or renewal order's actual line item total instead of what is due, to guard against not yet paid amounts in multi-switching - $last_order = $subscription->get_last_order( 'all' ); - $last_order_items = $last_order->get_items(); - foreach ( $last_order_items as $last_order_item ) { - if ( wcs_get_canonical_product_id( $last_order_item ) == $product_id ) { - $old_recurring_total = $last_order_item['line_total']; - break; - } - } - - if ( $subscription->get_prices_include_tax() ) { - $old_recurring_total += $existing_item['line_tax']; - } - - // Find the $price per day for the old subscription's recurring total - $old_price_per_day = $days_in_old_cycle > 0 ? $old_recurring_total / $days_in_old_cycle : $old_recurring_total; - $old_price_per_day = apply_filters( 'wcs_switch_proration_old_price_per_day', $old_price_per_day, $subscription, $cart_item, $old_recurring_total, $days_in_old_cycle ); - - // Find the price per day for the new subscription's recurring total based on billing schedule - $days_in_new_cycle = wcs_get_days_in_cycle( WC_Subscriptions_Product::get_period( $product_in_cart ), WC_Subscriptions_Product::get_interval( $product_in_cart ) ); - - // Whether the days in new cycle match the days in old,ignoring any rounding. - $days_in_new_and_old_cycle_match = ceil( $days_in_new_cycle ) == $days_in_old_cycle || floor( $days_in_new_cycle ) == $days_in_old_cycle; - - // Whether the new item uses the same billing interval & cycle as the old subscription, - $matching_billing_cycle = WC_Subscriptions_Product::get_period( $product_in_cart ) == $subscription->get_billing_period() && WC_Subscriptions_Product::get_interval( $product_in_cart ) == $subscription->get_billing_interval(); - $switch_during_trial = $subscription->get_time( 'trial_end' ) > gmdate( 'U' ); - - // Set the days in each cycle to match if they are equal (ignoring any rounding discrepancy) or if the subscription is switched during a trial and has a matching billing cycle. - if ( $days_in_new_and_old_cycle_match || ( $matching_billing_cycle && $switch_during_trial ) ) { - $days_in_new_cycle = $days_in_old_cycle; - } - - $days_in_new_cycle = apply_filters( 'wcs_switch_proration_days_in_new_cycle', $days_in_new_cycle, $subscription, $cart_item, $days_in_old_cycle ); - - // We need to use the cart items price to ensure we include extras added by extensions like Product Add-ons, but we don't want the sign-up fee accounted for in the price, so make sure WC_Subscriptions_Cart::set_subscription_prices_for_calculation() isn't adding that. - remove_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100 ); - $new_price_per_day = ( WC_Subscriptions_Product::get_price( $product_in_cart ) * $cart_item['quantity'] ) / $days_in_new_cycle; - add_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100, 2 ); - - $new_price_per_day = apply_filters( 'wcs_switch_proration_new_price_per_day', $new_price_per_day, $subscription, $cart_item, $days_in_new_cycle ); - - if ( $old_price_per_day < $new_price_per_day ) { - $switch_type = 'upgrade'; - } elseif ( $old_price_per_day > $new_price_per_day && $new_price_per_day >= 0 ) { - $switch_type = 'downgrade'; - } else { - $switch_type = 'crossgrade'; - } - - $switch_type = apply_filters( 'wcs_switch_proration_switch_type', $switch_type, $subscription, $cart_item, $old_price_per_day, $new_price_per_day ); - - if ( ! in_array( $switch_type, array( 'upgrade', 'downgrade', 'crossgrade' ) ) ) { - throw new UnexpectedValueException( sprintf( __( 'Invalid switch type "%s". Switch must be one of: "upgrade", "downgrade" or "crossgrade".', 'woocommerce-subscriptions' ), $switch_type ) ); - } - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['upgraded_or_downgraded'] = sprintf( '%sd', $switch_type ); // preserve past tense for backward compatibility (luckily past tense for all allowed switch types end with a d) - - // Now lets see if we should add a prorated amount to the sign-up fee (for upgrades) or extend the next payment date (for downgrades) - if ( in_array( $apportion_recurring_price, array( 'yes', 'yes-upgrade' ) ) || ( in_array( $apportion_recurring_price, array( 'virtual', 'virtual-upgrade' ) ) && $is_virtual_product ) ) { - - // If the customer is upgrading, we may need to add a gap payment to the sign-up fee or to reduce the pre-paid period (or both) - if ( 'upgrade' === $switch_type ) { - - // The new subscription may be more expensive, but it's also on a shorter billing cycle, so reduce the next pre-paid term by default, but also allow this to be customised - $reduce_pre_paid_term = apply_filters( 'wcs_switch_proration_reduce_pre_paid_term', $days_in_old_cycle > $days_in_new_cycle, $subscription, $cart_item, $days_in_old_cycle, $days_in_new_cycle, $old_price_per_day, $new_price_per_day ); - - if ( $reduce_pre_paid_term ) { - - // 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 = self::calculate_pre_paid_days( $old_recurring_total, $new_price_per_day ); - - // 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 ) { - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $last_order_time_created + ( $pre_paid_days * 60 * 60 * 24 ); - - // If the total amount the customer has paid entitles her to the same or less days at the new price then start the new subscription from today - } else { - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = 0; - - } - } else { - - // If we've already calculated the prorated price recalculate the amounts but reset the values so we don't double the amounts - if ( 0 < wcs_get_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_price_prorated', 'single', 0 ) ) { - $prorated_signup_fee = wcs_get_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee_prorated', 'single' ); - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee', $prorated_signup_fee, 'set_prop_only' ); - } - - $extra_to_pay = $days_until_next_payment * ( $new_price_per_day - $old_price_per_day ); - - // when calculating a subscription with one length (no more next payment date and the end date may have been pushed back) we need to pay for those extra days at the new price per day between the old next payment date and new end date - if ( 1 == WC_Subscriptions_Product::get_length( $product_in_cart ) ) { - $days_to_new_end = floor( ( $end_timestamp - $next_payment_timestamp ) / ( 60 * 60 * 24 ) ); - - if ( $days_to_new_end > 0 ) { - $extra_to_pay += $days_to_new_end * $new_price_per_day; - } - } - - // We need to find the per item extra to pay so we can set it as the sign-up fee (WC will then multiply it by the quantity) - $extra_to_pay = $extra_to_pay / $cart_item['quantity']; - $extra_to_pay = apply_filters( 'wcs_switch_proration_extra_to_pay', $extra_to_pay, $subscription, $cart_item, $days_in_old_cycle ); - - // Keep a record of the two separate amounts so we store these and calculate future switch amounts correctly - $existing_sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( WC()->cart->cart_contents[ $cart_item_key ]['data'] ); - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee_prorated', $existing_sign_up_fee, 'set_prop_only' ); - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_price_prorated', round( $extra_to_pay, wc_get_price_decimals() ), 'set_prop_only' ); - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_sign_up_fee', round( $existing_sign_up_fee + $extra_to_pay, wc_get_price_decimals() ), 'set_prop_only' ); - } - - // If the customer is downgrading, set the next payment date and maybe extend it if downgrades are prorated - } elseif ( 'downgrade' === $switch_type ) { - - $old_total_paid = $old_price_per_day * $days_until_next_payment; - - // if downgrades are apportioned, extend the next payment date for n more days - if ( in_array( $apportion_recurring_price, array( 'virtual', 'yes' ) ) ) { - - // Find how many more days at the new lower price it takes to exceed the amount already paid - $days_to_add = self::calculate_pre_paid_days( $old_total_paid, $new_price_per_day ); - - $days_to_add -= $days_until_next_payment; - } else { - $days_to_add = 0; - } - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $next_payment_timestamp + ( $days_to_add * 60 * 60 * 24 ); - - } // The old price per day == the new price per day, no need to change anything - - if ( 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']['recurring_payment_prorated'] = true; - } - } - - if ( 'yes' == $apportion_length || ( 'virtual' == $apportion_length && $is_virtual_product ) ) { - - $base_length = WC_Subscriptions_Product::get_length( $product_id ); - $completed_payments = $subscription->get_completed_payment_count(); - $length_remaining = $base_length - $completed_payments; - - // Default to the base length if more payments have already been made than this subscription requires - if ( $length_remaining <= 0 ) { - $length_remaining = $base_length; - } - - wcs_set_objects_property( WC()->cart->cart_contents[ $cart_item_key ]['data'], 'subscription_length', $length_remaining, 'set_prop_only' ); - } - } - } - - /** - * Calculate the number of days that have already been paid - * - * @param int $old_total_paid The amount paid previously, such as the old recurring total - * @param int $new_price_per_day The amount per day price for the new subscription - * @return int $pre_paid_days The number of days paid for already - */ - private static function calculate_pre_paid_days( $old_total_paid, $new_price_per_day ) { - $pre_paid_days = 0; - if ( 0 != $new_price_per_day ) { - $pre_paid_days = ceil( $old_total_paid / $new_price_per_day ); - } - return $pre_paid_days; } /** @@ -1795,13 +1846,13 @@ class WC_Subscriptions_Switcher { switch ( $cart_item['subscription_switch']['upgraded_or_downgraded'] ) { case 'downgraded' : - $direction = _x( 'Downgrade', 'a switch order', 'woocommerce-subscriptions' ); + $direction = _x( 'Downgrade', 'a switch type', 'woocommerce-subscriptions' ); break; case 'upgraded' : - $direction = _x( 'Upgrade', 'a switch order', 'woocommerce-subscriptions' ); + $direction = _x( 'Upgrade', 'a switch type', 'woocommerce-subscriptions' ); break; default : - $direction = _x( 'Crossgrade', 'a switch order', 'woocommerce-subscriptions' ); + $direction = _x( 'Crossgrade', 'a switch type', 'woocommerce-subscriptions' ); break; } @@ -1893,31 +1944,47 @@ class WC_Subscriptions_Switcher { if ( ! empty( $switch_data['switches'] ) && is_array( $switch_data['switches'] ) ) { foreach ( $switch_data['switches'] as $order_item_id => $switched_item_data ) { - // If we are adding a line item to an existing subscription - if ( isset( $switched_item_data['add_line_item'] ) ) { - wcs_update_order_item_type( $switched_item_data['add_line_item'], 'line_item', $subscription->get_id() ); - do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $switched_item_data['add_line_item'], $switched_item_data['remove_line_item'] ); + $add_subscription_item = isset( $switched_item_data['add_line_item'] ); + $remove_subscription_item = isset( $switched_item_data['remove_line_item'] ); + $switch_order_item = wcs_get_order_item( $order_item_id, $order ); + + if ( ! $add_subscription_item ) { + continue; } - // remove the existing subscription item - $old_subscription_item = wcs_get_order_item( $switched_item_data['remove_line_item'], $subscription ); - $switch_order_item = wcs_get_order_item( $order_item_id, $order ); + // Removing an existing subscription item? + if ( $remove_subscription_item ) { + $old_subscription_item = wcs_get_order_item( $switched_item_data['remove_line_item'], $subscription ); + } - if ( empty( $old_subscription_item ) ) { + if ( $remove_subscription_item && empty( $old_subscription_item ) ) { throw new Exception( __( 'The original subscription item being switched cannot be found.', 'woocommerce-subscriptions' ) ); } elseif ( empty( $switch_order_item ) ) { throw new Exception( __( 'The item on the switch order cannot be found.', 'woocommerce-subscriptions' ) ); - } else { - // We don't want to include switch item meta in order item name - add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); - $old_item_name = wcs_get_order_item_name( $old_subscription_item, array( 'attributes' => true ) ); - $new_item_name = wcs_get_order_item_name( $switch_order_item, array( 'attributes' => true ) ); - remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); + } + // If we are adding a line item to an existing subscription... + wcs_update_order_item_type( $switched_item_data['add_line_item'], 'line_item', $subscription->get_id() ); + + if ( $remove_subscription_item ) { + do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $switched_item_data['add_line_item'], $switched_item_data['remove_line_item'] ); + } + + // We don't want to include switch item meta in order item name + add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); + $old_item_name = $remove_subscription_item ? wcs_get_order_item_name( $old_subscription_item, array( 'attributes' => true ) ) : false; + $new_item_name = wcs_get_order_item_name( $switch_order_item, array( 'attributes' => true ) ); + remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); + + if ( $remove_subscription_item ) { wcs_update_order_item_type( $switched_item_data['remove_line_item'], 'line_item_switched', $subscription->get_id() ); + } + if ( $remove_subscription_item ) { // translators: 1$: old item, 2$: new item when switching $add_note = sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_item_name, $new_item_name ); + } else { + $add_note = sprintf( _x( 'Customer added %s.', 'used in order notes', 'woocommerce-subscriptions' ), $new_item_name ); } } } @@ -1955,6 +2022,30 @@ class WC_Subscriptions_Switcher { } } + // Archive the old coupons + foreach ( $subscription->get_items( 'coupon' ) as $coupon_id => $coupon ) { + wcs_update_order_item_type( $coupon_id, 'coupon_switched', $subscription->get_id() ); + } + + if ( ! empty( $switch_data['coupons'] ) && is_array( $switch_data['coupons'] ) ) { + // Flip the switched coupons "on" + foreach ( $switch_data['coupons'] as $coupon_code ) { + wcs_update_order_item_type( $coupon_code, 'coupon', $subscription->get_id() ); + } + } + + // Archive the old fees + foreach ( $subscription->get_fees() as $fee_item_id => $fee ) { + wcs_update_order_item_type( $fee_item_id, 'fee_switched', $subscription->get_id() ); + } + + if ( ! empty( $switch_data['fee_items'] ) && is_array( $switch_data['fee_items'] ) ) { + // Flip the switched fee items "on" + foreach ( $switch_data['fee_items'] as $fee_item_id ) { + wcs_update_order_item_type( $fee_item_id, 'fee', $subscription->get_id() ); + } + } + if ( ! empty( $switch_data['shipping_line_items'] ) && is_array( $switch_data['shipping_line_items'] ) ) { // Archive the old subscription shipping methods foreach ( $subscription->get_shipping_methods() as $shipping_line_item_id => $item ) { @@ -1994,7 +2085,7 @@ class WC_Subscriptions_Switcher { */ public static function set_force_payment_flag_in_cart( $total ) { - if ( $total > 0 || 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) || false === self::cart_contains_switches() ) { + if ( $total > 0 || 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) || false === self::cart_contains_switches( 'any' ) ) { return $total; } @@ -2047,7 +2138,7 @@ class WC_Subscriptions_Switcher { */ public static function cart_needs_payment( $needs_payment, $cart ) { - if ( false === $needs_payment && 0 == $cart->total && false !== ( $switch_items = self::cart_contains_switches() ) ) { + if ( false === $needs_payment && 0 == $cart->total && false !== ( $switch_items = self::cart_contains_switches( 'any' ) ) ) { foreach ( $switch_items as $switch_item ) { if ( isset( $switch_item['force_payment'] ) && true === $switch_item['force_payment'] ) { @@ -2122,6 +2213,106 @@ class WC_Subscriptions_Switcher { add_action( 'woocommerce_grant_product_download_permissions', 'WCS_Download_Handler::save_downloadable_product_permissions' ); } + /** + * Calculates the total amount a customer has paid in early renewals and switches since the last non-early renewal or parent order (inclusive). + * + * This function will map the current item back through multiple switches to make sure it finds the item that was present at the time of last parent/scheduled renewal. + * + * @since 2.6.0 + * + * @param WC_Subscription $subscription The Subscription. + * @param WC_Order_Item $subscription_item The current line item on the subscription to map back through the related orders. + * @param string $include_sign_up_fees Optional. Whether to include the sign-up fees paid. Can be 'include_sign_up_fees' or 'exclude_sign_up_fees'. Default 'include_sign_up_fees'. + * + * @return float The total amount paid for an existing subscription line item. + */ + public static function calculate_total_paid_since_last_order( $subscription, $subscription_item, $include_sign_up_fees = 'include_sign_up_fees' ) { + $found_item = false; + $item_total_paid = 0; + $orders = $subscription->get_related_orders( 'all', array( 'parent', 'renewal', 'switch' ) ); + + // We need the orders sorted by the date they were paid, with the newest first. + wcs_sort_objects( $orders, 'date_paid', 'descending' ); + + // We'll need to make sure we map switched items back through past orders so flag if the current item has been switched before. + $has_been_switched = $subscription_item->meta_exists( '_switched_subscription_item_id' ); + $switched_subscription_items = $subscription->get_items( 'line_item_switched' ); + + foreach ( $orders as $order ) { + $order_is_parent = $order->get_id() === $subscription->get_parent_id(); + + // Find the item on the order which matches the subscription item. + $order_item = wcs_find_matching_line_item( $order, $subscription_item ); + + if ( $order_item ) { + $found_item = true; + $item_total = $order_item->get_total(); + + if ( $order->get_prices_include_tax( 'edit' ) ) { + $item_total += $order_item->get_total_tax(); + } + + // Remove any signup fees if necessary. + if ( $order_is_parent && 'include_sign_up_fees' !== $include_sign_up_fees ) { + if ( $order_item->meta_exists( '_synced_sign_up_fee' ) ) { + $item_total -= $order_item->get_meta( '_synced_sign_up_fee' ); + } elseif ( $subscription_item->meta_exists( '_has_trial' ) ) { + // Where there's a free trial, the sign up fee is the entire item total so the non-sign-up fee portion is 0. + $item_total = 0; + } else { + // For non-free trial subscriptions, the sign up fee portion is the order total minus the recurring total (subscription item total). + // Use the subscription item's subtotal (without discounts) to avoid signup fee coupon discrepancies + $item_total -= max( $order_item->get_total() - $subscription_item->get_subtotal(), 0 ); + } + } + + $item_total_paid += $item_total; + } + + // If the current order in line contains a switch, we might need to start looking for the previous product in older related orders. + if ( $has_been_switched && wcs_order_contains_switch( $order ) ) { + // The new subscription item stores a reference to the old subscription item in meta. + $switched_subscription_item_id = $subscription_item->get_meta( '_switched_subscription_item_id' ); + + // Check that the switched subscription line item still exists. + if ( isset( $switched_subscription_items[ $switched_subscription_item_id ] ) ) { + $switched_subscription_item = $switched_subscription_items[ $switched_subscription_item_id ]; + + // The switched subscription item stores a reference to the new item on the switch order . + $switch_order_item_id = $switched_subscription_item->get_meta( '_switched_subscription_new_item_id' ); + + // Check that this switch order contains the switch for the current subscription item. + if ( $switch_order_item_id && (bool) wcs_get_order_item( $switch_order_item_id, $order ) ) { + // The item we need to look for now in older related orders is the subscription item which switched. + $subscription_item = $switched_subscription_item; + + // If the switched subscription item has been switched, make a note of it too as we might have a multi-switch. + $has_been_switched = $subscription_item->meta_exists( '_switched_subscription_item_id' ); + } + } + } + + // If this is a parent order, or it's a renewal order but not an early renewal, we've gone back far enough -- exit out. + if ( $order_is_parent || ( wcs_order_contains_renewal( $order ) && ! wcs_order_contains_early_renewal( $order ) ) ) { + break; + } + } + + // If we never found any amount paid, fall back to the existing item's line item total. + return $found_item ? $item_total_paid : $subscription_item['line_total']; + } + + /** + * Logs information about all the switches in the cart to the wcs-switch-cart-items log. + * + * @since 2.6.0 + */ + public static function log_switches() { + if ( isset( self::$switch_totals_calculator ) ) { + self::$switch_totals_calculator->log_switches(); + } + } + /** Deprecated Methods **/ /** @@ -2479,7 +2670,7 @@ class WC_Subscriptions_Switcher { $subscription = wcs_get_subscription_from_key( $subscription_key ); - if ( $subscription->has_status( 'active' ) && $subscription->get_parent_id() && wcs_order_contains_switch( $subscription->get_parent_id() ) && 1 >= $subscription->get_completed_payment_count() ) { + if ( $subscription->has_status( 'active' ) && $subscription->get_parent_id() && wcs_order_contains_switch( $subscription->get_parent_id() ) && 1 >= $subscription->get_payment_count() ) { $first_payment_timestamp = get_post_meta( $subscription->get_parent_id(), '_switched_subscription_first_payment_timestamp', true ); diff --git a/includes/class-wc-subscriptions-synchroniser.php b/includes/class-wc-subscriptions-synchroniser.php index 94f728d..8f97a40 100755 --- a/includes/class-wc-subscriptions-synchroniser.php +++ b/includes/class-wc-subscriptions-synchroniser.php @@ -1353,7 +1353,7 @@ class WC_Subscriptions_Synchroniser { $subscription = wcs_get_subscription_from_key( $order . '_' . $product_id ); - if ( self::order_contains_synced_subscription( wcs_get_objects_property( $order, 'id' ) ) && 1 >= $subscription->get_completed_payment_count() ) { + if ( self::order_contains_synced_subscription( wcs_get_objects_property( $order, 'id' ) ) && 1 >= $subscription->get_payment_count() ) { // Don't prematurely set the first payment date when manually adding a subscription from the admin if ( ! is_admin() || 'active' == $subscription->get_status() ) { diff --git a/includes/class-wcs-action-scheduler.php b/includes/class-wcs-action-scheduler.php index fef27de..61b97c8 100755 --- a/includes/class-wcs-action-scheduler.php +++ b/includes/class-wcs-action-scheduler.php @@ -10,7 +10,14 @@ */ class WCS_Action_Scheduler extends WCS_Scheduler { - /*@protected Array of $action_hook => $date_type values */ + /** + * An internal cache of action hooks and corresponding date types. + * + * This variable has been deprecated and will be removed completely in the future. You should use WCS_Action_Scheduler::get_scheduled_action_hook() and WCS_Action_Scheduler::get_date_types_to_schedule() instead. + * + * @deprecated 2.6.0 + * @var array An array of $action_hook => $date_type values + */ protected $action_hooks = array( 'woocommerce_scheduled_subscription_trial_end' => 'trial_end', 'woocommerce_scheduled_subscription_payment' => 'next_payment', @@ -75,7 +82,13 @@ class WCS_Action_Scheduler extends WCS_Scheduler { $this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) ); - foreach ( $this->action_hooks as $action_hook => $date_type ) { + foreach ( $this->get_date_types_to_schedule() as $date_type ) { + + $action_hook = $this->get_scheduled_action_hook( $subscription, $date_type ); + + if ( empty( $action_hook ) ) { + continue; + } $event_time = $subscription->get_time( $date_type ); @@ -101,7 +114,14 @@ class WCS_Action_Scheduler extends WCS_Scheduler { case 'pending-cancel' : // Now that we have the current times, clear the scheduled hooks - foreach ( $this->action_hooks as $action_hook => $date_type ) { + foreach ( $this->get_date_types_to_schedule() as $date_type ) { + + $action_hook = $this->get_scheduled_action_hook( $subscription, $date_type ); + + if ( empty( $action_hook ) ) { + continue; + } + $this->unschedule_actions( $action_hook, $this->get_action_args( $date_type, $subscription ) ); } @@ -123,9 +143,17 @@ class WCS_Action_Scheduler extends WCS_Scheduler { case 'switched' : case 'expired' : case 'trash' : - foreach ( $this->action_hooks as $action_hook => $date_type ) { + foreach ( $this->get_date_types_to_schedule() as $date_type ) { + + $action_hook = $this->get_scheduled_action_hook( $subscription, $date_type ); + + if ( empty( $action_hook ) ) { + continue; + } + $this->unschedule_actions( $action_hook, $this->get_action_args( $date_type, $subscription ) ); } + $this->unschedule_actions( 'woocommerce_scheduled_subscription_expiration', $this->get_action_args( 'end', $subscription ) ); $this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) ); break; } diff --git a/includes/class-wcs-add-cart-item.php b/includes/class-wcs-add-cart-item.php new file mode 100755 index 0000000..cb6685f --- /dev/null +++ b/includes/class-wcs-add-cart-item.php @@ -0,0 +1,73 @@ +old_price_per_day ) ) { + $this->old_price_per_day = apply_filters( 'wcs_switch_proration_old_price_per_day', 0, $this->subscription, $this->cart_item, 0, $this->get_days_in_old_cycle() ); + } + + return $this->old_price_per_day; + } + + /** + * Gets the total paid for the current period. + * + * For items being added to a subscription there isn't anything paid which needs to be honoured and so 0 has been paid. + * + * @since 2.6.0 + * @return float + */ + public function get_total_paid_for_current_period() { + return 0; + } + + /** Helper functions */ + + /** + * Determines whether the new product's trial period matches the old product's trial period. + * + * For items being added to a subscription there isn't an existing item to match so false is returned. + * + * @since 2.6.0 + * @return bool + */ + public function trial_periods_match() { + return false; + } +} diff --git a/includes/class-wcs-autoloader.php b/includes/class-wcs-autoloader.php index 3205373..1b3c0eb 100755 --- a/includes/class-wcs-autoloader.php +++ b/includes/class-wcs-autoloader.php @@ -140,6 +140,7 @@ class WCS_Autoloader { */ protected function is_class_abstract( $class ) { static $abstracts = array( + 'wcs_background_repairer' => true, 'wcs_background_updater' => true, 'wcs_background_upgrader' => true, 'wcs_cache_manager' => true, diff --git a/includes/class-wcs-cart-renewal.php b/includes/class-wcs-cart-renewal.php index e06240c..ed426da 100755 --- a/includes/class-wcs-cart-renewal.php +++ b/includes/class-wcs-cart-renewal.php @@ -66,6 +66,8 @@ class WCS_Cart_Renewal { // Work around WC changing the "created_via" meta to "checkout" regardless of its previous value during checkout. add_action( 'woocommerce_checkout_create_order', array( $this, 'maybe_preserve_order_created_via' ), 0, 1 ); + + add_action( 'plugins_loaded', array( $this, 'maybe_disable_manual_renewal_stock_validation' ) ); } /** @@ -118,7 +120,8 @@ class WCS_Cart_Renewal { add_filter( 'woocommerce_get_shop_coupon_data', array( &$this, 'renewal_coupon_data' ), 10, 2 ); add_action( 'woocommerce_remove_cart_item', array( &$this, 'maybe_remove_items' ), 10, 1 ); - add_action( 'woocommerce_before_cart_item_quantity_zero', array( &$this, 'maybe_remove_items' ), 10, 1 ); + wcs_add_woocommerce_dependent_action( 'woocommerce_before_cart_item_quantity_zero', array( &$this, 'maybe_remove_items' ), '3.7.0', '<' ); + add_action( 'woocommerce_cart_emptied', array( &$this, 'clear_coupons' ), 10 ); add_filter( 'woocommerce_cart_item_removed_title', array( &$this, 'items_removed_title' ), 10, 2 ); @@ -210,7 +213,7 @@ class WCS_Cart_Renewal { $this->setup_cart( $order, array( 'subscription_id' => $subscription->get_id(), 'renewal_order_id' => $order_id, - ) ); + ), 'all_items_required' ); } do_action( 'wcs_after_renewal_setup_cart_subscription', $subscription, $order ); @@ -234,9 +237,14 @@ class WCS_Cart_Renewal { * Set up cart item meta data to complete a subscription renewal via the cart. * * @since 2.2.0 - * @version 2.2.6 + * + * @param WC_Abstract_Order $subscription The subscription or Order object to set up the cart from. + * @param array $cart_item_data Additional cart item data to set on the cart items. + * @param string $validation_type Whether all items are required or not. Optional. Can be 'all_items_not_required' or 'all_items_required'. 'all_items_not_required' by default. + * 'all_items_not_required' - If an order/subscription line item fails to be added to the cart, the remaining items will be added. + * 'all_items_required' - If an order/subscription line item fails to be added to the cart, all items will be removed and the cart setup will be aborted. */ - protected function setup_cart( $subscription, $cart_item_data ) { + protected function setup_cart( $subscription, $cart_item_data, $validation_type = 'all_items_not_required' ) { WC()->cart->empty_cart( true ); $success = true; @@ -333,11 +341,19 @@ class WCS_Cart_Renewal { $success = $success && (bool) $cart_item_key; } - // If a product linked to a subscription failed to be added to the cart prevent partially paying for the order by removing all cart items. - if ( ! $success && wcs_is_subscription( $subscription ) ) { - // translators: %s is subscription's number - wc_add_notice( sprintf( esc_html__( 'Subscription #%s has not been added to the cart.', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) , 'error' ); + // If a product couldn't be added to the cart and if all items are required, prevent partially paying for the order by removing all cart items. + if ( ! $success && 'all_items_required' === $validation_type ) { + if ( wcs_is_subscription( $subscription ) ) { + // translators: %s is subscription's number + wc_add_notice( sprintf( esc_html__( 'Subscription #%s has not been added to the cart.', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) , 'error' ); + } else { + // translators: %s is order's number + wc_add_notice( sprintf( esc_html__( 'Order #%s has not been added to the cart.', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) , 'error' ); + } + WC()->cart->empty_cart( true ); + wp_safe_redirect( wc_get_page_permalink( 'cart' ) ); + exit; } do_action( 'woocommerce_setup_cart_for_' . $this->cart_item_key, $subscription, $cart_item_data ); @@ -1393,6 +1409,17 @@ class WCS_Cart_Renewal { } } + /** + * Disables renewal cart stock validation if the store has switched it off via a filter. + * + * @since 2.6.0 + */ + public function maybe_disable_manual_renewal_stock_validation() { + if ( apply_filters( 'woocommerce_subscriptions_disable_manual_renewal_stock_validation', false ) ) { + WCS_Renewal_Cart_Stock_Manager::attach_callbacks(); + } + } + /* Deprecated */ /** @@ -1491,7 +1518,7 @@ class WCS_Cart_Renewal { if ( wcs_is_subscription( $order ) || wcs_order_contains_renewal( $order ) ) { - $used_coupons = $order->get_used_coupons(); + $used_coupons = wcs_get_used_coupon_codes( $order ); $order_discount = wcs_get_objects_property( $order, 'cart_discount' ); // Add any used coupon discounts to the cart (as best we can) using our pseudo renewal coupons diff --git a/includes/class-wcs-cart-resubscribe.php b/includes/class-wcs-cart-resubscribe.php index 00cc053..3257186 100755 --- a/includes/class-wcs-cart-resubscribe.php +++ b/includes/class-wcs-cart-resubscribe.php @@ -85,7 +85,7 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal { $this->setup_cart( $subscription, array( 'subscription_id' => $subscription->get_id(), - ) ); + ), 'all_items_required' ); if ( WC()->cart->get_cart_contents_count() != 0 ) { wc_add_notice( __( 'Complete checkout to resubscribe.', 'woocommerce-subscriptions' ), 'success' ); @@ -124,7 +124,7 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal { if ( current_user_can( 'subscribe_again', $subscription->get_id() ) ) { $this->setup_cart( $subscription, array( 'subscription_id' => $subscription->get_id(), - ) ); + ), 'all_items_required' ); } 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' ) ) ); diff --git a/includes/class-wcs-custom-order-item-manager.php b/includes/class-wcs-custom-order-item-manager.php new file mode 100755 index 0000000..f709d34 --- /dev/null +++ b/includes/class-wcs-custom-order-item-manager.php @@ -0,0 +1,101 @@ + array( + 'group' => 'removed_line_items', + 'class' => 'WC_Subscription_Line_Item_Removed', + ), + 'line_item_switched' => array( + 'group' => 'switched_line_items', + 'class' => 'WC_Subscription_Line_Item_Switched', + ), + 'coupon_pending_switch' => array( + 'group' => 'pending_switch_coupons', + 'class' => 'WC_Subscription_Item_Coupon_Pending_Switch', + 'data_store' => 'WC_Order_Item_Coupon_Data_Store', + ), + 'fee_pending_switch' => array( + 'group' => 'pending_switch_fees', + 'class' => 'WC_Subscription_Item_Fee_Pending_Switch', + 'data_store' => 'WC_Order_Item_Fee_Data_Store', + ), + ); + + /** + * Initialise class hooks & filters when the file is loaded + * + * @since 2.6.0 + */ + public static function init() { + + add_filter( 'woocommerce_order_type_to_group', array( __CLASS__, 'add_extra_groups' ) ); + add_filter( 'woocommerce_get_order_item_classname', array( __CLASS__, 'map_classname_for_extra_items' ), 10, 2 ); + add_filter( 'woocommerce_data_stores', array( __CLASS__, 'register_data_stores' ) ); + } + + /** + * Adds extra groups. + * + * @param array $type_to_group_list Existing list of types and their groups + * @return array $type_to_group_list + * @since 2.6.0 + */ + public static function add_extra_groups( $type_to_group_list ) { + + foreach ( self::$line_item_type_args as $line_item_type => $args ) { + $type_to_group_list[ $line_item_type ] = $args['group']; + } + + return $type_to_group_list; + } + + /** + * Maps the classname for extra items. + * + * @param string $classname + * @param string $item_type + * @return string $classname + * @since 2.6.0 + */ + public static function map_classname_for_extra_items( $classname, $item_type ) { + + if ( isset( self::$line_item_type_args[ $item_type ] ) ) { + $classname = self::$line_item_type_args[ $item_type ]['class']; + } + + return $classname; + } + + /** + * Register the data stores to be used for our custom line item types. + * + * @param array $data_stores The registered data stores. + * @return array + * @since 2.6.0 + */ + public static function register_data_stores( $data_stores ) { + + foreach ( self::$line_item_type_args as $line_item_type => $args ) { + // By default use the WC_Order_Item_Product_Data_Store unless specified otherwise. + $data_store = isset( $args['data_store'] ) ? $args['data_store'] : 'WC_Order_Item_Product_Data_Store'; + $data_stores[ "order-item-{$line_item_type}" ] = $data_store; + } + + return $data_stores; + } +} diff --git a/includes/class-wcs-dependent-hook-manager.php b/includes/class-wcs-dependent-hook-manager.php new file mode 100755 index 0000000..c06e0d8 --- /dev/null +++ b/includes/class-wcs-dependent-hook-manager.php @@ -0,0 +1,88 @@ + $operators ) { + foreach ( $operators as $operator => $callbacks ) { + + if ( ! version_compare( WC_VERSION, $wc_version, $operator ) ) { + continue; + } + + foreach ( $callbacks as $callback ) { + add_action( $callback['tag'], $callback['function'], $callback['priority'], $callback['number_of_args'] ); + } + } + } + } + + /** + * Attach function callback if a certain WooCommerce version is present. + * + * @since 2.6.0 + * + * @param string $tag The action or filter tag to attach the callback too. + * @param string|array $function The callable function to attach to the hook. + * @param string $woocommerce_version The WooCommerce version to do a compare on. For example '3.0.0'. + * @param string $operator The version compare operator to use. @see https://www.php.net/manual/en/function.version-compare.php + * @param integer $priority The priority to attach this callback to. + * @param integer $number_of_args The number of arguments to pass to the callback function + */ + public static function add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority = 10, $number_of_args = 1 ) { + // Attach callbacks now if WooCommerce has already loaded. + if ( did_action( 'plugins_loaded' ) && version_compare( WC_VERSION, $woocommerce_version, $operator ) ) { + add_action( $tag, $function, $priority, $number_of_args ); + return; + } + + self::$dependent_callbacks['woocommerce'][ $woocommerce_version ][ $operator ][] = array( + 'tag' => $tag, + 'function' => $function, + 'priority' => $priority, + 'number_of_args' => $number_of_args, + ); + } +} diff --git a/includes/class-wcs-modal.php b/includes/class-wcs-modal.php new file mode 100755 index 0000000..4b4dd6a --- /dev/null +++ b/includes/class-wcs-modal.php @@ -0,0 +1,246 @@ +content_type = $content_type; + $this->trigger = $trigger; + $this->heading = $heading; + $this->actions = $actions; + + // Allow callers to provide the callback without any parameters. Assuming the content provided is the callback. + if ( 'callback' === $this->content_type && ! isset( $content['parameters'] ) ) { + $this->content = array( + 'callback' => $content, + 'parameters' => array(), + ); + } else { + $this->content = $content; + } + + self::register_scripts_and_styles(); + } + + /** + * Prints the modal HTML. + * + * @since 2.6.0 + */ + public function print_html() { + wc_get_template( 'html-modal.php', array( 'modal' => $this ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); + } + + /** + * Prints the modal inner content. + * + * @since 2.6.0 + */ + public function print_content() { + switch ( $this->content_type ) { + case 'plain-text': + echo '

' . wp_kses_post( $this->content ) . '

'; + break; + case 'html': + echo wp_kses_post( $this->content ); + break; + case 'template': + wc_get_template( $this->content['template_name'], $this->content['args'], '', $this->content['template_path'] ); + break; + case 'callback': + call_user_func_array( $this->content['callback'], $this->content['parameters'] ); + break; + } + } + + /** + * Determines if the modal has a heading. + * + * @since 2.6.0 + * + * @return bool + */ + public function has_heading() { + return ! empty( $this->heading ); + } + + /** + * Determines if the modal has actions. + * + * @since 2.6.0 + * + * @return bool + */ + public function has_actions() { + return ! empty( $this->actions ); + } + + /** + * Adds a button or link action which will be printed in the modal footer. + * + * @since 2.6.0 + * + * @param array $action_args { + * Action button or link details. + * + * @type string $type Optional. The element type. Can be 'button' or 'a'. Default 'a' (link element). + * @type array $attributes Optional. An array of HTML attributes in a array( 'attribute' => 'value' ) format. The value can also be an array of attribute values. Default is empty array. + * @type string $text Optional. The text should appear inside the button or a tag. Default is empty string. + * } + */ + public function add_action( $action_args ) { + $action = wp_parse_args( $action_args, array( + 'type' => 'a', + 'text' => '', + 'attributes' => array( + 'class' => 'button', + ), + ) ); + + $this->actions[] = $action; + } + + /** + * Returns the modal heading. + * + * @since 2.6.0 + * + * @return string + */ + public function get_heading() { + return $this->heading; + } + + /** + * Returns the array of actions. + * + * @since 2.6.0 + * + * @return array The modal actions. + */ + public function get_actions() { + return $this->actions; + } + + /** + * Returns the modal's trigger selector. + * + * @since 2.6.0 + * + * @return string The trigger element's selector. + */ + public function get_trigger() { + return $this->trigger; + } + + /** + * Returns a flattened string of HTML element attributes from an array of attributes and values. + * + * @since 2.6.0 + * + * @param array $attributes An array of attributes in a array( 'attribute' => 'value' ) or array( 'attribute' => array( 'value', 'value ) ). + * @return string + */ + public function get_attribute_string( $attributes ) { + foreach ( $attributes as $attribute => $values ) { + $attributes[ $attribute ] = $attribute . '="' . implode( ' ', array_map( 'esc_attr', (array) $values ) ) . '"'; + } + + return implode( ' ', $attributes ); + } +} diff --git a/includes/class-wcs-my-account-auto-renew-toggle.php b/includes/class-wcs-my-account-auto-renew-toggle.php index 9e4bb3f..ab2e71e 100755 --- a/includes/class-wcs-my-account-auto-renew-toggle.php +++ b/includes/class-wcs-my-account-auto-renew-toggle.php @@ -116,7 +116,10 @@ class WCS_My_Account_Auto_Renew_Toggle { * @param WC_Subscription $subscription */ protected static function send_ajax_response( $subscription ) { - wp_send_json( array( 'payment_method' => esc_attr( $subscription->get_payment_method_to_display( 'customer' ) ) ) ); + wp_send_json( array( + 'payment_method' => esc_attr( $subscription->get_payment_method_to_display( 'customer' ) ), + 'is_manual' => wc_bool_to_string( $subscription->is_manual() ), + ) ); } /** diff --git a/includes/class-wcs-object-sorter.php b/includes/class-wcs-object-sorter.php new file mode 100755 index 0000000..feba431 --- /dev/null +++ b/includes/class-wcs-object-sorter.php @@ -0,0 +1,75 @@ + get_id() + * + * @var string A valid object property. Could be 'date_created', 'date_modified', 'date_paid', 'date_completed' or 'id' for WC_Order or WC_Subscription objects, for example. + */ + protected $sort_by_property = ''; + + /** + * Constructor. + * + * @since 2.6.0 + * + * @param string $property The object property to use in comparisons. This will be used to generate the object getter by prepending 'get_'. + */ + public function __construct( $property ) { + $this->sort_by_property = $property; + } + + /** + * Compares two objects using the @see $this->sort_by_property getter. + * + * Designed to be used by uasort(), usort() or uksort() functions. + * + * @since 2.6.0 + * + * @param object $object_one + * @param object $object_two + * @return int 0. -1 or 1 Depending on the result of the comparison. + */ + public function ascending_compare( $object_one, $object_two ) { + $function = "get_{$this->sort_by_property}"; + + if ( ! is_callable( array( $object_one, $function ) ) || ! is_callable( array( $object_two, $function ) ) ) { + return 0; + } + + $value_one = $object_one->{$function}(); + $value_two = $object_two->{$function}(); + + if ( $value_one === $value_two ) { + return 0; + } + + return ( $value_one < $value_two ) ? -1 : 1; + } + + /** + * Compares two objects using the @see $this->sort_by_property getter in reverse order. + * + * Designed to be used by uasort(), or usort() style functions. + * + * @since 2.6.0 + * + * @param object $object_one + * @param object $object_two + * @return int 0. -1 or 1 Depending on the result of the comparison. + */ + public function descending_compare( $object_one, $object_two ) { + return -1 * $this->ascending_compare( $object_one, $object_two ); + } +} diff --git a/includes/class-wcs-renewal-cart-stock-manager.php b/includes/class-wcs-renewal-cart-stock-manager.php new file mode 100755 index 0000000..3324822 --- /dev/null +++ b/includes/class-wcs-renewal-cart-stock-manager.php @@ -0,0 +1,201 @@ +get_items() as $line_item ) { + $product = $line_item->get_product(); + + if ( ! $product ) { + continue; + } + + // Use the stock managed product in case we have a variation product which is managed on the variable (parent level) + $stock_managed_product = wc_get_product( $product->get_stock_managed_by_id() ); + + // Account for stock which is being held by other unpaid orders. + $held_stock = ( (int) get_option( 'woocommerce_hold_stock_minutes', 0 ) > 0 ) ? wc_get_held_stock_quantity( $product, $order->get_id() ) : 0; + $required_stock = wcs_get_total_line_item_product_quantity( $order, $stock_managed_product ); + + if ( ! $product->is_in_stock() || ( $required_stock + $held_stock ) > $stock_managed_product->get_stock_quantity() ) { + add_filter( 'woocommerce_product_is_in_stock', array( __CLASS__, 'adjust_is_in_stock' ), 10, 2 ); + add_filter( 'woocommerce_product_backorders_allowed', array( __CLASS__, 'adjust_backorder_status' ), 10, 3 ); + break; + } + } + } + + /** + * Adjusts the stock status of a product that is an out-of-stock renewal. + * + * @since 2.6.0 + * + * @param bool $is_in_stock Whether the product is in stock or not + * @param WC_Product $product The product which stock is being checked + * + * @return bool $is_in_stock + */ + public static function adjust_is_in_stock( $is_in_stock, $product ) { + if ( ! $is_in_stock ) { + $is_in_stock = self::cart_contains_renewal_to_product( $product ); + } + + return $is_in_stock; + + } + + /** + * Adjusts whether backorders are allowed so out-of-stock renewal item products bypass stock validation. + * + * @since 2.6.0 + * + * @param bool $backorders_allowed If the product has backorders enabled. + * @param int $product_id The product ID. + * @param WC_Product $product The product on which stock management is being changed. + * + * @return bool $backorders_allowed Whether backorders are allowed. + */ + public static function adjust_backorder_status( $backorders_allowed, $product_id, $product ) { + if ( ! $backorders_allowed ) { + $backorders_allowed = self::cart_contains_renewal_to_product( $product ); + } + + return $backorders_allowed; + } + + /** + * Removes the filters that adjust stock on out of stock renewals items. + * + * @since 2.6.0 + */ + public static function remove_filters() { + remove_filter( 'woocommerce_product_is_in_stock', array( __CLASS__, 'adjust_is_in_stock' ) ); + remove_filter( 'woocommerce_product_backorders_allowed', array( __CLASS__, 'adjust_backorder_status' ) ); + } + + /** + * Determines if the cart contains a renewal order with a specific product. + * + * @since 2.6.0 + * @param WC_Product $product The product object to look for. + * @return bool Whether the cart contains a renewal order to the given product. + */ + protected static function cart_contains_renewal_to_product( $product ) { + $cart_contains_renewal_to_product = false; + $renewal_order = self::get_order_from_cart(); + + if ( ! $renewal_order ) { + $renewal_order = self::get_order_from_query_vars(); + } + + if ( $renewal_order && wcs_order_contains_product( $renewal_order, $product ) ) { + $cart_contains_renewal_to_product = true; + } + + return $cart_contains_renewal_to_product; + } + + /** + * Gets the renewal order from the cart. + * + * @since 2.6.0 + * @return WC_Order|bool Renewal order obtained from the cart contents or false if the cart doesn't contain a renewal order. + */ + protected static function get_order_from_cart() { + $renewal_order = false; + $cart_item = wcs_cart_contains_renewal(); + + if ( false !== $cart_item && isset( $cart_item['subscription_renewal']['renewal_order_id'] ) ) { + $renewal_order = wc_get_order( $cart_item['subscription_renewal']['renewal_order_id'] ); + } + + return $renewal_order; + } + + /** + * Gets the renewal order from order-pay query vars. + * + * @since 2.6.0 + * @return WC_Order|bool Renewal order obtained from query vars or false if not set. + */ + protected static function get_order_from_query_vars() { + global $wp; + $renewal_order = false; + + if ( isset( $wp->query_vars['order-pay'] ) ) { + $order = wc_get_order( $wp->query_vars['order-pay'] ); + + if ( wcs_order_contains_renewal( $order ) ) { + $renewal_order = $order; + } + } + + return $renewal_order; + } +} diff --git a/includes/class-wcs-staging.php b/includes/class-wcs-staging.php index eab8f07..08e02c5 100755 --- a/includes/class-wcs-staging.php +++ b/includes/class-wcs-staging.php @@ -15,6 +15,7 @@ class WCS_Staging { add_action( 'woocommerce_generated_manual_renewal_order', array( __CLASS__, 'maybe_record_staging_site_renewal' ) ); add_filter( 'woocommerce_register_post_type_subscription', array( __CLASS__, 'maybe_add_menu_badge' ) ); add_action( 'wp_loaded', array( __CLASS__, 'maybe_reset_admin_notice' ) ); + add_action( 'woocommerce_admin_order_data_after_billing_address', array( __CLASS__, 'maybe_add_payment_method_note' ) ); } /** @@ -67,4 +68,30 @@ class WCS_Staging { wp_safe_redirect( remove_query_arg( array( 'wcs_display_staging_notice' ) ) ); } } + + /** + * Displays a note under the edit subscription payment method field to explain why the subscription is set to Manual Renewal. + * + * @param WC_Subscription $subscription + * @since 2.6.0 + */ + public static function maybe_add_payment_method_note( $subscription ) { + if ( wcs_is_subscription( $subscription ) && WC_Subscriptions::is_duplicate_site() && $subscription->has_payment_gateway() && ! $subscription->get_requires_manual_renewal() ) { + printf( + '

%s

', + esc_html__( 'Subscription locked to Manual Renewal while the store is in staging mode. Payment method changes will take effect in live mode.', 'woocommerce-subscriptions' ) + ); + } + } + + /** + * Returns the content for a tooltip explaining a subscription's payment method while in staging mode. + * + * @param WC_Subscription $subscription + * @return string HTML content for a tooltip. + * @since 2.6.0 + */ + public static function get_payment_method_tooltip( $subscription ) { + return '
'; + } } diff --git a/includes/class-wcs-switch-cart-item.php b/includes/class-wcs-switch-cart-item.php new file mode 100755 index 0000000..236f5ce --- /dev/null +++ b/includes/class-wcs-switch-cart-item.php @@ -0,0 +1,413 @@ +cart_item = $cart_item; + $this->subscription = $subscription; + $this->existing_item = $existing_item; + $this->canonical_product_id = wcs_get_canonical_product_id( $cart_item ); + $this->product = $cart_item['data']; + $this->next_payment_timestamp = $cart_item['subscription_switch']['next_payment_timestamp']; + $this->end_timestamp = wcs_date_to_time( WC_Subscriptions_Product::get_expiration_date( $this->canonical_product_id, $this->subscription->get_date( 'last_order_date_created' ) ) ); + } + + /** Getters */ + + /** + * Gets the number of days until the next payment. + * + * @since 2.6.0 + * @return int + */ + public function get_days_until_next_payment() { + if ( ! isset( $this->days_until_next_payment ) ) { + $this->days_until_next_payment = ceil( ( $this->next_payment_timestamp - gmdate( 'U' ) ) / DAY_IN_SECONDS ); + } + + return $this->days_until_next_payment; + } + + /** + * Gets the number of days in the old billing cycle. + * + * @since 2.6.0 + * @return int + */ + public function get_days_in_old_cycle() { + if ( ! isset( $this->days_in_old_cycle ) ) { + $this->days_in_old_cycle = $this->calculate_days_in_old_cycle(); + } + + return $this->days_in_old_cycle; + } + + /** + * Gets the old subscription's price per day. + * + * @since 2.6.0 + * @return float + */ + public function get_old_price_per_day() { + if ( ! isset( $this->old_price_per_day ) ) { + $days_in_old_cycle = $this->get_days_in_old_cycle(); + + $total_paid_for_current_period = $this->get_total_paid_for_current_period(); + + $old_price_per_day = $days_in_old_cycle > 0 ? $total_paid_for_current_period / $days_in_old_cycle : $total_paid_for_current_period; + $this->old_price_per_day = apply_filters( 'wcs_switch_proration_old_price_per_day', $old_price_per_day, $this->subscription, $this->cart_item, $total_paid_for_current_period, $days_in_old_cycle ); + } + + return $this->old_price_per_day; + } + + /** + * Gets the number of days in the new billing cycle. + * + * @since 2.6.0 + * @return int + */ + public function get_days_in_new_cycle() { + if ( ! isset( $this->days_in_new_cycle ) ) { + $this->days_in_new_cycle = $this->calculate_days_in_new_cycle(); + } + + return $this->days_in_new_cycle; + } + + /** + * Gets the number of days in the new billing cycle. + * + * @since 2.6.0 + * @return float + */ + public function get_new_price_per_day() { + if ( ! isset( $this->new_price_per_day ) ) { + $days_in_new_cycle = $this->get_days_in_new_cycle(); + + if ( $this->is_switch_during_trial() && $this->trial_periods_match() ) { + $new_price_per_day = 0; + } else { + // We need to use the cart items price to ensure we include extras added by extensions like Product Add-ons, but we don't want the sign-up fee accounted for in the price, so make sure WC_Subscriptions_Cart::set_subscription_prices_for_calculation() isn't adding that. + remove_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100 ); + $new_price_per_day = ( WC_Subscriptions_Product::get_price( $this->product ) * $this->cart_item['quantity'] ) / $days_in_new_cycle; + add_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100, 2 ); + } + + $this->new_price_per_day = apply_filters( 'wcs_switch_proration_new_price_per_day', $new_price_per_day, $this->subscription, $this->cart_item, $days_in_new_cycle ); + } + + return $this->new_price_per_day; + } + + /** + * Gets the subscription's last order paid time. + * + * @since 2.6.0 + * @return int The paid timestamp of the subscription's last non-early renewal or parent order. If none of those are present, the subscription's start time will be returned. + */ + public function get_last_order_paid_time() { + if ( ! isset( $this->last_order_paid_time ) ) { + $last_order = wcs_get_last_non_early_renewal_order( $this->subscription ); + + // If there haven't been any non-early renewals yet, use the parent + if ( ! $last_order ) { + $last_order = $this->subscription->get_parent(); + } + + // If there aren't any renewals or a parent order, use the subscription's created date. + if ( ! $last_order ) { + $this->last_order_paid_time = $this->subscription->get_time( 'start' ); + } else { + $order_date = $last_order->get_date_paid(); + + // If the order hasn't been paid, use the created date. This shouldn't occur because only active (paid) subscriptions can be switched. However, we provide a fallback just in case. + if ( ! $order_date ) { + $order_date = $last_order->get_date_created(); + } + + $this->last_order_paid_time = $order_date->getTimestamp(); + } + } + + return $this->last_order_paid_time; + } + + /** + * Gets the total paid for the existing item (@see $this->existing_item) in early renewals and switch orders since the last non-early renewal or parent order. + * + * @since 2.6.0 + * @return float + */ + public function get_total_paid_for_current_period() { + if ( ! isset( $this->total_paid_for_current_period ) ) { + $this->total_paid_for_current_period = WC_Subscriptions_Switcher::calculate_total_paid_since_last_order( $this->subscription, $this->existing_item, 'exclude_sign_up_fees' ); + } + + return $this->total_paid_for_current_period; + } + + /** + * Gets the number of days since the last payment. + * + * @since 2.6.0 + * @return int The number of days since the last non-early renewal or parent payment - rounded down. + */ + public function get_days_since_last_payment() { + if ( ! isset( $this->days_since_last_payment ) ) { + // Use the timestamp for the last non-early renewal order or parent order to avoid date miscalculations which early renewing creates. + $this->days_since_last_payment = floor( ( gmdate( 'U' ) - $this->get_last_order_paid_time() ) / DAY_IN_SECONDS ); + } + + return $this->days_since_last_payment; + } + + /** + * Gets the switch type. + * + * @since 2.6.0 + * @return string Can be upgrade, downgrade or crossgrade. + */ + public function get_switch_type() { + if ( ! isset( $this->switch_type ) ) { + $old_price_per_day = $this->get_old_price_per_day(); + $new_price_per_day = $this->get_new_price_per_day(); + + if ( $old_price_per_day < $new_price_per_day ) { + $switch_type = 'upgrade'; + } elseif ( $old_price_per_day > $new_price_per_day && $new_price_per_day >= 0 ) { + $switch_type = 'downgrade'; + } else { + $switch_type = 'crossgrade'; + } + + $switch_type = apply_filters( 'wcs_switch_proration_switch_type', $switch_type, $this->subscription, $this->cart_item, $old_price_per_day, $new_price_per_day ); + + if ( ! in_array( $switch_type, array( 'upgrade', 'downgrade', 'crossgrade' ) ) ) { + throw new UnexpectedValueException( sprintf( __( 'Invalid switch type "%s". Switch must be one of: "upgrade", "downgrade" or "crossgrade".', 'woocommerce-subscriptions' ), $switch_type ) ); + } + + $this->switch_type = $switch_type; + } + + return $this->switch_type; + } + + /** Calculator functions */ + + /** + * Calculates the number of days in the old cycle. + * + * @since 2.6.0 + * @return int + */ + public function calculate_days_in_old_cycle() { + $method_to_use = 'days_between_payments'; + + // If the subscription contains a synced product and the next payment is actually the first payment, determine the days in the "old" cycle from the subscription object + if ( WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $this->subscription ) ) { + $first_synced_payment = WC_Subscriptions_Synchroniser::calculate_first_payment_date( wc_get_product( $this->canonical_product_id ) , 'timestamp', $this->subscription->get_date( 'start' ) ); + + if ( $first_synced_payment === $this->next_payment_timestamp ) { + $method_to_use = 'days_in_billing_cycle'; + } + } + + // We need the product's billing cycle, not the trial length if the customer hasn't paid anything and it's still on trial. + if ( $this->is_switch_during_trial() && 0 === $this->get_total_paid_for_current_period() ) { + $method_to_use = 'days_in_billing_cycle'; + } + + // Find the number of days between the last payment and the next + if ( 'days_between_payments' === $method_to_use ) { + $days_in_old_cycle = round( ( $this->next_payment_timestamp - $this->get_last_order_paid_time() ) / DAY_IN_SECONDS ); + } else { + $days_in_old_cycle = wcs_get_days_in_cycle( $this->subscription->get_billing_period(), $this->subscription->get_billing_interval() ); + } + + return apply_filters( 'wcs_switch_proration_days_in_old_cycle', $days_in_old_cycle, $this->subscription, $this->cart_item ); + } + + /** + * Calculates the number of days in the new cycle. + * + * @since 2.6.0 + * @return int + */ + public function calculate_days_in_new_cycle() { + $last_order_time = $this->get_last_order_paid_time(); + $new_billing_period = WC_Subscriptions_Product::get_period( $this->product ); + $new_billing_interval = WC_Subscriptions_Product::get_interval( $this->product ); + + // Calculate the number of days in the new cycle by finding what the renewal date would have been if the customer purchased the (new) product at the last payment date. + // This gives us the most accurate number of days in the new cycle and a value that is similar to the number of days in the old cycle which is usually calculated by the the number of days between the last order and the next payment date. + $days_in_new_cycle = ( wcs_add_time( $new_billing_interval, $new_billing_period, $last_order_time ) - $last_order_time ) / DAY_IN_SECONDS; + + // Find if the days in new cycle match the days in the old cycle,ignoring any rounding. + $days_in_old_cycle = $this->get_days_in_old_cycle(); + $days_in_new_and_old_cycle_match = ceil( $days_in_new_cycle ) == $days_in_old_cycle || floor( $days_in_new_cycle ) == $days_in_old_cycle; + + // Set the days in each cycle to match if they are equal (ignoring any rounding discrepancy) or if the subscription is switched during a trial and has a matching trial period. + if ( $days_in_new_and_old_cycle_match || ( $this->is_switch_during_trial() && $this->trial_periods_match() ) ) { + $days_in_new_cycle = $days_in_old_cycle; + } + + return apply_filters( 'wcs_switch_proration_days_in_new_cycle', $days_in_new_cycle, $this->subscription, $this->cart_item, $days_in_old_cycle ); + } + + /** Helper functions */ + + /** + * Determines whether the new product is virtual or not. + * + * @since 2.6.0 + * @return bool + */ + public function is_virtual_product() { + return $this->product->is_virtual(); + } + + /** + * Determines whether the new product's trial period matches the old product's trial period. + * + * @since 2.6.0 + * @return bool + */ + public function trial_periods_match() { + $existing_product = $this->existing_item->get_product(); + + /** + * We need to cast the returned trial lengths as sometimes they may be strings. + * We also need to pass the new product's ID so the raw product's trial is used, not the filtered trial set by @see WC_Subscriptions_Switcher::maybe_unset_free_trial() && WC_Subscriptions_Switcher::maybe_set_free_trial(). + */ + $matching_length = (int) WC_Subscriptions_Product::get_trial_length( $this->product->get_id() ) === (int) WC_Subscriptions_Product::get_trial_length( $existing_product ); + $matching_period = WC_Subscriptions_Product::get_trial_period( $this->product->get_id() ) === WC_Subscriptions_Product::get_trial_period( $existing_product ); + + return $matching_period && $matching_length; + } + + /** + * Determines whether the switch is happening while the subscription is still on trial. + * + * @since 2.6.0 + * @return bool + */ + public function is_switch_during_trial() { + return $this->subscription->get_time( 'trial_end' ) > gmdate( 'U' ); + } +} diff --git a/includes/class-wcs-switch-totals-calculator.php b/includes/class-wcs-switch-totals-calculator.php new file mode 100755 index 0000000..d8db18e --- /dev/null +++ b/includes/class-wcs-switch-totals-calculator.php @@ -0,0 +1,510 @@ +cart = $cart; + $this->load_settings(); + } + + /** + * Loads the store's switch settings. + * + * @since 2.6.0 + */ + protected function load_settings() { + $this->apportion_recurring_price = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'no' ); + $this->apportion_sign_up_fee = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'no' ); + $this->apportion_length = get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'no' ); + $this->prices_include_tax = 'yes' === get_option( 'woocommerce_prices_include_tax' ); + } + + /** + * Calculates the upgrade cost, and next payment dates for switch cart items. + * + * @since 2.6.0 + */ + public function calculate_prorated_totals() { + foreach ( $this->get_switches_from_cart() as $cart_item_key => $switch_item ) { + $this->set_first_payment_timestamp( $cart_item_key, $switch_item->next_payment_timestamp ); + $this->set_end_timestamp( $cart_item_key, $switch_item->end_timestamp ); + + $this->apportion_sign_up_fees( $switch_item ); + + $switch_type = $switch_item->get_switch_type(); + $this->set_switch_type_in_cart( $cart_item_key, $switch_type ); + + if ( $this->should_prorate_recurring_price( $switch_item ) ) { + if ( 'upgrade' === $switch_type ) { + if ( $this->should_reduce_prepaid_term( $switch_item ) ) { + $this->reduce_prepaid_term( $cart_item_key, $switch_item ); + } else { + // Reset any previously calculated prorated price so we don't double the amounts + $this->reset_prorated_price( $switch_item ); + + $upgrade_cost = $this->calculate_upgrade_cost( $switch_item ); + $this->set_upgrade_cost( $switch_item, $upgrade_cost ); + } + } elseif ( 'downgrade' === $switch_type && $this->should_extend_prepaid_term() ) { + $this->extend_prepaid_term( $cart_item_key, $switch_item ); + } + + // Set a flag if the prepaid term has been adjusted. + if ( $this->get_first_payment_timestamp( $cart_item_key ) !== $switch_item->next_payment_timestamp ) { + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['recurring_payment_prorated'] = true; + } + } + + if ( $this->should_apportion_length( $switch_item ) ) { + $this->apportion_length( $switch_item ); + } + + if ( defined( 'WCS_DEBUG' ) && WCS_DEBUG && ! wcs_doing_ajax() ) { + $this->log_switch( $switch_item ); + } + + // Cache the calculated switched item so we can log it later. + $this->calculated_switch_items[ $cart_item_key ] = $switch_item; + } + } + + /** + * Gets all the switch items in the cart as instances of @see WCS_Switch_Cart_Item. + * + * @since 2.6.0 + * @return WCS_Switch_Cart_Item[] + */ + protected function get_switches_from_cart() { + $switches = array(); + + foreach ( $this->cart->get_cart() as $cart_item_key => $cart_item ) { + + // This item may not exist if its linked to an item that got removed with 'remove_cart_item' below. + if ( empty( $this->cart->cart_contents[ $cart_item_key ] ) ) { + continue; + } + + if ( ! isset( $cart_item['subscription_switch']['subscription_id'] ) ) { + continue; + } + + $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); + + if ( empty( $subscription ) ) { + $this->cart->remove_cart_item( $cart_item_key ); + continue; + } + + if ( ! empty( $cart_item['subscription_switch']['item_id'] ) ) { + + $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); + + if ( empty( $existing_item ) ) { + $this->cart->remove_cart_item( $cart_item_key ); + continue; + } + + $switches[ $cart_item_key ] = new WCS_Switch_Cart_Item( $cart_item, $subscription, $existing_item ); + } else { + $switches[ $cart_item_key ] = new WCS_Add_Cart_Item( $cart_item, $subscription ); + } + } + + return $switches; + } + + /** Logic Functions */ + + /** + * Determines whether the recurring price should be prorated based on the store's switch settings. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + * @return bool + */ + protected function should_prorate_recurring_price( $switch_item ) { + $prorate_all = in_array( $this->apportion_recurring_price, array( 'yes', 'yes-upgrade' ) ); + $prorate_virtual = in_array( $this->apportion_recurring_price, array( 'virtual', 'virtual-upgrade' ) ); + + return $prorate_all || ( $prorate_virtual && $switch_item->is_virtual_product() ); + } + + /** + * Determines whether the current subscription's prepaid term should reduced. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + * @return bool + */ + protected function should_reduce_prepaid_term( $switch_item ) { + $days_in_old_cycle = $switch_item->get_days_in_old_cycle(); + $days_in_new_cycle = $switch_item->get_days_in_new_cycle(); + + $is_switch_out_of_trial = 0 == $switch_item->get_total_paid_for_current_period() && ! $switch_item->trial_periods_match() && $switch_item->is_switch_during_trial(); + + /** + * Allow third-parties to filter whether to reduce the prepaid term or not. + * + * By default, reduce the prepaid term if: + * - The customer is leaving a free trial, this occurs if: + * - The subscription is still on trial, + * - The customer hasn't paid anything in sign-up fees or early renewals since sign-up. + * - The old trial period and length doesn't match the new one. + * - Or there are more days in the in old cycle as there are in the in new cycle (for example switching from yearly to monthly) + * + * @param bool Whether the switch should reduce the current subscription's prepaid term. + * @param WC_Subscription $switch_item->subscription The subscription being switched. + * @param array $switch_item->cart_item The cart item recording the switch. + * @param int $days_in_old_cycle The number of days in the current subscription's billing cycle. + * @param int $days_in_new_cycle The number of days in the new product's billing cycle. + * @param float $switch_item->get_old_price_per_day() The current subscription's price per day. + * @param float $switch_item->get_new_price_per_day() The new product's price per day. + */ + return apply_filters( 'wcs_switch_proration_reduce_pre_paid_term', $is_switch_out_of_trial || $days_in_old_cycle > $days_in_new_cycle, $switch_item->subscription, $switch_item->cart_item, $days_in_old_cycle, $days_in_new_cycle, $switch_item->get_old_price_per_day(), $switch_item->get_new_price_per_day() ); + } + + /** + * Determines whether the current subscription's prepaid term should extended based on the store's switch settings. + * + * @since 2.6.0 + * @return bool + */ + protected function should_extend_prepaid_term() { + return in_array( $this->apportion_recurring_price, array( 'virtual', 'yes' ) ); + } + + /** + * Determines whether the subscription length should be apportioned based on the store's switch settings and product type. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + * @return bool + */ + protected function should_apportion_length( $switch_item ) { + return 'yes' == $this->apportion_length || ( 'virtual' == $this->apportion_length && $switch_item->is_virtual_product() ); + } + + /** Total Calculators */ + + /** + * Apportions any sign-up fees if required. + * + * Implements the store's apportion sign-up fee setting (@see $this->apportion_sign_up_fee). + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + */ + protected function apportion_sign_up_fees( $switch_item ) { + if ( 'no' === $this->apportion_sign_up_fee ) { + $switch_item->product->update_meta_data( '_subscription_sign_up_fee', 0 ); + } elseif ( $switch_item->existing_item && 'yes' === $this->apportion_sign_up_fee ) { + $product = wc_get_product( $switch_item->canonical_product_id ); + + // Make sure we get a fresh copy of the product's meta to avoid prorating an already prorated sign-up fee + $product->read_meta_data( true ); + + // Because product add-ons etc. don't apply to sign-up fees, it's safe to use the product's sign-up fee value rather than the cart item's + $sign_up_fee_due = WC_Subscriptions_Product::get_sign_up_fee( $product ); + $sign_up_fee_paid = $switch_item->subscription->get_items_sign_up_fee( $switch_item->existing_item, $this->prices_include_tax ? 'inclusive_of_tax' : 'exclusive_of_tax' ); + + // Make sure total prorated sign-up fee is prorated across total amount of sign-up fee so that customer doesn't get extra discounts + if ( $switch_item->cart_item['quantity'] > $switch_item->existing_item['qty'] ) { + $sign_up_fee_paid = ( $sign_up_fee_paid * $switch_item->existing_item['qty'] ) / $switch_item->cart_item['quantity']; + } + + $switch_item->product->update_meta_data( '_subscription_sign_up_fee', max( $sign_up_fee_due - $sign_up_fee_paid, 0 ) ); + $switch_item->product->update_meta_data( '_subscription_sign_up_fee_prorated', WC_Subscriptions_Product::get_sign_up_fee( $switch_item->product ) ); + } + } + + /** + * Calculates the number of days the customer is entitled to at the new product's price per day and reduce the subscription's prepaid term to match. + * + * @since 2.6.0 + * @param string $cart_item_key + * @param WCS_Switch_Cart_Item $switch_item + */ + protected function reduce_prepaid_term( $cart_item_key, $switch_item ) { + // 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 = $this->calculate_pre_paid_days( $switch_item->get_total_paid_for_current_period(), $switch_item->get_new_price_per_day() ); + + // 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 ( $switch_item->get_days_since_last_payment() < $pre_paid_days ) { + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $switch_item->get_last_order_paid_time() + ( $pre_paid_days * DAY_IN_SECONDS ); + } else { + // If the total amount the customer has paid entitles her to the same or fewer days at the new price then start the new subscription from today + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = 0; + } + } + + /** + * Calculates the upgrade cost for a given switch. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + * @return float The amount to pay for the upgrade. + */ + protected function calculate_upgrade_cost( $switch_item ) { + $extra_to_pay = $switch_item->get_days_until_next_payment() * ( $switch_item->get_new_price_per_day() - $switch_item->get_old_price_per_day() ); + + // When calculating a subscription with one length (no more next payment date and the end date may have been pushed back) we need to pay for those extra days at the new price per day between the old next payment date and new end date + if ( ! $switch_item->is_switch_during_trial() && 1 == WC_Subscriptions_Product::get_length( $switch_item->product ) ) { + $days_to_new_end = floor( ( $switch_item->end_timestamp - $switch_item->next_payment_timestamp ) / DAY_IN_SECONDS ); + + if ( $days_to_new_end > 0 ) { + $extra_to_pay += $days_to_new_end * $switch_item->get_new_price_per_day(); + } + } + + // We need to find the per item extra to pay so we can set it as the sign-up fee (WC will then multiply it by the quantity) + $extra_to_pay = $extra_to_pay / $switch_item->cart_item['quantity']; + return apply_filters( 'wcs_switch_proration_extra_to_pay', $extra_to_pay, $switch_item->subscription, $switch_item->cart_item, $switch_item->get_days_in_old_cycle() ); + } + + /** + * Calculates the number of days that have already been paid. + * + * @since 2.6.0 + * @param int $old_total_paid The amount paid previously, such as the old recurring total + * @param int $new_price_per_day The amount per day price for the new subscription + * @return int $pre_paid_days The number of days paid for already + */ + protected function calculate_pre_paid_days( $old_total_paid, $new_price_per_day ) { + $pre_paid_days = 0; + + if ( 0 != $new_price_per_day ) { + // PHP says you cannot trust floats (http://php.net/float), and they do not lie. A calculation of 25/(25/31) doesn't equal 31. It equals 31.000000000000004. + // This is then rounded up to 32 :see-no-evil:. To get around this, round the result of the division to 8 decimal places. This should be more than enough. + $pre_paid_days = ceil( round( $old_total_paid / $new_price_per_day, 8 ) ); + } + + return $pre_paid_days; + } + + /** + * Calculates the number of days the customer is owed at the new product's price per day and extend the subscription's prepaid term accordingly. + * + * @since 2.6.0 + * @param string $cart_item_key + * @param WCS_Switch_Cart_Item $switch_item + */ + protected function extend_prepaid_term( $cart_item_key, $switch_item ) { + $amount_still_owing = $switch_item->get_old_price_per_day() * $switch_item->get_days_until_next_payment(); + + // Find how many more days at the new lower price it takes to exceed the amount owed + $days_to_add = $this->calculate_pre_paid_days( $amount_still_owing, $switch_item->get_new_price_per_day() ); + + $days_to_add -= $switch_item->get_days_until_next_payment(); + + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $switch_item->next_payment_timestamp + ( $days_to_add * DAY_IN_SECONDS ); + } + + /** + * Calculates the new subscription's remaining length based on the expected number of payments and the number of payments which have already occurred. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + */ + protected function apportion_length( $switch_item ) { + $base_length = WC_Subscriptions_Product::get_length( $switch_item->canonical_product_id ); + $completed_payments = $switch_item->subscription->get_payment_count(); + $length_remaining = $base_length - $completed_payments; + + // Default to the base length if more payments have already been made than this subscription requires + if ( $length_remaining <= 0 ) { + $length_remaining = $base_length; + } + + $switch_item->product->update_meta_data( '_subscription_length', $length_remaining ); + } + + /** Setters */ + + /** + * Sets the first payment timestamp on the cart item. + * + * @since 2.6.0 + * @param string $cart_item_key The cart item key. + * @param int $first_payment_timestamp The first payment timestamp. + */ + public function set_first_payment_timestamp( $cart_item_key, $first_payment_timestamp ) { + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp'] = $first_payment_timestamp; + } + + /** + * Sets the end timestamp on the cart item. + * + * @since 2.6.0 + * @param string $cart_item_key The cart item key. + * @param int $end_timestamp The subscription's end date timestamp. + */ + public function set_end_timestamp( $cart_item_key, $end_timestamp ) { + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['end_timestamp'] = $end_timestamp; + } + + /** + * Sets the switch type on the cart item. + * + * To preserve past tense for backward compatibility 'd' will be appended to the $switch_type. + * + * @since 2.6.0 + * @param string $cart_item_key The cart item's key. + * @param string $switch_type Can be upgrade, downgrade or crossgrade. + */ + public function set_switch_type_in_cart( $cart_item_key, $switch_type ) { + $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['upgraded_or_downgraded'] = sprintf( '%sd', $switch_type ); + } + + /** + * Resets any previously calculated prorated price. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + */ + public function reset_prorated_price( $switch_item ) { + if ( $switch_item->product->meta_exists( '_subscription_price_prorated' ) ) { + $prorated_sign_up_fee = $switch_item->product->get_meta( '_subscription_sign_up_fee_prorated' ); + $switch_item->product->update_meta_data( '_subscription_sign_up_fee', $prorated_sign_up_fee ); + } + } + + /** + * Sets the upgrade cost on the cart item product instance as a sign up fee. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + * @param float $extra_to_pay The upgrade cost. + */ + public function set_upgrade_cost( $switch_item, $extra_to_pay ) { + // Keep a record of the original sign-up fees + $existing_sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $switch_item->product ); + $switch_item->product->update_meta_data( '_subscription_sign_up_fee_prorated', $existing_sign_up_fee ); + + $switch_item->product->update_meta_data( '_subscription_price_prorated', $extra_to_pay ); + $switch_item->product->update_meta_data( '_subscription_sign_up_fee', $existing_sign_up_fee + $extra_to_pay ); + } + + /** Getters */ + + /** + * Gets the first payment timestamp. + * + * @since 2.6.0 + * @param string $cart_item_key The cart item's key. + * @return int + */ + protected function get_first_payment_timestamp( $cart_item_key ) { + return $this->cart->cart_contents[ $cart_item_key ]['subscription_switch']['first_payment_timestamp']; + } + + /** Helpers */ + + /** + * Logs the switch item data to the wcs-switch-cart-items file. + * + * @since 2.6.0 + * @param WCS_Switch_Cart_Item $switch_item + */ + protected function log_switch( $switch_item ) { + static $logger = null; + static $items_logged = array(); // A cache of the switch items already logged in this request. Prevents multiple log entries for the same item. + $messages = array(); + + if ( ! $logger ) { + $logger = wc_get_logger(); + } + + $messages[] = sprintf( 'Switch details for subscription #%s (%s):', $switch_item->subscription->get_id(), $switch_item->existing_item ? $switch_item->existing_item->get_id() : 'new item' ); + + foreach ( $switch_item as $property => $value ) { + if ( is_scalar( $value ) ) { + $messages[ $property ] = "$property: $value"; + } + } + + // Prevent logging the same switch item to the log in the same request. + $key = md5( serialize( $messages ) ); + + if ( ! isset( $items_logged[ $key ] ) ) { + // Add a separator to the bottom of the log entry. + $messages[] = str_repeat( '=', 60 ) . PHP_EOL; + $items_logged[ $key ] = 1; + + $logger->info( implode( PHP_EOL, $messages ), array( 'source' => 'wcs-switch-cart-items' ) ); + } + } + + /** + * Logs information about all the calculated switches currently in the cart. + * + * @since 2.6.0 + */ + public function log_switches() { + foreach ( $this->calculated_switch_items as $switch_item ) { + $this->log_switch( $switch_item ); + } + } +} diff --git a/includes/class-wcs-template-loader.php b/includes/class-wcs-template-loader.php index 4a2000d..a668c08 100755 --- a/includes/class-wcs-template-loader.php +++ b/includes/class-wcs-template-loader.php @@ -12,6 +12,7 @@ class WCS_Template_Loader { add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'get_subscription_details_template' ) ); add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_subscription_totals_template' ) ); add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_order_downloads_template' ), 20 ); + add_action( 'woocommerce_subscription_totals', array( __CLASS__, 'get_subscription_totals_table_template' ), 10, 4 ); } /** @@ -62,4 +63,29 @@ class WCS_Template_Loader { wc_get_template( 'order/order-downloads.php', array( 'downloads' => $subscription->get_downloadable_items(), 'show_title' => true ) ); } } + + /** + * Gets the subscription totals table. + * + * @since 2.6.0 + * + * @param WC_Subscription $subscription The subscription to print the totals table for. + * @param bool $include_item_removal_links Whether the remove line item links should be included. + * @param array $totals The subscription totals rows to be displayed. + * @param bool $include_switch_links Whether the line item switch links should be included. + */ + public static function get_subscription_totals_table_template( $subscription, $include_item_removal_links, $totals, $include_switch_links = true ) { + + // If the switch links shouldn't be printed, remove the callback which prints them. + if ( false === $include_switch_links ) { + $callback_detached = remove_action( 'woocommerce_order_item_meta_end', 'WC_Subscriptions_Switcher::print_switch_link' ); + } + + wc_get_template( 'myaccount/subscription-totals-table.php', array( 'subscription' => $subscription, 'allow_item_removal' => $include_item_removal_links, 'totals' => $totals ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); + + // Reattach the callback if it was successfully removed. + if ( false === $include_switch_links && $callback_detached ) { + add_action( 'woocommerce_order_item_meta_end', 'WC_Subscriptions_Switcher::print_switch_link', 10, 3 ); + } + } } diff --git a/includes/data-stores/class-wcs-related-order-store-cpt.php b/includes/data-stores/class-wcs-related-order-store-cpt.php index 58ac967..c765aeb 100755 --- a/includes/data-stores/class-wcs-related-order-store-cpt.php +++ b/includes/data-stores/class-wcs-related-order-store-cpt.php @@ -60,16 +60,11 @@ class WCS_Related_Order_Store_CPT extends WCS_Related_Order_Store { * @return array */ public function get_related_order_ids( WC_Order $subscription, $relation_type ) { - $related_order_ids = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'shop_order', 'post_status' => 'any', 'fields' => 'ids', - 'orderby' => array( - 'date' => 'DESC', - 'ID' => 'DESC', - ), 'meta_query' => array( array( 'key' => $this->get_meta_key( $relation_type ), @@ -81,6 +76,8 @@ class WCS_Related_Order_Store_CPT extends WCS_Related_Order_Store { 'update_post_term_cache' => false, ) ); + rsort( $related_order_ids ); + return $related_order_ids; } diff --git a/includes/early-renewal/class-wcs-cart-early-renewal.php b/includes/early-renewal/class-wcs-cart-early-renewal.php index 446939a..41a22e7 100755 --- a/includes/early-renewal/class-wcs-cart-early-renewal.php +++ b/includes/early-renewal/class-wcs-cart-early-renewal.php @@ -68,7 +68,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { $actions['subscription_renewal_early'] = array( 'url' => wcs_get_early_renewal_url( $subscription ), - 'name' => __( 'Renew Now', 'woocommerce-subscriptions' ), + 'name' => __( 'Renew now', 'woocommerce-subscriptions' ), ); } @@ -108,7 +108,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { 'subscription_id' => $subscription->get_id(), 'subscription_renewal_early' => true, 'renewal_order_id' => $subscription->get_id(), - ) ); + ), 'all_items_required' ); do_action( 'wcs_after_early_renewal_setup_cart_subscription', $subscription ); @@ -220,37 +220,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { return; } - $next_payment_time = $subscription->get_time( 'next_payment' ); - $dates_to_update = array(); - - if ( $next_payment_time > 0 && $next_payment_time > current_time( 'timestamp', true ) ) { - $next_payment_timestamp = wcs_add_time( $subscription->get_billing_interval(), $subscription->get_billing_period(), $next_payment_time ); - - if ( $subscription->get_time( 'end' ) === 0 || $next_payment_timestamp < $subscription->get_time( 'end' ) ) { - $dates_to_update['next_payment'] = gmdate( 'Y-m-d H:i:s', $next_payment_timestamp ); - } else { - // Delete the next payment date if the calculated next payment date occurs after the end date. - $dates_to_update['next_payment'] = 0; - } - } elseif ( $subscription->get_time( 'end' ) > 0 ) { - $dates_to_update['end'] = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription->get_billing_interval(), $subscription->get_billing_period(), $subscription->get_time( 'end' ) ) ); - } - - if ( ! empty( $dates_to_update ) ) { - - $order_number = sprintf( _x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), $order->get_order_number() ); - $order_link = sprintf( '
%s', esc_url( wcs_get_edit_post_link( $order->get_id() ) ), $order_number ); - - try { - $subscription->update_dates( $dates_to_update ); - - // translators: placeholder contains a link to the order's edit screen. - $subscription->add_order_note( sprintf( __( 'Customer successfully renewed early with order %s.', 'woocommerce-subscriptions' ), $order_link ) ); - } catch ( Exception $e ) { - // translators: placeholder contains a link to the order's edit screen. - $subscription->add_order_note( sprintf( __( 'Failed to update subscription dates after customer renewed early with order %s.', 'woocommerce-subscriptions' ), $order_link ) ); - } - } + wcs_update_dates_after_early_renewal( $subscription, $order ); } /** diff --git a/includes/early-renewal/class-wcs-early-renewal-manager.php b/includes/early-renewal/class-wcs-early-renewal-manager.php index 82b9d52..891a038 100755 --- a/includes/early-renewal/class-wcs-early-renewal-manager.php +++ b/includes/early-renewal/class-wcs-early-renewal-manager.php @@ -20,13 +20,21 @@ class WCS_Early_Renewal_Manager { */ protected static $setting_id; + /** + * The early renewal via modal enabled setting ID. + * + * @var string + */ + protected static $via_modal_setting_id; + /** * Initialize filters and hooks for class. * * @since 2.3.0 */ public static function init() { - self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal'; + self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal'; + self::$via_modal_setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal_via_modal'; add_filter( 'woocommerce_subscription_settings', array( __CLASS__, 'add_settings' ) ); } @@ -39,14 +47,33 @@ class WCS_Early_Renewal_Manager { * @return array */ public static function add_settings( $settings ) { - WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_subscriptions_turn_off_automatic_payments', array( - 'id' => self::$setting_id, - 'name' => __( 'Early Renewal', 'woocommerce-subscriptions' ), - 'desc' => __( 'Accept Early Renewal Payments', 'woocommerce-subscriptions' ), - 'desc_tip' => __( 'With early renewals enabled, customers can renew their subscriptions before the next payment date.', 'woocommerce-subscriptions' ), - 'default' => 'no', - 'type' => 'checkbox', - ) ); + $early_renewal_settings = array( + array( + 'id' => self::$setting_id, + 'name' => __( 'Early Renewal', 'woocommerce-subscriptions' ), + 'desc' => __( 'Accept Early Renewal Payments', 'woocommerce-subscriptions' ), + 'desc_tip' => __( 'With early renewals enabled, customers can renew their subscriptions before the next payment date.', 'woocommerce-subscriptions' ), + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'show_if_checked' => 'option', + ), + array( + 'id' => self::$via_modal_setting_id, + 'desc' => __( 'Accept Early Renewal Payments via a Modal', 'woocommerce-subscriptions' ), + 'desc_tip' => sprintf( + __( 'Allow customers to bypass the checkout and renew their subscription early from their %1$sMy Account > View Subscription%2$s page. %3$sLearn more.%4$s', 'woocommerce-subscriptions' ), + '', '', + '', '' + ), + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', + 'show_if_checked' => 'yes', + ), + ); + + WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_subscriptions_turn_off_automatic_payments', $early_renewal_settings, 'multiple_settings' ); return $settings; } @@ -69,4 +96,42 @@ class WCS_Early_Renewal_Manager { return apply_filters( 'wcs_is_early_renewal_enabled', 'yes' === $enabled ); } + + /** + * Finds if the store has enabled early renewal via a modal. + * + * @since 2.6.0 + * @return bool + */ + public static function is_early_renewal_via_modal_enabled() { + return self::is_early_renewal_enabled() && apply_filters( 'wcs_is_early_renewal_via_modal_enabled', 'yes' === get_option( self::$via_modal_setting_id, 'no' ) ); + } + + /** + * Gets the dates which need to be updated after an early renewal is processed. + * + * @since 2.6.0 + * + * @param WC_Subscription $subscription The subscription to calculate the dates for. + * @return array The subscription dates which need to be updated. For example array( $date_type => $mysql_form_date_string ). + */ + public static function get_dates_to_update( $subscription ) { + $next_payment_time = $subscription->get_time( 'next_payment' ); + $dates_to_update = array(); + + if ( $next_payment_time > 0 && $next_payment_time > current_time( 'timestamp', true ) ) { + $next_payment_timestamp = wcs_add_time( $subscription->get_billing_interval(), $subscription->get_billing_period(), $next_payment_time ); + + if ( $subscription->get_time( 'end' ) === 0 || $next_payment_timestamp < $subscription->get_time( 'end' ) ) { + $dates_to_update['next_payment'] = gmdate( 'Y-m-d H:i:s', $next_payment_timestamp ); + } else { + // Delete the next payment date if the calculated next payment date occurs after the end date. + $dates_to_update['next_payment'] = 0; + } + } elseif ( $subscription->get_time( 'end' ) > 0 ) { + $dates_to_update['end'] = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription->get_billing_interval(), $subscription->get_billing_period(), $subscription->get_time( 'end' ) ) ); + } + + return $dates_to_update; + } } diff --git a/includes/early-renewal/class-wcs-early-renewal-modal-handler.php b/includes/early-renewal/class-wcs-early-renewal-modal-handler.php new file mode 100755 index 0000000..84dba4a --- /dev/null +++ b/includes/early-renewal/class-wcs-early-renewal-modal-handler.php @@ -0,0 +1,159 @@ + __( 'Pay now', 'woocommerce-subscriptions' ), + 'attributes' => array( + 'id' => 'early_renewal_modal_submit', + 'class' => 'button alt ', + 'href' => add_query_arg( array( + 'subscription_id' => $subscription->get_id(), + 'process_early_renewal' => true, + 'wcs_nonce' => wp_create_nonce( 'wcs-renew-early-modal-' . $subscription->get_id() ), + ) ), + ), + ); + + $callback_args = array( + 'callback' => array( __CLASS__, 'output_early_renewal_modal' ), + 'parameters' => array( 'subscription' => $subscription ), + ); + + $modal = new WCS_Modal( $callback_args, '.subscription_renewal_early', 'callback', __( 'Renew early', 'woocommerce-subscriptions' ) ); + $modal->add_action( $place_order_action ); + $modal->print_html(); + } + + /** + * Prints the early renewal modal HTML. + * + * @since 2.6.0 + * @param WC_Subscription $subscription The subscription to print the modal for. + */ + public static function output_early_renewal_modal( $subscription ) { + $totals = $subscription->get_order_item_totals(); + $date_changes = WCS_Early_Renewal_Manager::get_dates_to_update( $subscription ); + + if ( isset( $totals['payment_method'] ) ) { + $totals['payment_method']['label'] = __( 'Payment:', 'woocommerce-subscriptions' ); + } + + // Convert the new next payment date into the site's timezone. + if ( ! empty( $date_changes['next_payment'] ) ) { + $new_next_payment_date = new WC_DateTime( $date_changes['next_payment'], new DateTimeZone( 'UTC' ) ); + $new_next_payment_date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } else { + $new_next_payment_date = null; + } + + wc_get_template( 'html-early-renewal-modal-content.php', array( 'subscription' => $subscription, 'totals' => $totals, 'new_next_payment_date' => $new_next_payment_date ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . '/templates/' ); + } + + /** + * Processes the request to renew early via the modal. + * + * @since 2.6.0 + */ + public static function process_early_renewal_request() { + if ( ! isset( $_GET['process_early_renewal'], $_GET['subscription_id'], $_GET['wcs_nonce'] ) ) { + return; + } + + if ( ! wp_verify_nonce( $_GET['wcs_nonce'], 'wcs-renew-early-modal-' . $_GET['subscription_id'] ) ) { + wc_add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' ); + self::redirect(); + } + + $subscription = wcs_get_subscription( absint( $_GET['subscription_id'] ) ); + + if ( ! $subscription ) { + wc_add_notice( __( 'We were unable to locate that subscription, please try again.', 'woocommerce-subscriptions' ), 'error' ); + self::redirect(); + } + + // Before processing the request, detach the functions which handle standard renewal orders. Note we don't need to reattach them as this request will terminate soon. + self::detach_renewal_callbacks(); + + $renewal_order = wcs_create_renewal_order( $subscription ); + + if ( ! wcs_is_order( $renewal_order ) ) { + wc_add_notice( __( "We couldn't create a renewal order for your subscription, please try again.", 'woocommerce-subscriptions' ), 'error' ); + self::redirect(); + } + + $renewal_order->set_payment_method( wc_get_payment_gateway_by_order( $subscription ) ); + $renewal_order->update_meta_data( '_subscription_renewal_early', $subscription->get_id() ); + $renewal_order->save(); + + // Attempt to collect payment with the subscription's current payment method. + WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $renewal_order ); + + // Now that we've attempted to process the payment, refresh the order. + $renewal_order = wc_get_order( $renewal_order->get_id() ); + + // Failed early renewals won't place the subscription on-hold so delete unsuccessful early renewal orders. + if ( $renewal_order->needs_payment() ) { + $renewal_order->delete( true ); + wc_add_notice( __( 'Payment for this renewal order was unsuccessful, please try again.', 'woocommerce-subscriptions' ), 'error' ); + } else { + wcs_update_dates_after_early_renewal( $subscription, $renewal_order ); + wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-subscriptions' ), 'success' ); + } + + self::redirect(); + } + + /** + * Redirect the user after processing their early renewal request. + * + * @since 2.6.0 + */ + private static function redirect() { + wp_redirect( remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ) ); + exit(); + } + + /** + * Removes filters which shouldn't run while processing early renewals via the modal. + * + * @since 2.6.0 + */ + private static function detach_renewal_callbacks() { + remove_filter( 'wcs_renewal_order_created', 'WC_Subscriptions_Renewal_Order::add_order_note', 10, 2 ); + remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment', 10, 2 ); + } +} diff --git a/includes/early-renewal/wcs-early-renewal-functions.php b/includes/early-renewal/wcs-early-renewal-functions.php index 602f784..58a2cee 100755 --- a/includes/early-renewal/wcs-early-renewal-functions.php +++ b/includes/early-renewal/wcs-early-renewal-functions.php @@ -145,3 +145,30 @@ function wcs_get_early_renewal_url( $subscription ) { */ return apply_filters( 'woocommerce_subscriptions_get_early_renewal_url', $url, $subscription_id ); } + +/** + * Update the subscription dates after processing an early renewal. + * + * @since 2.6.0 + * + * @param WC_Subscription $subscription The subscription to update. + * @param WC_Order $early_renewal The early renewal. + */ +function wcs_update_dates_after_early_renewal( $subscription, $early_renewal ) { + $dates_to_update = WCS_Early_Renewal_Manager::get_dates_to_update( $subscription ); + + if ( ! empty( $dates_to_update ) ) { + $order_number = sprintf( _x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), $early_renewal->get_order_number() ); + $order_link = sprintf( '%s', esc_url( wcs_get_edit_post_link( $early_renewal->get_id() ) ), $order_number ); + + try { + $subscription->update_dates( $dates_to_update ); + + // translators: placeholder contains a link to the order's edit screen. + $subscription->add_order_note( sprintf( __( 'Customer successfully renewed early with order %s.', 'woocommerce-subscriptions' ), $order_link ) ); + } catch ( Exception $e ) { + // translators: placeholder contains a link to the order's edit screen. + $subscription->add_order_note( sprintf( __( 'Failed to update subscription dates after customer renewed early with order %s.', 'woocommerce-subscriptions' ), $order_link ) ); + } + } +} diff --git a/includes/emails/class-wcs-email-cancelled-subscription.php b/includes/emails/class-wcs-email-cancelled-subscription.php index 38eb1b2..c31b3e2 100755 --- a/includes/emails/class-wcs-email-cancelled-subscription.php +++ b/includes/emails/class-wcs-email-cancelled-subscription.php @@ -94,20 +94,19 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -117,20 +116,19 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** diff --git a/includes/emails/class-wcs-email-completed-renewal-order.php b/includes/emails/class-wcs-email-completed-renewal-order.php index 037aa85..d023043 100755 --- a/includes/emails/class-wcs-email-completed-renewal-order.php +++ b/includes/emails/class-wcs-email-completed-renewal-order.php @@ -132,20 +132,19 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -155,19 +154,18 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } } diff --git a/includes/emails/class-wcs-email-completed-switch-order.php b/includes/emails/class-wcs-email-completed-switch-order.php index b46cc32..afc6c0f 100755 --- a/includes/emails/class-wcs-email-completed-switch-order.php +++ b/includes/emails/class-wcs-email-completed-switch-order.php @@ -133,21 +133,20 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'subscriptions' => $this->subscriptions, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'subscriptions' => $this->subscriptions, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -157,20 +156,19 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'subscriptions' => $this->subscriptions, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'subscriptions' => $this->subscriptions, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + '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-payment-retry.php b/includes/emails/class-wcs-email-customer-payment-retry.php index a03e576..c9d55ae 100755 --- a/includes/emails/class-wcs-email-customer-payment-retry.php +++ b/includes/emails/class-wcs-email-customer-payment-retry.php @@ -110,21 +110,20 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'retry' => $this->retry, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'retry' => $this->retry, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -134,20 +133,19 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'retry' => $this->retry, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'retry' => $this->retry, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + '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-renewal-invoice.php b/includes/emails/class-wcs-email-customer-renewal-invoice.php index 4259a00..8fd0780 100755 --- a/includes/emails/class-wcs-email-customer-renewal-invoice.php +++ b/includes/emails/class-wcs-email-customer-renewal-invoice.php @@ -148,20 +148,19 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -171,20 +170,19 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** diff --git a/includes/emails/class-wcs-email-expired-subscription.php b/includes/emails/class-wcs-email-expired-subscription.php index 01111b8..a085246 100755 --- a/includes/emails/class-wcs-email-expired-subscription.php +++ b/includes/emails/class-wcs-email-expired-subscription.php @@ -92,20 +92,19 @@ class WCS_Email_Expired_Subscription extends WC_Email { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -115,20 +114,19 @@ class WCS_Email_Expired_Subscription extends WC_Email { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** diff --git a/includes/emails/class-wcs-email-new-renewal-order.php b/includes/emails/class-wcs-email-new-renewal-order.php index ffcedc3..c1d9ed4 100755 --- a/includes/emails/class-wcs-email-new-renewal-order.php +++ b/includes/emails/class-wcs-email-new-renewal-order.php @@ -113,20 +113,19 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -136,19 +135,18 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } } diff --git a/includes/emails/class-wcs-email-new-switch-order.php b/includes/emails/class-wcs-email-new-switch-order.php index 83ab856..b9e419b 100755 --- a/includes/emails/class-wcs-email-new-switch-order.php +++ b/includes/emails/class-wcs-email-new-switch-order.php @@ -110,21 +110,20 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'subscriptions' => $this->subscriptions, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'subscriptions' => $this->subscriptions, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -134,20 +133,19 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'subscriptions' => $this->subscriptions, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'subscriptions' => $this->subscriptions, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } } diff --git a/includes/emails/class-wcs-email-on-hold-subscription.php b/includes/emails/class-wcs-email-on-hold-subscription.php index e17b90c..fee83f1 100755 --- a/includes/emails/class-wcs-email-on-hold-subscription.php +++ b/includes/emails/class-wcs-email-on-hold-subscription.php @@ -92,20 +92,19 @@ class WCS_Email_On_Hold_Subscription extends WC_Email { * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => false, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -115,20 +114,19 @@ class WCS_Email_On_Hold_Subscription extends WC_Email { * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'subscription' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => true, - 'plain_text' => true, - 'email' => $this, + 'subscription' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** diff --git a/includes/emails/class-wcs-email-payment-retry.php b/includes/emails/class-wcs-email-payment-retry.php index a4d4f27..4799df9 100755 --- a/includes/emails/class-wcs-email-payment-retry.php +++ b/includes/emails/class-wcs-email-payment-retry.php @@ -92,12 +92,13 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order { 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, + 'order' => $this->object, + 'retry' => $this->retry, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base @@ -113,12 +114,13 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order { 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, + 'order' => $this->object, + 'retry' => $this->retry, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base diff --git a/includes/emails/class-wcs-email-processing-renewal-order.php b/includes/emails/class-wcs-email-processing-renewal-order.php index 484a201..108d2f1 100755 --- a/includes/emails/class-wcs-email-processing-renewal-order.php +++ b/includes/emails/class-wcs-email-processing-renewal-order.php @@ -126,20 +126,19 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or * @return string */ function get_content_html() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_html, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } /** @@ -149,19 +148,18 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or * @return string */ function get_content_plain() { - ob_start(); - wc_get_template( + return wc_get_template_html( $this->template_plain, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => true, - 'email' => $this, + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => is_callable( array( $this, 'get_additional_content' ) ) ? $this->get_additional_content() : '', // WC 3.7 introduced an additional content field for all emails. + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, ), '', $this->template_base ); - return ob_get_clean(); } } diff --git a/includes/gateways/class-wc-subscriptions-payment-gateways.php b/includes/gateways/class-wc-subscriptions-payment-gateways.php index d11849c..743addb 100755 --- a/includes/gateways/class-wc-subscriptions-payment-gateways.php +++ b/includes/gateways/class-wc-subscriptions-payment-gateways.php @@ -130,7 +130,11 @@ class WC_Subscriptions_Payment_Gateways { */ public static function no_available_payment_methods_message( $no_gateways_message ) { if ( WC_Subscriptions_Cart::cart_contains_subscription() && 'no' == get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', 'no' ) ) { - $no_gateways_message = __( 'Sorry, it seems there are no available payment methods which support subscriptions. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce-subscriptions' ); + if ( current_user_can( 'manage_woocommerce' ) ) { + $no_gateways_message = sprintf( __( 'Sorry, it seems there are no available payment methods which support subscriptions. Please see %sEnabling Payment Gateways for Subscriptions%s if you require assistance.', 'woocommerce-subscriptions' ), '', '' ); + } else { + $no_gateways_message = __( 'Sorry, it seems there are no available payment methods which support subscriptions. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce-subscriptions' ); + } } return $no_gateways_message; @@ -225,13 +229,13 @@ class WC_Subscriptions_Payment_Gateways { return $status_html; } - $core_features = $gateway->supports; + $core_features = (array) apply_filters( 'woocommerce_subscriptions_payment_gateway_features_list', $gateway->supports, $gateway ); $subscription_features = $change_payment_method_features = array(); foreach ( $core_features as $key => $feature ) { // Skip any non-subscription related features. - if ( 0 !== strpos( $feature, 'subscription' ) ) { + if ( 'gateway_scheduled_payments' !== $feature && false === strpos( $feature, 'subscription' ) ) { continue; } 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 8819b4c..48211cf 100755 --- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php +++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php @@ -103,21 +103,31 @@ class WCS_PayPal_Admin { $notices = array(); if ( ! WCS_PayPal::are_credentials_set() ) { - $notices[] = array( - 'type' => 'warning', - // translators: placeholders are opening and closing link tags. 1$-2$: to docs on woocommerce, 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' ), - '', - '', - '', - '' - ), - ); + if ( 'yes' === WCS_PayPal::get_option( 'enabled_for_subscriptions' ) ) { + $notices[] = array( + 'type' => 'warning', + // translators: placeholders are opening and closing link tags. 1$-2$: to docs on woocommerce, 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' ), + '', + '', + '', + '' + ), + ); + } } elseif ( 'woocommerce_page_wc-settings' === get_current_screen()->base && isset( $_GET['tab'] ) && in_array( $_GET['tab'], array( 'subscriptions', 'checkout' ) ) && ! WCS_PayPal::are_reference_transactions_enabled() ) { + if ( 'yes' === WCS_PayPal::get_option( 'enabled_for_subscriptions' ) ) { + $notice_type = 'warning'; + $notice_text = 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 %3$sLearn more %7$s', 'woocommerce-subscriptions' ); + } else { + $notice_type = 'info'; + $notice_text = esc_html__( '%1$sPayPal Reference Transactions are not enabled on your account%2$s. If you wish to use PayPal Reference Transactions with Subscriptions, please contact PayPal and request they %3$senable PayPal Reference Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %3$sLearn more %7$s', 'woocommerce-subscriptions' ); + } + $notices[] = array( - 'type' => 'warning', + 'type' => $notice_type, // translators: placeholders are opening and closing strong and link tags. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce - '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 %3$sLearn more %7$s', 'woocommerce-subscriptions' ), + 'text' => sprintf( $notice_text, '', '', '', diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php index 58b1450..e219fac 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php @@ -239,7 +239,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { } } - $is_first_payment = $subscription->get_completed_payment_count() < 1; + $is_first_payment = $subscription->get_payment_count() < 1; if ( $subscription->has_status( 'switched' ) ) { WC_Gateway_Paypal::log( 'IPN ignored, subscription has been switched.' ); @@ -352,7 +352,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' ); // Ignore the first IPN message if the PDT should have handled it (if it didn't handle it, it will have been dealt with as first payment), but set a flag to make sure we only ignore it once - } elseif ( $subscription->get_completed_payment_count() == 1 && '' !== WCS_PayPal::get_option( 'identity_token' ) && 'true' != get_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', true ) && false === $is_renewal_sign_up_after_failure ) { + } elseif ( $subscription->get_payment_count() == 1 && '' !== WCS_PayPal::get_option( 'identity_token' ) && 'true' != get_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', true ) && false === $is_renewal_sign_up_after_failure ) { WC_Gateway_Paypal::log( 'IPN subscription payment ignored for subscription ' . $subscription->get_id() . ' due to PDT previously handling the payment.' ); diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php index fedb250..cfe083f 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php @@ -167,7 +167,7 @@ class WCS_PayPal_Standard_Request { if ( $order_contains_failed_renewal ) { $subscription_trial_length = 0; - $subscription_installments = max( $subscription_installments - $subscription->get_completed_payment_count(), 0 ); + $subscription_installments = max( $subscription_installments - $subscription->get_payment_count(), 0 ); // If we're changing the payment date or switching subs, we need to set the trial period to the next payment date & installments to be the number of installments left } elseif ( $is_payment_change || $is_synced_subscription || $is_early_resubscribe ) { @@ -193,7 +193,7 @@ class WCS_PayPal_Standard_Request { // If this is a payment change, we need to account for completed payments on the number of installments owing if ( $is_payment_change && $subscription_length > 0 ) { - $subscription_installments = max( $subscription_installments - $subscription->get_completed_payment_count(), 0 ); + $subscription_installments = max( $subscription_installments - $subscription->get_payment_count(), 0 ); } } else { diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php index 4e92c16..1da8f72 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php @@ -50,6 +50,8 @@ class WCS_PayPal_Supports { // Check for specific subscription support based on whether the subscription is using a billing agreement or subscription for recurring payments with PayPal add_filter( 'woocommerce_subscription_payment_gateway_supports', __CLASS__ . '::add_feature_support_for_subscription', 10, 3 ); + + add_filter( 'woocommerce_subscriptions_payment_gateway_features_list', array( __CLASS__, 'add_paypal_billing_type_supported_features' ), 10, 2 ); } /** @@ -108,4 +110,34 @@ class WCS_PayPal_Supports { return $is_supported; } + /** + * Adds the payment gateway features supported by the type of billing the PayPal account supports (Reference Transactions or Standard). + * + * @since 2.6.0 + * + * @param array $features The list of features the payment gateway supports. + * @param WC_Payment_Gateway $gateway The payment gateway object. + * @return array $features + */ + public static function add_paypal_billing_type_supported_features( $features, $gateway ) { + + if ( 'paypal' !== $gateway->id ) { + return $features; + } + + // The base feature list is the PayPal Standard features + the basic features the payment gateways support ($gateway->supports). + $features = array_merge( self::$standard_supported_features, $features ); + + // Reference Transactions support all base features + Reference Transactions features - 'gateway_scheduled_payments'. + if ( WCS_PayPal::are_reference_transactions_enabled() ) { + // Remove gateway scheduled payments. + if ( false !== ( $key = array_search( 'gateway_scheduled_payments', $features ) ) ) { + unset( $features[ $key ] ); + } + + $features = array_merge( self::$reference_transaction_supported_features, $features ); + } + + return array_unique( $features ); + } } diff --git a/includes/gateways/paypal/includes/templates/admin-notices.php b/includes/gateways/paypal/includes/templates/admin-notices.php index 9a924fb..663fe6b 100755 --- a/includes/gateways/paypal/includes/templates/admin-notices.php +++ b/includes/gateways/paypal/includes/templates/admin-notices.php @@ -23,6 +23,9 @@ foreach ( $notices as $notice_args ) { case 'warning' : $notice = new WCS_Admin_Notice( 'updated', array( 'style' => array( 'border-left: 4px solid #ffba00' ) ) ); break; + case 'info' : + $notice = new WCS_Admin_Notice( 'notice notice-info' ); + break; case 'error' : $notice = new WCS_Admin_Notice( 'updated error' ); break; diff --git a/includes/upgrades/class-wc-subscriptions-upgrader.php b/includes/upgrades/class-wc-subscriptions-upgrader.php index fe69a7d..1bd16b4 100755 --- a/includes/upgrades/class-wc-subscriptions-upgrader.php +++ b/includes/upgrades/class-wc-subscriptions-upgrader.php @@ -240,6 +240,11 @@ class WC_Subscriptions_Upgrader { WCS_PayPal::set_enabled_for_subscriptions_default(); } + // Upon upgrading to 2.6.0 from a version after 2.2.0, schedule missing _has_trial line item meta repair. + if ( version_compare( self::$active_version, '2.6.0', '<' ) && version_compare( self::$active_version, '2.2.0', '>=' ) ) { + self::$background_updaters['2.6']['has_trial_item_meta']->schedule_repair(); + } + self::upgrade_complete(); } @@ -837,7 +842,8 @@ class WC_Subscriptions_Upgrader { $logger = new WC_logger(); self::$background_updaters['2.3']['suspended_paypal_repair'] = new WCS_Repair_Suspended_PayPal_Subscriptions( $logger ); self::$background_updaters['2.3']['address_indexes_repair'] = new WCS_Repair_Subscription_Address_Indexes( $logger ); - self::$background_updaters['2.4']['start_date_metadata'] = new WCS_Repair_Start_Date_Metadata( $logger ); + self::$background_updaters['2.4']['start_date_metadata'] = new WCS_Repair_Start_Date_Metadata( $logger ); + self::$background_updaters['2.6']['has_trial_item_meta'] = new WCS_Repair_Line_Item_Has_Trial_Meta( $logger ); // Init the updaters foreach ( self::$background_updaters as $version => $updaters ) { diff --git a/includes/upgrades/class-wcs-repair-line-item-has-trial-meta.php b/includes/upgrades/class-wcs-repair-line-item-has-trial-meta.php new file mode 100755 index 0000000..36249f9 --- /dev/null +++ b/includes/upgrades/class-wcs-repair-line-item-has-trial-meta.php @@ -0,0 +1,113 @@ +scheduled_hook = 'wcs_schedule_trial_subscription_repairs'; + $this->repair_hook = 'wcs_free_trial_line_item_meta_repair'; + $this->log_handle = 'wcs-repair-line-item-has-trial-meta'; + $this->logger = $logger; + } + + /** + * Get a batch of subscriptions which have or had free trials at the time of purchase. + * + * @param int $page The page number to get results from. + * @return array A list of subscription ids. + * + * @since 2.6.0 + */ + protected function get_items_to_repair( $page ) { + $query = new WP_Query(); + $query_args = array( + 'post_type' => 'shop_subscription', + 'posts_per_page' => 20, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'ASC', // Get the subscriptions in ascending order by ID so any new subscriptions created after the repairs start running will be at the end and not cause issues with paging. + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => '_trial_period', + 'compare' => '!=', + 'value' => '', + ), + ), + ); + + return $query->query( $query_args ); + + } + + /** + * Repair the line item meta for a given subscription ID. + * + * @param int $subscription_id + * + * @since 2.6.0 + */ + public function repair_item( $subscription_id ) { + try { + $subscription = wcs_get_subscription( $subscription_id ); + + if ( false === $subscription ) { + throw new Exception( 'Failed to instantiate subscription object' ); + } + + $parent_order = $subscription->get_parent(); + + if ( ! $parent_order ) { + $this->log( sprintf( "Subscription ID %d doesn't have a parent order -- skipping", $subscription_id ) ); + return; + } + + // Build an array of product IDs so we can match corresponding subscription items. + $parent_order_product_ids = array(); + foreach ( $parent_order->get_items() as $line_item ) { + $parent_order_product_ids[ wcs_get_canonical_product_id( $line_item ) ] = true; + } + + // Set the has_trial meta if this subscription line item exists in the parent order, + foreach ( $subscription->get_items() as $line_item ) { + if ( isset( $parent_order_product_ids[ wcs_get_canonical_product_id( $line_item ) ] ) && ! $line_item->meta_exists( '_has_trial' ) ) { + $line_item->update_meta_data( '_has_trial', 'true' ); + $line_item->save(); + } + } + + $this->log( sprintf( 'Subscription ID %d "_has_trial" line item meta repaired.', $subscription_id ) ); + } catch ( Exception $e ) { + $this->log( sprintf( 'ERROR: Exception caught trying to repair free trial line item meta for subscription %d - exception message: %s ---', $subscription_id, $e->getMessage() ) ); + } + } +} diff --git a/includes/upgrades/class-wcs-upgrade-notice-manager.php b/includes/upgrades/class-wcs-upgrade-notice-manager.php index 4f4fdd7..4cf082c 100755 --- a/includes/upgrades/class-wcs-upgrade-notice-manager.php +++ b/includes/upgrades/class-wcs-upgrade-notice-manager.php @@ -19,7 +19,7 @@ class WCS_Upgrade_Notice_Manager { * * @var string */ - protected static $version = '2.5.0'; + protected static $version = '2.6.0'; /** * The number of times the notice will be displayed before being dismissed automatically. @@ -77,38 +77,38 @@ class WCS_Upgrade_Notice_Manager { return; } - $version = _x( '2.5', 'plugin version number used in admin notice', 'woocommerce-subscriptions' ); + $version = _x( '2.6', 'plugin version number used in admin notice', 'woocommerce-subscriptions' ); $dismiss_url = wp_nonce_url( add_query_arg( 'dismiss_upgrade_notice', self::$version ), 'dismiss_upgrade_notice', '_wcsnonce' ); $notice = new WCS_Admin_Notice( 'notice notice-info', array(), $dismiss_url ); $features = array( array( - 'title' => __( 'New options to allow customers to sign up without a credit card', 'woocommerce-subscriptions' ), - 'description' => __( 'Allow customers to access free trial and other $0 subscription products without needing to enter their credit card details on sign up.', 'woocommerce-subscriptions' ), + 'title' => __( 'Improved experience for customers who renew their subscriptions early', 'woocommerce-subscriptions' ), + 'description' => sprintf( __( 'Allow customers to renew early from their %sMy Account > Subscription%s page without going through the checkout.', 'woocommerce-subscriptions' ), '', '' ), ), array( - 'title' => __( 'Improved subscription payment method information', 'woocommerce-subscriptions' ), - 'description' => __( 'Customers can now see more information about what payment method will be used for future payments.', 'woocommerce-subscriptions' ), + 'title' => __( 'Improved subscription switching proration calculations', 'woocommerce-subscriptions' ), + 'description' => __( "We've made improvements to the code which calculates the upgrade costs when a customer switches their subscription. This has enabled us to fix a number of complicated switching scenarios.", 'woocommerce-subscriptions' ), ), array( - 'title' => __( 'Auto-renewal toggle', 'woocommerce-subscriptions' ), - 'description' => sprintf( __( 'Enabled via a setting, this new feature will allow your customers to turn on and off automatic payments from the %sMy Account > View Subscription%s pages.', 'woocommerce-subscriptions' ), '', '' ), - ), - array( - 'title' => __( 'Update all subscription payment methods', 'woocommerce-subscriptions' ), - 'description' => __( "Customers will now have the option to update all their subscriptions when they are changing one of their subscription's payment methods - provided the payment gateway supports it.", 'woocommerce-subscriptions' ), + 'title' => __( 'View subscriptions and orders contributing to reports', 'woocommerce-subscriptions' ), + 'description' => sprintf( + __( 'Want to view which specific orders and subscriptions are contributing to subscription-related reports? You can now do just that by clicking on the %1$s link while viewing %2$ssubscription reports%3$s.', 'woocommerce-subscriptions' ), + '', + '', '' + ), ), ); // translators: placeholder is Subscription version string ('2.3') - $notice->set_heading( sprintf( __( 'Welcome to Subscriptions %s', 'woocommerce-subscriptions' ), $version ) ); + $notice->set_heading( sprintf( __( 'Welcome to WooCommerce Subscriptions %s!', 'woocommerce-subscriptions' ), $version ) ); $notice->set_content_template( 'update-welcome-notice.php', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/upgrades/templates/', array( 'version' => $version, 'features' => $features, ) ); $notice->set_actions( array( array( - 'name' => __( 'Learn More', 'woocommerce-subscriptions' ), - 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-5/', + 'name' => __( 'Learn more', 'woocommerce-subscriptions' ), + 'url' => 'https://docs.woocommerce.com/document/subscriptions/whats-new-in-subscriptions-2-6/', ), ) ); diff --git a/includes/upgrades/templates/update-welcome-notice.php b/includes/upgrades/templates/update-welcome-notice.php index d380317..1c1357a 100755 --- a/includes/upgrades/templates/update-welcome-notice.php +++ b/includes/upgrades/templates/update-welcome-notice.php @@ -5,7 +5,7 @@ ', '' ) ); ?>

-

+


-

+

diff --git a/includes/wcs-compatibility-functions.php b/includes/wcs-compatibility-functions.php index 3ff1b97..69e4ad4 100755 --- a/includes/wcs-compatibility-functions.php +++ b/includes/wcs-compatibility-functions.php @@ -534,3 +534,36 @@ function wcs_doing_cron() { function wcs_doing_ajax() { return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : defined( 'DOING_AJAX' ) && DOING_AJAX; } + +/** + * A wrapper function for getting an order's used coupon codes. + * + * WC 3.7 deprecated @see WC_Abstract_Order::get_used_coupons() in favour of WC_Abstract_Order::get_coupon_codes(). + * + * @since 2.6.0 + * + * @param WC_Abstract_Order $order An order or subscription object to get the coupon codes for. + * @return array The coupon codes applied to the $order. + */ +function wcs_get_used_coupon_codes( $order ) { + return is_callable( array( $order, 'get_coupon_codes' ) ) ? $order->get_coupon_codes() : $order->get_used_coupons(); +} + +/** + * Attach a function callback for a certain WooCommerce versions. + * + * Enables attaching a callback if WooCommerce is before, after, equal or not equal to a given version. + * This function is a wrapper for @see WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action(). + * + * @since 2.6.0 + * + * @param string $tag The action or filter tag to attach the callback too. + * @param string|array $function The callable function to attach to the hook. + * @param string $woocommerce_version The WooCommerce version to do a compare on. For example '3.0.0'. + * @param string $operator The version compare operator to use. @see https://www.php.net/manual/en/function.version-compare.php + * @param integer $priority The priority to attach this callback to. + * @param integer $number_of_args The number of arguments to pass to the callback function + */ +function wcs_add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority = 10, $number_of_args = 1 ) { + WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority, $number_of_args ); +} diff --git a/includes/wcs-deprecated-functions.php b/includes/wcs-deprecated-functions.php index 6a0a7ec..cc13041 100755 --- a/includes/wcs-deprecated-functions.php +++ b/includes/wcs-deprecated-functions.php @@ -194,7 +194,7 @@ function wcs_get_subscription_in_deprecated_structure( WC_Subscription $subscrip $completed_payments = array(); - if ( $subscription->get_completed_payment_count() ) { + if ( $subscription->get_payment_count() ) { $order = $subscription->get_parent(); @@ -257,3 +257,33 @@ function wcs_get_subscription_in_deprecated_structure( WC_Subscription $subscrip return $deprecated_subscription_object; } + +/** + * Wrapper for wc_deprecated_hook to improve handling of ajax requests, even when + * WooCommerce 3.3.0's wc_deprecated_hook method is not available. + * + * @since 2.6.0 + * @param string $hook The hook that was used. + * @param string $version The version that deprecated the hook. + * @param string $replacement The hook that should have been used. + * @param string $message A message regarding the change. + */ +function wcs_deprecated_hook( $hook, $version, $replacement = null, $message = null ) { + + if ( function_exists( 'wc_deprecated_hook' ) ) { + wc_deprecated_hook( $hook, $version, $replacement, $message ); + } else { + // Reimplement wcs_deprecated_function() when WC 3.0 is not active + if ( is_ajax() ) { + do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message ); + + $message = empty( $message ) ? '' : ' ' . $message; + $log_string = "{$hook} is deprecated since version {$version}"; + $log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.'; + + error_log( $log_string . $message ); + } else { + _deprecated_hook( $hook, $version, $replacement, $message ); + } + } +} diff --git a/includes/wcs-helper-functions.php b/includes/wcs-helper-functions.php index 9448d1d..bf7356b 100755 --- a/includes/wcs-helper-functions.php +++ b/includes/wcs-helper-functions.php @@ -245,3 +245,23 @@ function wcs_get_minor_version_string( $version ) { function wcs_is_frontend_request() { return ( ! is_admin() || wcs_doing_ajax() ) && ! wcs_doing_cron() && ! wcs_is_rest_api_request(); } + +/** + * Sorts an array of objects by a given property in a given order. + * + * @since 2.6.0 + * + * @param array $objects An array of objects to sort. + * @param string $property The property to sort by. + * @param string $sort_order Optional. The order to sort by. Must be 'ascending' or 'descending'. Default is 'ascending'. + * + * @throws InvalidArgumentException Thrown if an invalid sort order is given. + * @return array The array of objects sorted. + */ +function wcs_sort_objects( &$objects, $property, $sort_order = 'ascending' ) { + if ( 'ascending' !== $sort_order && 'descending' !== $sort_order ) { + throw new InvalidArgumentException( sprintf( __( 'Invalid sort order type: %s. The $sort_order argument must be %s or %s.', 'woocommerce-subscriptions' ), $sort_order, '"descending"', '"ascending"' ) ); + } + uasort( $objects, array( new WCS_Object_Sorter( $property ), "{$sort_order}_compare" ) ); + return $objects; +} diff --git a/includes/wcs-limit-functions.php b/includes/wcs-limit-functions.php index 9c9339d..bf07d88 100755 --- a/includes/wcs-limit-functions.php +++ b/includes/wcs-limit-functions.php @@ -67,7 +67,7 @@ function wcs_is_product_limited_for_user( $product, $user_id = 0 ) { ) ); foreach ( $user_subscriptions as $subscription ) { - if ( ! $subscription->has_status( 'cancelled' ) || 0 !== $subscription->get_completed_payment_count() ) { + if ( ! $subscription->has_status( 'cancelled' ) || 0 !== $subscription->get_payment_count() ) { $is_limited_for_user = true; break; } diff --git a/includes/wcs-order-functions.php b/includes/wcs-order-functions.php index 8dac055..6de6666 100755 --- a/includes/wcs-order-functions.php +++ b/includes/wcs-order-functions.php @@ -24,9 +24,9 @@ if ( ! defined( 'ABSPATH' ) ) { * 'customer_id' The user ID of a customer on the site. * 'product_id' The post ID of a WC_Product_Subscription, WC_Product_Variable_Subscription or WC_Product_Subscription_Variation object * 'order_id' The post ID of a shop_order post/WC_Order object which was used to create the subscription - * 'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'suspended', 'expired', 'pending' or 'trash'. Defaults to 'any'. + * 'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'on-hold', 'expired', 'pending' or 'trash'. Defaults to 'any'. * 'order_type' Get subscriptions for the any order type in this array. Can include 'any', 'parent', 'renewal' or 'switch', defaults to parent. - * @return array Subscription details in post_id => WC_Subscription form. + * @return WC_Subscription[] Subscription details in post_id => WC_Subscription form. * @since 2.0 */ function wcs_get_subscriptions_for_order( $order, $args = array() ) { @@ -896,3 +896,64 @@ function wcs_minutes_since_order_created( $order ) { function wcs_seconds_since_order_created( $order ) { return time() - $order->get_date_created()->getTimestamp(); } + +/** + * Finds a corresponding subscription line item on an order. + * + * @since 2.6.0 + * + * @param WC_Abstract_Order $order The order object to look for the item in. + * @param WC_Order_Item $subscription_item The line item on the the subscription to find on the order. + * @param string $match_type Optional. The type of comparison to make. Can be 'match_product_ids' to compare product|variation IDs or 'match_attributes' to also compare by item attributes on top of matching product IDs. Default 'match_product_ids'. + * + * @return WC_Order_Item|bool The order item which matches the subscription item or false if one cannot be found. + */ +function wcs_find_matching_line_item( $order, $subscription_item, $match_type = 'match_product_ids' ) { + $matching_item = false; + + if ( 'match_attributes' === $match_type ) { + $subscription_item_attributes = wp_list_pluck( $subscription_item->get_formatted_meta_data( '_', true ), 'value', 'key' ); + } + + $subscription_item_canonical_product_id = wcs_get_canonical_product_id( $subscription_item ); + + foreach ( $order->get_items() as $order_item ) { + if ( wcs_get_canonical_product_id( $order_item ) !== $subscription_item_canonical_product_id ) { + continue; + } + + // Check if we have matching meta key and value pairs loosely - they can appear in any order, + if ( 'match_attributes' === $match_type && wp_list_pluck( $order_item->get_formatted_meta_data( '_', true ), 'value', 'key' ) != $subscription_item_attributes ) { + continue; + } + + $matching_item = $order_item; + break; + } + + return $matching_item; +} + +/** + * Checks if an order contains a product. + * + * @since 2.6.0 + * + * @param WC_Order $order An order object + * @param WC_Product $product A product object + * + * @return bool $order_has_product Whether the order contains a line item matching that product + */ +function wcs_order_contains_product( $order, $product ) { + $order_has_product = false; + $product_id = wcs_get_canonical_product_id( $product ); + + foreach ( $order->get_items() as $line_item ) { + if ( wcs_get_canonical_product_id( $line_item ) === $product_id ) { + $order_has_product = true; + break; + } + } + + return $order_has_product; +} diff --git a/includes/wcs-renewal-functions.php b/includes/wcs-renewal-functions.php index 8c0ea0c..9b81189 100755 --- a/includes/wcs-renewal-functions.php +++ b/includes/wcs-renewal-functions.php @@ -116,3 +116,28 @@ function wcs_cart_contains_failed_renewal_order_payment() { function wcs_get_subscriptions_for_renewal_order( $order ) { return wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'renewal' ) ); } + +/** + * Get the last renewal order which isn't an early renewal order. + * + * @since 2.6.0 + * + * @param WC_Subscription $subscription The subscription object. + * @return WC_Order|bool The last non-early renewal order, otherwise false. + */ +function wcs_get_last_non_early_renewal_order( $subscription ) { + $last_non_early_renewal = false; + $renewal_orders = $subscription->get_related_orders( 'all', 'renewal' ); + + // We need the orders sorted by the date they were created, with the newest first. + wcs_sort_objects( $renewal_orders, 'date_created', 'descending' ); + + foreach ( $renewal_orders as $renewal_order ) { + if ( ! wcs_order_contains_early_renewal( $renewal_order ) ) { + $last_non_early_renewal = $renewal_order; + break; + } + } + + return $last_non_early_renewal; +} diff --git a/includes/wcs-resubscribe-functions.php b/includes/wcs-resubscribe-functions.php index 70e6d71..d1b5d29 100755 --- a/includes/wcs-resubscribe-functions.php +++ b/includes/wcs-resubscribe-functions.php @@ -219,7 +219,7 @@ function wcs_can_user_resubscribe_to( $subscription, $user_id = '' ) { } } - if ( empty( $resubscribe_order_ids ) && $subscription->get_completed_payment_count() > 0 && true === $all_line_items_exist && false === $has_active_limited_subscription ) { + if ( empty( $resubscribe_order_ids ) && $subscription->get_payment_count() > 0 && true === $all_line_items_exist && false === $has_active_limited_subscription ) { $can_user_resubscribe = true; } else { $can_user_resubscribe = false; diff --git a/languages/woocommerce-subscriptions.pot b/languages/woocommerce-subscriptions.pot index 40c2e60..e87f8ce 100755 --- 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.5.7\n" +"Project-Id-Version: WooCommerce Subscriptions 2.6.1\n" "Report-Msgid-Bugs-To: " "https://github.com/Prospress/woocommerce-subscriptions/issues\n" -"POT-Creation-Date: 2019-07-04 13:25:48+00:00\n" +"POT-Creation-Date: 2019-09-04 04:35:07+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -19,82 +19,82 @@ msgstr "" msgid "Invalid relation type: %s. Order relationship type must be one of: %s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:191 +#: includes/admin/class-wc-subscriptions-admin.php:195 msgid "Simple subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:192 +#: includes/admin/class-wc-subscriptions-admin.php:196 msgid "Variable subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:213 +#: includes/admin/class-wc-subscriptions-admin.php:217 msgid "Downloadable" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:214 +#: includes/admin/class-wc-subscriptions-admin.php:218 msgid "Virtual" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:278 +#: includes/admin/class-wc-subscriptions-admin.php:282 msgid "Choose the subscription price, billing interval and period." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:291 +#: includes/admin/class-wc-subscriptions-admin.php:295 #: templates/admin/html-variation-price.php:44 #. translators: placeholder is a currency symbol / code msgid "Subscription price (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:294 +#: includes/admin/class-wc-subscriptions-admin.php:298 msgid "Subscription interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:300 -#: includes/admin/class-wc-subscriptions-admin.php:437 +#: includes/admin/class-wc-subscriptions-admin.php:304 +#: includes/admin/class-wc-subscriptions-admin.php:441 msgid "Subscription period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:314 -#: includes/admin/class-wc-subscriptions-admin.php:438 +#: includes/admin/class-wc-subscriptions-admin.php:318 +#: includes/admin/class-wc-subscriptions-admin.php:442 #: templates/admin/html-variation-price.php:66 msgid "Expire after" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:317 +#: includes/admin/class-wc-subscriptions-admin.php:321 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:327 +#: includes/admin/class-wc-subscriptions-admin.php:331 #: templates/admin/html-variation-price.php:20 #. translators: %s is a currency symbol / code msgid "Sign-up fee (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:329 +#: includes/admin/class-wc-subscriptions-admin.php:333 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:341 +#: includes/admin/class-wc-subscriptions-admin.php:345 #: templates/admin/html-variation-price.php:25 msgid "Free trial" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:344 +#: includes/admin/class-wc-subscriptions-admin.php:348 #: templates/admin/deprecated/html-variation-price.php:115 msgid "Subscription Trial Period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:376 +#: includes/admin/class-wc-subscriptions-admin.php:380 msgid "One time shipping" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:377 +#: includes/admin/class-wc-subscriptions-admin.php:381 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 " @@ -102,63 +102,63 @@ msgid "" "not have a free trial or a synced renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:434 +#: includes/admin/class-wc-subscriptions-admin.php:438 msgid "Subscription pricing" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:435 +#: includes/admin/class-wc-subscriptions-admin.php:439 msgid "Subscription sign-up fee" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:436 +#: includes/admin/class-wc-subscriptions-admin.php:440 msgid "Subscription billing interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:439 +#: includes/admin/class-wc-subscriptions-admin.php:443 msgid "Free trial length" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:440 +#: includes/admin/class-wc-subscriptions-admin.php:444 msgid "Free trial period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:764 +#: includes/admin/class-wc-subscriptions-admin.php:768 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:806 +#: includes/admin/class-wc-subscriptions-admin.php:810 msgid "" "Trashing this order will also trash the subscriptions purchased with the " "order." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:819 +#: includes/admin/class-wc-subscriptions-admin.php:823 msgid "Enter the new period, either day, week, month or year:" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:820 +#: includes/admin/class-wc-subscriptions-admin.php:824 msgid "Enter a new length (e.g. 5):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:821 +#: includes/admin/class-wc-subscriptions-admin.php:825 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:822 +#: includes/admin/class-wc-subscriptions-admin.php:826 msgid "Delete all variations without a subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:825 +#: includes/admin/class-wc-subscriptions-admin.php:829 msgid "" "Product type can not be changed because this product is associated with " "active subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:829 +#: includes/admin/class-wc-subscriptions-admin.php:833 msgid "" "You are about to trash one or more orders which contain a subscription.\n" "\n" @@ -166,7 +166,7 @@ msgid "" "orders." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:842 +#: includes/admin/class-wc-subscriptions-admin.php:846 msgid "" "WARNING: Bad things are about to happen!\n" "\n" @@ -178,13 +178,13 @@ msgid "" "gateway." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:843 +#: includes/admin/class-wc-subscriptions-admin.php:847 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:850 +#: includes/admin/class-wc-subscriptions-admin.php:854 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,87 +193,87 @@ msgid "" "subscriptions?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:854 +#: includes/admin/class-wc-subscriptions-admin.php:858 msgid "" "PayPal Standard has a number of limitations and does not support all " "subscription features." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:854 +#: includes/admin/class-wc-subscriptions-admin.php:858 msgid "" "Because of this, it is not recommended as a payment method for " "Subscriptions unless it is the only available option for your country." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:917 +#: includes/admin/class-wc-subscriptions-admin.php:921 msgid "Active subscriber?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:960 +#: includes/admin/class-wc-subscriptions-admin.php:964 msgid "Manage Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:964 -#: woocommerce-subscriptions.php:263 +#: includes/admin/class-wc-subscriptions-admin.php:968 +#: woocommerce-subscriptions.php:264 msgid "Search Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:984 -#: includes/admin/class-wc-subscriptions-admin.php:1099 +#: includes/admin/class-wc-subscriptions-admin.php:988 +#: includes/admin/class-wc-subscriptions-admin.php:1140 #: includes/admin/class-wcs-admin-reports.php:46 #: includes/admin/class-wcs-admin-system-status.php:56 -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:688 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:780 #: includes/class-wcs-query.php:116 includes/class-wcs-query.php:143 #: includes/class-wcs-query.php:297 #: includes/privacy/class-wcs-privacy-exporters.php:51 -#: woocommerce-subscriptions.php:254 woocommerce-subscriptions.php:267 +#: woocommerce-subscriptions.php:255 woocommerce-subscriptions.php:268 msgid "Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1139 +#: includes/admin/class-wc-subscriptions-admin.php:1180 msgid "Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1146 +#: includes/admin/class-wc-subscriptions-admin.php:1187 msgid "Add to Cart Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1147 +#: includes/admin/class-wc-subscriptions-admin.php:1188 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 " +"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:1151 -#: includes/admin/class-wc-subscriptions-admin.php:1154 -#: includes/admin/class-wc-subscriptions-admin.php:1163 -#: includes/admin/class-wc-subscriptions-admin.php:1166 +#: includes/admin/class-wc-subscriptions-admin.php:1192 +#: includes/admin/class-wc-subscriptions-admin.php:1195 +#: includes/admin/class-wc-subscriptions-admin.php:1204 +#: includes/admin/class-wc-subscriptions-admin.php:1207 #: includes/class-wc-product-subscription-variation.php:98 #: includes/class-wc-product-subscription.php:72 #: includes/class-wc-product-variable-subscription.php:73 #: includes/class-wc-subscriptions-product.php:99 -#: woocommerce-subscriptions.php:580 -msgid "Sign Up Now" +#: woocommerce-subscriptions.php:581 +msgid "Sign up now" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1158 +#: includes/admin/class-wc-subscriptions-admin.php:1199 msgid "Place Order Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1159 +#: includes/admin/class-wc-subscriptions-admin.php:1200 msgid "" "Use this field to customise the text displayed on the checkout button when " "an order contains a subscription. Normally the checkout submission button " -"displays \"Place Order\". When the cart contains a subscription, this is " -"changed to \"Sign Up Now\"." +"displays \"Place order\". When the cart contains a subscription, this is " +"changed to \"Sign up now\"." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1172 +#: includes/admin/class-wc-subscriptions-admin.php:1213 msgid "Roles" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1175 +#: includes/admin/class-wc-subscriptions-admin.php:1216 #. translators: placeholders are tags msgid "" "Choose the default roles to assign to active and inactive subscribers. For " @@ -282,46 +282,46 @@ msgid "" "allocated these roles to prevent locking out administrators." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1180 +#: includes/admin/class-wc-subscriptions-admin.php:1221 msgid "Subscriber Default Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1181 +#: includes/admin/class-wc-subscriptions-admin.php:1222 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:1192 +#: includes/admin/class-wc-subscriptions-admin.php:1233 msgid "Inactive Subscriber Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1193 +#: includes/admin/class-wc-subscriptions-admin.php:1234 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:1213 +#: includes/admin/class-wc-subscriptions-admin.php:1254 msgid "Manual Renewal Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1214 +#: includes/admin/class-wc-subscriptions-admin.php:1255 msgid "Accept Manual Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1219 +#: includes/admin/class-wc-subscriptions-admin.php:1260 #. 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:1225 +#: includes/admin/class-wc-subscriptions-admin.php:1266 msgid "Turn off Automatic Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1230 +#: includes/admin/class-wc-subscriptions-admin.php:1271 #. translators: placeholders are opening and closing link tags msgid "" "If you don't want new subscription purchases to automatically charge " @@ -330,11 +330,11 @@ msgid "" "more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1245 +#: includes/admin/class-wc-subscriptions-admin.php:1286 msgid "Customer Suspensions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1252 +#: includes/admin/class-wc-subscriptions-admin.php:1293 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 " @@ -344,29 +344,29 @@ msgid "" "this to 0 to turn off the customer suspension feature completely." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1256 +#: includes/admin/class-wc-subscriptions-admin.php:1297 msgid "Mixed Checkout" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1257 +#: includes/admin/class-wc-subscriptions-admin.php:1298 msgid "Allow multiple subscriptions and products to be purchased simultaneously." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1261 +#: includes/admin/class-wc-subscriptions-admin.php:1302 msgid "" "Allow a subscription product to be purchased with other products and " "subscriptions in the same transaction." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1265 +#: includes/admin/class-wc-subscriptions-admin.php:1306 msgid "$0 Initial Checkout" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1266 +#: includes/admin/class-wc-subscriptions-admin.php:1307 msgid "Allow $0 initial checkout without a payment method." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1270 +#: includes/admin/class-wc-subscriptions-admin.php:1311 msgid "" "Allow a subscription product with a $0 initial payment to be purchased " "without providing a payment method. The customer will be required to " @@ -374,16 +374,16 @@ msgid "" "subscription active." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1274 +#: includes/admin/class-wc-subscriptions-admin.php:1315 #: includes/upgrades/templates/wcs-about-2-0.php:108 msgid "Drip Downloadable Content" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1275 +#: includes/admin/class-wc-subscriptions-admin.php:1316 msgid "Enable dripping for downloadable content on subscription products." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1279 +#: includes/admin/class-wc-subscriptions-admin.php:1320 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 " @@ -391,7 +391,7 @@ msgid "" "customer that has an active subscription with that product." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1315 +#: includes/admin/class-wc-subscriptions-admin.php:1356 #. translators: $1-$2: opening and closing tags, $3-$4: opening and #. closing tags msgid "" @@ -399,77 +399,77 @@ msgid "" "start selling subscriptions!%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1320 +#: includes/admin/class-wc-subscriptions-admin.php:1361 msgid "Add a Subscription Product" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1321 +#: includes/admin/class-wc-subscriptions-admin.php:1362 #: includes/upgrades/templates/wcs-about-2-0.php:35 #: includes/upgrades/templates/wcs-about.php:34 -#: woocommerce-subscriptions.php:1112 +#: woocommerce-subscriptions.php:1113 msgid "Settings" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1405 +#: includes/admin/class-wc-subscriptions-admin.php:1445 #. 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:1445 +#: includes/admin/class-wc-subscriptions-admin.php:1545 msgid "We can't find a paid subscription order for this user." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1477 -#: includes/admin/class-wc-subscriptions-admin.php:1482 +#: includes/admin/class-wc-subscriptions-admin.php:1577 +#: includes/admin/class-wc-subscriptions-admin.php:1582 #. 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:1506 +#: includes/admin/class-wc-subscriptions-admin.php:1606 #. 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:1511 +#: includes/admin/class-wc-subscriptions-admin.php:1611 #. 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:1574 -#: includes/admin/class-wc-subscriptions-admin.php:1641 +#: includes/admin/class-wc-subscriptions-admin.php:1674 +#: includes/admin/class-wc-subscriptions-admin.php:1741 #: includes/admin/class-wcs-admin-system-status.php:95 #: includes/admin/reports/class-wcs-report-cache-manager.php:331 msgid "Yes" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1574 +#: includes/admin/class-wc-subscriptions-admin.php:1674 #: includes/admin/class-wcs-admin-system-status.php:95 #: includes/admin/reports/class-wcs-report-cache-manager.php:331 msgid "No" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1610 +#: includes/admin/class-wc-subscriptions-admin.php:1710 msgid "Automatic Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1641 +#: includes/admin/class-wc-subscriptions-admin.php:1741 msgid "" "Supports automatic renewal payments with the WooCommerce Subscriptions " "extension." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1736 +#: includes/admin/class-wc-subscriptions-admin.php:1836 msgid "Subscription items can no longer be edited." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1740 +#: includes/admin/class-wc-subscriptions-admin.php:1840 msgid "" "This subscription is no longer editable because the payment gateway does " "not allow modification of recurring amounts." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1759 +#: includes/admin/class-wc-subscriptions-admin.php:1859 #. translators: $1-2: opening and closing tags of a link that takes to Woo #. marketplace / Stripe product page msgid "" @@ -478,18 +478,18 @@ msgid "" "the %1$sfree Stripe extension%2$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1764 +#: includes/admin/class-wc-subscriptions-admin.php:1864 msgid "Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1772 +#: includes/admin/class-wc-subscriptions-admin.php:1872 #. translators: placeholders are opening and closing link tags msgid "" "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:1779 +#: includes/admin/class-wc-subscriptions-admin.php:1879 #. translators: $1-$2: opening and closing tags. Link to documents->payment #. gateways, 3$-4$: opening and closing tags. Link to WooCommerce extensions #. shop page @@ -498,13 +498,12 @@ msgid "" "the official %3$sWooCommerce Marketplace%4$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1881 +#: includes/admin/class-wc-subscriptions-admin.php:1981 msgid "Note that purchasing a subscription still requires an account." msgstr "" #: includes/admin/class-wcs-admin-meta-boxes.php:65 #: includes/admin/class-wcs-admin-meta-boxes.php:69 -#: templates/myaccount/related-orders.php:15 msgid "Related Orders" msgstr "" @@ -598,25 +597,24 @@ msgstr[1] "" #: includes/admin/class-wcs-admin-post-types.php:422 #: includes/admin/meta-boxes/views/html-related-orders-table.php:20 -#: templates/myaccount/my-subscriptions.php:26 -#: templates/myaccount/my-subscriptions.php:41 +#: templates/myaccount/my-subscriptions.php:22 +#: templates/myaccount/my-subscriptions.php:37 #: templates/myaccount/related-orders.php:24 #: templates/myaccount/related-orders.php:50 -#: templates/myaccount/related-subscriptions.php:21 -#: templates/myaccount/related-subscriptions.php:35 -#: templates/myaccount/subscription-details.php:17 +#: templates/myaccount/related-subscriptions.php:22 +#: templates/myaccount/related-subscriptions.php:36 +#: templates/myaccount/subscription-details.php:18 msgid "Status" msgstr "" #: includes/admin/class-wcs-admin-post-types.php:423 -#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:63 -#: templates/emails/cancelled-subscription.php:26 -#: templates/emails/expired-subscription.php:26 -#: templates/emails/on-hold-subscription.php:26 +#: templates/emails/cancelled-subscription.php:21 +#: templates/emails/expired-subscription.php:21 +#: templates/emails/on-hold-subscription.php:21 #: templates/emails/subscription-info.php:18 -#: templates/myaccount/my-subscriptions.php:25 -#: templates/myaccount/related-subscriptions.php:20 -#: woocommerce-subscriptions.php:255 +#: templates/myaccount/my-subscriptions.php:21 +#: templates/myaccount/related-subscriptions.php:21 +#: woocommerce-subscriptions.php:256 msgid "Subscription" msgstr "" @@ -669,12 +667,12 @@ msgid "Delete Permanently" msgstr "" #: includes/admin/class-wcs-admin-post-types.php:487 -#: includes/class-wc-subscriptions-product.php:748 +#: includes/class-wc-subscriptions-product.php:751 msgid "Restore this item from the Trash" msgstr "" #: includes/admin/class-wcs-admin-post-types.php:487 -#: includes/class-wc-subscriptions-product.php:749 +#: includes/class-wc-subscriptions-product.php:752 msgid "Restore" msgstr "" @@ -711,73 +709,73 @@ msgstr[0] "" msgstr[1] "" #: includes/admin/class-wcs-admin-post-types.php:601 -#: includes/class-wc-subscription.php:1949 +#: includes/class-wc-subscription.php:2005 #. 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:637 +#: includes/admin/class-wcs-admin-post-types.php:643 msgid "Y/m/d g:i:s A" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:640 +#: includes/admin/class-wcs-admin-post-types.php:646 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:893 -#: includes/admin/class-wcs-admin-post-types.php:896 #: includes/admin/class-wcs-admin-post-types.php:899 +#: includes/admin/class-wcs-admin-post-types.php:902 +#: includes/admin/class-wcs-admin-post-types.php:905 msgid "Subscription updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:894 +#: includes/admin/class-wcs-admin-post-types.php:900 msgid "Custom field updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:895 +#: includes/admin/class-wcs-admin-post-types.php:901 msgid "Custom field deleted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:900 +#: includes/admin/class-wcs-admin-post-types.php:906 msgid "Subscription saved." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:901 +#: includes/admin/class-wcs-admin-post-types.php:907 msgid "Subscription submitted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:903 +#: includes/admin/class-wcs-admin-post-types.php:909 #. translators: php date string msgid "Subscription scheduled for: %1$s." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:904 +#: includes/admin/class-wcs-admin-post-types.php:910 msgid "Subscription draft updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:940 +#: includes/admin/class-wcs-admin-post-types.php:946 msgid "Any Payment Method" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:941 +#: includes/admin/class-wcs-admin-post-types.php:947 msgid "None" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:947 -#: includes/class-wc-subscription.php:1932 +#: includes/admin/class-wcs-admin-post-types.php:953 +#: includes/class-wc-subscription.php:1988 #: includes/class-wcs-change-payment-method-admin.php:155 msgid "Manual Renewal" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1136 +#: includes/admin/class-wcs-admin-post-types.php:1142 #. translators: 1: user display name 2: user ID 3: user email msgid "%1$s (#%2$s – %3$s)" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1143 +#: includes/admin/class-wcs-admin-post-types.php:1149 #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:84 msgid "Search for a customer…" msgstr "" @@ -965,7 +963,7 @@ 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:549 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:639 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:190 #: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:204 #: templates/myaccount/related-orders.php:23 @@ -1020,6 +1018,7 @@ msgid "" msgstr "" #: includes/admin/meta-boxes/views/html-subscription-schedule.php:22 +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:72 msgid "Payment:" msgstr "" @@ -1096,7 +1095,7 @@ msgid "Unended Subscription Count" msgstr "" #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:22 -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:95 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:97 msgid "Customer" msgstr "" @@ -1156,45 +1155,45 @@ msgstr "" msgid "Average Lifetime Value" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:50 msgid "The average value of all customers' sign-up, switch and renewal orders." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:98 msgid "Active Subscriptions %s" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:98 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 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:99 msgid "Total Subscriptions %s" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:97 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:99 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 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:100 msgid "Total Subscription Orders %s" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:98 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:100 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 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:101 msgid "Lifetime Value from Subscriptions %s" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:99 +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:101 msgid "The total value of this customer's sign-up, switch and renewal orders." msgstr "" @@ -1245,189 +1244,201 @@ msgstr "" msgid "subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:398 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:472 msgid "%s signup revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:399 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:473 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:405 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:479 msgid "%s renewal revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:406 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:480 msgid "The sum of all renewal orders including tax and shipping." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:412 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:486 msgid "%s resubscribe revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:413 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:487 msgid "The sum of all resubscribe orders including tax and shipping." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:419 -msgid "%s new subscriptions" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:493 +msgid "%s switch revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:420 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:494 +msgid "The sum of all switch orders including tax and shipping." +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:500 +msgid "%2$s %1$s new subscriptions" +msgstr "" + +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:502 msgid "" "The number of subscriptions created during this period, either by being " "manually created, imported or a customer placing an order. This includes " "orders pending payment." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:426 -msgid "%s subscription signups" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508 +msgid "%2$s %1$s subscription signups" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:427 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:510 msgid "" -"The number of subscription parent orders created during this period. This " -"represents the new subscriptions created by customers placing an order via " -"checkout." +"The number of subscriptions purchased in 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:433 -msgid "%s subscription resubscribes" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:516 +msgid "%2$s %1$s subscription resubscribes" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:434 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:518 msgid "The number of resubscribe orders processed during this period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:440 -msgid "%s subscription renewals" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:524 +msgid "%2$s %1$s subscription renewals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:441 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:526 msgid "The number of renewal orders processed during this period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:447 -msgid "%s subscription switches" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:532 +msgid "%2$s %1$s subscription switches" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:448 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:534 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:454 -msgid "%s subscription cancellations" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:540 +msgid "%2$s %1$s subscription cancellations" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:455 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:542 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:461 -msgid "%s subscriptions ended" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:548 +msgid "%2$s %1$s ended subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:462 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:550 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:468 -msgid "%s current subscriptions" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:556 +msgid "%2$s %1$s current subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:469 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:558 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:485 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:574 msgid "%s net subscription gain" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:487 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:576 msgid "%s net subscription loss" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:492 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:581 msgid "Change in subscriptions between the start and end of the period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:506 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:595 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:154 msgid "Year" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:507 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:596 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:155 msgid "Last Month" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:597 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:156 msgid "This Month" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:509 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:598 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:157 msgid "Last 7 Days" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:553 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:643 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:194 #: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:208 msgid "Export CSV" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:610 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:702 msgid "Switched subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:626 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:718 msgid "New Subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:642 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:734 msgid "Subscriptions signups" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:657 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:749 msgid "Number of resubscribes" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:672 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:764 msgid "Number of renewals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:704 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:796 msgid "Subscriptions Ended" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:720 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:812 msgid "Cancellations" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:735 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:827 msgid "Signup Totals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:755 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:847 msgid "Resubscribe Totals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:775 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:867 msgid "Renewal Totals" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:887 +msgid "Switch Totals" +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:112 msgid "%s renewal revenue recovered" msgstr "" @@ -1525,16 +1536,16 @@ msgstr "" msgid "Renewals amount" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:116 +#: includes/api/class-wc-rest-subscriptions-controller.php:183 msgid "Customer ID is invalid." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:221 +#: includes/api/class-wc-rest-subscriptions-controller.php:288 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:171 msgid "Invalid subscription id." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:292 +#: includes/api/class-wc-rest-subscriptions-controller.php:359 #: includes/api/legacy/class-wc-api-subscriptions.php:307 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:303 msgid "" @@ -1542,7 +1553,7 @@ msgid "" "Subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:330 +#: includes/api/class-wc-rest-subscriptions-controller.php:397 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:333 #. translators: 1$: gateway id, 2$: error message msgid "" @@ -1550,70 +1561,154 @@ msgid "" "%2$s" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:396 -#: includes/api/class-wc-rest-subscriptions-controller.php:562 +#: includes/api/class-wc-rest-subscriptions-controller.php:463 +#: includes/api/class-wc-rest-subscriptions-controller.php:762 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:284 msgid "Updating subscription dates errored with message: %s" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:421 +#: includes/api/class-wc-rest-subscriptions-controller.php:488 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:347 msgid "The number of billing periods between subscription renewals." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:426 +#: includes/api/class-wc-rest-subscriptions-controller.php:493 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:352 msgid "Billing period for the subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:432 +#: includes/api/class-wc-rest-subscriptions-controller.php:499 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:358 msgid "Subscription payment details." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:437 +#: includes/api/class-wc-rest-subscriptions-controller.php:504 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:363 msgid "Payment gateway ID." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:444 +#: includes/api/class-wc-rest-subscriptions-controller.php:511 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:370 msgid "The subscription's start date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:449 +#: includes/api/class-wc-rest-subscriptions-controller.php:516 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:375 msgid "The subscription's trial date" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:454 +#: includes/api/class-wc-rest-subscriptions-controller.php:521 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:380 msgid "The subscription's next payment date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:459 +#: includes/api/class-wc-rest-subscriptions-controller.php:526 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:385 msgid "The subscription's end date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:464 +#: includes/api/class-wc-rest-subscriptions-controller.php:531 msgid "" "The subscription's original subscription ID if this is a resubscribed " "subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:470 +#: includes/api/class-wc-rest-subscriptions-controller.php:537 msgid "The subscription's resubscribed subscription ID." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:476 +#: includes/api/class-wc-rest-subscriptions-controller.php:543 msgid "The date the subscription's latest order was completed, in GMT." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:482 +#: includes/api/class-wc-rest-subscriptions-controller.php:549 msgid "The date the subscription's latest order was paid, in GMT." msgstr "" +#: includes/api/class-wc-rest-subscriptions-controller.php:555 +msgid "Removed line items data." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:562 +msgid "Item ID." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:568 +msgid "Product name." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:574 +msgid "Product SKU." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:580 +msgid "Product ID." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:585 +msgid "Variation ID, if applicable." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:590 +msgid "Quantity ordered." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:595 +msgid "Tax class of product." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:601 +msgid "Product price." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:607 +msgid "Line subtotal (before discounts)." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:612 +msgid "Line subtotal tax (before discounts)." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:617 +msgid "Line total (after discounts)." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:622 +msgid "Line total tax (after discounts)." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:627 +msgid "Line taxes." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:635 +msgid "Tax rate ID." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:641 +msgid "Tax total." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:647 +msgid "Tax subtotal." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:656 +msgid "Removed line item meta data." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:664 +msgid "Meta key." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:670 +msgid "Meta label." +msgstr "" + +#: includes/api/class-wc-rest-subscriptions-controller.php:676 +msgid "Meta value." +msgstr "" + #: includes/api/legacy/class-wc-api-subscriptions.php:102 wcs-functions.php:178 msgid "Invalid subscription status given." msgstr "" @@ -1673,56 +1768,56 @@ msgstr "" msgid "Error during subscription status transition." msgstr "" -#: includes/class-wc-subscription.php:1132 -#: includes/class-wc-subscriptions-manager.php:2279 +#: includes/class-wc-subscription.php:1181 +#: includes/class-wc-subscriptions-manager.php:2280 #. translators: placeholder is human time diff (e.g. "3 weeks") msgid "In %s" msgstr "" -#: includes/class-wc-subscription.php:1135 +#: includes/class-wc-subscription.php:1184 #: 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:1142 +#: includes/class-wc-subscription.php:1191 msgid "Not yet ended" msgstr "" -#: includes/class-wc-subscription.php:1145 +#: includes/class-wc-subscription.php:1194 msgid "Not cancelled" msgstr "" -#: includes/class-wc-subscription.php:1260 +#: includes/class-wc-subscription.php:1309 msgid "The creation date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:1263 +#: includes/class-wc-subscription.php:1312 msgid "The start date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:1267 +#: includes/class-wc-subscription.php:1316 msgid "The %s date of a subscription can not be deleted. You must delete the order." msgstr "" -#: includes/class-wc-subscription.php:1275 -#: includes/class-wc-subscription.php:2360 +#: includes/class-wc-subscription.php:1324 +#: includes/class-wc-subscription.php:2416 msgid "Subscription #%d: " msgstr "" -#: includes/class-wc-subscription.php:1682 +#: includes/class-wc-subscription.php:1738 msgid "Payment status marked complete." msgstr "" -#: includes/class-wc-subscription.php:1710 +#: includes/class-wc-subscription.php:1766 msgid "Payment failed." msgstr "" -#: includes/class-wc-subscription.php:1715 +#: includes/class-wc-subscription.php:1771 msgid "Subscription Cancelled: maximum number of failed payments reached." msgstr "" -#: includes/class-wc-subscription.php:1825 +#: includes/class-wc-subscription.php:1881 msgid "" "The \"all\" value for $order_type parameter is deprecated. It was a " "misnomer, as it did not return resubscribe orders. It was also inconsistent " @@ -1732,52 +1827,52 @@ msgid "" "resubscribe." msgstr "" -#: includes/class-wc-subscription.php:2022 wcs-functions.php:794 +#: includes/class-wc-subscription.php:2078 wcs-functions.php:823 msgid "Payment method meta must be an array." msgstr "" -#: includes/class-wc-subscription.php:2258 +#: includes/class-wc-subscription.php:2314 msgid "Invalid format. First parameter needs to be an array." msgstr "" -#: includes/class-wc-subscription.php:2262 +#: includes/class-wc-subscription.php:2318 msgid "Invalid data. First parameter was empty when passed to update_dates()." msgstr "" -#: includes/class-wc-subscription.php:2269 +#: includes/class-wc-subscription.php:2325 msgid "" "Invalid data. First parameter has a date that is not in the registered date " "types." msgstr "" -#: includes/class-wc-subscription.php:2333 +#: includes/class-wc-subscription.php:2389 msgid "The %s date must occur after the cancellation date." msgstr "" -#: includes/class-wc-subscription.php:2338 +#: includes/class-wc-subscription.php:2394 msgid "The %s date must occur after the last payment date." msgstr "" -#: includes/class-wc-subscription.php:2342 +#: includes/class-wc-subscription.php:2398 msgid "The %s date must occur after the next payment date." msgstr "" -#: includes/class-wc-subscription.php:2347 +#: includes/class-wc-subscription.php:2403 msgid "The %s date must occur after the trial end date." msgstr "" -#: includes/class-wc-subscription.php:2351 +#: includes/class-wc-subscription.php:2407 msgid "The %s date must occur after the start date." msgstr "" -#: includes/class-wc-subscription.php:2380 -#: includes/class-wc-subscriptions-checkout.php:325 +#: includes/class-wc-subscription.php:2436 +#: includes/class-wc-subscriptions-checkout.php:328 #: includes/wcs-order-functions.php:305 msgid "Backordered" msgstr "" #: includes/class-wc-subscriptions-addresses.php:47 -msgid "Change Address" +msgid "Change address" msgstr "" #: includes/class-wc-subscriptions-addresses.php:71 @@ -1792,21 +1887,50 @@ msgstr "" msgid "Update the %1$s used for %2$sall%3$s of my active subscriptions" msgstr "" -#: includes/class-wc-subscriptions-cart.php:944 -msgid "Please enter a valid postcode/ZIP." +#: includes/class-wc-subscriptions-cart-validator.php:56 +#: woocommerce-subscriptions.php:498 +msgid "" +"A subscription renewal has been removed from your cart. Multiple " +"subscriptions can not be purchased at the same time." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1102 +#: includes/class-wc-subscriptions-cart-validator.php:62 +#: woocommerce-subscriptions.php:504 +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 "" + +#: includes/class-wc-subscriptions-cart-validator.php:68 +#: woocommerce-subscriptions.php:510 +msgid "" +"A subscription has been removed from your cart. Products and subscriptions " +"can not be purchased at the same time." +msgstr "" + +#: includes/class-wc-subscriptions-cart-validator.php:110 +msgid "" +"Your cart has been emptied of subscription products. Products and " +"subscriptions cannot be purchased at the same time." +msgstr "" + +#: includes/class-wc-subscriptions-cart-validator.php:133 +#: includes/class-wc-subscriptions-cart.php:1412 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:1190 +#: includes/class-wc-subscriptions-cart.php:942 +msgid "Please enter a valid postcode/ZIP." +msgstr "" + +#: includes/class-wc-subscriptions-cart.php:1171 msgid "Invalid recurring shipping method." msgstr "" -#: includes/class-wc-subscriptions-cart.php:2069 +#: includes/class-wc-subscriptions-cart.php:2082 msgid "now" msgstr "" @@ -1841,11 +1965,12 @@ msgid "" msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:242 +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:97 msgid "There was an error with your request. Please try again." msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:246 -#: includes/class-wcs-template-loader.php:27 +#: includes/class-wcs-template-loader.php:28 msgid "Invalid Subscription." msgstr "" @@ -1895,18 +2020,18 @@ msgid "" "subscription." msgstr "" -#: includes/class-wc-subscriptions-checkout.php:185 -#: includes/class-wc-subscriptions-checkout.php:356 +#: includes/class-wc-subscriptions-checkout.php:188 +#: includes/class-wc-subscriptions-checkout.php:374 #. translators: placeholder is an internal error number msgid "Error %d: Unable to create subscription. Please try again." msgstr "" -#: includes/class-wc-subscriptions-checkout.php:202 +#: includes/class-wc-subscriptions-checkout.php:205 #. translators: placeholder is an internal error number msgid "Error %d: Unable to add tax to subscription. Please try again." msgstr "" -#: includes/class-wc-subscriptions-checkout.php:214 +#: includes/class-wc-subscriptions-checkout.php:217 #. translators: placeholder is an internal error number msgid "Error %d: Unable to create order. Please try again." msgstr "" @@ -2031,7 +2156,7 @@ msgid "Error: Unable to create renewal order with note \"%s\"" msgstr "" #: includes/class-wc-subscriptions-manager.php:168 -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:209 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:213 msgid "Subscription doesn't exist in scheduled action: %d" msgstr "" @@ -2076,41 +2201,41 @@ msgstr "" msgid "Pending subscription created." msgstr "" -#: includes/class-wc-subscriptions-manager.php:1819 +#: includes/class-wc-subscriptions-manager.php:1820 #. translators: all fields are full html nodes: 1$: month input, 2$: day input, #. 3$: year input, 4$: hour input, 5$: minute input. Change the order if you'd #. like msgid "%1$s%2$s, %3$s @ %4$s : %5$s" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1823 +#: includes/class-wc-subscriptions-manager.php:1824 #. translators: all fields are full html nodes: 1$: month input, 2$: day input, #. 3$: year input. Change the order if you'd like msgid "%1$s%2$s, %3$s" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1828 +#: includes/class-wc-subscriptions-manager.php:1829 msgid "Change" msgstr "" -#: includes/class-wc-subscriptions-manager.php:2161 +#: includes/class-wc-subscriptions-manager.php:2162 #. translators: placeholder is subscription ID msgid "Failed sign-up for subscription %s." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2252 +#: includes/class-wc-subscriptions-manager.php:2253 msgid "Invalid security token, please reload the page and try again." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2256 +#: includes/class-wc-subscriptions-manager.php:2257 msgid "Only store managers can edit payment dates." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2260 +#: includes/class-wc-subscriptions-manager.php:2261 msgid "Please enter all date fields." msgstr "" -#: includes/class-wc-subscriptions-manager.php:2285 +#: includes/class-wc-subscriptions-manager.php:2286 msgid "Date Changed" msgstr "" @@ -2252,7 +2377,7 @@ msgstr "" msgid "%1$s and a %2$s sign-up fee" msgstr "" -#: includes/class-wc-subscriptions-product.php:951 +#: includes/class-wc-subscriptions-product.php:954 msgid "" "This variation can not be removed because it is associated with active " "subscriptions. To remove this variation, please cancel and delete the " @@ -2268,18 +2393,18 @@ msgstr "" msgid "Subscription renewal orders cannot be cancelled." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:171 +#: includes/class-wc-subscriptions-switcher.php:180 msgid "" "You have a subscription to this product. Choosing a new subscription will " "replace your existing subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:173 +#: includes/class-wc-subscriptions-switcher.php:182 msgid "Choose a new subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:195 -#: includes/class-wc-subscriptions-switcher.php:1006 +#: includes/class-wc-subscriptions-switcher.php:222 +#: includes/class-wc-subscriptions-switcher.php:1236 msgid "" "Your cart contained an invalid subscription switch request. It has been " "removed." @@ -2289,13 +2414,13 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wc-subscriptions-switcher.php:237 +#: includes/class-wc-subscriptions-switcher.php:264 msgid "" "You have already subscribed to this product and it is limited to one per " "customer. You can not purchase the product again." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:246 +#: includes/class-wc-subscriptions-switcher.php:273 #. translators: 1$: is the "You have already subscribed to this product" #. notice, 2$-4$: opening/closing link tags, 3$: an order number msgid "" @@ -2303,123 +2428,110 @@ msgid "" "subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:341 +#: includes/class-wc-subscriptions-switcher.php:368 msgid "Switching" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:344 +#: includes/class-wc-subscriptions-switcher.php:371 #. 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:349 -msgid "Allow Switching" -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:350 -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:366 +#: includes/class-wc-subscriptions-switcher.php:381 msgid "Prorate Recurring Payment" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:367 +#: includes/class-wc-subscriptions-switcher.php:382 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:384 +#: includes/class-wc-subscriptions-switcher.php:399 msgid "Prorate Sign up Fee" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:385 +#: includes/class-wc-subscriptions-switcher.php:400 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:400 +#: includes/class-wc-subscriptions-switcher.php:415 msgid "Prorate Subscription Length" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:401 +#: includes/class-wc-subscriptions-switcher.php:416 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:416 +#: includes/class-wc-subscriptions-switcher.php:431 msgid "Switch Button Text" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:417 +#: includes/class-wc-subscriptions-switcher.php:432 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:421 -#: includes/class-wc-subscriptions-switcher.php:447 -#: includes/class-wc-subscriptions-switcher.php:2363 +#: includes/class-wc-subscriptions-switcher.php:436 +#: includes/class-wc-subscriptions-switcher.php:528 +#: includes/class-wc-subscriptions-switcher.php:2544 msgid "Upgrade or Downgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:871 +#: includes/class-wc-subscriptions-switcher.php:468 +msgid "Allow Switching" +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:1095 msgid "Switch order cancelled due to a new switch order being created #%s." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:958 +#: includes/class-wc-subscriptions-switcher.php:1183 msgid "Switch Order" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:973 +#: includes/class-wc-subscriptions-switcher.php:1198 msgid "Switched Subscription" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1078 +#: includes/class-wc-subscriptions-switcher.php:1364 msgid "You can only switch to a subscription product." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1084 +#: includes/class-wc-subscriptions-switcher.php:1370 msgid "We can not find your old subscription item." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1106 +#: includes/class-wc-subscriptions-switcher.php:1392 msgid "You can not switch to the same subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1153 +#: includes/class-wc-subscriptions-switcher.php:1439 msgid "" "You can not switch this subscription. It appears you do not own the " "subscription." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1194 +#: includes/class-wc-subscriptions-switcher.php:1480 msgid "There was an error locating the switch details." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1413 -msgid "" -"Invalid switch type \"%s\". Switch must be one of: \"upgrade\", " -"\"downgrade\" or \"crossgrade\"." -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:1907 +#: includes/class-wc-subscriptions-switcher.php:1952 msgid "The original subscription item being switched cannot be found." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1909 +#: includes/class-wc-subscriptions-switcher.php:1954 msgid "The item on the switch order cannot be found." msgstr "" @@ -2533,33 +2645,38 @@ msgid "Weekly" msgstr "" #: includes/class-wcs-cart-initial-payment.php:59 -#: includes/class-wcs-cart-renewal.php:191 +#: includes/class-wcs-cart-renewal.php:194 msgid "That doesn't appear to be your order." msgstr "" -#: includes/class-wcs-cart-renewal.php:207 +#: includes/class-wcs-cart-renewal.php:210 msgid "" "This order can no longer be paid because the corresponding subscription " "does not require payment at this time." msgstr "" -#: includes/class-wcs-cart-renewal.php:224 +#: includes/class-wcs-cart-renewal.php:227 msgid "Complete checkout to renew your subscription." msgstr "" -#: includes/class-wcs-cart-renewal.php:305 +#: includes/class-wcs-cart-renewal.php:313 #. 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:339 +#: includes/class-wcs-cart-renewal.php:348 #. translators: %s is subscription's number msgid "Subscription #%s has not been added to the cart." msgstr "" -#: includes/class-wcs-cart-renewal.php:374 +#: includes/class-wcs-cart-renewal.php:351 +#. translators: %s is order's number +msgid "Order #%s has not been added to the cart." +msgstr "" + +#: includes/class-wcs-cart-renewal.php:390 msgid "" "We couldn't find the original subscription for an item in your cart. The " "item was removed." @@ -2569,7 +2686,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:381 +#: includes/class-wcs-cart-renewal.php:397 msgid "" "We couldn't find the original renewal order for an item in your cart. The " "item was removed." @@ -2579,7 +2696,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:648 +#: includes/class-wcs-cart-renewal.php:664 msgid "All linked subscription items have been removed from the cart." msgstr "" @@ -2616,6 +2733,7 @@ msgid "Ignore this error" msgstr "" #: includes/class-wcs-failed-scheduled-action-manager.php:139 +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:110 msgid "Learn more" msgstr "" @@ -2642,15 +2760,15 @@ msgstr "" msgid "Limit to one of any status" msgstr "" -#: includes/class-wcs-my-account-auto-renew-toggle.php:132 +#: includes/class-wcs-my-account-auto-renew-toggle.php:135 msgid "Auto Renewal Toggle" msgstr "" -#: includes/class-wcs-my-account-auto-renew-toggle.php:133 +#: includes/class-wcs-my-account-auto-renew-toggle.php:136 msgid "Display the auto renewal toggle" msgstr "" -#: includes/class-wcs-my-account-auto-renew-toggle.php:134 +#: includes/class-wcs-my-account-auto-renew-toggle.php:137 msgid "" "Allow customers to turn on and off automatic renewals from their View " "Subscription page." @@ -2761,17 +2879,29 @@ msgid "" "subscriptions with no payment method in common." msgstr "" -#: includes/class-wcs-staging.php:38 +#: includes/class-wcs-staging.php:39 msgid "" "Payment processing skipped - renewal order created on %sstaging site%s " "under staging site lock. Live site is at %s" msgstr "" -#: includes/class-wcs-staging.php:53 +#: includes/class-wcs-staging.php:54 msgid "staging" msgstr "" -#: includes/class-wcs-template-loader.php:27 +#: includes/class-wcs-staging.php:82 +msgid "" +"Subscription locked to Manual Renewal while the store is in staging mode. " +"Payment method changes will take effect in live mode." +msgstr "" + +#: includes/class-wcs-switch-cart-item.php:302 +msgid "" +"Invalid switch type \"%s\". Switch must be one of: \"upgrade\", " +"\"downgrade\" or \"crossgrade\"." +msgstr "" + +#: includes/class-wcs-template-loader.php:28 msgid "My Account" msgstr "" @@ -2863,7 +2993,7 @@ msgid "" msgstr "" #: includes/early-renewal/class-wcs-cart-early-renewal.php:71 -msgid "Renew Now" +msgid "Renew now" msgstr "" #: includes/early-renewal/class-wcs-cart-early-renewal.php:99 @@ -2876,40 +3006,74 @@ msgstr "" msgid "Complete checkout to renew now." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:248 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:309 +msgid "Order %s created to record early renewal." +msgstr "" + +#: includes/early-renewal/class-wcs-cart-early-renewal.php:364 +msgid "Cancel" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-manager.php:53 +msgid "Early Renewal" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-manager.php:54 +msgid "Accept Early Renewal Payments" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-manager.php:55 +msgid "" +"With early renewals enabled, customers can renew their subscriptions before " +"the next payment date." +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-manager.php:63 +msgid "Accept Early Renewal Payments via a Modal" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-manager.php:65 +msgid "" +"Allow customers to bypass the checkout and renew their subscription early " +"from their %1$sMy Account > View Subscription%2$s page. %3$sLearn more.%4$s" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:39 +msgid "Pay now" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:56 +msgid "Renew early" +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:104 +msgid "We were unable to locate that subscription, please try again." +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:114 +msgid "We couldn't create a renewal order for your subscription, please try again." +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:131 +msgid "Payment for this renewal order was unsuccessful, please try again." +msgstr "" + +#: includes/early-renewal/class-wcs-early-renewal-modal-handler.php:134 +msgid "Your early renewal order was successful." +msgstr "" + +#: includes/early-renewal/wcs-early-renewal-functions.php:168 #. translators: placeholder contains a link to the order's edit screen. msgid "Customer successfully renewed early with order %s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:251 +#: includes/early-renewal/wcs-early-renewal-functions.php:171 #. translators: placeholder contains a link to the order's edit screen. msgid "" "Failed to update subscription dates after customer renewed early with order " "%s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:339 -msgid "Order %s created to record early renewal." -msgstr "" - -#: includes/early-renewal/class-wcs-cart-early-renewal.php:394 -msgid "Cancel" -msgstr "" - -#: includes/early-renewal/class-wcs-early-renewal-manager.php:44 -msgid "Early Renewal" -msgstr "" - -#: includes/early-renewal/class-wcs-early-renewal-manager.php:45 -msgid "Accept Early Renewal Payments" -msgstr "" - -#: includes/early-renewal/class-wcs-early-renewal-manager.php:46 -msgid "" -"With early renewals enabled, customers can renew their subscriptions before " -"the next payment date." -msgstr "" - #: includes/emails/class-wcs-email-cancelled-subscription.php:26 msgid "Cancelled Subscription" msgstr "" @@ -2924,37 +3088,37 @@ msgstr "" msgid "Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:147 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:214 -#: includes/emails/class-wcs-email-expired-subscription.php:145 -#: includes/emails/class-wcs-email-on-hold-subscription.php:145 +#: includes/emails/class-wcs-email-cancelled-subscription.php:145 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:212 +#: includes/emails/class-wcs-email-expired-subscription.php:143 +#: includes/emails/class-wcs-email-on-hold-subscription.php:143 msgid "Enable this email notification" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:154 -#: includes/emails/class-wcs-email-expired-subscription.php:152 -#: includes/emails/class-wcs-email-on-hold-subscription.php:152 +#: includes/emails/class-wcs-email-cancelled-subscription.php:152 +#: includes/emails/class-wcs-email-expired-subscription.php:150 +#: includes/emails/class-wcs-email-on-hold-subscription.php:150 #. 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:161 -#: includes/emails/class-wcs-email-expired-subscription.php:159 -#: includes/emails/class-wcs-email-on-hold-subscription.php:159 +#: includes/emails/class-wcs-email-cancelled-subscription.php:159 +#: includes/emails/class-wcs-email-expired-subscription.php:157 +#: includes/emails/class-wcs-email-on-hold-subscription.php:157 msgid "" "This controls the email subject line. Leave blank to use the default " "subject: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:168 +#: includes/emails/class-wcs-email-cancelled-subscription.php:166 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:175 -#: includes/emails/class-wcs-email-expired-subscription.php:173 -#: includes/emails/class-wcs-email-on-hold-subscription.php:173 +#: includes/emails/class-wcs-email-cancelled-subscription.php:173 +#: includes/emails/class-wcs-email-expired-subscription.php:171 +#: includes/emails/class-wcs-email-on-hold-subscription.php:171 msgid "Choose which format of email to send." msgstr "" @@ -3056,8 +3220,8 @@ msgstr "" msgid "Subscription argument passed in is not an object." msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:166 -#: includes/emails/class-wcs-email-on-hold-subscription.php:166 +#: includes/emails/class-wcs-email-expired-subscription.php:164 +#: includes/emails/class-wcs-email-on-hold-subscription.php:164 msgid "" "This controls the main heading contained within the email notification. " "Leave blank to use the default heading: %s." @@ -3150,22 +3314,29 @@ msgstr "" msgid "Your {blogname} renewal order receipt from {order_date}" msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:133 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:134 +msgid "" +"Sorry, it seems there are no available payment methods which support " +"subscriptions. Please see %sEnabling Payment Gateways for Subscriptions%s " +"if you require assistance." +msgstr "" + +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:136 msgid "" "Sorry, it seems there are no available payment methods which support " "subscriptions. Please contact us if you require assistance or wish to make " "alternate arrangements." msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:266 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:270 msgid "Supported features:" msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:269 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:273 msgid "Subscription features:" msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:273 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:277 msgid "Change payment features:" msgstr "" @@ -3212,7 +3383,7 @@ msgid "" "subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:109 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:110 #. translators: placeholders are opening and closing link tags. 1$-2$: to docs #. on woocommerce, 3$-4$ to gateway settings on the site msgid "" @@ -3221,9 +3392,7 @@ msgid "" "Subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:120 -#. translators: placeholders are opening and closing strong and link tags. -#. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:121 msgid "" "%1$sPayPal Reference Transactions are not enabled on your account%2$s, some " "subscription management features are not enabled. Please contact PayPal and " @@ -3231,14 +3400,23 @@ msgid "" "%5$sCheck PayPal Account%6$s %3$sLearn more %7$s" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:136 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:124 +msgid "" +"%1$sPayPal Reference Transactions are not enabled on your account%2$s. If " +"you wish to use PayPal Reference Transactions with Subscriptions, please " +"contact PayPal and request they %3$senable PayPal Reference " +"Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %3$sLearn " +"more %7$s" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:146 #. translators: placeholders are opening and closing strong tags. msgid "" "%1$sPayPal Reference Transactions are enabled on your account%2$s. All " "subscription management features are now enabled. Happy selling!" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:147 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:157 #. translators: placeholders are link opening and closing tags. 1$-2$: to #. gateway settings, 3$-4$: support docs on woocommerce.com msgid "" @@ -3246,7 +3424,7 @@ msgid "" "Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:160 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:170 #. translators: placeholders are opening and closing link tags. 1$-2$: docs on #. woocommerce, 3$-4$: dismiss link msgid "" @@ -3254,23 +3432,23 @@ msgid "" "subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:182 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:192 msgid "Ignore this error (not recommended)" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:187 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:197 msgid "Open a ticket" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:274 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:284 msgid "PayPal Subscription ID:" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:300 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:310 msgid "Enable PayPal Standard for Subscriptions" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:308 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:318 #. translators: Placeholders are the opening and closing link tags. msgid "" "Before enabling PayPal Standard for Subscriptions, please note, when using " @@ -3488,8 +3666,6 @@ msgid "Created Date" msgstr "" #: includes/privacy/class-wcs-privacy-exporters.php:79 -#: templates/checkout/recurring-totals.php:123 -#: templates/checkout/recurring-totals.php:124 msgid "Recurring Total" msgstr "" @@ -3621,13 +3797,13 @@ msgstr "" msgid "Customers with a subscription are excluded from this setting." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:332 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:337 #. 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:349 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:354 #. translators: 1$: number of action scheduler hooks upgraded, 2$: #. "{execution_time}", will be replaced on front end with actual time msgid "" @@ -3635,13 +3811,13 @@ msgid "" "seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:361 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:366 #. 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:374 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:379 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, #. 4$: break tag msgid "" @@ -3649,15 +3825,15 @@ msgid "" "and try again. If problem persists, %2$scontact support%3$s." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:625 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:630 msgid "Welcome to WooCommerce Subscriptions 2.1" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:625 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:630 msgid "About WooCommerce Subscriptions" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:806 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:811 msgid "" "%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce " "Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may " @@ -3665,7 +3841,7 @@ msgid "" "ticket%6$s for further assistance. %7$sLearn more »%8$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:875 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:881 msgid "" "%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions " "2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and " @@ -3684,54 +3860,40 @@ msgid "Subscription end date in the past" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:85 -msgid "New options to allow customers to sign up without a credit card" +msgid "Improved experience for customers who renew their subscriptions early" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:86 msgid "" -"Allow customers to access free trial and other $0 subscription products " -"without needing to enter their credit card details on sign up." +"Allow customers to renew early from their %sMy Account > Subscription%s " +"page without going through the checkout." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:89 -msgid "Improved subscription payment method information" +msgid "Improved subscription switching proration calculations" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:90 msgid "" -"Customers can now see more information about what payment method will be " -"used for future payments." +"We've made improvements to the code which calculates the upgrade costs when " +"a customer switches their subscription. This has enabled us to fix a number " +"of complicated switching scenarios." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:93 -msgid "Auto-renewal toggle" +msgid "View subscriptions and orders contributing to reports" msgstr "" -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:94 +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:95 msgid "" -"Enabled via a setting, this new feature will allow your customers to turn " -"on and off automatic payments from the %sMy Account > View Subscription%s " -"pages." -msgstr "" - -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:97 -msgid "Update all subscription payment methods" -msgstr "" - -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:98 -msgid "" -"Customers will now have the option to update all their subscriptions when " -"they are changing one of their subscription's payment methods - provided " -"the payment gateway supports it." +"Want to view which specific orders and subscriptions are contributing to " +"subscription-related reports? You can now do just that by clicking on the " +"%1$s link while viewing %2$ssubscription reports%3$s." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:103 #. translators: placeholder is Subscription version string ('2.3') -msgid "Welcome to Subscriptions %s" -msgstr "" - -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:110 -msgid "Learn More" +msgid "Welcome to WooCommerce Subscriptions %s!" msgstr "" #: includes/upgrades/templates/update-welcome-notice.php:2 @@ -3756,12 +3918,12 @@ msgid "We hope you enjoy it!" msgstr "" #: includes/upgrades/templates/update-welcome-notice.php:8 -msgid "What's New?" +msgid "What's new?" msgstr "" #: includes/upgrades/templates/update-welcome-notice.php:16 #. translators: placeholder is Subscription version string ('2.3') -msgid "Want to know more about %s and these new features?" +msgid "Want to know more about Subscriptions %s and these new features?" msgstr "" #: includes/upgrades/templates/wcs-about-2-0.php:20 @@ -4510,6 +4672,10 @@ msgstr "" msgid "MM" msgstr "" +#: includes/wcs-helper-functions.php:263 +msgid "Invalid sort order type: %s. The $sort_order argument must be %s or %s." +msgstr "" + #: includes/wcs-order-functions.php:344 msgid "Subscription Renewal Order – %s" msgstr "" @@ -4559,8 +4725,8 @@ msgstr[0] "" msgstr[1] "" #: includes/wcs-user-functions.php:340 -#: templates/single-product/add-to-cart/subscription.php:43 -#: templates/single-product/add-to-cart/variable-subscription.php:29 +#: templates/single-product/add-to-cart/subscription.php:30 +#: templates/single-product/add-to-cart/variable-subscription.php:28 msgid "Resubscribe" msgstr "" @@ -4650,34 +4816,30 @@ msgstr "" msgid "Billing Period:" msgstr "" -#: templates/cart/cart-recurring-shipping.php:19 -msgid "Recurring shipping options can be selected on checkout." -msgstr "" - -#: templates/cart/cart-recurring-shipping.php:33 +#: templates/cart/cart-recurring-shipping.php:32 msgid "Shipping costs will be calculated once you have provided your address." msgstr "" -#: templates/cart/cart-recurring-shipping.php:35 +#: templates/cart/cart-recurring-shipping.php:34 msgid "" "There are no shipping methods available. Please double check your address, " "or contact us if you need any help." msgstr "" -#: templates/checkout/form-change-payment-method.php:92 +#: templates/checkout/form-change-payment-method.php:81 msgid "" "Sorry, it seems no payment gateways support changing the recurring payment " "method. Please contact us if you require assistance or to make alternate " "arrangements." msgstr "" -#: templates/checkout/form-change-payment-method.php:101 +#: templates/checkout/form-change-payment-method.php:90 #. translators: $1: opening tag, $2: closing tag msgid "Update the payment method used for %1$sall%2$s of my current subscriptions" msgstr "" #: templates/checkout/recurring-totals.php:19 -msgid "Recurring Totals" +msgid "Recurring totals" msgstr "" #: templates/checkout/recurring-totals.php:28 @@ -4685,21 +4847,26 @@ msgstr "" msgid "Subtotal" msgstr "" -#: templates/emails/admin-new-switch-order.php:24 +#: templates/checkout/recurring-totals.php:123 +#: templates/checkout/recurring-totals.php:124 +msgid "Recurring total" +msgstr "" + +#: templates/emails/admin-new-switch-order.php:20 msgid "Switch Order Details" msgstr "" -#: templates/emails/admin-new-switch-order.php:30 -#: templates/emails/customer-completed-switch-order.php:28 -msgid "New Subscription Details" +#: templates/emails/admin-new-switch-order.php:28 +#: templates/emails/customer-completed-switch-order.php:26 +msgid "New subscription details" msgstr "" -#: templates/emails/admin-payment-retry.php:28 +#: templates/emails/admin-payment-retry.php:24 #: templates/emails/plain/admin-payment-retry.php:21 msgid "The renewal order is as follows:" msgstr "" -#: templates/emails/cancelled-subscription.php:19 +#: templates/emails/cancelled-subscription.php:16 #: templates/emails/plain/cancelled-subscription.php:16 #. translators: $1: customer's billing first name and last name msgid "" @@ -4707,41 +4874,52 @@ msgid "" "details are as follows:" msgstr "" -#: templates/emails/cancelled-subscription.php:46 -#: templates/emails/expired-subscription.php:46 -#: templates/emails/on-hold-subscription.php:46 +#: templates/emails/cancelled-subscription.php:41 +#: templates/emails/expired-subscription.php:41 +#: templates/emails/on-hold-subscription.php:41 msgid "-" msgstr "" -#: templates/emails/customer-completed-renewal-order.php:20 -#: templates/emails/plain/customer-completed-renewal-order.php:16 -#. translators: placeholder is the name of the site -msgid "" -"Hi there. Your subscription renewal order with %s has been completed. Your " -"order details are shown below for your reference:" -msgstr "" - -#: templates/emails/customer-completed-switch-order.php:20 -#: templates/emails/plain/customer-completed-switch-order.php:16 -#. translators: placeholder is the name of the site -msgid "" -"Hi there. You have successfully changed your subscription items on %s. Your " -"new order and subscription details are shown below for your reference:" -msgstr "" - +#: templates/emails/customer-completed-renewal-order.php:17 +#: templates/emails/customer-completed-switch-order.php:17 +#: templates/emails/customer-payment-retry.php:16 #: templates/emails/customer-processing-renewal-order.php:17 -#: templates/emails/plain/customer-processing-renewal-order.php:15 -msgid "" -"Your subscription renewal order has been received and is now being " -"processed. Your order details are shown below for your reference:" +#: templates/emails/customer-renewal-invoice.php:16 +#: templates/emails/plain/customer-completed-renewal-order.php:16 +#: templates/emails/plain/customer-completed-switch-order.php:16 +#: templates/emails/plain/customer-payment-retry.php:16 +#: templates/emails/plain/customer-processing-renewal-order.php:16 +#: templates/emails/plain/customer-renewal-invoice.php:16 +#. translators: %s: Customer first name +msgid "Hi %s," msgstr "" -#: templates/emails/customer-renewal-invoice.php:20 -#: templates/emails/customer-renewal-invoice.php:27 +#: templates/emails/customer-completed-renewal-order.php:18 +#: templates/emails/plain/customer-completed-renewal-order.php:17 +msgid "We have finished processing your subscription renewal order." +msgstr "" + +#: templates/emails/customer-completed-switch-order.php:18 +#: templates/emails/plain/customer-completed-switch-order.php:17 +msgid "" +"You have successfully changed your subscription items. Your new order and " +"subscription details are shown below for your reference:" +msgstr "" + +#: templates/emails/customer-processing-renewal-order.php:19 +#: templates/emails/plain/customer-processing-renewal-order.php:18 +#. translators: %s: Order number +msgid "" +"Just to let you know — we've received your subscription renewal order " +"#%s, and it is now being processed:" +msgstr "" + +#: templates/emails/customer-renewal-invoice.php:24 +#: templates/emails/customer-renewal-invoice.php:33 msgid "Pay Now »" msgstr "" -#: templates/emails/expired-subscription.php:19 +#: templates/emails/expired-subscription.php:16 #: templates/emails/plain/expired-subscription.php:16 #. translators: $1: customer's billing first name and last name msgid "" @@ -4749,7 +4927,7 @@ msgid "" "are as follows:" msgstr "" -#: templates/emails/on-hold-subscription.php:19 +#: templates/emails/on-hold-subscription.php:16 #: templates/emails/plain/on-hold-subscription.php:16 #. translators: $1: customer's billing first name and last name msgid "" @@ -4768,12 +4946,12 @@ msgstr "" msgid "End of Prepaid Term: %s" msgstr "" -#: templates/emails/plain/customer-completed-switch-order.php:23 +#: templates/emails/plain/customer-completed-switch-order.php:24 #. translators: placeholder is order's view url msgid "View your order: %s" msgstr "" -#: templates/emails/plain/customer-completed-switch-order.php:34 +#: templates/emails/plain/customer-completed-switch-order.php:35 #. translators: placeholder is subscription's view url msgid "View your subscription: %s" msgstr "" @@ -4803,31 +4981,41 @@ msgstr "" #: templates/emails/plain/subscription-info.php:16 #: templates/emails/subscription-info.php:14 -msgid "Subscription Information:" +msgid "Subscription information" msgstr "" -#: templates/myaccount/my-subscriptions.php:17 -msgid "My Subscriptions" +#: templates/html-early-renewal-modal-content.php:23 +msgid "By renewing your subscription early your next payment will be %s." msgstr "" -#: templates/myaccount/my-subscriptions.php:37 -#: templates/myaccount/related-subscriptions.php:30 +#: templates/html-early-renewal-modal-content.php:28 +msgid "" +"By renewing your subscription early, your scheduled next payment on %s will " +"be cancelled." +msgstr "" + +#: templates/html-early-renewal-modal-content.php:34 +msgid "Want to renew early via the checkout? Click %shere.%s" +msgstr "" + +#: templates/myaccount/my-subscriptions.php:33 +#: templates/myaccount/related-subscriptions.php:31 msgid "ID" msgstr "" -#: templates/myaccount/my-subscriptions.php:65 +#: templates/myaccount/my-subscriptions.php:61 msgid "Previous" msgstr "" -#: templates/myaccount/my-subscriptions.php:69 +#: templates/myaccount/my-subscriptions.php:65 msgid "Next" msgstr "" -#: templates/myaccount/my-subscriptions.php:76 +#: templates/myaccount/my-subscriptions.php:72 msgid "You have reached the end of subscriptions. Go to the %sfirst page%s." msgstr "" -#: templates/myaccount/my-subscriptions.php:79 +#: templates/myaccount/my-subscriptions.php:75 #. translators: placeholders are opening and closing link tags to take to the #. shop page msgid "" @@ -4835,6 +5023,10 @@ msgid "" "%sstore%s." msgstr "" +#: templates/myaccount/related-orders.php:15 +msgid "Related orders" +msgstr "" + #: templates/myaccount/related-orders.php:22 msgid "Order" msgstr "" @@ -4848,55 +5040,59 @@ msgstr[0] "" msgstr[1] "" #: templates/myaccount/related-subscriptions.php:15 -msgid "Related Subscriptions" +msgid "Related subscriptions" msgstr "" -#: templates/myaccount/subscription-details.php:40 -msgid "Auto Renew" +#: templates/myaccount/subscription-details.php:43 +msgid "Auto renew" msgstr "" -#: templates/myaccount/subscription-details.php:45 +#: templates/myaccount/subscription-details.php:51 msgid "Enable auto renew" msgstr "" -#: templates/myaccount/subscription-details.php:50 +#: templates/myaccount/subscription-details.php:58 msgid "Disable auto renew" msgstr "" -#: templates/myaccount/subscription-details.php:60 +#: templates/myaccount/subscription-details.php:63 +msgid "Using the auto-renewal toggle is disabled while in staging mode." +msgstr "" + +#: templates/myaccount/subscription-details.php:71 msgid "Payment" msgstr "" -#: templates/myaccount/subscription-details.php:70 +#: templates/myaccount/subscription-details.php:81 msgid "Actions" msgstr "" -#: templates/myaccount/subscription-details.php:83 +#: templates/myaccount/subscription-details.php:94 msgid "Subscription Updates" msgstr "" -#: templates/myaccount/subscription-totals.php:17 -msgid "Subscription Totals" -msgstr "" - -#: templates/myaccount/subscription-totals.php:40 +#: templates/myaccount/subscription-totals-table.php:35 msgid "Are you sure you want remove this item from your subscription?" msgstr "" -#: templates/single-product/add-to-cart/subscription.php:45 -#: templates/single-product/add-to-cart/variable-subscription.php:31 +#: templates/myaccount/subscription-totals.php:23 +msgid "Subscription totals" +msgstr "" + +#: templates/single-product/add-to-cart/subscription.php:32 +#: templates/single-product/add-to-cart/variable-subscription.php:30 msgid "You have an active subscription to this product already." msgstr "" -#: templates/single-product/add-to-cart/variable-subscription.php:24 +#: templates/single-product/add-to-cart/variable-subscription.php:23 msgid "This product is currently out of stock and unavailable." msgstr "" -#: templates/single-product/add-to-cart/variable-subscription.php:35 +#: templates/single-product/add-to-cart/variable-subscription.php:34 msgid "You have added a variation of this product to the cart already." msgstr "" -#: templates/single-product/add-to-cart/variable-subscription.php:46 +#: templates/single-product/add-to-cart/variable-subscription.php:45 msgid "Clear" msgstr "" @@ -4916,81 +5112,62 @@ msgstr "" msgid "Date type can not be an empty string." msgstr "" -#: woocommerce-subscriptions.php:269 +#: woocommerce-subscriptions.php:270 msgid "This is where subscriptions are stored." msgstr "" -#: woocommerce-subscriptions.php:314 +#: woocommerce-subscriptions.php:315 msgid "No Subscriptions found" msgstr "" -#: woocommerce-subscriptions.php:316 +#: woocommerce-subscriptions.php:317 msgid "" "Subscriptions will appear here for you to view and manage once purchased by " "a customer." msgstr "" -#: woocommerce-subscriptions.php:318 +#: woocommerce-subscriptions.php:319 #. translators: placeholders are opening and closing link tags msgid "%sLearn more about managing subscriptions »%s" msgstr "" -#: woocommerce-subscriptions.php:320 +#: woocommerce-subscriptions.php:321 #. translators: placeholders are opening and closing link tags msgid "%sAdd a subscription product »%s" msgstr "" -#: woocommerce-subscriptions.php:377 +#: woocommerce-subscriptions.php:378 msgid "" "To enable automatic renewals for this subscription, you will first need to " "add a payment method." msgstr "" -#: woocommerce-subscriptions.php:377 +#: woocommerce-subscriptions.php:378 msgid "Would you like to add a payment method now?" msgstr "" -#: woocommerce-subscriptions.php:495 -msgid "" -"A subscription renewal has been removed from your cart. Multiple " -"subscriptions can not be purchased at the same time." -msgstr "" - -#: woocommerce-subscriptions.php:501 -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:507 -msgid "" -"A subscription has been removed from your cart. Products and subscriptions " -"can not be purchased at the same time." -msgstr "" - -#: woocommerce-subscriptions.php:649 woocommerce-subscriptions.php:666 +#: woocommerce-subscriptions.php:650 woocommerce-subscriptions.php:667 #. 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:654 +#: woocommerce-subscriptions.php:655 #. translators: placeholder is a number, numbers ending in 1 msgid "%sst" msgstr "" -#: woocommerce-subscriptions.php:658 +#: woocommerce-subscriptions.php:659 #. translators: placeholder is a number, numbers ending in 2 msgid "%snd" msgstr "" -#: woocommerce-subscriptions.php:662 +#: woocommerce-subscriptions.php:663 #. translators: placeholder is a number, numbers ending in 3 msgid "%srd" msgstr "" -#: woocommerce-subscriptions.php:692 +#: woocommerce-subscriptions.php:693 #. 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 @@ -5000,7 +5177,7 @@ msgid "" "%5$sinstall & activate WooCommerce »%6$s" msgstr "" -#: woocommerce-subscriptions.php:695 +#: woocommerce-subscriptions.php:696 #. translators: 1$-2$: opening and closing tags, 3$: minimum supported #. WooCommerce version, 4$-5$: opening and closing link tags, leads to plugin #. admin @@ -5010,11 +5187,11 @@ msgid "" "WooCommerce to version %3$s or newer »%5$s" msgstr "" -#: woocommerce-subscriptions.php:726 +#: woocommerce-subscriptions.php:727 msgid "Variable Subscription" msgstr "" -#: woocommerce-subscriptions.php:822 +#: woocommerce-subscriptions.php:823 msgid "" "%1$sWarning!%2$s We can see the %1$sWooCommerce Subscriptions Early " "Renewal%2$s plugin is active. Version %3$s of %1$sWooCommerce " @@ -5023,11 +5200,11 @@ msgid "" "avoid any conflicts." msgstr "" -#: woocommerce-subscriptions.php:825 +#: woocommerce-subscriptions.php:826 msgid "Installed Plugins" msgstr "" -#: woocommerce-subscriptions.php:894 +#: woocommerce-subscriptions.php:895 #. translators: 1$-2$: opening and closing tags. 3$-4$: opening and #. closing link tags for learn more. Leads to duplicate site article on docs. #. 5$-6$: Opening and closing link to production URL. 7$: Production URL . @@ -5039,19 +5216,19 @@ msgid "" "the site's URL. %3$sLearn more »%4$s." msgstr "" -#: woocommerce-subscriptions.php:903 +#: woocommerce-subscriptions.php:904 msgid "Quit nagging me (but don't enable automatic payments)" msgstr "" -#: woocommerce-subscriptions.php:908 +#: woocommerce-subscriptions.php:909 msgid "Enable automatic payments" msgstr "" -#: woocommerce-subscriptions.php:1114 +#: woocommerce-subscriptions.php:1115 msgid "Support" msgstr "" -#: woocommerce-subscriptions.php:1197 +#: woocommerce-subscriptions.php:1198 #. translators: placeholders are opening and closing tags. Leads to docs on #. version 2 msgid "" @@ -5062,14 +5239,14 @@ msgid "" "2.0 »%s" msgstr "" -#: woocommerce-subscriptions.php:1212 +#: woocommerce-subscriptions.php:1213 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:1213 +#: woocommerce-subscriptions.php:1214 msgid "" "Please upgrade the WooCommerce Subscriptions plugin to version 2.0 or newer " "immediately. If you need assistance, after upgrading to Subscriptions v2.0, " @@ -5094,7 +5271,7 @@ msgstr "" msgid "https://woocommerce.com/" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:280 +#: includes/admin/class-wc-subscriptions-admin.php:284 #. translators: placeholder is trial period validation message if passed an #. invalid value (e.g. "Trial period can not exceed 4 weeks") msgctxt "Trial period field tooltip on Edit Product administration screen" @@ -5104,12 +5281,12 @@ msgid "" "subscription. %s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:293 +#: includes/admin/class-wc-subscriptions-admin.php:297 msgctxt "example price" msgid "e.g. 5.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:328 +#: includes/admin/class-wc-subscriptions-admin.php:332 #: templates/admin/deprecated/html-variation-price.php:31 #: templates/admin/deprecated/html-variation-price.php:86 #: templates/admin/html-variation-price.php:21 @@ -5118,7 +5295,7 @@ msgctxt "example price" msgid "e.g. 9.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:872 +#: includes/admin/class-wc-subscriptions-admin.php:876 #. translators: placeholders are for HTML tags. They are 1$: "

", 2$: #. "

", 3$: "

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

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

", 2$: #. "

", 3$: "

", 4$: "

" msgctxt "used in admin pointer script params in javascript as price pointer content" @@ -5139,17 +5316,17 @@ msgid "" "sign-up fee and free trial.%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1206 +#: includes/admin/class-wc-subscriptions-admin.php:1247 msgctxt "option section heading" msgid "Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1238 +#: includes/admin/class-wc-subscriptions-admin.php:1279 msgctxt "options section heading" msgid "Miscellaneous" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1246 +#: includes/admin/class-wc-subscriptions-admin.php:1287 msgctxt "there's a number immediately in front of this text" msgid "suspensions per billing period." msgstr "" @@ -5159,25 +5336,25 @@ msgctxt "there's a number immediately in front of this text" msgid "days prior to Renewal Day" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1573 +#: includes/admin/class-wc-subscriptions-admin.php:1673 #: includes/admin/class-wcs-admin-system-status.php:93 msgctxt "label that indicates whether debugging is turned on for the plugin" msgid "WCS_DEBUG" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1579 +#: includes/admin/class-wc-subscriptions-admin.php:1679 #: includes/admin/class-wcs-admin-system-status.php:107 msgctxt "Live or Staging, Label on WooCommerce -> System Status page" msgid "Subscriptions Mode" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1580 +#: includes/admin/class-wc-subscriptions-admin.php:1680 #: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to staging site" msgid "Staging" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1580 +#: includes/admin/class-wc-subscriptions-admin.php:1680 #: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to live site" msgid "Live" @@ -5205,7 +5382,7 @@ msgstr "" #: includes/admin/class-wcs-admin-post-types.php:260 #: includes/admin/class-wcs-admin-post-types.php:473 -#: includes/class-wc-subscriptions-manager.php:1829 +#: includes/class-wc-subscriptions-manager.php:1830 #: includes/wcs-user-functions.php:349 #: templates/myaccount/related-orders.php:78 msgctxt "an action on a subscription" @@ -5234,13 +5411,13 @@ msgctxt "Subscription title on admin table. (e.g.: #211 for John Doe)" msgid "%1$s#%2$s%3$s for %4$s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:898 +#: includes/admin/class-wcs-admin-post-types.php:904 #. translators: placeholder is previous post title msgctxt "used in post updated messages" msgid "Subscription restored to revision from %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:903 +#: includes/admin/class-wcs-admin-post-types.php:909 msgctxt "used in \"Subscription scheduled for \"" msgid "M j, Y @ G:i" msgstr "" @@ -5310,29 +5487,39 @@ msgctxt "label for the system status page" msgid "Retries Migration Status" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:78 +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:77 msgctxt "relation to order" -msgid "Resubscribed Subscription" +msgid "Subscription" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:78 -msgctxt "relation to order" -msgid "Resubscribe Order" -msgstr "" - -#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:87 +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:82 msgctxt "relation to order" msgid "Initial Subscription" msgstr "" +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:93 +msgctxt "relation to order" +msgid "Renewal Order" +msgstr "" + #: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:96 msgctxt "relation to order" msgid "Parent Order" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:106 +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:99 msgctxt "relation to order" -msgid "Renewal Order" +msgid "Resubscribed Subscription" +msgstr "" + +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:99 +msgctxt "relation to order" +msgid "Resubscribe Order" +msgstr "" + +#: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:102 +msgctxt "relation to order" +msgid "Unknown Order Type" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:48 @@ -5355,12 +5542,13 @@ msgid "Subscription linked to parent order %s via admin." msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-row.php:19 +#: includes/admin/meta-boxes/views/html-unknown-related-orders-row.php:16 #: includes/class-wc-subscriptions-renewal-order.php:157 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:241 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:338 -#: templates/myaccount/my-subscriptions.php:38 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:308 +#: includes/early-renewal/wcs-early-renewal-functions.php:161 +#: templates/myaccount/my-subscriptions.php:34 #: templates/myaccount/related-orders.php:44 -#: templates/myaccount/related-subscriptions.php:32 +#: templates/myaccount/related-subscriptions.php:33 msgctxt "hash before order number" msgid "#%s" msgstr "" @@ -5380,10 +5568,10 @@ msgid "Y/m/d g:i:s A" msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-table.php:21 -#: templates/myaccount/my-subscriptions.php:28 +#: templates/myaccount/my-subscriptions.php:24 #: templates/myaccount/related-orders.php:25 -#: templates/myaccount/related-subscriptions.php:23 -#: templates/myaccount/subscription-totals.php:25 +#: templates/myaccount/related-subscriptions.php:24 +#: templates/myaccount/subscription-totals-table.php:22 msgctxt "table heading" msgid "Total" msgstr "" @@ -5393,23 +5581,23 @@ msgctxt "table heading" msgid "Renewal Payment Retry" msgstr "" -#: templates/emails/cancelled-subscription.php:28 -#: templates/emails/expired-subscription.php:28 -#: templates/emails/on-hold-subscription.php:28 wcs-functions.php:303 +#: templates/emails/cancelled-subscription.php:23 +#: templates/emails/expired-subscription.php:23 +#: templates/emails/on-hold-subscription.php:23 wcs-functions.php:303 msgctxt "table heading" msgid "Last Order Date" msgstr "" #: templates/emails/subscription-info.php:19 -#: templates/myaccount/subscription-details.php:21 wcs-functions.php:300 +#: templates/myaccount/subscription-details.php:22 msgctxt "table heading" -msgid "Start Date" +msgid "Start date" msgstr "" #: templates/emails/subscription-info.php:20 -#: templates/myaccount/subscription-details.php:27 wcs-functions.php:305 +#: templates/myaccount/subscription-details.php:29 msgctxt "table heading" -msgid "End Date" +msgid "End date" msgstr "" #: templates/emails/subscription-info.php:21 @@ -5417,14 +5605,23 @@ msgctxt "table heading" msgid "Price" msgstr "" -#: templates/myaccount/my-subscriptions.php:27 -#: templates/myaccount/my-subscriptions.php:44 -#: templates/myaccount/related-subscriptions.php:22 -#: templates/myaccount/related-subscriptions.php:38 wcs-functions.php:302 +#: templates/myaccount/my-subscriptions.php:23 +#: templates/myaccount/related-subscriptions.php:23 +#: templates/myaccount/related-subscriptions.php:39 +msgctxt "table heading" +msgid "Next payment" +msgstr "" + +#: templates/myaccount/my-subscriptions.php:40 wcs-functions.php:302 msgctxt "table heading" msgid "Next Payment" msgstr "" +#: wcs-functions.php:300 +msgctxt "table heading" +msgid "Start Date" +msgstr "" + #: wcs-functions.php:301 msgctxt "table heading" msgid "Trial End" @@ -5435,6 +5632,11 @@ msgctxt "table heading" msgid "Cancelled Date" msgstr "" +#: wcs-functions.php:305 +msgctxt "table heading" +msgid "End Date" +msgstr "" + #: includes/admin/reports/class-wcs-report-cache-manager.php:329 msgctxt "Whether the Report Cache has been enabled" msgid "Report Cache Enabled" @@ -5466,12 +5668,12 @@ msgctxt "API response confirming order note deleted from a subscription" msgid "Permanently deleted subscription note" msgstr "" -#: includes/class-wc-subscription.php:1150 +#: includes/class-wc-subscription.php:1199 msgctxt "original denotes there is no date to display" msgid "-" msgstr "" -#: includes/class-wc-subscription.php:2296 +#: includes/class-wc-subscription.php:2352 #. translators: placeholder is date type (e.g. "end", "next_payment"...) msgctxt "appears in an error message if date is wrong format" msgid "Invalid %s date. The date must be of the format: \"Y-m-d H:i:s\"." @@ -5484,12 +5686,12 @@ msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:318 msgctxt "label on button, imperative" -msgid "Change Payment" +msgid "Change payment" msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:320 msgctxt "label on button, imperative" -msgid "Add Payment" +msgid "Add payment" msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:570 @@ -5502,18 +5704,18 @@ msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:745 #: includes/class-wc-subscriptions-change-payment-gateway.php:782 msgctxt "the page title of the change payment method form" -msgid "Change Payment Method" +msgid "Change payment method" msgstr "" #: includes/class-wc-subscriptions-change-payment-gateway.php:747 #: includes/class-wc-subscriptions-change-payment-gateway.php:787 msgctxt "the page title of the add payment method form" -msgid "Add Payment Method" +msgid "Add payment method" msgstr "" #: includes/class-wc-subscriptions-manager.php:87 -#: includes/class-wc-subscriptions-manager.php:1893 -#: includes/class-wc-subscriptions-manager.php:1911 +#: includes/class-wc-subscriptions-manager.php:1894 +#: includes/class-wc-subscriptions-manager.php:1912 msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment due:" msgstr "" @@ -5529,37 +5731,37 @@ msgctxt "used in order note as reason for why subscription status changed" msgid "Customer requested to renew early:" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1040 wcs-functions.php:233 +#: includes/class-wc-subscriptions-manager.php:1041 wcs-functions.php:233 msgctxt "Subscription status" msgid "Active" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1043 wcs-functions.php:235 +#: includes/class-wc-subscriptions-manager.php:1044 wcs-functions.php:235 msgctxt "Subscription status" msgid "Cancelled" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1046 wcs-functions.php:237 +#: includes/class-wc-subscriptions-manager.php:1047 wcs-functions.php:237 msgctxt "Subscription status" msgid "Expired" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1049 wcs-functions.php:232 +#: includes/class-wc-subscriptions-manager.php:1050 wcs-functions.php:232 msgctxt "Subscription status" msgid "Pending" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1052 +#: includes/class-wc-subscriptions-manager.php:1053 msgctxt "Subscription status" msgid "Failed" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1056 +#: includes/class-wc-subscriptions-manager.php:1057 msgctxt "Subscription status" msgid "On-hold" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:2504 wcs-functions.php:236 +#: includes/class-wc-subscriptions-switcher.php:2685 wcs-functions.php:236 msgctxt "Subscription status" msgid "Switched" msgstr "" @@ -5574,7 +5776,7 @@ msgctxt "Subscription status" msgid "Pending Cancellation" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1806 +#: includes/class-wc-subscriptions-manager.php:1807 #. translators: 1$: month number (e.g. "01"), 2$: month abbreviation (e.g. #. "Jan") msgctxt "used in a select box" @@ -5611,70 +5813,54 @@ msgctxt "An order type" msgid "Non-subscription" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:357 -#: includes/class-wc-subscriptions-switcher.php:374 -#: includes/class-wc-subscriptions-switcher.php:408 +#: includes/class-wc-subscriptions-switcher.php:389 +#: includes/class-wc-subscriptions-switcher.php:423 msgctxt "when to allow a setting" msgid "Never" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:358 -msgctxt "when to allow switching" -msgid "Between Subscription Variations" -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:359 -msgctxt "when to allow switching" -msgid "Between Grouped Subscriptions" -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:360 -msgctxt "when to allow switching" -msgid "Between Both Variations & Grouped Subscriptions" -msgstr "" - -#: includes/class-wc-subscriptions-switcher.php:375 +#: includes/class-wc-subscriptions-switcher.php:390 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades of Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:376 +#: includes/class-wc-subscriptions-switcher.php:391 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades of All Subscription Products" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:377 +#: includes/class-wc-subscriptions-switcher.php:392 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades & Downgrades of Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:378 +#: includes/class-wc-subscriptions-switcher.php:393 msgctxt "when to prorate recurring fee when switching" msgid "For Upgrades & Downgrades of All Subscription Products" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:392 +#: includes/class-wc-subscriptions-switcher.php:407 msgctxt "when to prorate signup fee when switching" msgid "Never (do not charge a sign up fee)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:393 +#: includes/class-wc-subscriptions-switcher.php:408 msgctxt "when to prorate signup fee when switching" msgid "Never (charge the full sign up fee)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:394 +#: includes/class-wc-subscriptions-switcher.php:409 msgctxt "when to prorate signup fee when switching" msgid "Always" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:409 +#: includes/class-wc-subscriptions-switcher.php:424 #: includes/class-wc-subscriptions-synchroniser.php:232 msgctxt "when to prorate first payment / subscription length" msgid "For Virtual Subscription Products Only" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:410 +#: includes/class-wc-subscriptions-switcher.php:425 #: includes/class-wc-subscriptions-synchroniser.php:233 msgctxt "when to prorate first payment / subscription length" msgid "For All Subscription Products" @@ -5690,34 +5876,49 @@ msgctxt "when to prorate first payment / subscription length" msgid "Never (charge the full recurring amount at sign-up)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1798 -msgctxt "a switch order" +#: includes/class-wc-subscriptions-switcher.php:475 +msgctxt "when to allow switching" +msgid "Between Subscription Variations" +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:479 +msgctxt "when to allow switching" +msgid "Between Grouped Subscriptions" +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:1840 +msgctxt "a switch type" msgid "Downgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1801 -msgctxt "a switch order" +#: includes/class-wc-subscriptions-switcher.php:1843 +msgctxt "a switch type" msgid "Upgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1804 -msgctxt "a switch order" +#: includes/class-wc-subscriptions-switcher.php:1846 +msgctxt "a switch type" msgid "Crossgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1809 +#: includes/class-wc-subscriptions-switcher.php:1851 #. translators: %1: product subtotal, %2: HTML span tag, %3: direction #. (upgrade, downgrade, crossgrade), %4: closing HTML span tag msgctxt "product subtotal string" msgid "%1$s %2$s(%3$s)%4$s" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1920 +#: includes/class-wc-subscriptions-switcher.php:1976 #. translators: 1$: old item, 2$: new item when switching msgctxt "used in order notes" msgid "Customer switched from: %1$s to %2$s." msgstr "" +#: includes/class-wc-subscriptions-switcher.php:1978 +msgctxt "used in order notes" +msgid "Customer added %s." +msgstr "" + #: includes/class-wc-subscriptions-synchroniser.php:51 #. translators: placeholder is a year (e.g. "2016") msgctxt "used in subscription product edit screen" @@ -5743,7 +5944,7 @@ msgctxt "input field placeholder for day field for annual subscriptions" msgid "Day" msgstr "" -#: includes/class-wcs-cart-renewal.php:677 +#: includes/class-wcs-cart-renewal.php:693 msgctxt "" "Used in WooCommerce by removed item notification: \"_All linked " "subscription items were_ removed. Undo?\" Filter for item title." @@ -5846,62 +6047,62 @@ msgctxt "default email subject for cancelled emails sent to the admin" msgid "[%s] Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:145 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:212 -#: includes/emails/class-wcs-email-expired-subscription.php:143 -#: includes/emails/class-wcs-email-on-hold-subscription.php:143 +#: includes/emails/class-wcs-email-cancelled-subscription.php:143 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:210 +#: includes/emails/class-wcs-email-expired-subscription.php:141 +#: includes/emails/class-wcs-email-on-hold-subscription.php:141 msgctxt "an email notification" msgid "Enable/Disable" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:151 -#: includes/emails/class-wcs-email-expired-subscription.php:149 -#: includes/emails/class-wcs-email-on-hold-subscription.php:149 +#: 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 msgctxt "of an email" msgid "Recipient(s)" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:159 -#: includes/emails/class-wcs-email-expired-subscription.php:157 -#: includes/emails/class-wcs-email-on-hold-subscription.php:157 +#: includes/emails/class-wcs-email-cancelled-subscription.php:157 +#: includes/emails/class-wcs-email-expired-subscription.php:155 +#: includes/emails/class-wcs-email-on-hold-subscription.php:155 msgctxt "of an email" msgid "Subject" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:166 -#: includes/emails/class-wcs-email-expired-subscription.php:164 -#: includes/emails/class-wcs-email-on-hold-subscription.php:164 +#: includes/emails/class-wcs-email-cancelled-subscription.php:164 +#: includes/emails/class-wcs-email-expired-subscription.php:162 +#: includes/emails/class-wcs-email-on-hold-subscription.php:162 msgctxt "" "Name the setting that controls the main heading contained within the email " "notification" msgid "Email Heading" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:173 -#: includes/emails/class-wcs-email-expired-subscription.php:171 -#: includes/emails/class-wcs-email-on-hold-subscription.php:171 +#: includes/emails/class-wcs-email-cancelled-subscription.php:171 +#: includes/emails/class-wcs-email-expired-subscription.php:169 +#: includes/emails/class-wcs-email-on-hold-subscription.php:169 msgctxt "text, html or multipart" msgid "Email type" msgstr "" +#: includes/emails/class-wcs-email-cancelled-subscription.php:177 +#: includes/emails/class-wcs-email-expired-subscription.php:175 +#: includes/emails/class-wcs-email-on-hold-subscription.php:175 +msgctxt "email type" +msgid "Plain text" +msgstr "" + +#: includes/emails/class-wcs-email-cancelled-subscription.php:178 +#: includes/emails/class-wcs-email-expired-subscription.php:176 +#: includes/emails/class-wcs-email-on-hold-subscription.php:176 +msgctxt "email type" +msgid "HTML" +msgstr "" + #: includes/emails/class-wcs-email-cancelled-subscription.php:179 #: includes/emails/class-wcs-email-expired-subscription.php:177 #: includes/emails/class-wcs-email-on-hold-subscription.php:177 msgctxt "email type" -msgid "Plain text" -msgstr "" - -#: includes/emails/class-wcs-email-cancelled-subscription.php:180 -#: includes/emails/class-wcs-email-expired-subscription.php:178 -#: includes/emails/class-wcs-email-on-hold-subscription.php:178 -msgctxt "email type" -msgid "HTML" -msgstr "" - -#: includes/emails/class-wcs-email-cancelled-subscription.php:181 -#: includes/emails/class-wcs-email-expired-subscription.php:179 -#: includes/emails/class-wcs-email-on-hold-subscription.php:179 -msgctxt "email type" msgid "Multipart" msgstr "" @@ -6027,21 +6228,21 @@ msgctxt "Admin menu name" msgid "Renewal Payment Retries" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:340 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:345 #. translators: placeholder is number of upgraded subscriptions msgctxt "used in the subscriptions upgrader" msgid "Marked %s subscription products as \"sold individually\"." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:364 -#: includes/upgrades/class-wc-subscriptions-upgrader.php:410 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:369 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:415 #. translators: placeholder is "{time_left}", will be replaced on front end #. with actual time msgctxt "Message that gets sent to front end." msgid "Estimated time left (minutes:seconds): %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:389 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:394 #. translators: placeholder is the number of subscriptions repaired msgctxt "Repair message that gets sent to front end." msgid "" @@ -6049,7 +6250,7 @@ msgid "" "customer notes." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:395 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:400 #. translators: placeholder is number of subscriptions that were checked and #. did not need repairs. There's a space at the beginning! msgctxt "Repair message that gets sent to front end." @@ -6058,14 +6259,14 @@ msgid_plural "%d other subscriptions were checked and did not need any repairs." msgstr[0] "" msgstr[1] "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:399 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:404 #. translators: placeholder is "{execution_time}", which will be replaced on #. front end with actual time msgctxt "Repair message that gets sent to front end." msgid "(in %s seconds)" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:402 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:407 #. translators: $1: "Repaired x subs with incorrect dates...", $2: "X others #. were checked and no repair needed", $3: "(in X seconds)". Ordering for RTL #. languages. @@ -6073,7 +6274,7 @@ msgctxt "The assembled repair message that gets sent to front end." msgid "%1$s%2$s %3$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:421 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:426 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, #. 4$: break tag msgctxt "Error message that gets sent to front end when upgrading Subscriptions" @@ -6084,11 +6285,11 @@ msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:80 msgctxt "plugin version number used in admin notice" -msgid "2.5" +msgid "2.6" msgstr "" #: includes/upgrades/templates/wcs-about-2-0.php:36 -#: woocommerce-subscriptions.php:1113 +#: woocommerce-subscriptions.php:1114 msgctxt "short for documents" msgid "Docs" msgstr "" @@ -6318,14 +6519,14 @@ msgid "" msgstr "" #: templates/checkout/form-change-payment-method.php:20 -#: templates/emails/email-order-details.php:33 -#: templates/myaccount/subscription-totals.php:24 +#: templates/emails/email-order-details.php:34 +#: templates/myaccount/subscription-totals-table.php:21 msgctxt "table headings in notification email" msgid "Product" msgstr "" #: templates/checkout/form-change-payment-method.php:21 -#: templates/emails/email-order-details.php:34 +#: templates/emails/email-order-details.php:35 msgctxt "table headings in notification email" msgid "Quantity" msgstr "" @@ -6335,37 +6536,37 @@ msgctxt "table headings in notification email" msgid "Totals" msgstr "" -#: templates/emails/cancelled-subscription.php:27 -#: templates/emails/email-order-details.php:35 -#: templates/emails/expired-subscription.php:27 -#: templates/emails/on-hold-subscription.php:27 +#: templates/emails/cancelled-subscription.php:22 +#: templates/emails/email-order-details.php:36 +#: templates/emails/expired-subscription.php:22 +#: templates/emails/on-hold-subscription.php:22 msgctxt "table headings in notification email" msgid "Price" msgstr "" -#: templates/emails/cancelled-subscription.php:29 +#: templates/emails/cancelled-subscription.php:24 msgctxt "table headings in notification email" msgid "End of Prepaid Term" msgstr "" -#: templates/emails/expired-subscription.php:29 +#: templates/emails/expired-subscription.php:24 msgctxt "table headings in notification email" msgid "End Date" msgstr "" -#: templates/emails/on-hold-subscription.php:29 +#: templates/emails/on-hold-subscription.php:24 msgctxt "table headings in notification email" msgid "Date Suspended" msgstr "" -#: templates/checkout/form-change-payment-method.php:57 +#: templates/checkout/form-change-payment-method.php:47 msgctxt "text on button on checkout page" -msgid "Change Payment Method" +msgid "Change payment method" msgstr "" -#: templates/checkout/form-change-payment-method.php:59 +#: templates/checkout/form-change-payment-method.php:49 msgctxt "text on button on checkout page" -msgid "Add Payment Method" +msgid "Add payment method" msgstr "" #: templates/emails/admin-new-renewal-order.php:16 @@ -6377,7 +6578,7 @@ msgid "" "follows:" msgstr "" -#: templates/emails/admin-new-switch-order.php:20 +#: templates/emails/admin-new-switch-order.php:18 #: templates/emails/plain/admin-new-switch-order.php:18 #. translators: $1: customer's first name and last name, $2: how many #. subscriptions customer switched @@ -6391,7 +6592,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: templates/emails/admin-payment-retry.php:25 +#: templates/emails/admin-payment-retry.php:23 #. translators: %1$s: an order number, %2$s: the customer's full name, %3$s: #. lowercase human time diff in the form returned by wcs_get_human_time_diff(), #. e.g. 'in 12 hours' @@ -6401,44 +6602,44 @@ msgid "" "payment will be retried %3$s." msgstr "" -#: templates/emails/customer-payment-retry.php:19 -#: templates/emails/plain/customer-payment-retry.php:16 -#. translators: %1$s: name of the blog, %2$s: lowercase human time diff in the -#. form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours' +#: templates/emails/customer-payment-retry.php:18 +#: templates/emails/plain/customer-payment-retry.php:18 +#. translators: %s: lowercase human time diff in the form returned by +#. wcs_get_human_time_diff(), e.g. 'in 12 hours' msgctxt "In customer renewal invoice email" msgid "" -"The automatic payment to renew your subscription with %1$s has failed. We " -"will retry the payment %2$s." +"The automatic payment to renew your subscription has failed. We will retry " +"the payment %s." msgstr "" -#: templates/emails/customer-payment-retry.php:25 +#: templates/emails/customer-payment-retry.php:21 #. translators: %1$s %2$s: link markup to checkout payment url, note: no full #. stop due to url at the end msgctxt "In customer renewal invoice email" msgid "" -"To reactivate the subscription now, you can also login and pay for the " +"To reactivate the subscription now, you can also log in and pay for the " "renewal from your account page: %1$sPay Now »%2$s" msgstr "" -#: templates/emails/customer-renewal-invoice.php:20 -#: templates/emails/plain/customer-renewal-invoice.php:17 -#. translators: %1$s: name of the blog, %2$s: link to checkout payment url, -#. note: no full stop due to url at the end -msgctxt "In customer renewal invoice email" -msgid "" -"An invoice has been created for you to renew your subscription with %1$s. " -"To pay for this invoice please use the following link: %2$s" -msgstr "" - -#: templates/emails/customer-renewal-invoice.php:27 +#: templates/emails/customer-renewal-invoice.php:22 #: templates/emails/plain/customer-renewal-invoice.php:20 #. translators: %1$s: name of the blog, %2$s: link to checkout payment url, #. note: no full stop due to url at the end msgctxt "In customer renewal invoice email" msgid "" +"An order has been created for you to renew your subscription on %1$s. To " +"pay for this invoice please use the following link: %2$s" +msgstr "" + +#: templates/emails/customer-renewal-invoice.php:31 +#: templates/emails/plain/customer-renewal-invoice.php:23 +#. translators: %1$s: name of the blog, %2$s: link to checkout payment url, +#. note: no full stop due to url at the end +msgctxt "In customer renewal invoice email" +msgid "" "The automatic payment to renew your subscription with %1$s has failed. To " -"reactivate the subscription, please login and pay for the renewal from your " -"account page: %2$s" +"reactivate the subscription, please log in and pay for the renewal from " +"your account page: %2$s" msgstr "" #: templates/emails/plain/admin-payment-retry.php:20 @@ -6451,12 +6652,12 @@ msgid "" "payment will be retried %3$s." msgstr "" -#: templates/emails/plain/customer-payment-retry.php:19 +#: templates/emails/plain/customer-payment-retry.php:21 #. translators: %1$s: link to checkout payment url, note: no full stop due to #. url at the end msgctxt "In customer renewal invoice email" msgid "" -"To reactivate the subscription now, you can also login and pay for the " +"To reactivate the subscription now, you can also log in and pay for the " "renewal from your account page: %1$s" msgstr "" @@ -6508,26 +6709,30 @@ msgid "Price: %s" msgstr "" #: templates/emails/plain/subscription-info.php:25 -#: templates/emails/subscription-info.php:29 msgctxt "Used as end date for an indefinite subscription" msgid "When Cancelled" msgstr "" +#: templates/emails/subscription-info.php:29 +msgctxt "Used as end date for an indefinite subscription" +msgid "When cancelled" +msgstr "" + #: templates/emails/subscription-info.php:27 msgctxt "subscription number in email table. (eg: #106)" msgid "#%s" msgstr "" -#: templates/myaccount/my-subscriptions.php:50 +#: templates/myaccount/my-subscriptions.php:46 #: templates/myaccount/related-orders.php:53 -#: templates/myaccount/related-subscriptions.php:41 +#: templates/myaccount/related-subscriptions.php:42 msgctxt "Used in data attribute. Escaped" msgid "Total" msgstr "" -#: templates/myaccount/my-subscriptions.php:54 +#: templates/myaccount/my-subscriptions.php:50 #: templates/myaccount/related-orders.php:84 -#: templates/myaccount/related-subscriptions.php:45 +#: templates/myaccount/related-subscriptions.php:46 msgctxt "view a subscription" msgid "View" msgstr "" @@ -6537,22 +6742,22 @@ msgctxt "pay for a subscription" msgid "Pay" msgstr "" -#: templates/myaccount/subscription-details.php:25 +#: templates/myaccount/subscription-details.php:27 msgctxt "admin subscription table header" -msgid "Last Order Date" -msgstr "" - -#: templates/myaccount/subscription-details.php:26 -msgctxt "admin subscription table header" -msgid "Next Payment Date" +msgid "Last order date" msgstr "" #: templates/myaccount/subscription-details.php:28 msgctxt "admin subscription table header" -msgid "Trial End Date" +msgid "Next payment date" msgstr "" -#: templates/myaccount/subscription-details.php:89 +#: templates/myaccount/subscription-details.php:30 +msgctxt "admin subscription table header" +msgid "Trial end date" +msgstr "" + +#: templates/myaccount/subscription-details.php:100 msgctxt "date on subscription updates list. Will be localized" msgid "l jS \\o\\f F Y, h:ia" msgstr "" @@ -6585,68 +6790,68 @@ msgctxt "The post title for the new subscription" msgid "Subscription – %s" msgstr "" -#: woocommerce-subscriptions.php:256 +#: woocommerce-subscriptions.php:257 msgctxt "custom post type setting" msgid "Add Subscription" msgstr "" -#: woocommerce-subscriptions.php:257 +#: woocommerce-subscriptions.php:258 msgctxt "custom post type setting" msgid "Add New Subscription" msgstr "" -#: woocommerce-subscriptions.php:258 +#: woocommerce-subscriptions.php:259 msgctxt "custom post type setting" msgid "Edit" msgstr "" -#: woocommerce-subscriptions.php:259 +#: woocommerce-subscriptions.php:260 msgctxt "custom post type setting" msgid "Edit Subscription" msgstr "" -#: woocommerce-subscriptions.php:260 +#: woocommerce-subscriptions.php:261 msgctxt "custom post type setting" msgid "New Subscription" msgstr "" -#: woocommerce-subscriptions.php:261 woocommerce-subscriptions.php:262 +#: woocommerce-subscriptions.php:262 woocommerce-subscriptions.php:263 msgctxt "custom post type setting" msgid "View Subscription" msgstr "" -#: woocommerce-subscriptions.php:265 +#: woocommerce-subscriptions.php:266 msgctxt "custom post type setting" msgid "No Subscriptions found in trash" msgstr "" -#: woocommerce-subscriptions.php:266 +#: woocommerce-subscriptions.php:267 msgctxt "custom post type setting" msgid "Parent Subscriptions" msgstr "" -#: woocommerce-subscriptions.php:334 +#: woocommerce-subscriptions.php:335 msgctxt "post status label including post count" msgid "Active (%s)" msgid_plural "Active (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:335 +#: woocommerce-subscriptions.php:336 msgctxt "post status label including post count" msgid "Switched (%s)" msgid_plural "Switched (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:336 +#: woocommerce-subscriptions.php:337 msgctxt "post status label including post count" msgid "Expired (%s)" msgid_plural "Expired (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:337 +#: woocommerce-subscriptions.php:338 msgctxt "post status label including post count" msgid "Pending Cancellation (%s)" msgid_plural "Pending Cancellation (%s)" diff --git a/templates/admin/status.php b/templates/admin/status.php index 1b35da7..66f2ce6 100755 --- a/templates/admin/status.php +++ b/templates/admin/status.php @@ -2,7 +2,7 @@ /** * Outputs the Status section for Subscriptions. * - * @version 2.3.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -14,7 +14,7 @@ if ( ! isset( $debug_data ) || ! is_array( $debug_data ) ) { } ?> - +
- + $data ) { // Use mark key if available, otherwise default back to the success key. if ( isset( $data['mark'] ) ) { @@ -43,7 +43,7 @@ if ( ! isset( $debug_data ) || ! is_array( $debug_data ) ) { $mark_icon = 'no-alt'; } ?> - + - get_order_item_totals() ) { - foreach ( $totals as $total ) : ?> + get_order_item_totals() as $total ) : ?> - + - get_items(); - if ( sizeof( $recurring_order_items ) > 0 ) : - foreach ( $recurring_order_items as $item ) : - echo ' - - - - - '; - endforeach; - endif; - ?> + get_items() as $item ) : ?> + + + + + +
@@ -23,7 +23,7 @@ if ( ! isset( $debug_data ) || ! is_array( $debug_data ) ) {
:   diff --git a/templates/cart/cart-recurring-shipping.php b/templates/cart/cart-recurring-shipping.php index 7530651..c8ef878 100755 --- a/templates/cart/cart-recurring-shipping.php +++ b/templates/cart/cart-recurring-shipping.php @@ -6,8 +6,9 @@ * * @author Prospress * @package WooCommerce Subscriptions/Templates - * @version 2.0.12 + * @version 2.6.0 */ + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -15,9 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) {
- - - +
  • diff --git a/templates/checkout/form-change-payment-method.php b/templates/checkout/form-change-payment-method.php index 7a71f09..b0861a5 100755 --- a/templates/checkout/form-change-payment-method.php +++ b/templates/checkout/form-change-payment-method.php @@ -5,11 +5,11 @@ * * @author Prospress * @package WooCommerce/Templates - * @version 2.5.2 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { - exit; // Exit if accessed directly + exit; // Exit if accessed directly. } ?>
    @@ -23,75 +23,64 @@ if ( ! defined( 'ABSPATH' ) ) {
' . wp_kses_post( $item['name'] ) . '' . esc_html( $item['qty'] ) . '' . wp_kses_post( $subscription->get_formatted_line_subtotal( $item ) ) . '
get_formatted_line_subtotal( $item ) ); ?>
has_payment_gateway() ) { - $pay_order_button_text = _x( 'Change Payment Method', 'text on button on checkout page', 'woocommerce-subscriptions' ); + $pay_order_button_text = _x( 'Change payment method', 'text on button on checkout page', 'woocommerce-subscriptions' ); } else { - $pay_order_button_text = _x( 'Add Payment Method', 'text on button on checkout page', 'woocommerce-subscriptions' ); + $pay_order_button_text = _x( 'Add payment method', 'text on button on checkout page', 'woocommerce-subscriptions' ); } - $pay_order_button_text = apply_filters( 'woocommerce_change_payment_button_text', $pay_order_button_text ); + $pay_order_button_text = apply_filters( 'woocommerce_change_payment_button_text', $pay_order_button_text ); $customer_subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $subscription->get_customer_id() ); - if ( $available_gateways = WC()->payment_gateways->get_available_payment_gateways() ) { ?> -
    - set_current(); - } - - foreach ( $available_gateways as $gateway ) { - $supports_payment_method_changes = WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $gateway, $subscription ); - ?> -
  • - chosen, true ); ?> data-order_button_text="" /> - - has_fields() || $gateway->get_description() ) { - echo ''; - } - ?> -
  • + if ( $available_gateways = WC()->payment_gateways->get_available_payment_gateways() ) : ?> +
      -
    - -
    -

    -
    - + + if ( count( $available_gateways ) ) { + current( $available_gateways )->set_current(); + } + + foreach ( $available_gateways as $gateway ) : + $supports_payment_method_changes = WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $gateway, $subscription ); + ?> +
  • + chosen, true ); ?> data-order_button_text=""/> + + has_fields() || $gateway->get_description() ) { + echo ''; + } + ?> +
  • + +
+ +
+

+
+ 1 && WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_admin' ) ) : ?> @@ -111,7 +100,7 @@ if ( ! defined( 'ABSPATH' ) ) { ); ?> - +
' ), array( 'input' => array( 'type' => array(), 'class' => array(), 'id' => array(), 'value' => array(), 'data-value' => array() ) ) ); ?> diff --git a/templates/checkout/recurring-totals.php b/templates/checkout/recurring-totals.php index 7118ad2..d66c06f 100755 --- a/templates/checkout/recurring-totals.php +++ b/templates/checkout/recurring-totals.php @@ -4,7 +4,7 @@ * * @author Prospress * @package WooCommerce Subscriptions/Templates - * @version 2.0.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -16,7 +16,7 @@ $display_th = true; ?> - + $recurring_cart ) : ?> @@ -70,7 +70,7 @@ $display_th = true; - cart->tax_display_cart === 'excl' ) : ?> + cart->tax_display_cart === 'excl' ) : ?> cart->get_taxes() as $tax_id => $tax_total ) : ?> @@ -120,8 +120,8 @@ $display_th = true; - - + + diff --git a/templates/emails/admin-new-renewal-order.php b/templates/emails/admin-new-renewal-order.php index c99e1bd..ea2597d 100755 --- a/templates/emails/admin-new-renewal-order.php +++ b/templates/emails/admin-new-renewal-order.php @@ -4,20 +4,18 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

get_formatted_billing_full_name() ) ); - ?> -

- +

get_formatted_billing_full_name() ) );?>

+ + diff --git a/templates/emails/admin-new-switch-order.php b/templates/emails/admin-new-switch-order.php index d6ff841..85d13ce 100755 --- a/templates/emails/admin-new-switch-order.php +++ b/templates/emails/admin-new-switch-order.php @@ -4,35 +4,41 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); -

- get_formatted_billing_full_name(), $count ) ); - ?> -

+$switched_count = count( $subscriptions ); + +/* translators: $1: customer's first name and last name, $2: how many subscriptions customer switched */ ?> +

get_formatted_billing_full_name(), $switched_count ) );?>

- + +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); +?> -

+

+ - - +foreach ( $subscriptions as $subscription ) { + do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); +} - +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/admin-payment-retry.php b/templates/emails/admin-payment-retry.php index 02d23fb..4da9a09 100755 --- a/templates/emails/admin-payment-retry.php +++ b/templates/emails/admin-payment-retry.php @@ -7,7 +7,7 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -19,34 +19,37 @@ if ( ! defined( 'ABSPATH' ) ) { */ do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) ); - ?> -

+ +

get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) ); ?>

- +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- get_formatted_billing_full_name() ) ); - ?> -

+ +

get_formatted_billing_full_name() ) );?>

@@ -54,9 +49,17 @@ if ( ! defined( 'ABSPATH' ) ) {

+ +do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); - +do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/customer-completed-renewal-order.php b/templates/emails/customer-completed-renewal-order.php index 4e86664..6e1fa99 100755 --- a/templates/emails/customer-completed-renewal-order.php +++ b/templates/emails/customer-completed-renewal-order.php @@ -4,27 +4,31 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- -

+ +

get_billing_first_name() ) ); ?>

+

- + +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); - +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/customer-completed-switch-order.php b/templates/emails/customer-completed-switch-order.php index 64096a7..7be5029 100755 --- a/templates/emails/customer-completed-switch-order.php +++ b/templates/emails/customer-completed-switch-order.php @@ -4,32 +4,39 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } + +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> + + +

get_billing_first_name() ) ); ?>

+

+ + - +

-

- -

+ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} -

- - - - - - - +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/customer-payment-retry.php b/templates/emails/customer-payment-retry.php index c920a3f..ed8d6eb 100755 --- a/templates/emails/customer-payment-retry.php +++ b/templates/emails/customer-payment-retry.php @@ -4,28 +4,30 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- get_time() ) ), array( 'a' => array( 'href' => true ) ) ); - ?> -

-

- get_checkout_payment_url() ) . '">', '' ), array( 'a' => array( 'href' => true ) ) ); - ?> -

+ +

get_billing_first_name() ) ); ?>

+ +

get_time() ) ) ); ?>

- + +

get_checkout_payment_url() ) . '">', '' ), array( 'a' => array( 'href' => true ) ) );?>

- - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

+ +

get_billing_first_name() ) ); ?>

+ +

get_order_number() ) ); ?>

- + +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); - +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/customer-renewal-invoice.php b/templates/emails/customer-renewal-invoice.php index d0a5167..94522a0 100755 --- a/templates/emails/customer-renewal-invoice.php +++ b/templates/emails/customer-renewal-invoice.php @@ -4,29 +4,45 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -get_status() ) : ?> -

- +

get_billing_first_name() ) ); ?>

+ +has_status( 'pending' ) ) : ?> +

get_checkout_payment_url() ) . '">' . esc_html__( 'Pay Now »', 'woocommerce-subscriptions' ) . '' ), array( 'a' => array( 'href' => true ) ) ); - ?> + _x( 'An order has been created for you to renew your subscription on %1$s. To pay for this invoice please use the following link: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), + esc_html( get_bloginfo( 'name' ) ), + '' . esc_html__( 'Pay Now »', 'woocommerce-subscriptions' ) . '' + ), array( 'a' => array( 'href' => true ) ) ); ?>

-get_status() ) : ?> -

- has_status( 'failed' ) ) : ?> +

get_checkout_payment_url() ) . '">' . esc_html__( 'Pay Now »', 'woocommerce-subscriptions' ) . '' ), array( 'a' => array( 'href' => true ) ) ); ?>

+ _x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please log in and pay for the renewal from your account page: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), + esc_html( get_bloginfo( 'name' ) ), + '' . esc_html__( 'Pay Now »', 'woocommerce-subscriptions' ) . '' + ), array( 'a' => array( 'href' => true ) ) ); ?> +

- + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/email-order-details.php b/templates/emails/email-order-details.php index bda0beb..54f4c38 100755 --- a/templates/emails/email-order-details.php +++ b/templates/emails/email-order-details.php @@ -4,7 +4,7 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -27,33 +27,35 @@ if ( 'cancelled_subscription' != $email->id ) { echo ''; } ?> - - - - - - - - - - - - - get_order_item_totals() ) { - $i = 0; - foreach ( $totals as $total ) { - $i++; - ?> - - - - - +
+ + + + + + + + + + + + get_order_item_totals() ) { + $i = 0; + foreach ( $totals as $total ) { + $i++; + ?> + + + + + - -
+ ?> + + +
diff --git a/templates/emails/expired-subscription.php b/templates/emails/expired-subscription.php index f446973..5c62c88 100755 --- a/templates/emails/expired-subscription.php +++ b/templates/emails/expired-subscription.php @@ -4,21 +4,16 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- get_formatted_billing_full_name() ) ); - ?> -

+ +

get_formatted_billing_full_name() ) );?>

@@ -55,8 +50,17 @@ if ( ! defined( 'ABSPATH' ) ) {

- + +do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); - +do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, $plain_text, $email ); + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/on-hold-subscription.php b/templates/emails/on-hold-subscription.php index 1b9e733..358aae5 100755 --- a/templates/emails/on-hold-subscription.php +++ b/templates/emails/on-hold-subscription.php @@ -4,21 +4,16 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -?> - +do_action( 'woocommerce_email_header', $email_heading, $email ); ?> -

- get_formatted_billing_full_name() ) ); - ?> -

+ +

get_formatted_billing_full_name() ) );?>

@@ -55,8 +50,16 @@ if ( ! defined( 'ABSPATH' ) ) {

- + +do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, $plain_text, $email ); - +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) ); +} + +do_action( 'woocommerce_email_footer', $email ); diff --git a/templates/emails/plain/admin-new-renewal-order.php b/templates/emails/plain/admin-new-renewal-order.php index c914f52..d459b9e 100755 --- a/templates/emails/plain/admin-new-renewal-order.php +++ b/templates/emails/plain/admin-new-renewal-order.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -33,4 +33,12 @@ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_ echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/admin-new-switch-order.php b/templates/emails/plain/admin-new-switch-order.php index b6449cd..ae6993e 100755 --- a/templates/emails/plain/admin-new-switch-order.php +++ b/templates/emails/plain/admin-new-switch-order.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -41,4 +41,13 @@ echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/admin-payment-retry.php b/templates/emails/plain/admin-payment-retry.php index 73b851d..f05c535 100755 --- a/templates/emails/plain/admin-payment-retry.php +++ b/templates/emails/plain/admin-payment-retry.php @@ -7,7 +7,7 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -43,4 +43,12 @@ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_ echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/cancelled-subscription.php b/templates/emails/plain/cancelled-subscription.php index 01520de..c9cd104 100755 --- a/templates/emails/plain/cancelled-subscription.php +++ b/templates/emails/plain/cancelled-subscription.php @@ -4,7 +4,7 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -49,4 +49,12 @@ do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/customer-completed-renewal-order.php b/templates/emails/plain/customer-completed-renewal-order.php index 9a7b125..5ed1318 100755 --- a/templates/emails/plain/customer-completed-renewal-order.php +++ b/templates/emails/plain/customer-completed-renewal-order.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -12,10 +12,11 @@ if ( ! defined( 'ABSPATH' ) ) { echo $email_heading . "\n\n"; -// translators: placeholder is the name of the site -printf( __( 'Hi there. Your subscription renewal order with %s has been completed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ), get_option( 'blogname' ) ); +/* translators: %s: Customer first name */ +echo sprintf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ) . "\n\n"; +esc_html_e( 'We have finished processing your subscription renewal order.', 'woocommerce-subscriptions' ) . "\n\n"; -echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; +echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); @@ -27,4 +28,12 @@ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_ echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/customer-completed-switch-order.php b/templates/emails/plain/customer-completed-switch-order.php index 38791f0..070cd76 100755 --- a/templates/emails/plain/customer-completed-switch-order.php +++ b/templates/emails/plain/customer-completed-switch-order.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -12,8 +12,9 @@ if ( ! defined( 'ABSPATH' ) ) { echo $email_heading . "\n\n"; -// translators: placeholder is the name of the site -echo sprintf( __( 'Hi there. You have successfully changed your subscription items on %s. Your new order and subscription details are shown below for your reference:', 'woocommerce-subscriptions' ), get_option( 'blogname' ) ); +/* translators: %s: Customer first name */ +echo sprintf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ) . "\n\n"; +esc_html_e( 'You have successfully changed your subscription items. Your new order and subscription details are shown below for your reference:', 'woocommerce-subscriptions' ); echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; @@ -39,4 +40,12 @@ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_ echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/customer-payment-retry.php b/templates/emails/plain/customer-payment-retry.php index ba36409..774f729 100755 --- a/templates/emails/plain/customer-payment-retry.php +++ b/templates/emails/plain/customer-payment-retry.php @@ -4,7 +4,7 @@ * * @author Prospress * @package WooCommerce_Subscriptions/Templates/Emails - * @version 2.1.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -12,11 +12,13 @@ if ( ! defined( 'ABSPATH' ) ) { echo $email_heading . "\n\n"; -// translators: %1$s: name of the blog, %2$s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours' -echo sprintf( esc_html_x( 'The automatic payment to renew your subscription with %1$s has failed. We will retry the payment %2$s.', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( wcs_get_human_time_diff( $retry->get_time() ) ) ) . "\n\n"; +/* translators: %s: Customer first name */ +echo sprintf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ) . "\n\n"; +/* translators: %s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours' */ +echo sprintf( esc_html_x( 'The automatic payment to renew your subscription has failed. We will retry the payment %s.', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( wcs_get_human_time_diff( $retry->get_time() ) ) ) . "\n\n"; // translators: %1$s: link to checkout payment url, note: no full stop due to url at the end -echo sprintf( esc_html_x( 'To reactivate the subscription now, you can also login and pay for the renewal from your account page: %1$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_attr( $order->get_checkout_payment_url() ) ); +echo sprintf( esc_html_x( 'To reactivate the subscription now, you can also log in and pay for the renewal from your account page: %1$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_attr( $order->get_checkout_payment_url() ) ); echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; @@ -24,4 +26,12 @@ do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_adm echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/customer-processing-renewal-order.php b/templates/emails/plain/customer-processing-renewal-order.php index 9ebf658..4be518e 100755 --- a/templates/emails/plain/customer-processing-renewal-order.php +++ b/templates/emails/plain/customer-processing-renewal-order.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -12,7 +12,10 @@ if ( ! defined( 'ABSPATH' ) ) { echo $email_heading . "\n\n"; -echo __( 'Your subscription renewal order has been received and is now being processed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ); +/* translators: %s: Customer first name */ +echo sprintf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ) . "\n\n"; +/* translators: %s: Order number */ +echo sprintf( esc_html__( 'Just to let you know — we\'ve received your subscription renewal order #%s, and it is now being processed:', 'woocommerce-subscriptions' ), esc_html( $order->get_order_number() ) ); echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; @@ -26,4 +29,12 @@ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_ echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/customer-renewal-invoice.php b/templates/emails/plain/customer-renewal-invoice.php index 8538d5c..9cf4dbe 100755 --- a/templates/emails/plain/customer-renewal-invoice.php +++ b/templates/emails/plain/customer-renewal-invoice.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd * @package WooCommerce_Subscriptions/Templates/Emails/Plain - * @version 1.4.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly @@ -12,12 +12,15 @@ if ( ! defined( 'ABSPATH' ) ) { echo $email_heading . "\n\n"; -if ( 'pending' == $order->get_status() ) { +/* translators: %s: Customer first name */ +echo sprintf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ) . "\n\n"; + +if ( $order->has_status( 'pending' ) ) { // translators: %1$s: name of the blog, %2$s: link to checkout payment url, note: no full stop due to url at the end - printf( esc_html_x( 'An invoice has been created for you to renew your subscription with %1$s. To pay for this invoice please use the following link: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $order->get_checkout_payment_url() ) ) . "\n\n"; -} elseif ( 'failed' == $order->get_status() ) { + printf( esc_html_x( 'An order has been created for you to renew your subscription on %1$s. To pay for this invoice please use the following link: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $order->get_checkout_payment_url() ) ) . "\n\n"; +} elseif ( $order->has_status( 'failed' ) ) { // translators: %1$s: name of the blog, %2$s: link to checkout payment url, note: no full stop due to url at the end - printf( esc_html_x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please login and pay for the renewal from your account page: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $order->get_checkout_payment_url() ) ); + printf( esc_html_x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please log in and pay for the renewal from your account page: %2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $order->get_checkout_payment_url() ) ); } echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; @@ -26,4 +29,12 @@ do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_adm echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/expired-subscription.php b/templates/emails/plain/expired-subscription.php index 0da5586..d03ad6c 100755 --- a/templates/emails/plain/expired-subscription.php +++ b/templates/emails/plain/expired-subscription.php @@ -49,4 +49,12 @@ do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/on-hold-subscription.php b/templates/emails/plain/on-hold-subscription.php index da30fa7..235095c 100755 --- a/templates/emails/plain/on-hold-subscription.php +++ b/templates/emails/plain/on-hold-subscription.php @@ -45,4 +45,12 @@ do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; +} + echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); diff --git a/templates/emails/plain/subscription-info.php b/templates/emails/plain/subscription-info.php index a1533ee..d4c6365 100755 --- a/templates/emails/plain/subscription-info.php +++ b/templates/emails/plain/subscription-info.php @@ -4,7 +4,7 @@ * * @author Brent Shepherd / Chuck Mac * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! empty( $subscriptions ) ) { - echo "\n\n" . __( 'Subscription Information:', 'woocommerce-subscriptions' ) . "\n\n"; + echo "\n\n" . __( 'Subscription information', 'woocommerce-subscriptions' ) . "\n\n"; foreach ( $subscriptions as $subscription ) { // translators: placeholder is subscription's number echo sprintf( _x( 'Subscription: %s', 'in plain emails for subscription information', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) . "\n"; diff --git a/templates/emails/subscription-info.php b/templates/emails/subscription-info.php index dc04e43..d298c9a 100755 --- a/templates/emails/subscription-info.php +++ b/templates/emails/subscription-info.php @@ -4,20 +4,20 @@ * * @author Brent Shepherd / Chuck Mac * @package WooCommerce_Subscriptions/Templates/Emails - * @version 1.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } ?> -

+

- - + + @@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) { - + diff --git a/templates/html-early-renewal-modal-content.php b/templates/html-early-renewal-modal-content.php new file mode 100755 index 0000000..467f402 --- /dev/null +++ b/templates/html-early-renewal-modal-content.php @@ -0,0 +1,38 @@ + +
+ +
+

+' . esc_html( date_i18n( wc_date_format(), $new_next_payment_date->getOffsetTimestamp() ) ) . '' + ) ); +} else { + echo wp_kses_post( sprintf( + __( 'By renewing your subscription early, your scheduled next payment on %s will be cancelled.', 'woocommerce-subscriptions' ), + '' . esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'next_payment', 'site' ) ) ) . '' + ) ); +}?> +
+', + '' +) ) ?> +

diff --git a/templates/html-modal.php b/templates/html-modal.php new file mode 100755 index 0000000..928ed71 --- /dev/null +++ b/templates/html-modal.php @@ -0,0 +1,35 @@ + +
+
+ + +
+ print_content(); ?> +
+ has_actions() ) : ?> +
get_actions() as $action ) { + $element_type = $action['type']; + $attributes = $modal->get_attribute_string( $action['attributes'] ); + + echo wp_kses_post( "<{$element_type} {$attributes}>{$action['text']}" ); + }?> +
+ +
+
diff --git a/templates/myaccount/my-subscriptions.php b/templates/myaccount/my-subscriptions.php index e187269..20d64bd 100755 --- a/templates/myaccount/my-subscriptions.php +++ b/templates/myaccount/my-subscriptions.php @@ -4,7 +4,7 @@ * * @author Prospress * @category WooCommerce Subscriptions/Templates - * @version 2.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -13,45 +13,41 @@ if ( ! defined( 'ABSPATH' ) ) { ?>
get_order_number() ) ); ?> get_time( 'start_date', 'site' ) ) ); ?>get_time( 'end' ) ) ? date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) : _x( 'When Cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ) ); ?>get_time( 'end' ) ) ? date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) : _x( 'When cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ) ); ?> get_formatted_order_total() ); ?>
+ - - - - - + + + + + $subscription ) : ?> - - + - - - - @@ -71,7 +67,7 @@ if ( ! defined( 'ABSPATH' ) ) { -

+

', '' ); else : @@ -83,5 +79,3 @@ if ( ! defined( 'ABSPATH' ) ) { - -

-

+

- + - - - - - + + + + + - get_item_count(); - $order_date = wcs_get_datetime_utc_string( wcs_get_objects_property( $order, 'date_created' ) ); + $order_date = wcs_get_datetime_utc_string( $order->get_date_created() ); - ?> - + - - - - - + diff --git a/templates/myaccount/related-subscriptions.php b/templates/myaccount/related-subscriptions.php index 5e8b84e..aed83ce 100755 --- a/templates/myaccount/related-subscriptions.php +++ b/templates/myaccount/related-subscriptions.php @@ -4,7 +4,7 @@ * * @author Prospress * @category WooCommerce Subscriptions/Templates - * @version 2.0.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -12,37 +12,38 @@ if ( ! defined( 'ABSPATH' ) ) { } ?>
-

+

- + + - - - - - + + + + + $subscription ) : ?> - - + - - - - diff --git a/templates/myaccount/subscription-details.php b/templates/myaccount/subscription-details.php index 677db09..1e1a993 100755 --- a/templates/myaccount/subscription-details.php +++ b/templates/myaccount/subscription-details.php @@ -5,89 +5,100 @@ * @author Prospress * @package WooCommerce_Subscription/Templates * @since 2.2.19 - * @version 2.5.0 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { - exit; // Exit if accessed directly + exit; // Exit if accessed directly. } ?> - - - - - - - - - _x( 'Last Order Date', 'admin subscription table header', 'woocommerce-subscriptions' ), - 'next_payment' => _x( 'Next Payment Date', 'admin subscription table header', 'woocommerce-subscriptions' ), - 'end' => _x( 'End Date', 'table heading', 'woocommerce-subscriptions' ), - 'trial_end' => _x( 'Trial End Date', 'admin subscription table header', 'woocommerce-subscriptions' ), - ) as $date_type => $date_title ) : ?> - get_date( $date_type ); ?> - + + + + + + + + + + _x( 'Last order date', 'admin subscription table header', 'woocommerce-subscriptions' ), + 'next_payment' => _x( 'Next payment date', 'admin subscription table header', 'woocommerce-subscriptions' ), + 'end' => _x( 'End date', 'table heading', 'woocommerce-subscriptions' ), + 'trial_end' => _x( 'Trial end date', 'admin subscription table header', 'woocommerce-subscriptions' ), + ) as $date_type => $date_title + ) : ?> + get_date( $date_type ); ?> + + + + + + + + - - + + - - - - - - - - get_time( 'next_payment' ) > 0 ) : ?> - - - - - - - - - - - - - - + get_time( 'next_payment' ) > 0 ) : ?> + + + + + + + + + + + + + + +
get_status() ) ); ?>
get_date_to_display( 'start_date' ) ); ?>
get_status() ) ); ?>
get_date_to_display( 'start_date' ) ); ?>
get_date_to_display( $date_type ) ); ?>
get_date_to_display( $date_type ) ); ?> +
+ is_manual() ) { + $toggle_label = __( 'Enable auto renew', 'woocommerce-subscriptions' ); + $toggle_classes[] = 'subscription-auto-renew-toggle--off'; + + if ( WC_Subscriptions::is_duplicate_site() ) { + $toggle_classes[] = 'subscription-auto-renew-toggle--disabled'; + } + } else { + $toggle_label = __( 'Disable auto renew', 'woocommerce-subscriptions' ); + $toggle_classes[] = 'subscription-auto-renew-toggle--on'; + }?> + + + + +
+
-
- is_manual() ) : ?> - - - - - - - - -
-
- get_payment_method_to_display( 'customer' ) ); ?> -
- $action ) : ?> - - -
+ get_payment_method_to_display( 'customer' ) ); ?> +
+ $action ) : ?> + + +
-get_customer_order_notes() ) : - ?> +get_customer_order_notes() ) : ?>

-
    +
      -
    1. -
      -
      -

      comment_date ) ) ); ?>

      -
      +
    2. +
      +
      +

      comment_date ) ) ); ?>

      +
      comment_content ) ) ); ?>
      diff --git a/templates/myaccount/subscription-totals-table.php b/templates/myaccount/subscription-totals-table.php new file mode 100755 index 0000000..161d02e --- /dev/null +++ b/templates/myaccount/subscription-totals-table.php @@ -0,0 +1,99 @@ + + + + + + + + + + + + + get_items() as $item_id => $item ) { + $_product = apply_filters( 'woocommerce_subscriptions_order_item_product', $subscription->get_product_from_item( $item ), $item ); + if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + ?> + + + + + + + + has_status( array( 'completed', 'processing' ) ) && ( $purchase_note = get_post_meta( $_product->id, '_purchase_note', true ) ) ) { + ?> + + + + + + + $total ) : ?> + + + + + + +
       
      + + + × + + + is_visible() ) { + echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item['name'], $item, false ) ); + } else { + echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', sprintf( '%s', get_permalink( $item['product_id'] ), $item['name'] ), $item, false ) ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_order_item_quantity_html', ' ' . sprintf( '× %s', $item['qty'] ) . '', $item ) ); + + /** + * Allow other plugins to add additional product information here. + * + * @param int $item_id The subscription line item ID. + * @param WC_Order_Item|array $item The subscription line item. + * @param WC_Subscription $subscription The subscription. + * @param bool $plain_text Wether the item meta is being generated in a plain text context. + */ + do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $subscription, false ); + + wcs_display_item_meta( $item, $subscription ); + + /** + * Allow other plugins to add additional product information here. + * + * @param int $item_id The subscription line item ID. + * @param WC_Order_Item|array $item The subscription line item. + * @param WC_Subscription $subscription The subscription. + * @param bool $plain_text Wether the item meta is being generated in a plain text context. + */ + do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $subscription, false ); + ?> + + get_formatted_line_subtotal( $item ) ); ?> +
      >
      diff --git a/templates/myaccount/subscription-totals.php b/templates/myaccount/subscription-totals.php index 8fc4e34..3ee3795 100755 --- a/templates/myaccount/subscription-totals.php +++ b/templates/myaccount/subscription-totals.php @@ -5,108 +5,21 @@ * @author Prospress * @package WooCommerce_Subscription/Templates * @since 2.2.19 - * @version 2.5.2 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { - exit; // Exit if accessed directly + exit; // Exit if accessed directly. } ?> +get_order_item_totals(); - -

      - - - - - - - - - - - - get_items() ) > 0 ) { +// Don't display the payment method as it is included in the main subscription details table. +unset( $totals['payment_method'] ); +?> +

      - foreach ( $subscription_items as $item_id => $item ) { - $_product = apply_filters( 'woocommerce_subscriptions_order_item_product', $subscription->get_product_from_item( $item ), $item ); - if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { - ?> - - - - - - - - has_status( array( 'completed', 'processing' ) ) && ( $purchase_note = get_post_meta( $_product->id, '_purchase_note', true ) ) ) { - ?> - - - - - - - get_order_item_totals() ) { - // Don't display the payment method as it is included in the main subscription details table. - unset( $totals['payment_method'] ); - foreach ( $totals as $key => $total ) { - ?> - - - - - - -
       
      - - - × - - - is_visible() ) { - echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item['name'], $item, false ) ); - } else { - echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', sprintf( '%s', get_permalink( $item['product_id'] ), $item['name'] ), $item, false ) ); - } - - echo wp_kses_post( apply_filters( 'woocommerce_order_item_quantity_html', ' ' . sprintf( '× %s', $item['qty'] ) . '', $item ) ); - - /** - * Allow other plugins to add additional product information here. - * - * @param int $item_id The subscription line item ID. - * @param WC_Order_Item|array $item The subscription line item. - * @param WC_Subscription $subscription The subscription. - * @param bool $plain_text Wether the item meta is being generated in a plain text context. - */ - do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $subscription, false ); - - wcs_display_item_meta( $item, $subscription ); - - /** - * Allow other plugins to add additional product information here. - * - * @param int $item_id The subscription line item ID. - * @param WC_Order_Item|array $item The subscription line item. - * @param WC_Subscription $subscription The subscription. - * @param bool $plain_text Wether the item meta is being generated in a plain text context. - */ - do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $subscription, false ); - ?> - - get_formatted_line_subtotal( $item ) ); ?> -
      >
      + diff --git a/templates/myaccount/view-subscription.php b/templates/myaccount/view-subscription.php index 6a259a4..c774f3c 100755 --- a/templates/myaccount/view-subscription.php +++ b/templates/myaccount/view-subscription.php @@ -6,7 +6,7 @@ * * @author Prospress * @package WooCommerce_Subscription/Templates - * @version 2.5.3 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/templates/single-product/add-to-cart/subscription.php b/templates/single-product/add-to-cart/subscription.php index 59174fa..1cc84d1 100755 --- a/templates/single-product/add-to-cart/subscription.php +++ b/templates/single-product/add-to-cart/subscription.php @@ -4,42 +4,29 @@ * * @author Prospress * @package WooCommerce-Subscriptions/Templates - * @version 2.0.18 + * @version 2.6.0 */ -if ( ! defined( 'ABSPATH' ) ) { - exit; // Exit if accessed directly -} +defined( 'ABSPATH' ) || exit; global $product; -// Bail if the product isn't purchasable and that's not because it's limited -if ( ! $product->is_purchasable() && ( ! is_user_logged_in() || 'no' == wcs_get_product_limitation( $product ) ) ) { +// Bail if the product isn't purchasable and that's not because it's limited. +if ( ! $product->is_purchasable() && ( ! is_user_logged_in() || 'no' === wcs_get_product_limitation( $product ) ) ) { return; } $user_id = get_current_user_id(); -if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) : - $availability = $product->get_availability(); - if ( $availability['availability'] ) : - echo wp_kses_post( apply_filters( 'woocommerce_stock_html', '

      '.$availability['availability'].'

      ', $availability['availability'] ) ); - endif; -else : - echo wp_kses_post( wc_get_stock_html( $product ) ); -endif; +echo wp_kses_post( wc_get_stock_html( $product ) ); -if ( ! $product->is_in_stock() ) : ?> - - - - +if ( $product->is_in_stock() ) : ?> - is_purchasable() && 0 != $user_id && 'no' != wcs_get_product_limitation( $product ) && wcs_is_product_limited_for_user( $product, $user_id ) ) : ?> + is_purchasable() && 0 !== $user_id && 'no' !== wcs_get_product_limitation( $product ) && wcs_is_product_limited_for_user( $product, $user_id ) ) : ?> get_id() ); ?> - get_id(), wcs_get_product_limitation( $product ) ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'active' ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'on-hold' ) ) : // customer has an inactive subscription, maybe offer the renewal button ?> + get_id(), wcs_get_product_limitation( $product ) ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'active' ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'on-hold' ) ) : // customer has an inactive subscription, maybe offer the renewal button. ?>

      @@ -49,18 +36,19 @@ if ( ! $product->is_in_stock() ) : ?> - - is_sold_individually() ) { - woocommerce_quantity_input( array( - 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 1, $product ), - 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->backorders_allowed() ? '' : $product->get_stock_quantity(), $product ), - ) ); - } + do_action( 'woocommerce_before_add_to_cart_quantity' ); + + woocommerce_quantity_input( array( + 'min_value' => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ), + 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // WPCS: CSRF ok, input var ok. + ) ); + + do_action( 'woocommerce_after_add_to_cart_quantity' ); ?> - + diff --git a/templates/single-product/add-to-cart/variable-subscription.php b/templates/single-product/add-to-cart/variable-subscription.php index 2d04dca..dda0d19 100755 --- a/templates/single-product/add-to-cart/variable-subscription.php +++ b/templates/single-product/add-to-cart/variable-subscription.php @@ -4,16 +4,15 @@ * * @author Prospress * @package WooCommerce-Subscriptions/Templates - * @version 2.2.20 + * @version 2.6.0 */ -if ( ! defined( 'ABSPATH' ) ) { - exit; -} + +defined( 'ABSPATH' ) || exit; global $product; $attribute_keys = array_keys( $attributes ); -$user_id = get_current_user_id(); +$user_id = get_current_user_id(); do_action( 'woocommerce_before_add_to_cart_form' ); ?> @@ -23,10 +22,10 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>

      - is_purchasable() && 0 != $user_id && 'no' != wcs_get_product_limitation( $product ) && wcs_is_product_limited_for_user( $product, $user_id ) ) : ?> + is_purchasable() && 0 !== $user_id && 'no' !== wcs_get_product_limitation( $product ) && wcs_is_product_limited_for_user( $product, $user_id ) ) : ?> get_id() ); ?> - get_id(), wcs_get_product_limitation( $product ) ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'active' ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'on-hold' ) ) : // customer has an inactive subscription, maybe offer the renewal button ?> - + get_id(), wcs_get_product_limitation( $product ) ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'active' ) && ! wcs_user_has_subscription( $user_id, $product->get_id(), 'on-hold' ) ) : // customer has an inactive subscription, maybe offer the renewal button. ?> +

      @@ -69,7 +68,8 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?> /** * woocommerce_single_variation hook. Used to output the cart button and placeholder for variation data. - * @since 2.4.0 + * + * @since 2.4.0 * @hooked woocommerce_single_variation - 10 Empty div for variation data. * @hooked woocommerce_single_variation_add_to_cart_button - 20 Qty and cart button. */ diff --git a/wcs-functions.php b/wcs-functions.php index 5ef98c9..ef13956 100755 --- a/wcs-functions.php +++ b/wcs-functions.php @@ -415,7 +415,7 @@ function wcs_sanitize_subscription_status_key( $status_key ) { * 'customer_id' The user ID of a customer on the site. * 'product_id' The post ID of a WC_Product_Subscription, WC_Product_Variable_Subscription or WC_Product_Subscription_Variation object * 'order_id' The post ID of a shop_order post/WC_Order object which was used to create the subscription - * 'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'suspended', 'expired', 'pending' or 'trash'. Defaults to 'any'. + * 'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'on-hold', 'expired', 'pending' or 'trash'. Defaults to 'any'. * @return array Subscription details in post_id => WC_Subscription form. * @since 2.0 */ @@ -536,28 +536,57 @@ function wcs_get_subscriptions( $args ) { /** * Get subscriptions that contain a certain product, specified by ID. * - * @param int | array $product_ids Either the post ID of a product or variation or an array of product or variation IDs + * @param int|array $product_ids Either the post ID of a product or variation or an array of product or variation IDs * @param string $fields The fields to return, either "ids" to receive only post ID's for the match subscriptions, or "subscription" to receive WC_Subscription objects + * @param array $args A set of name value pairs to determine the returned subscriptions. + * 'subscription_statuses' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'on-hold', 'expired', 'pending' or 'trash' or an array of statuses. Defaults to 'any'. + * 'limit' The number of subscriptions to return. Default is all (-1). + * 'offset' An optional number of subscriptions to displace or pass over. Default 0. A limit arg is required for the offset to be applied. * @return array * @since 2.0 */ -function wcs_get_subscriptions_for_product( $product_ids, $fields = 'ids' ) { +function wcs_get_subscriptions_for_product( $product_ids, $fields = 'ids', $args = array() ) { global $wpdb; - // If we have an array of IDs, convert them to a comma separated list and sanatise them to make sure they're all integers - if ( is_array( $product_ids ) ) { - $ids_for_query = implode( "', '", array_map( 'absint', array_unique( array_filter( $product_ids ) ) ) ); - } else { - $ids_for_query = absint( $product_ids ); + $args = wp_parse_args( $args, array( + 'subscription_status' => 'any', + 'limit' => -1, + 'offset' => 0, + ) ); + + // Allow for inputs of single status strings or an array of statuses. + $args['subscription_status'] = (array) $args['subscription_status']; + $args['limit'] = (int) $args['limit']; + $args['offset'] = (int) $args['offset']; + + // Start to build the query WHERE array. + $where = array( + "posts.post_type = 'shop_subscription'", + "itemmeta.meta_key IN ( '_variation_id', '_product_id' )", + "order_items.order_item_type = 'line_item'", + ); + + $product_ids = implode( "', '", array_map( 'absint', array_unique( array_filter( (array) $product_ids ) ) ) ); + $where[] = sprintf( "itemmeta.meta_value IN ( '%s' )", $product_ids ); + + if ( ! in_array( 'any', $args['subscription_status'] ) ) { + // Sanitize and format statuses into status string keys. + $statuses = array_map( 'wcs_sanitize_subscription_status_key', array_map( 'esc_sql', array_unique( array_filter( $args['subscription_status'] ) ) ) ); + $statuses = implode( "', '", $statuses ); + $where[] = sprintf( "posts.post_status IN ( '%s' )", $statuses ); } - $subscription_ids = $wpdb->get_col( " - SELECT DISTINCT order_items.order_id FROM {$wpdb->prefix}woocommerce_order_items as order_items - LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS itemmeta ON order_items.order_item_id = itemmeta.order_item_id - LEFT JOIN {$wpdb->posts} AS posts ON order_items.order_id = posts.ID - WHERE posts.post_type = 'shop_subscription' - AND itemmeta.meta_value IN ( '" . $ids_for_query . "' ) - AND itemmeta.meta_key IN ( '_variation_id', '_product_id' )" + $limit = ( $args['limit'] > 0 ) ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : ''; + $offset = ( $args['limit'] > 0 && $args['offset'] > 0 ) ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : ''; + $where = implode( ' AND ', $where ); + + $subscription_ids = $wpdb->get_col( + "SELECT DISTINCT order_items.order_id + FROM {$wpdb->prefix}woocommerce_order_items as order_items + LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS itemmeta ON order_items.order_item_id = itemmeta.order_item_id + LEFT JOIN {$wpdb->posts} AS posts ON order_items.order_id = posts.ID + WHERE {$where} + ORDER BY order_items.order_id {$limit} {$offset}" ); $subscriptions = array(); @@ -804,11 +833,8 @@ function wcs_set_payment_meta( $subscription, $payment_meta ) { break; case 'post_meta': case 'postmeta': - if ( is_callable( array( $subscription, 'update_meta_data' ) ) ) { - $subscription->update_meta_data( $meta_key, $meta_data['value'] ); - } else { - update_post_meta( wcs_get_objects_property( $subscription, 'id' ), $meta_key, $meta_data['value'] ); - } + $subscription->update_meta_data( $meta_key, $meta_data['value'] ); + $subscription->save(); break; case 'options': update_option( $meta_key, $meta_data['value'] ); @@ -820,3 +846,44 @@ function wcs_set_payment_meta( $subscription, $payment_meta ) { } } } + +/** + * Get total quantity of a product on a subscription or order, even across multiple line items. + * + * @since 2.6.0 + * + * @param WC_Order|WC_Subscription $subscription Order or subscription object. + * @param WC_Product $product The product to get the total quantity of. + * @param string $product_match_method The way to find matching products. Optional. Default is 'stock_managed' Can be: + * 'stock_managed' - Products with matching stock managed IDs are grouped. Helpful for getting the total quantity of variation parents if they are managed on the product level, not on the variation level - @see WC_Product::get_stock_managed_by_id(). + * 'parent' - Products with the same parent ID are grouped. Standard products are matched together by ID. Variations are matched with variations with the same parent product ID. + * 'strict_product' - Products with the exact same product ID are grouped. Variations are only grouped with other variations that share the variation ID. + * + * @return int $quantity The total quantity of a product on an order or subscription. + */ +function wcs_get_total_line_item_product_quantity( $order, $product, $product_match_method = 'stock_managed' ) { + $quantity = 0; + + foreach ( $order->get_items() as $line_item ) { + switch ( $product_match_method ) { + case 'parent': + $line_item_product_id = $line_item->get_product_id(); // Returns the parent product ID. + $product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); // The parent ID if a variation or product ID for standard products. + break; + case 'strict_product': + $line_item_product_id = $line_item->get_variation_id() ? $line_item->get_variation_id() : $line_item->get_product_id(); // The line item variation ID if it exists otherwise the product ID. + $product_id = $product->get_id(); // The variation ID for variations or product ID. + break; + default: + $line_item_product_id = $line_item->get_product()->get_stock_managed_by_id(); + $product_id = $product->get_stock_managed_by_id(); + break; + } + + if ( $product_id === $line_item_product_id ) { + $quantity += $line_item->get_quantity(); + } + } + + return $quantity; +} diff --git a/woocommerce-subscriptions.php b/woocommerce-subscriptions.php index 8265518..d50fcee 100755 --- a/woocommerce-subscriptions.php +++ b/woocommerce-subscriptions.php @@ -5,7 +5,7 @@ * Description: Sell products and services with recurring payments in your WooCommerce Store. * Author: Automattic * Author URI: https://woocommerce.com/ - * Version: 2.5.7 + * Version: 2.6.1 * * WC requires at least: 3.0 * WC tested up to: 3.6 @@ -73,6 +73,7 @@ WC_Subscriptions_Product::init(); WC_Subscriptions_Admin::init(); WC_Subscriptions_Manager::init(); WC_Subscriptions_Cart::init(); +WC_Subscriptions_Cart_Validator::init(); WC_Subscriptions_Order::init(); WC_Subscriptions_Renewal_Order::init(); WC_Subscriptions_Checkout::init(); @@ -94,6 +95,9 @@ WCS_Admin_System_Status::init(); WCS_Upgrade_Notice_Manager::init(); WCS_Staging::init(); WCS_Permalink_Manager::init(); +WCS_Custom_Order_Item_Manager::init(); +WCS_Early_Renewal_Modal_Handler::init(); +WCS_Dependent_Hook_Manager::init(); // Some classes run init on a particular hook. add_action( 'init', array( 'WC_Subscriptions_Synchroniser', 'init' ) ); @@ -113,7 +117,7 @@ class WC_Subscriptions { public static $plugin_file = __FILE__; - public static $version = '2.5.7'; + public static $version = '2.6.1'; public static $wc_minimum_supported_version = '3.0'; @@ -149,15 +153,12 @@ class WC_Subscriptions { register_deactivation_hook( __FILE__, __CLASS__ . '::deactivate_woocommerce_subscriptions' ); - // Override the WC default "Add to Cart" text to "Sign Up Now" (in various places/templates) + // Override the WC default "Add to cart" text to "Sign up now" (in various places/templates) add_filter( 'woocommerce_order_button_text', __CLASS__ . '::order_button_text' ); add_action( 'woocommerce_subscription_add_to_cart', __CLASS__ . '::subscription_add_to_cart', 30 ); add_action( 'woocommerce_variable-subscription_add_to_cart', __CLASS__ . '::variable_subscription_add_to_cart', 30 ); add_action( 'wcopc_subscription_add_to_cart', __CLASS__ . '::wcopc_subscription_add_to_cart' ); // One Page Checkout compatibility - // Ensure a subscription is never in the cart with products - add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::maybe_empty_cart', 10, 5 ); - // Enqueue front-end styles, run after Storefront because it sets the styles to be empty add_filter( 'woocommerce_enqueue_styles', __CLASS__ . '::enqueue_styles', 100, 1 ); @@ -465,9 +466,11 @@ class WC_Subscriptions { * * If multiple purchase flag is set, allow them to be added at the same time. * + * @deprecated 2.6.0 * @since 1.0 */ public static function maybe_empty_cart( $valid, $product_id, $quantity, $variation_id = '', $variations = array() ) { + wcs_deprecated_function( __METHOD__, '2.6.0', 'WC_Subscriptions_Cart_Validator::maybe_empty_cart()' ); $is_subscription = WC_Subscriptions_Product::is_subscription( $product_id ); $cart_contains_subscription = WC_Subscriptions_Cart::cart_contains_subscription(); @@ -490,19 +493,19 @@ class WC_Subscriptions { } } elseif ( $is_subscription && wcs_cart_contains_renewal() && ! $multiple_subscriptions_possible && ! $manual_renewals_enabled ) { - self::remove_subscriptions_from_cart(); + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); wc_add_notice( __( 'A subscription renewal has been removed from your cart. Multiple subscriptions can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); } elseif ( $is_subscription && $cart_contains_subscription && ! $multiple_subscriptions_possible && ! $manual_renewals_enabled && ! WC_Subscriptions_Cart::cart_contains_product( $canonical_product_id ) ) { - self::remove_subscriptions_from_cart(); + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); wc_add_notice( __( 'A subscription has been removed from your cart. Due to payment gateway restrictions, different subscription products can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); } elseif ( $cart_contains_subscription && 'yes' != get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) { - self::remove_subscriptions_from_cart(); + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); wc_add_notice( __( 'A subscription has been removed from your cart. Products and subscriptions can not be purchased at the same time.', 'woocommerce-subscriptions' ), 'notice' ); @@ -514,26 +517,24 @@ class WC_Subscriptions { } } - return $valid; + return WC_Subscriptions_Cart_Validator::maybe_empty_cart( $valid, $product_id, $quantity, $variation_id, $variations ); } /** * Removes all subscription products from the shopping cart. * + * @deprecated 2.6.0 * @since 1.0 */ public static function remove_subscriptions_from_cart() { + wcs_deprecated_function( __METHOD__, '2.6.0', 'WC_Subscriptions_Cart::remove_subscriptions_from_cart()' ); - foreach ( WC()->cart->cart_contents as $cart_item_key => $cart_item ) { - if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { - WC()->cart->set_quantity( $cart_item_key, 0 ); - } - } + WC_Subscriptions_Cart::remove_subscriptions_from_cart(); } /** * For a smoother sign up process, tell WooCommerce to redirect the shopper immediately to - * the checkout page after she clicks the "Sign Up Now" button + * the checkout page after she clicks the "Sign up now" button * * Only enabled if multiple checkout is not enabled. * @@ -569,7 +570,7 @@ class WC_Subscriptions { } /** - * Override the WooCommerce "Place Order" text with "Sign Up Now" + * Override the WooCommerce "Place order" text with "Sign up now" * * @since 1.0 */ @@ -577,7 +578,7 @@ class WC_Subscriptions { global $product; if ( WC_Subscriptions_Cart::cart_contains_subscription() ) { - $button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_order_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) ); + $button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_order_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) ); } return $button_text; @@ -830,8 +831,8 @@ class WC_Subscriptions { $notice->display(); } else { WCS_Early_Renewal_Manager::init(); + require_once( dirname( __FILE__ ) . '/includes/early-renewal/wcs-early-renewal-functions.php' ); if ( WCS_Early_Renewal_Manager::is_early_renewal_enabled() ) { - require_once( dirname( __FILE__ ) . '/includes/early-renewal/wcs-early-renewal-functions.php' ); new WCS_Cart_Early_Renewal(); } } @@ -1029,7 +1030,7 @@ class WC_Subscriptions { // Let the default source be WP if ( 'subscriptions_install' === $source ) { $site_url = self::get_site_url(); - } elseif ( defined( 'WP_SITEURL' ) ) { + } elseif ( ! is_multisite() && defined( 'WP_SITEURL' ) ) { $site_url = WP_SITEURL; } else { $site_url = get_site_url();