This commit is contained in:
Prospress Inc
2019-09-12 11:49:11 +02:00
committed by Remco Tolsma
parent 42964bef17
commit a60db3815d
131 changed files with 6719 additions and 3502 deletions

View File

@@ -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";
}

View File

@@ -2,12 +2,14 @@
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";
font-family: Dashicons;
content: "\f321";
color: #f29ec4;
}
@@ -16,14 +18,15 @@
}
#woocommerce_dashboard_status .wc_status_list li.cancel-count a:before {
content: "\e02c";
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;
color: #59cf8a;
}
#woocommerce_dashboard_status .wc_status_list li.renewal-revenue a:before {
@@ -39,3 +42,16 @@
#woocommerce_dashboard_status .wc_status_list li.renewal-count {
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%
}
}

136
assets/css/modal.css Executable file
View File

@@ -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%;
}
}

View File

@@ -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;
}

View File

@@ -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,12 +561,12 @@ 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(),
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' ),
@@ -567,17 +574,20 @@ jQuery(document).ready(function($){
$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

View File

@@ -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 );
});

114
assets/js/modal.js Executable file
View File

@@ -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%' );
}
}
});

View File

@@ -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

View File

@@ -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"
}
}

1252
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
<?php
/**
* WCS_Background_Repairer Class
*
* Provide APIs for a repair script to find objects which need repairing, schedule a separate background process for each object using Action Scheduler, and then in that background process update data for that object.
*
* @author Automattic
* @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades
* @since 2.6.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class WCS_Background_Repairer extends WCS_Background_Upgrader {
/**
* @var string The hook used to schedule background repairs for a specific object.
*/
protected $repair_hook;
/**
* An internal cache of items which need to be repaired. Used in cases where the updater runs out of processing time, so we can ensure remaining items are processed in the next request.
*
* @var array
*/
protected $items_to_repair = array();
/**
* Attaches callbacks to hooks.
*
* @since 2.6.0
* @see WCS_Background_Updater::init() for additional hooks and callbacks.
*/
public function init() {
parent::init();
add_action( $this->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 );
}

View File

@@ -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 ) ) );
}

View File

@@ -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 .= '<small class="meta">';
// translators: placeholder is the display name of a payment gateway a subscription was paid by
$column_content .= '<small class="meta">' . esc_html( sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $the_subscription->get_payment_method_to_display() ) ) . '</small>';
$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 .= '</small>';
break;
case 'start_date':

View File

@@ -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 ) {

View File

@@ -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();
$orders = array();
$is_subscription_screen = wcs_is_subscription( $post->ID );
$initial_subscriptions = array();
$orders_by_type = array();
$unknown_orders = array(); // Orders which couldn't be loaded.
// 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 ) {
// Resubscribed subscriptions and orders.
$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' ) ) ) {
$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;
}
// 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;
}
}
// 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
foreach ( $subscriptions as $subscription ) {
$orders_by_type['renewal'] = $subscription->get_related_orders( 'ids', 'renewal' );
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;
// 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;
}
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,
);
}
}
}
$orders = apply_filters( 'woocommerce_subscriptions_admin_related_orders_to_display', $orders, $subscriptions, $post );
$orders_to_display = apply_filters( 'woocommerce_subscriptions_admin_related_orders_to_display', $orders_to_display, $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' );
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Display a row in the related orders table for a unknown subscription or order.
*
* @var int $order_id A WC_Order or WC_Subscription order id.
* @var string $relationship The order's or subscription's relationship.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<tr>
<td>
<?php echo sprintf( esc_html_x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), esc_html( $order_id ) ); ?>
<div class="wcs-unknown-order-info-wrapper">
<a href="https://docs.woocommerce.com/document/subscriptions/orders/#section-8"><?php echo wcs_help_tip( sprintf( "This %s couldn't be loaded from the database. %s Click to learn more.", $relationship, '</br>' ) ); ?></a>
</div>
</td>
<td><?php echo esc_html( $relationship ); ?></td>
<td>&mdash;</td>
<td>&mdash;</td>
<td>&mdash;</td>
</tr>

View File

@@ -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 ];

View File

@@ -45,7 +45,9 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
echo ' <strong>' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->active_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->total_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . '</strong>: ' . 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' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . '</strong>: ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '</p>';
echo ' <strong>' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . '</strong>: ';
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' ) ) . '</p>';
echo '</div></div>';
$this->display();
echo '</div>';
@@ -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

View File

@@ -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' ), '<strong>' . $this->report_data->new_subscription_total_count . '</strong>' ),
'title' => sprintf( __( '%s switch revenue in this period', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $data->switch_orders_total_amount ) . '</strong>' ),
'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' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->new_subscription_total_count . '</span> </strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_subscription', '_subscriptions_list_key' => $this->report_data->new_subscriptions_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'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' ), '<strong>' . $this->report_data->signup_orders_total_count . '</strong>' ),
'placeholder' => __( 'The number of subscription parent orders created during this period. This represents the new subscriptions created by customers placing an order via checkout.', 'woocommerce-subscriptions' ),
'title' => sprintf( __( '%2$s %1$s subscription signups', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->signup_orders_total_count . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_subscription', '_subscriptions_list_key' => $this->report_data->signup_orders_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'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' ), '<strong>' . $data->resubscribe_orders_total_count . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s subscription resubscribes', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->resubscribe_orders_total_count . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_order', '_orders_list_key' => $this->report_data->resubscribes_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'placeholder' => __( 'The number of resubscribe orders processed during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['resubscribe_count'],
'highlight_series' => 3,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription renewals', 'woocommerce-subscriptions' ), '<strong>' . $data->renewal_orders_total_count . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s subscription renewals', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->renewal_orders_total_count . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_order', '_orders_list_key' => $this->report_data->renewals_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'placeholder' => __( 'The number of renewal orders processed during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['renewal_count'],
'highlight_series' => 4,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription switches', 'woocommerce-subscriptions' ), '<strong>' . $data->switch_orders_total_count . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s subscription switches', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->switch_orders_total_count . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_order', '_orders_list_key' => $this->report_data->switches_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'placeholder' => __( 'The number of subscriptions upgraded, downgraded or cross-graded during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['switch_count'],
'highlight_series' => 0,
);
$legend[] = array(
'title' => sprintf( __( '%s subscription cancellations', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_cancelled . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s subscription cancellations', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->total_subscriptions_cancelled . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_subscription', '_subscriptions_list_key' => $this->report_data->cancelled_subscriptions_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'placeholder' => __( 'The number of subscriptions cancelled by the customer or store manager during this period. The pre-paid term may not yet have ended during this period.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['cancel_count'],
'highlight_series' => 7,
);
$legend[] = array(
'title' => sprintf( __( '%s subscriptions ended', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_ended . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s ended subscriptions', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->total_subscriptions_ended . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_subscription', '_subscriptions_list_key' => $this->report_data->ended_subscriptions_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'placeholder' => __( 'The number of subscriptions which have either expired or reached the end of the prepaid term if it was previously cancelled.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['ended_count'],
'highlight_series' => 6,
);
$legend[] = array(
'title' => sprintf( __( '%s current subscriptions', 'woocommerce-subscriptions' ), '<strong>' . $data->total_subscriptions_at_period_end . '</strong>' ),
'title' => sprintf( __( '%2$s %1$s current subscriptions', 'woocommerce-subscriptions' ), '<strong> <span class="woocommerce-subscriptions-count count">' . $this->report_data->total_subscriptions_at_period_end . '</strong> </a>',
'<a href="' . esc_url( add_query_arg( array( 'post_type' => 'shop_subscription', '_subscriptions_list_key' => $this->report_data->current_subscriptions_query_hash, '_report' => strtolower( get_class( $this ) ) ), admin_url( 'edit.php' ) ) ). '">' ),
'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,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
{
label: "<?php echo esc_js( __( 'Switch Totals', 'woocommerce-subscriptions' ) ) ?>",
data: order_data.switch_orders_amount,
yaxis: 2,
color: '<?php echo esc_js( $this->chart_colours['switch_total'] ); ?>',
points: {
show: true,
radius: 5,
lineWidth: 4,
fillColor: '#fff',
fill: true
},
lines: {
show: true,
lineWidth: 5,
fill: false
},
shadowSize: 0,
<?php echo wp_kses_post( $this->get_currency_tooltip() ); ?>
},
];
if ( highlight !== 'undefined' && series[ highlight ] ) {

View File

@@ -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;

View File

@@ -1,6 +1,23 @@
<?php
/**
* Line Item (product) Pending Switch
*
* Line items added to a subscription to record a switch are first given this line item type before transitioning to a fully fledged WC_Order_Item_Product.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.2.0
*/
class WC_Order_Item_Pending_Switch extends WC_Order_Item_Product {
/**
* Get item type.
*
* @return string
* @since 2.2.0
*/
public function get_type() {
return 'line_item_pending_switch';
}

View File

@@ -95,7 +95,7 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Coupon Pending Switch
*
* Coupons which have been added during switch by a customer have the coupon_pending_switch type. This class extends WC_Order_Item_Coupon to implement this coupon item type.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.6.0
*/
class WC_Subscription_Item_Coupon_Pending_Switch extends WC_Order_Item_Coupon {
/**
* Get item type.
*
* @return string
* @since 2.6.0
*/
public function get_type() {
return 'coupon_pending_switch';
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Subscription Fee Item Pending Switch
*
* Fee items which have been added during switch by a customer have the fee_pending_switch type. This class extends WC_Order_Item_Fee to implement this fee item type.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.6.0
*/
class WC_Subscription_Item_Fee_Pending_Switch extends WC_Order_Item_Fee {
/**
* Get item type.
*
* @return string
* @since 2.6.0
*/
public function get_type() {
return 'fee_pending_switch';
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Subscription Line Item (product) Removed
*
* Line items removed from a subscription by a customer have the line_item_removed line item type. This class extends WC_Order_Item_Product to implement this line item type.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.6.0
*/
class WC_Subscription_Line_Item_Removed extends WC_Order_Item_Product {
/**
* Get item type.
*
* @return string
* @since 2.6.0
*/
public function get_type() {
return 'line_item_removed';
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Subscription Line Item (product) Switched
*
* Line items which have been switched by a customer have the line_item_switched line item type. This class extends WC_Order_Item_Product to implement this line item type.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.6.0
*/
class WC_Subscription_Line_Item_Switched extends WC_Order_Item_Product {
/**
* Get item type.
*
* @return string
* @since 2.6.0
*/
public function get_type() {
return 'line_item_switched';
}
}

View File

@@ -20,8 +20,8 @@ class WC_Subscription extends WC_Order {
/** @public string Order type */
public $order_type = 'shop_subscription';
/** @private int Stores get_completed_payment_count when used multiple times in payment_complete() */
private $cached_completed_payment_count = false;
/** @private int Stores get_payment_count when used multiple times */
private $cached_payment_count = null;
/**
* Which data store to load. WC 3.0+ property.
@@ -666,49 +666,98 @@ class WC_Subscription extends WC_Order {
}
/**
* Get the number of payments completed for a subscription
* Get the number of payments 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).
* Default payment count includes 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).
*
* @since 2.0
* @param string $payment_type Type of count (completed|refunded|net). Optional. Default completed.
* @param string|array $order_types Type of order relation(s) to count. Optional. Default array(parent,renewal).
* @return integer Count.
* @since 2.6.0
*/
public function get_completed_payment_count() {
public function get_payment_count( $payment_type = 'completed', $order_types = '' ) {
// If not cached, calculate the completed payment count otherwise return the cached version
if ( false === $this->cached_completed_payment_count ) {
if ( empty( $order_types ) ) {
$order_types = array( 'parent', 'renewal' );
} elseif ( ! is_array( $order_types ) ) {
$order_types = array( $order_types );
}
$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;
// 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' ) );
}
$paid_renewal_orders = array();
$renewal_order_ids = $this->get_related_order_ids( 'renewal' );
// 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 ( ! empty( $renewal_order_ids ) ) {
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 ( $renewal_order_ids as $renewal_order_id ) {
foreach ( $this->get_related_orders( 'all', $order_type ) as $related_order ) {
if ( null !== $related_order->get_date_paid() ) {
$completed_payment_count++;
$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 ( $related_order->has_status( 'refunded' ) ) {
$refunded_payment_count++;
}
}
if ( ! empty( $paid_renewal_orders ) ) {
$completed_payment_count += count( $paid_renewal_orders );
}
}
} else {
$completed_payment_count = $this->cached_completed_payment_count;
$completed_payment_count = $this->cached_payment_count['completed'][ $order_type ];
$refunded_payment_count = $this->cached_payment_count['refunded'][ $order_type ];
}
// 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 );
// 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 );
return $this->cached_completed_payment_count;
$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;
}
}

View File

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

View File

@@ -0,0 +1,140 @@
<?php
/**
* Subscriptions Cart Validator Class
*
* Validates the Cart contents
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Cart_Validator
* @category Class
* @since 2.6.0
*/
class WC_Subscriptions_Cart_Validator {
/**
* Bootstraps the class and hooks required actions & filters.
*/
public static function init() {
add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'maybe_empty_cart' ), 10, 5 );
add_filter( 'woocommerce_cart_loaded_from_session', array( __CLASS__, 'validate_cart_contents_for_mixed_checkout' ), 10 );
add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'can_add_subscription_product_to_cart' ), 10, 6 );
}
/**
* When a subscription is added to the cart, remove other products/subscriptions to
* work with PayPal Standard, which only accept one subscription per checkout.
*
* If multiple purchase flag is set, allow them to be added at the same time.
*
* @since 2.6.0
*/
public static function maybe_empty_cart( $valid, $product_id, $quantity, $variation_id = '', $variations = array() ) {
$is_subscription = WC_Subscriptions_Product::is_subscription( $product_id );
$cart_contains_subscription = WC_Subscriptions_Cart::cart_contains_subscription();
$multiple_subscriptions_possible = WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'multiple_subscriptions' );
$manual_renewals_enabled = ( 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', 'no' ) );
$canonical_product_id = ! empty( $variation_id ) ? $variation_id : $product_id;
if ( $is_subscription && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
// Generate a cart item key from variation and cart item data - which may be added by other plugins
$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', array(), $product_id, $variation_id, $quantity );
$cart_item_id = WC()->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;
}
}

View File

@@ -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.

View File

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

View File

@@ -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.
*

View File

@@ -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;
}

View File

@@ -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 );
}
/**

View File

@@ -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();
}

View File

@@ -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 );
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -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() ) {

View File

@@ -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;
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* WooCommerce Subscriptions Add Cart Item.
*
* A class to assist in the calculations required to add an item to an existing subscription.
* To enable proration, adding a product to a subscription inherits all the switch item (@see WCS_Switch_Cart_Item) functionality, however, doesn't have an existing item (@see WCS_Switch_Cart_Item::$existing_item) to replace.
*
* @package WooCommerce Subscriptions
* @author Prospress
* @since 2.6.0
*/
class WCS_Add_Cart_Item extends WCS_Switch_Cart_Item {
/**
* Constructor.
*
* An item being added to a subscription is just a switch item, without an existing item.
*
* @since 2.6.0
*
* @param array $cart_item The cart item.
* @param WC_Subscription $subscription The subscription being switched.
*
* @throws Exception If WC_Subscriptions_Product::get_expiration_date() returns an invalid date.
*/
public function __construct( $cart_item, $subscription ) {
parent::__construct( $cart_item, $subscription, null );
}
/** Getters */
/**
* Gets the old subscription's price per day.
*
* For items being added to a subscription, there is no old item's price and so 0 should be returned.
*
* @since 2.6.0
* @return float
*/
public function get_old_price_per_day() {
if ( ! isset( $this->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;
}
}

View File

@@ -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,

View File

@@ -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 ) ) {
// 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

View File

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

View File

@@ -0,0 +1,101 @@
<?php
/**
* Subscriptions Custom Order Item Manager
*
* @author Prospress
* @since 2.6.0
*/
class WCS_Custom_Order_Item_Manager {
/**
* The custom line item types managed by this class.
*
* @var array Each item type should have:
* - A 'group' arg which is registered with WC_Abstract_Order::get_items() APIs via the woocommerce_order_type_to_group hook.
* - A 'class' arg which WooCommerce's WC_Abstract_Order::get_item() APIs will use to instantiate the line item object.
* - Optional. A 'data_store' arg. If provided, the line item will use this data store to load the line item data. Default is WC_Order_Item_Product_Data_Store.
*/
protected static $line_item_type_args = array(
'line_item_removed' => 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;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* WooCommerce Subscriptions Dependent Hook Manager
*
* An API for attaching callbacks which depend on WC versions.
*
* @package WooCommerce Subscriptions
* @category Class
* @author Automattic
* @since 2.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Dependent_Hook_Manager {
/**
* An array of callbacks which need to be attached on for certain WC versions.
*
* @var array
*/
protected static $dependent_callbacks = array();
/**
* Initialise the class.
*
* @since 2.6.0
*/
public static function init() {
add_action( 'plugins_loaded', array( __CLASS__, 'attach_woocommerce_dependent_hooks' ) );
}
/**
* Attach all the WooCommerce version dependent hooks.
*
* This attaches all the hooks registered via @see add_woocommerce_dependent_action()
* if the WooCommerce version requirements are met.
*
* @since 2.6.0
*/
public static function attach_woocommerce_dependent_hooks() {
if ( ! isset( self::$dependent_callbacks['woocommerce'] ) ) {
return;
}
foreach ( self::$dependent_callbacks['woocommerce'] as $wc_version => $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,
);
}
}

246
includes/class-wcs-modal.php Executable file
View File

@@ -0,0 +1,246 @@
<?php
/**
* A class to create and display a modal popup.
*
* @package WooCommerce Subscriptions
* @category Class
* @since 2.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WCS_Modal {
/**
* The content to display inside the modal body.
*
* Can be plain text, raw HTML, a template file path or a PHP callback function.
*
* @var string
*/
private $content;
/**
* The type of content to display.
*
* Can be 'plain-text', 'html', 'template' or 'callback'.
*
* @var string
*/
private $content_type;
/**
* A selector of the element which triggers the modal to be displayed.
*
* @var string
*/
private $trigger = '';
/**
* The modal heading.
*
* @var string
*/
private $heading = '';
/**
* The modal actions.
*
* @var array
*/
private $actions = array();
/**
* Registers the scripts and stylesheets needed to display the modals.
*
* The required files will only be enqueued once. Subsequent calls will do nothing.
*
* @since 2.6.0
*/
public static function register_scripts_and_styles() {
static $registered = false;
// No need to proceed if the styles and scripts have already been enqueued.
if ( $registered ) {
return;
}
$registered = true;
// If the scripts are being registered late (after 'wp_enqueue_scripts' has run), it's safe to enqueue them immediately.
if ( did_action( 'wp_enqueue_scripts' ) ) {
self::enqueue_scripts_and_styles();
} else {
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts_and_styles' ) );
}
}
/**
* Enqueues the modal scripts and styles.
*
* @since 2.6.0
*/
public static function enqueue_scripts_and_styles() {
wp_enqueue_script( 'wcs-modal-scripts', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/modal.js', array( 'jquery' ), WC_Subscriptions::$version, true );
wp_enqueue_style( 'wcs-modal-styles', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/css/modal.css', array(), WC_Subscriptions::$version );
}
/**
* Constructor.
*
* @since 2.6.0
*
* @param string|callable $content The content to display in the modal. This should be a string when $content_type is either 'plain-text' or 'html',
* a WooCommerce template filename when $content_type is 'template' or a function that echoes out the content when $content_type is 'callback'.
* @param string $trigger A jQuery selector of the element which triggers the modal to be displayed.
* @param string $content_type Optional. The modal content type. Can be 'plain-text', 'html', 'template' or 'callback'. Default is 'plain-text'.
* @param string $heading Optional. The modal heading text.
* @param array $actions Optional. An array of actions to add to the modal. See {@see 'WCS_Modal::add_action'} for details on the action array format.
*/
function __construct( $content, $trigger, $content_type = 'plain-text', $heading = '', $actions = array() ) {
$this->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 '<p>' . wp_kses_post( $this->content ) . '</p>';
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 );
}
}

View File

@@ -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() ),
) );
}
/**

View File

@@ -0,0 +1,75 @@
<?php
/**
* A class to sort objects by an object property.
*
* @author Prospress
* @category Class
* @package WooCommerce Subscriptions
* @since 2.6.0
*/
class WCS_Object_Sorter {
/**
* The object property to compare.
*
* Used to generate the getter by prepending the 'get_' prefix. For example id -> 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 );
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* A Renewal Cart Stock Manager class.
*
* Contains functions which assists in overriding WC core functionality to allow renewal carts to bypass stock validation.
*
* @package WooCommerce Subscriptions
* @category Class
* @author Prospress
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
class WCS_Renewal_Cart_Stock_Manager {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.6.0
*/
public static function attach_callbacks() {
add_action( 'wcs_before_renewal_setup_cart_subscription', array( __CLASS__, 'maybe_adjust_stock_cart' ), 10, 2 );
add_action( 'woocommerce_check_cart_items', array( __CLASS__, 'maybe_adjust_stock_checkout' ), 0 );
add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'remove_filters' ) );
add_action( 'woocommerce_check_cart_items', array( __CLASS__, 'remove_filters' ), 20 );
}
/**
* Attaches filters that allow a manual renewal to add to the cart an otherwise out of stock product.
*
* Hooked onto 'wcs_before_renewal_setup_cart_subscription'.
*
* @since 2.6.0
*
* @param WC_Subscription $subscription The subscription object. This param is unused. It is the first parameter of the hook.
* @param WC_Order $order The renewal order object.
*/
public static function maybe_adjust_stock_cart( $subscription, $order ) {
self::maybe_attach_stock_filters( $order );
}
/**
* Attaches filters that allow manual renewal carts to pass checkout validity checks for an otherwise out of stock product.
*
* @since 2.6.0
*/
public static function maybe_adjust_stock_checkout() {
$renewal_order = self::get_order_from_cart();
// Get the order from query vars if the cart isn't loaded yet.
if ( ! $renewal_order ) {
$renewal_order = self::get_order_from_query_vars();
}
if ( $renewal_order ) {
self::maybe_attach_stock_filters( $renewal_order );
}
}
/**
* Attaches stock override filters for out of stock renewal products.
*
* @since 2.6.0
* @param WC_Order $order Renewal order.
*/
protected static function maybe_attach_stock_filters( $order ) {
if ( ! $order instanceof WC_Order ) {
return;
}
foreach ( $order->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;
}
}

View File

@@ -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(
'<p>%s</p>',
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 '<div class="woocommerce-help-tip" data-tip="' . esc_attr__( sprintf( 'Subscription locked to Manual Renewal while the store is in staging mode. Live payment method: %s', $subscription->get_payment_method_title() ), 'woocommerce-subscriptions' ) . '"></div>';
}
}

View File

@@ -0,0 +1,413 @@
<?php
/**
* WooCommerce Subscriptions Switch Cart Item.
*
* A class to assist in the calculations required to record a switch.
*
* @package WooCommerce Subscriptions
* @author Prospress
* @since 2.6.0
*/
class WCS_Switch_Cart_Item {
/**
* The cart item.
* @var array
*/
public $cart_item;
/**
* The subscription being switched.
* @var WC_Subscription
*/
public $subscription;
/**
* The existing subscription line item being switched.
* @var WC_Order_Item_Product
*/
public $existing_item;
/**
* The instance of the new product in the cart.
* @var WC_Product
*/
public $product;
/**
* The new product's variation or product ID.
* @var int
*/
public $canonical_product_id;
/**
* The subscription's next payment timestamp.
* @var int
*/
public $next_payment_timestamp;
/**
* The subscription's end timestamp.
* @var int
*/
public $end_timestamp;
/**
* The subscription's last non-early renewal or parent order paid timestamp.
* @var int
*/
public $last_order_paid_time;
/**
* The number of days since the @see $last_order_created_time.
* @var int
*/
public $days_since_last_payment;
/**
* The number of days until the @see $next_payment_timestamp.
* @var int
*/
public $days_until_next_payment;
/**
* The number of days in the old subscription's billing cycle.
* @var int
*/
public $days_in_old_cycle;
/**
* The total paid for the existing item (@see $existing_item) in early renewals and switch orders since the last non-early renewal or parent order.
* @var float
*/
public $total_paid_for_current_period;
/**
* The existing subscription item's price per day.
* @var float
*/
public $old_price_per_day;
/**
* The number of days in the new subscription's billing cycle.
* @var float
*/
public $days_in_new_cycle;
/**
* The new subscription product's price per day.
* @var float
*/
public $new_price_per_day;
/**
* The switch type.
* @var string Can be upgrade, downgrade or crossgrade.
*/
public $switch_type;
/**
* Constructor.
*
* @since 2.6.0
*
* @param array $cart_item The cart item.
* @param WC_Subscription $subscription The subscription being switched.
* @param WC_Order_Item_Product $existing_item The subscription line item being switched.
*
* @throws Exception If WC_Subscriptions_Product::get_expiration_date() returns an invalid date.
*/
public function __construct( $cart_item, $subscription, $existing_item ) {
$this->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' );
}
}

View File

@@ -0,0 +1,510 @@
<?php
/**
* WooCommerce Subscriptions Switch Totals Calculator.
*
* A class to assist in calculating the upgrade cost, and next payment dates for switch items in the cart.
*
* @package WooCommerce Subscriptions
* @author Prospress
* @since 2.6.0
*/
class WCS_Switch_Totals_Calculator {
/**
* Reference to the cart object.
*
* @var WC_Cart
*/
protected $cart = null;
/**
* Whether to prorate the recurring price for all product types ('yes', 'yes-upgrade') or only for virtual products ('virtual', 'virtual-upgrade').
*
* @var string
*/
protected $apportion_recurring_price = '';
/**
* Whether to charge the full sign-up fee, a prorated sign-up fee or no sign-up fee.
*
* @var string Can be 'full', 'yes', or 'no'.
*/
protected $apportion_sign_up_fee = '';
/**
* Whether to take into account the number of payments completed when determining how many payments the subscriber needs to make for the new subscription.
*
* @var string Can be 'virtual' (for virtual products only), 'yes', or 'no'
*/
protected $apportion_length = '';
/**
* Whether store prices include tax.
*
* @var bool
*/
protected $prices_include_tax;
/**
* A cache of the cart item switch objects after they have had their totals calculated.
*
* @var WCS_Switch_Cart_Item[]
*/
protected $calculated_switch_items = array();
/**
* Constructor.
*
* @since 2.6.0
*
* @param WC_Cart $cart Cart object to calculate totals for.
* @throws Exception If $cart is invalid WC_Cart object.
*/
public function __construct( &$cart = null ) {
if ( ! is_a( $cart, 'WC_Cart' ) ) {
throw new InvalidArgumentException( 'A valid WC_Cart object parameter is required for ' . __METHOD__ );
}
$this->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 );
}
}
}

View File

@@ -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 );
}
}
}

View File

@@ -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;
}

View File

@@ -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( '<a href="%s">%s</a>', 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 );
}
/**

View File

@@ -20,6 +20,13 @@ 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.
*
@@ -27,6 +34,7 @@ class WCS_Early_Renewal_Manager {
*/
public static function init() {
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(
$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' ),
'<strong>', '</strong>',
'<a href="https://docs.woocommerce.com/document/subscriptions/early-renewal/">', '</a>'
),
'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;
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* A class to display and handle early renewal requests via the modal.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Early_Renewal
* @category Class
* @since 2.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Early_Renewal_Modal_Handler {
/**
* Attach callbacks.
*
* @since 2.6.0
*/
public static function init() {
add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'maybe_print_early_renewal_modal' ) );
add_action( 'wp_loaded', array( __CLASS__, 'process_early_renewal_request' ), 20 );
}
/**
* Prints the early renewal modal for a specific subscription. If eligible.
*
* @since 2.6.0
*
* @param WC_Subscription $subscription The subscription to print the modal for.
*/
public static function maybe_print_early_renewal_modal( $subscription ) {
if ( ! WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled() || ! wcs_can_user_renew_early( $subscription ) ) {
return;
}
$place_order_action = array(
'text' => __( '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 );
}
}

View File

@@ -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( '<a href="%s">%s</a>', 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 ) );
}
}
}

View File

@@ -94,12 +94,12 @@ 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(),
'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,
@@ -107,7 +107,6 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -117,12 +116,12 @@ 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(),
'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,
@@ -130,7 +129,6 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**

View File

@@ -132,12 +132,12 @@ 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(),
'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,
@@ -145,7 +145,6 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -155,12 +154,12 @@ 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(),
'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,
@@ -168,6 +167,5 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -133,13 +133,13 @@ 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(),
'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,
@@ -147,7 +147,6 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -157,13 +156,13 @@ 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(),
'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,
@@ -171,6 +170,5 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -110,13 +110,13 @@ 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(),
'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,
@@ -124,7 +124,6 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -134,13 +133,13 @@ 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(),
'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,
@@ -148,6 +147,5 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -148,12 +148,12 @@ 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(),
'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,
@@ -161,7 +161,6 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -171,12 +170,12 @@ 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(),
'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,
@@ -184,7 +183,6 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
'',
$this->template_base
);
return ob_get_clean();
}
/**

View File

@@ -92,12 +92,12 @@ 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(),
'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,
@@ -105,7 +105,6 @@ class WCS_Email_Expired_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -115,12 +114,12 @@ 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(),
'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,
@@ -128,7 +127,6 @@ class WCS_Email_Expired_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**

View File

@@ -113,12 +113,12 @@ 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(),
'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,
@@ -126,7 +126,6 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -136,12 +135,12 @@ 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(),
'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,
@@ -149,6 +148,5 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -110,13 +110,13 @@ 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(),
'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,
@@ -124,7 +124,6 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -134,13 +133,13 @@ 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(),
'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,
@@ -148,6 +147,5 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -92,12 +92,12 @@ 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(),
'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,
@@ -105,7 +105,6 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -115,12 +114,12 @@ 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(),
'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,
@@ -128,7 +127,6 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
'',
$this->template_base
);
return ob_get_clean();
}
/**

View File

@@ -95,6 +95,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
'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,
@@ -116,6 +117,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
'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,

View File

@@ -126,12 +126,12 @@ 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(),
'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,
@@ -139,7 +139,6 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
'',
$this->template_base
);
return ob_get_clean();
}
/**
@@ -149,12 +148,12 @@ 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(),
'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,
@@ -162,6 +161,5 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -130,8 +130,12 @@ 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' ) ) {
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' ), '<a href="https://docs.woocommerce.com/document/subscriptions/enabling-payment-gateways-for-subscriptions/">', '</a>' );
} 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;
}

View File

@@ -103,6 +103,7 @@ class WCS_PayPal_Admin {
$notices = array();
if ( ! WCS_PayPal::are_credentials_set() ) {
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
@@ -113,11 +114,20 @@ class WCS_PayPal_Admin {
'</a>'
),
);
}
} 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,
'<strong>',
'</strong>',
'<a href="https://docs.woocommerce.com/document/subscriptions/faq/paypal-reference-transactions/" target="_blank">',

View File

@@ -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.' );

View File

@@ -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 {

View File

@@ -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 );
}
}

View File

@@ -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;

View File

@@ -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();
}
@@ -838,6 +843,7 @@ class WC_Subscriptions_Upgrader {
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.6']['has_trial_item_meta'] = new WCS_Repair_Line_Item_Has_Trial_Meta( $logger );
// Init the updaters
foreach ( self::$background_updaters as $version => $updaters ) {

View File

@@ -0,0 +1,113 @@
<?php
/**
* Between WCS 2.2.0 and WCS 2.6.0 subscription purchases of free trial products haven't set the `_has_trial` line item meta
* on subscription line items.
*
* This script will repair that missing data by:
* 1. Getting all subscriptions which were purchased with a free trial. Those with '_trial_period' subscription meta.
* 2. Schedule a background job, via Action Scheduler, to repair each of those subscriptions.
* 3. For each subscription line item that was purchased in the parent order (has not been switched or added manually), set the _has_trial line item meta.
*
* All line items on subscriptions with _trial_period meta, that were purchased in the parent order must have had a free trial because subscriptions are grouped in the cart by trial period and length.
* For more details @see https://github.com/Prospress/woocommerce-subscriptions/pull/3239
*
* @author Automattic
* @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades
* @version 2.6.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WCS_Repair_Line_Item_Has_Trial_Meta extends WCS_Background_Repairer {
/**
* Constructor
*
* @param WC_Logger_Interface $logger The WC_Logger instance.
*
* @since 2.6.0
*/
public function __construct( WC_Logger_Interface $logger ) {
$this->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() ) );
}
}
}

View File

@@ -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' ), '<strong>', '</strong>' ),
),
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' ), '<strong>', '</strong>' ),
'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' ),
'<span style="font-size: 17px;" class="dashicons dashicons-external"></span>',
'<a href="' . esc_url( add_query_arg( array( 'page' => 'wc-reports', 'tab' => 'subscriptions' ), admin_url( 'admin.php' ) ) ) . '">', '</a>'
),
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' ),
),
);
// 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/',
),
) );

View File

@@ -5,7 +5,7 @@
<?php echo wp_kses_post( sprintf( __( 'Version %1$s brings some great new features requested by store managers just like you (and possibly even by %2$syou%3$s).', 'woocommerce-subscriptions' ), $version, '<em>', '</em>' ) ); ?>
<?php esc_html_e( 'We hope you enjoy it!', 'woocommerce-subscriptions' ); ?>
</p>
<h3><?php esc_html_e( "What's New?", 'woocommerce-subscriptions' ); ?></h3>
<h3><?php esc_html_e( "What's new?", 'woocommerce-subscriptions' ); ?></h3>
<ul style="list-style-type: disc; padding-left: 2em;">
<?php foreach ( $features as $feature ) : ?>
<li><b><?php echo wp_kses_post( $feature['title'] ); ?></b> &ndash; <?php echo wp_kses_post( $feature['description'] ); ?></li>
@@ -13,4 +13,4 @@
</ul>
<hr>
<?php // translators: placeholder is Subscription version string ('2.3') ?>
<p><?php echo esc_html( sprintf( __( 'Want to know more about %s and these new features?', 'woocommerce-subscriptions' ), $version ) ); ?></p>
<p><?php echo esc_html( sprintf( __( 'Want to know more about Subscriptions %s and these new features?', 'woocommerce-subscriptions' ), $version ) ); ?></p>

View File

@@ -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 );
}

View File

@@ -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 );
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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 ) ) {
}
?>
<table class="wc_status_table widefat" cellspacing="0">
<table class="wc_status_table wc_status_table--wcs widefat" cellspacing="0">
<thead>
<tr>
<th colspan="3" data-export-label="<?php echo esc_attr( $section_title ); ?>">
@@ -23,7 +23,7 @@ if ( ! isset( $debug_data ) || ! is_array( $debug_data ) ) {
</h2></th>
</tr>
</thead>
<tbody>
<tbody class="wcs">
<?php foreach ( $debug_data as $section => $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';
}
?>
<tr>
<tr class="<?php echo sanitize_html_class( $section ); ?>">
<td data-export-label="<?php echo esc_attr( $data['label'] ) ?>"><?php echo esc_html( $data['name'] ) ?>:</td>
<td class="help">&nbsp;</td>
<td>

View File

@@ -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' ) ) {
<tr class="shipping recurring-total <?php echo esc_attr( $recurring_cart_key ); ?>">
<th><?php echo wp_kses_post( $package_name ); ?></th>
<td data-title="<?php echo esc_attr( $package_name ); ?>">
<?php if ( WC_Subscriptions::is_woocommerce_pre( '2.6' ) && is_cart() ) : // WC < 2.6 did not allow string indexes for shipping methods on the cart page and there was no way to hook in ?>
<?php echo wp_kses_post( wpautop( __( 'Recurring shipping options can be selected on checkout.', 'woocommerce-subscriptions' ) ) ); ?>
<?php elseif ( 1 < count( $available_methods ) ) : ?>
<?php if ( 1 < count( $available_methods ) ) : ?>
<ul id="shipping_method_<?php echo esc_attr( $recurring_cart_key ); ?>">
<?php foreach ( $available_methods as $method ) : ?>
<li>

View File

@@ -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.
}
?>
<form id="order_review" method="post">
@@ -23,59 +23,49 @@ if ( ! defined( 'ABSPATH' ) ) {
</tr>
</thead>
<tfoot>
<?php
if ( $totals = $subscription->get_order_item_totals() ) {
foreach ( $totals as $total ) : ?>
<?php foreach ( $subscription->get_order_item_totals() as $total ) : ?>
<tr>
<th scope="row" colspan="2"><?php echo esc_html( $total['label'] ); ?></th>
<td class="product-total"><?php echo wp_kses_post( $total['value'] ); ?></td>
</tr>
<?php endforeach;
};
?>
<?php endforeach; ?>
</tfoot>
<tbody>
<?php
$recurring_order_items = $subscription->get_items();
if ( sizeof( $recurring_order_items ) > 0 ) :
foreach ( $recurring_order_items as $item ) :
echo '
<?php foreach ( $subscription->get_items() as $item ) : ?>
<tr>
<td class="product-name">' . wp_kses_post( $item['name'] ) . '</td>
<td class="product-quantity">' . esc_html( $item['qty'] ) . '</td>
<td class="product-subtotal">' . wp_kses_post( $subscription->get_formatted_line_subtotal( $item ) ) . '</td>
</tr>';
endforeach;
endif;
?>
<td class="product-name"><?php echo esc_html( $item['name'] ); ?></td>
<td class="product-quantity"><?php echo esc_html( $item['qty'] ); ?></td>
<td class="product-subtotal"><?php echo wp_kses_post( $subscription->get_formatted_line_subtotal( $item ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div id="payment">
<?php
if ( $subscription->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 );
$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() ) { ?>
if ( $available_gateways = WC()->payment_gateways->get_available_payment_gateways() ) : ?>
<ul class="payment_methods methods">
<?php
if ( sizeof( $available_gateways ) ) {
if ( count( $available_gateways ) ) {
current( $available_gateways )->set_current();
}
foreach ( $available_gateways as $gateway ) {
foreach ( $available_gateways as $gateway ) :
$supports_payment_method_changes = WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $gateway, $subscription );
?>
<li class="wc_payment_method payment_method_<?php echo esc_attr( $gateway->id ); ?>">
<input id="payment_method_<?php echo esc_attr( $gateway->id ); ?>" type="radio" class="input-radio <?php echo $supports_payment_method_changes ? 'supports-payment-method-changes' : ''; ?>" name="payment_method" value="<?php echo esc_attr( $gateway->id ); ?>" <?php checked( $gateway->chosen, true ); ?> data-order_button_text="<?php echo esc_attr( apply_filters( 'wcs_gateway_change_payment_button_text', $pay_order_button_text, $gateway ) ); ?>" />
<label for="payment_method_<?php echo esc_attr( $gateway->id ); ?>"><?php echo esc_html( $gateway->get_title() ); ?> <?php echo wp_kses_post( $gateway->get_icon() ); ?></label>
<input id="payment_method_<?php echo esc_attr( $gateway->id ); ?>" type="radio" class="input-radio <?php echo $supports_payment_method_changes ? 'supports-payment-method-changes' : ''; ?>" name="payment_method" value="<?php echo esc_attr( $gateway->id ); ?>" <?php checked( $gateway->chosen, true ); ?> data-order_button_text="<?php echo esc_attr( apply_filters( 'wcs_gateway_change_payment_button_text', $pay_order_button_text, $gateway ) ); ?>"/>
<label for="payment_method_<?php echo esc_attr( $gateway->id ); ?>"><?php echo esc_html( $gateway->get_title() ); ?><?php echo wp_kses_post( $gateway->get_icon() ); ?></label>
<?php
if ( $gateway->has_fields() || $gateway->get_description() ) {
echo '<div class="payment_box payment_method_' . esc_attr( $gateway->id ) . '" style="display:none;">';
@@ -84,14 +74,13 @@ if ( ! defined( 'ABSPATH' ) ) {
}
?>
</li>
<?php
} ?>
<?php endforeach; ?>
</ul>
<?php } else { ?>
<?php else : ?>
<div class="woocommerce-error">
<p> <?php echo esc_html( apply_filters( 'woocommerce_no_available_payment_methods_message', __( 'Sorry, it seems no payment gateways support changing the recurring payment method. Please contact us if you require assistance or to make alternate arrangements.', 'woocommerce-subscriptions' ) ) ); ?></p>
</div>
<?php } ?>
<?php endif; ?>
<?php if ( $available_gateways ) : ?>
<?php if ( count( $customer_subscription_ids ) > 1 && WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_admin' ) ) : ?>
@@ -111,7 +100,7 @@ if ( ! defined( 'ABSPATH' ) ) {
);
?>
</span>
<?php endif ; ?>
<?php endif; ?>
<div class="form-row">
<?php wp_nonce_field( 'wcs_change_payment_method', '_wcsnonce', true, true ); ?>
<?php echo wp_kses( apply_filters( 'woocommerce_change_payment_button_html', '<input type="submit" class="button alt" id="place_order" value="' . esc_attr( $pay_order_button_text ) . '" data-value="' . esc_attr( $pay_order_button_text ) . '" />' ), array( 'input' => array( 'type' => array(), 'class' => array(), 'id' => array(), 'value' => array(), 'data-value' => array() ) ) ); ?>

View File

@@ -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;
?>
<tr class="recurring-totals">
<th colspan="2"><?php esc_html_e( 'Recurring Totals', 'woocommerce-subscriptions' ); ?></th>
<th colspan="2"><?php esc_html_e( 'Recurring totals', 'woocommerce-subscriptions' ); ?></th>
</tr>
<?php foreach ( $recurring_carts as $recurring_cart_key => $recurring_cart ) : ?>
@@ -70,7 +70,7 @@ $display_th = true;
<?php endforeach; ?>
<?php endforeach; ?>
<?php if ( WC()->cart->tax_display_cart === 'excl' ) : ?>
<?php if ( wc_tax_enabled() && WC()->cart->tax_display_cart === 'excl' ) : ?>
<?php if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) : ?>
<?php foreach ( WC()->cart->get_taxes() as $tax_id => $tax_total ) : ?>
@@ -120,8 +120,8 @@ $display_th = true;
<?php endif; ?>
<tr class="order-total recurring-total">
<?php if ( $display_th ) : $display_th = false; ?>
<th rowspan="<?php echo esc_attr( $carts_with_multiple_payments ); ?>"><?php esc_html_e( 'Recurring Total', 'woocommerce-subscriptions' ); ?></th>
<td data-title="<?php esc_attr_e( 'Recurring Total', 'woocommerce-subscriptions' ); ?>"><?php wcs_cart_totals_order_total_html( $recurring_cart ); ?></td>
<th rowspan="<?php echo esc_attr( $carts_with_multiple_payments ); ?>"><?php esc_html_e( 'Recurring total', 'woocommerce-subscriptions' ); ?></th>
<td data-title="<?php esc_attr_e( 'Recurring total', 'woocommerce-subscriptions' ); ?>"><?php wcs_cart_totals_order_total_html( $recurring_cart ); ?></td>
<?php else : ?>
<td><?php wcs_cart_totals_order_total_html( $recurring_cart ); ?></td>
<?php endif; ?>

View File

@@ -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 ); ?>
<p><?php
// translators: $1: customer's billing first name and last name
printf( esc_html_x( 'You have received a subscription renewal order from %1$s. Their order is as follows:', 'Used in admin email: new renewal order', 'woocommerce-subscriptions' ), esc_html( $order->get_formatted_billing_full_name() ) );
?>
</p>
<?php
<?php /* translators: $1: customer's billing first name and last name */ ?>
<p><?php printf( esc_html_x( 'You have received a subscription renewal order from %1$s. Their order is as follows:', 'Used in admin email: new renewal order', 'woocommerce-subscriptions' ), esc_html( $order->get_formatted_billing_full_name() ) );?></p>
<?php
/**
* @hooked WC_Subscriptions_Email::order_details() Shows the order details table.
* @since 2.1.0
@@ -28,5 +26,11 @@ do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text,
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 );
?>

View File

@@ -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
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
do_action( 'woocommerce_email_header', $email_heading, $email );
<p>
<?php
$count = count( $subscriptions );
// translators: $1: customer's first name and last name, $2: how many subscriptions customer switched
echo esc_html( sprintf( _nx( 'Customer %1$s has switched their subscription. The details of their new subscription are as follows:', 'Customer %1$s has switched %2$d of their subscriptions. The details of their new subscriptions are as follows:', $count, 'Used in switch notification admin email', 'woocommerce-subscriptions' ), $order->get_formatted_billing_full_name(), $count ) );
?>
</p>
$switched_count = count( $subscriptions );
/* translators: $1: customer's first name and last name, $2: how many subscriptions customer switched */ ?>
<p><?php echo esc_html( sprintf( _nx( 'Customer %1$s has switched their subscription. The details of their new subscription are as follows:', 'Customer %1$s has switched %2$d of their subscriptions. The details of their new subscriptions are as follows:', $switched_count, 'Used in switch notification admin email', 'woocommerce-subscriptions' ), $order->get_formatted_billing_full_name(), $switched_count ) );?></p>
<h2><?php esc_html_e( 'Switch Order Details', 'woocommerce-subscriptions' ); ?></h2>
<?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php
do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
?>
<h2><?php esc_html_e( 'New Subscription Details', 'woocommerce-subscriptions' ); ?></h2>
<h2><?php esc_html_e( 'New subscription details', 'woocommerce-subscriptions' ); ?></h2>
<?php
<?php foreach ( $subscriptions as $subscription ) : ?>
<?php do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); ?>
<?php endforeach; ?>
foreach ( $subscriptions as $subscription ) {
do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email );
}
<?php do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_footer', $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 );

View File

@@ -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 ); ?>
<p>
<?php
// 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'
echo esc_html( sprintf( _x( 'The automatic recurring payment for order #%d from %s has failed. The payment will be retried %3$s.', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), $order->get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) );
?>
</p>
<?php /* 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' */ ?>
<p><?php echo esc_html( sprintf( _x( 'The automatic recurring payment for order #%d from %s has failed. The payment will be retried %3$s.', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), $order->get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) ); ?></p>
<p><?php esc_html_e( 'The renewal order is as follows:', 'woocommerce-subscriptions' ); ?></p>
<?php
/**
* @hooked WC_Emails::order_details() Shows the order details table.
* @since 2.5.0
*/
* @hooked WC_Emails::order_details() Shows the order details table.
* @since 2.5.0
*/
do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email );
/**
* @hooked WC_Emails::order_meta() Shows order meta data.
*/
* @hooked WC_Emails::order_meta() Shows order meta data.
*/
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
/**
* @hooked WC_Emails::customer_details() Shows customer details
* @hooked WC_Emails::email_address() Shows email address
*/
* @hooked WC_Emails::customer_details() Shows customer details
* @hooked WC_Emails::email_address() Shows email address
*/
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
/**
* @hooked WC_Emails::email_footer() Output the email footer
*/
* Show user-defined additional content - this is set in each email's settings.
*/
if ( $additional_content ) {
echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) );
}
/**
* @hooked WC_Emails::email_footer() Output the email footer
*/
do_action( 'woocommerce_email_footer', $email );

View File

@@ -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
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p>
<?php
// translators: $1: customer's billing first name and last name
printf( esc_html__( 'A subscription belonging to %1$s has been cancelled. Their subscription\'s details are as follows:', 'woocommerce-subscriptions' ), esc_html( $subscription->get_formatted_billing_full_name() ) );
?>
</p>
<?php /* translators: $1: customer's billing first name and last name */ ?>
<p><?php printf( esc_html__( 'A subscription belonging to %1$s has been cancelled. Their subscription\'s details are as follows:', 'woocommerce-subscriptions' ), esc_html( $subscription->get_formatted_billing_full_name() ) );?></p>
<table class="td" cellspacing="0" cellpadding="6" style="width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;" border="1">
<thead>
@@ -54,9 +49,17 @@ if ( ! defined( 'ABSPATH' ) ) {
</tbody>
</table>
<br/>
<?php
<?php do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_customer_details', $subscription, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_footer', $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 );

View File

@@ -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
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p>
<?php
// translators: placeholder is the name of the site
printf( esc_html__( 'Hi there. Your subscription renewal order with %s has been completed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ), esc_html( get_option( 'blogname' ) ) );
?>
</p>
<?php /* translators: %s: Customer first name */ ?>
<p><?php printf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ); ?></p>
<p><?php esc_html_e( 'We have finished processing your subscription renewal order.', 'woocommerce-subscriptions' ); ?></p>
<?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php
do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_footer', $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 );

View File

@@ -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 ); ?>
<?php /* translators: %s: Customer first name */ ?>
<p><?php printf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ); ?></p>
<p><?php esc_html_e( 'You have successfully changed your subscription items. Your new order and subscription details are shown below for your reference:', 'woocommerce-subscriptions' ); ?></p>
<?php
do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email );
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<h2><?php echo esc_html__( 'New subscription details', 'woocommerce-subscriptions' ); ?></h2>
<p>
<?php
// translators: placeholder is the name of the site
printf( esc_html__( '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' ), esc_html( get_option( 'blogname' ) ) );
?>
</p>
<?php
foreach ( $subscriptions as $subscription ) {
do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email );
}
<?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
<?php do_action( 'woocommerce_email_order_meta', $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 ) ) );
}
<h2><?php echo esc_html__( 'New Subscription Details', 'woocommerce-subscriptions' ); ?></h2>
<?php foreach ( $subscriptions as $subscription ) : ?>
<?php do_action( 'woocommerce_subscriptions_email_order_details', $subscription, $sent_to_admin, $plain_text, $email ); ?>
<?php endforeach; ?>
<?php do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_email_footer', $email ); ?>
do_action( 'woocommerce_email_footer', $email );

View File

@@ -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
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p>
<?php
// 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 wp_kses( sprintf( _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' ) ), wcs_get_human_time_diff( $retry->get_time() ) ), array( 'a' => array( 'href' => true ) ) );
?>
</p>
<p>
<?php
// translators: %1$s %2$s: link markup to checkout payment url, note: no full stop due to url at the end
echo wp_kses( sprintf( _x( 'To reactivate the subscription now, you can also login and pay for the renewal from your account page: %1$sPay Now &raquo;%2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), '<a href="' . esc_url( $order->get_checkout_payment_url() ) . '">', '</a>' ), array( 'a' => array( 'href' => true ) ) );
?>
</p>
<?php /* translators: %s: Customer first name */ ?>
<p><?php printf( esc_html__( 'Hi %s,', 'woocommerce-subscriptions' ), esc_html( $order->get_billing_first_name() ) ); ?></p>
<?php /* translators: %s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours' */ ?>
<p><?php printf( 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() ) ) ); ?></p>
<?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php /* translators: %1$s %2$s: link markup to checkout payment url, note: no full stop due to url at the end */ ?>
<p><?php echo wp_kses( sprintf( _x( 'To reactivate the subscription now, you can also log in and pay for the renewal from your account page: %1$sPay Now &raquo;%2$s', 'In customer renewal invoice email', 'woocommerce-subscriptions' ), '<a href="' . esc_url( $order->get_checkout_payment_url() ) . '">', '</a>' ), array( 'a' => array( 'href' => true ) ) );?></p>
<?php do_action( 'woocommerce_email_footer', $email );
<?php
do_action( 'woocommerce_subscriptions_email_order_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 );

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