Compare commits
No commits in common. "main" and "v6.8.0" have entirely different histories.
|
|
@ -163,7 +163,7 @@ mkdir( $plugins_dir );
|
|||
|
||||
$plugin_dir = $plugins_dir . '/woocommerce-subscriptions';
|
||||
|
||||
$zip_file = $archives_dir . '/woocommerce-subscriptions.' . $version . '.zip';
|
||||
$zip_file = $archives_dir . '/woocommerce-subscriptions-' . $version . '.zip';
|
||||
|
||||
/**
|
||||
* Download ZIP.
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
name: Deploy to Pronamic WordPress directory
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to deploy'
|
||||
type: string
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: Pronamic WordPress directory
|
||||
url: https://wp.pronamic.directory/plugins/woocommerce-subscriptions/
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- name: Deploy
|
||||
uses: pronamic/action-wp-pronamic-directory-plugin-deploy@main
|
||||
with:
|
||||
username: ${{ vars.WP_PRONAMIC_DIRECTORY_USERNAME }}
|
||||
password: ${{ secrets.WP_PRONAMIC_DIRECTORY_PASSWORD }}
|
||||
slug: woocommerce-subscriptions
|
||||
tag: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"plugins": [
|
||||
"https://downloads.wordpress.org/plugin/woocommerce.zip",
|
||||
"https://downloads.wordpress.org/plugin/email-log.zip",
|
||||
"https://github.com/woocommerce/woocommerce-gateway-dummy/releases/download/1.0.8/woocommerce-gateway-dummy.zip",
|
||||
".",
|
||||
"./tests/e2e/test-configuration-plugin"
|
||||
],
|
||||
"themes": [
|
||||
"https://downloads.wordpress.org/theme/storefront.zip"
|
||||
],
|
||||
"env": {
|
||||
"tests": {
|
||||
"mappings": {
|
||||
"wp-cli.yml": "./tests/e2e/bin/wp-cli.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
.wc-shortcode-components-validation-error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#shortcode-validate-error-invalid-gifting-recipient {
|
||||
font-size: 0.75em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
|
||||
#shortcode-validate-error-invalid-gifting-recipient svg {
|
||||
fill: var(--wc-red, #cc1818);
|
||||
|
||||
}
|
||||
|
||||
#shortcode-validate-error-invalid-gifting-recipient span {
|
||||
color: var(--wc-red, #cc1818);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.woocommerce .woocommerce_subscriptions_gifting_recipient_email .input-text.recipient_email.wcsg-email-error {
|
||||
border-color: var(--wc-red, #cc1818);
|
||||
color: var(--wc-red, #cc1818);
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container label {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-cart-form__cart-item .wcsg_add_recipient_fields_container label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields .woocommerce_subscriptions_gifting_recipient_email {
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .recipient_email:focus {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
#woocommerce_subscriptions_gifting_field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#woocommerce_subscriptions_gifting_field div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<svg width="141" height="115" viewBox="0 0 141 115" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M104.686 23.5272H42.6647C38.3831 23.5272 34.9121 26.9982 34.9121 31.2798V86.8401C34.9121 91.1218 38.3831 94.5927 42.6647 94.5927H104.686C108.967 94.5927 112.438 91.1218 112.438 86.8401V31.2798C112.438 26.9982 108.967 23.5272 104.686 23.5272Z" fill="#D1C1FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M104.686 23.5272H42.6647C38.3827 23.5272 34.9121 26.9978 34.9121 31.2798V42.9087H112.438V31.2798C112.438 26.9978 108.968 23.5272 104.686 23.5272Z" fill="#A77EFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M112.434 61.4844V86.8405C112.434 91.1226 108.963 94.5932 104.681 94.5932H79.3252C97.611 94.5932 112.434 79.7702 112.434 61.4844Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M73.6715 34.8336C76.3476 34.8336 78.5169 32.6642 78.5169 29.9882C78.5169 27.3122 76.3476 25.1428 73.6715 25.1428C70.9955 25.1428 68.8262 27.3122 68.8262 29.9882C68.8262 32.6642 70.9955 34.8336 73.6715 34.8336Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M54.2917 34.8336C56.9677 34.8336 59.137 32.6642 59.137 29.9882C59.137 27.3122 56.9677 25.1428 54.2917 25.1428C51.6156 25.1428 49.4463 27.3122 49.4463 29.9882C49.4463 32.6642 51.6156 34.8336 54.2917 34.8336Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M76.9023 29.0193V19.6516C76.9023 18.2244 75.7454 17.0674 74.3181 17.0674H73.026C71.5988 17.0674 70.4418 18.2244 70.4418 19.6516V29.0193C70.4418 30.4465 71.5988 31.6035 73.026 31.6035H74.3181C75.7454 31.6035 76.9023 30.4465 76.9023 29.0193Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M57.5215 29.0193V19.6516C57.5215 18.2244 56.3645 17.0674 54.9373 17.0674H53.6452C52.218 17.0674 51.061 18.2244 51.061 19.6516V29.0193C51.061 30.4465 52.218 31.6035 53.6452 31.6035H54.9373C56.3645 31.6035 57.5215 30.4465 57.5215 29.0193Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M61.6236 82.6491C59.458 82.6491 57.6956 80.8866 57.6956 78.7211V63.6707C57.6956 63.5285 57.5457 63.5234 57.5431 63.5234C57.5121 63.5234 57.4863 63.5337 57.4553 63.557L56.4733 64.3503C55.8867 64.8258 55.1864 65.0765 54.4499 65.0765C52.659 65.0765 51.2041 63.6242 51.2041 61.8385C51.2041 60.8126 51.6977 59.8383 52.5246 59.231L58.7164 54.6828C59.458 54.1376 60.3367 53.8507 61.2566 53.8507C63.6238 53.8507 65.549 55.7759 65.549 58.1431V78.7236C65.549 80.8892 63.7866 82.6516 61.621 82.6516L61.6236 82.6491Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-miterlimit="10"/>
|
||||
<path d="M72.0717 82.6493C70.0587 82.6493 68.4229 81.0109 68.4229 79.0004C68.4229 77.7109 69.1154 76.504 70.2266 75.8502L80.4213 69.8807C82.3078 68.7979 83.7911 67.8547 84.8377 67.0743C85.8274 66.3378 86.5278 65.6581 86.9231 65.056C87.2849 64.5056 87.4581 63.9371 87.4581 63.3194C87.4581 62.6656 87.2875 62.1049 86.9387 61.6087C86.5872 61.11 86.0393 60.7094 85.308 60.4174C84.5198 60.1021 83.4887 59.9419 82.2432 59.9419C80.7727 59.9419 79.5659 60.1512 78.6589 60.5621C77.7906 60.9549 77.1394 61.4795 76.7233 62.1204C76.5786 62.3426 76.452 62.5752 76.346 62.8129C75.7155 64.2368 74.3303 65.1568 72.8212 65.1568C71.312 65.1568 70.1388 64.4229 69.4488 63.1902C68.7691 61.9757 68.795 60.5466 69.5186 59.3656C69.7356 59.0142 69.976 58.6705 70.237 58.3423C71.5265 56.7297 73.2527 55.4686 75.3666 54.5978C77.4469 53.7398 79.8347 53.3057 82.4576 53.3057C85.0806 53.3057 87.1945 53.6985 89.0965 54.4711C91.0398 55.2619 92.5825 56.3938 93.686 57.8358C94.8127 59.3114 95.3838 61.0453 95.3838 62.9912C95.3838 64.4952 94.9884 65.9114 94.2106 67.2009C93.4586 68.4465 92.2621 69.6973 90.6573 70.9248C89.1016 72.1135 87.0213 73.4056 84.4707 74.7649L82.8995 75.6306C82.8659 75.6487 82.7988 75.6848 82.8272 75.796C82.8556 75.9071 82.9306 75.9071 82.9693 75.9071H92.7712C94.6292 75.9071 96.141 77.4188 96.141 79.2769C96.141 81.1349 94.6292 82.6467 92.7712 82.6467H72.0717V82.6493Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-miterlimit="10"/>
|
||||
<path d="M33.3676 95.9851L2.50293 46.8893L21.0797 35.2107L51.9444 84.3065C52.6064 85.3596 52.998 86.5619 53.0802 87.8047L55.4775 106.262C55.754 108.384 53.4144 109.855 51.6221 108.686L36.0278 98.5249C34.9456 97.9108 34.0297 97.0381 33.3676 95.9851Z" fill="#B999FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.0813 35.211L27.7547 45.8263C29.1375 48.0258 26.0982 52.4217 20.9689 55.6463C15.8396 58.8709 10.5607 59.7043 9.17795 57.5049L2.5045 46.8896C1.12176 44.6901 4.16103 40.2942 9.29035 37.0695C14.4197 33.8449 19.6985 33.0115 21.0813 35.211Z" fill="#E1D7FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.0786 35.2102L18.4248 36.8785L51.7921 89.9551L51.9433 84.306L21.0786 35.2102Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M27.7526 45.8261L31.0893 51.1337C32.472 53.3332 29.4328 57.7291 24.3034 60.9538C19.1741 64.1784 13.8952 65.0118 12.5125 62.8123L9.17578 57.5046C10.5585 59.7041 15.8374 58.8707 20.9667 55.6461C26.096 52.4215 29.1353 48.0256 27.7526 45.8261Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.2954 45.031C19.4253 41.8061 22.4634 37.4095 21.0813 35.211C19.6992 33.0125 14.4202 33.8446 9.29035 37.0695C4.16051 40.2945 1.12238 44.6911 2.5045 46.8896C3.88662 49.0881 9.1656 48.256 14.2954 45.031Z" fill="#D1C1FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M51.6229 108.686C53.4153 109.855 55.7549 108.384 55.4784 106.262L53.0811 87.8044C52.9967 86.5629 51.7731 84.0323 51.1111 82.9793L50.0662 87.3389C49.5551 88.9902 48.5434 91.0184 46.833 90.7666L45.6279 90.5883C43.9182 90.333 42.2792 91.3634 41.766 93.0161L41.4043 94.1794C40.8933 95.8307 38.6256 95.8611 36.9139 95.6073L32.5322 94.6592C33.1942 95.7122 34.9422 97.9131 36.0265 98.5259L51.6208 108.687L51.6229 108.686Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M54.3923 97.999L55.4678 106.269C55.7443 108.39 53.4046 109.861 51.6123 108.692L44.6782 104.174C46.3741 104.186 48.3947 103.588 50.3012 102.39C52.2077 101.191 53.6832 99.5615 54.3923 97.999Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M136.455 23.6709C134.348 23.6709 132.471 22.3832 131.667 20.4353C131.658 20.4135 131.649 20.3936 131.64 20.3718C130.827 18.4203 131.237 16.1768 132.732 14.6823L135.238 12.1758L128.825 5.76269L126.319 8.26918C124.824 9.76365 122.581 10.1717 120.629 9.3592C120.607 9.35013 120.587 9.34106 120.566 9.33199C118.618 8.52854 117.33 6.65139 117.33 4.54391V1H108.262V4.54572C108.262 6.6532 106.974 8.53035 105.026 9.33381C105.004 9.34288 104.984 9.35194 104.963 9.36101C103.011 10.1735 100.768 9.76365 99.2731 8.271L96.7666 5.76451L90.3535 12.1776L92.86 14.6841C94.3545 16.1786 94.7625 18.4239 93.9518 20.3736C93.9428 20.3954 93.9337 20.4153 93.9246 20.4371C93.1212 22.385 91.244 23.6727 89.1365 23.6727H85.5908V32.741H89.1365C91.244 32.741 93.1212 34.0287 93.9246 35.9766C93.9337 35.9984 93.9428 36.0183 93.9518 36.0401C94.7643 37.9916 94.3545 40.2351 92.86 41.7296L90.3535 44.236L96.7666 50.6492L99.2731 48.1427C100.768 46.6482 103.011 46.2401 104.963 47.0527C104.984 47.0617 105.004 47.0708 105.026 47.0799C106.974 47.8833 108.262 49.7605 108.262 51.868V55.4137H117.33V51.868C117.33 49.7605 118.618 47.8833 120.566 47.0799C120.587 47.0708 120.607 47.0617 120.629 47.0527C122.581 46.2401 124.824 46.65 126.319 48.1427L128.825 50.6492L135.238 44.236L132.732 41.7296C131.237 40.2351 130.829 37.9898 131.64 36.0401C131.649 36.0183 131.658 35.9984 131.667 35.9766C132.471 34.0287 134.348 32.741 136.455 32.741H140.001V23.6727H136.455V23.6709ZM112.796 37.2734C107.788 37.2734 103.728 33.2126 103.728 28.205C103.728 23.1975 107.788 19.1367 112.796 19.1367C117.803 19.1367 121.864 23.1975 121.864 28.205C121.864 33.2126 117.803 37.2734 112.796 37.2734Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g style="mix-blend-mode:multiply">
|
||||
<path d="M121.832 28.2051C121.832 33.2127 117.771 37.2735 112.764 37.2735C110.259 37.2735 107.992 36.2578 106.351 34.6183L93.5264 47.4427L96.7329 50.6493L99.2394 48.1428C100.734 46.6483 102.977 46.2403 104.929 47.0528C104.951 47.0619 104.971 47.0709 104.992 47.08C106.94 47.8834 108.228 49.7606 108.228 51.8681V55.4138H117.296V51.8681C117.296 49.7606 118.584 47.8834 120.532 47.08C120.554 47.0709 120.574 47.0619 120.595 47.0528C122.547 46.2403 124.79 46.6502 126.285 48.1428L128.791 50.6493L135.204 44.2362L132.698 41.7297C131.204 40.2352 130.795 37.9899 131.606 36.0402C131.615 36.0184 131.624 35.9985 131.633 35.9767C132.437 34.0288 134.314 32.7411 136.421 32.7411H139.967V23.6728H136.421C134.314 23.6728 132.437 22.3851 131.633 20.4372C131.624 20.4154 131.615 20.3955 131.606 20.3737C130.794 18.4222 131.204 16.1787 132.698 14.6842L135.204 12.1778L131.998 8.97119L119.173 21.7956C120.815 23.437 121.829 25.7041 121.829 28.2088L121.832 28.2051Z" fill="#B999FF"/>
|
||||
</g>
|
||||
<path d="M112.798 14.6027C105.286 14.6027 99.1953 20.693 99.1953 28.2052C99.1953 35.7174 105.286 41.8077 112.798 41.8077C120.31 41.8077 126.4 35.7174 126.4 28.2052C126.4 20.693 120.31 14.6027 112.798 14.6027ZM112.798 37.2735C107.79 37.2735 103.729 33.2127 103.729 28.2052C103.729 23.1976 107.79 19.1368 112.798 19.1368C117.805 19.1368 121.866 23.1976 121.866 28.2052C121.866 33.2127 117.805 37.2735 112.798 37.2735Z" fill="#F2EDFF"/>
|
||||
<path d="M112.796 37.2735C117.804 37.2735 121.864 33.2135 121.864 28.2052C121.864 23.1969 117.804 19.1368 112.796 19.1368C107.788 19.1368 103.728 23.1969 103.728 28.2052C103.728 33.2135 107.788 37.2735 112.796 37.2735Z" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M112.798 14.6027C120.31 14.6027 126.4 20.693 126.4 28.2052C126.4 35.7174 120.31 41.8077 112.798 41.8077C105.286 41.8077 99.1953 35.7174 99.1953 28.2052" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M121.893 18.0848C124.105 20.5043 125.458 23.7253 125.458 27.2638C125.458 34.776 119.368 40.8663 111.856 40.8663C108.317 40.8663 105.096 39.5151 102.677 37.3006C105.163 40.0193 108.74 41.7242 112.715 41.7242C120.228 41.7242 126.318 35.6339 126.318 28.1217C126.318 24.1461 124.613 20.5714 121.894 18.083L121.893 18.0848Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,358 +0,0 @@
|
|||
jQuery( document ).ready( function ( $ ) {
|
||||
setShippingAddressNoticeVisibility( true );
|
||||
|
||||
$( document ).on(
|
||||
'change',
|
||||
'.woocommerce_subscription_gifting_checkbox[type="checkbox"]',
|
||||
function ( e, eventContext ) {
|
||||
if ( $( this ).is( ':checked' ) ) {
|
||||
$( this )
|
||||
.closest( '.wcsg_add_recipient_fields_container' )
|
||||
.find( '.wcsg_add_recipient_fields' )
|
||||
.slideDown( 250, function () {
|
||||
if (
|
||||
typeof eventContext === 'undefined' ||
|
||||
eventContext !== 'pageload'
|
||||
) {
|
||||
$( this )
|
||||
.find( '.recipient_email' )
|
||||
.trigger( 'focus' );
|
||||
}
|
||||
} )
|
||||
.removeClass( 'hidden' );
|
||||
|
||||
const shipToDifferentAddressCheckbox = $( document ).find(
|
||||
'#ship-to-different-address-checkbox'
|
||||
);
|
||||
if ( ! shipToDifferentAddressCheckbox.is( ':checked' ) ) {
|
||||
shipToDifferentAddressCheckbox.click();
|
||||
}
|
||||
setShippingAddressNoticeVisibility( false );
|
||||
} else {
|
||||
$( this )
|
||||
.closest( '.wcsg_add_recipient_fields_container' )
|
||||
.find( '.wcsg_add_recipient_fields' )
|
||||
.slideUp( 250 )
|
||||
.addClass( 'hidden' );
|
||||
|
||||
const recipientEmailElement = $( this )
|
||||
.closest( '.wcsg_add_recipient_fields_container' )
|
||||
.find( '.recipient_email' );
|
||||
recipientEmailElement.val( '' );
|
||||
hideValidationErrorForEmailField( recipientEmailElement );
|
||||
setShippingAddressNoticeVisibility( true );
|
||||
|
||||
if ( $( 'form.checkout' ).length !== 0 ) {
|
||||
// Trigger the event to update the checkout after the recipient field has been cleared.
|
||||
updateCheckout();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles showing and hiding the gifting checkbox on variable subscription products.
|
||||
*/
|
||||
function hideGiftingCheckbox() {
|
||||
$( '.woocommerce_subscription_gifting_checkbox[type="checkbox"]' )
|
||||
.prop( 'checked', false )
|
||||
.trigger( 'change' );
|
||||
$( '.wcsg_add_recipient_fields_container' ).hide();
|
||||
}
|
||||
|
||||
// When a variation is found, show the gifting checkbox if it's enabled for the variation, otherwise hide it.
|
||||
$( document ).on( 'found_variation', function ( event, variationData ) {
|
||||
if ( variationData.gifting ) {
|
||||
$( '.wcsg_add_recipient_fields_container' ).show();
|
||||
return;
|
||||
}
|
||||
|
||||
hideGiftingCheckbox();
|
||||
} );
|
||||
|
||||
// When the data is reset, hide the gifting checkbox.
|
||||
$( document ).on( 'reset_data', hideGiftingCheckbox );
|
||||
|
||||
/**
|
||||
* Handles recipient e-mail inputs on the cart page.
|
||||
*/
|
||||
const cart = {
|
||||
init: function () {
|
||||
$( document ).on(
|
||||
'submit',
|
||||
'div.woocommerce > form',
|
||||
this.set_update_cart_as_clicked
|
||||
);
|
||||
|
||||
// We need to make sure our callback is hooked before WC's.
|
||||
const handlers = $._data( document, 'events' );
|
||||
if ( typeof handlers.submit !== 'undefined' ) {
|
||||
handlers.submit.unshift( handlers.submit.pop() );
|
||||
}
|
||||
},
|
||||
|
||||
set_update_cart_as_clicked: function ( evt ) {
|
||||
const $form = $( evt.target );
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const $submit = $( document.activeElement );
|
||||
|
||||
// If we're not on the cart page exit.
|
||||
if ( $form.find( 'table.shop_table.cart' ).length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the recipient email element is the active element, the clicked button is the update cart button.
|
||||
if ( $submit.is( 'input.recipient_email' ) ) {
|
||||
$( ':input[type="submit"][name="update_cart"]' ).attr(
|
||||
'clicked',
|
||||
'true'
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
cart.init();
|
||||
|
||||
/**
|
||||
* Email validation function
|
||||
*
|
||||
* @param {string} email - The email to validate
|
||||
* @return {boolean} - Whether the email is valid
|
||||
*/
|
||||
function isValidEmail( email ) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test( email );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all recipient emails and return overall validation status
|
||||
*
|
||||
* @param {boolean} showErrors - Whether to show validation errors
|
||||
* @return {boolean} - Whether all emails are valid
|
||||
*/
|
||||
function validateAllRecipientEmails( showErrors = true ) {
|
||||
const $allEmailFields = $( '.recipient_email' );
|
||||
let allValid = true;
|
||||
|
||||
// Check each email field
|
||||
$allEmailFields.each( function () {
|
||||
const $emailField = $( this );
|
||||
const $giftingCheckbox = $( this )
|
||||
.closest( '.wcsg_add_recipient_fields_container' )
|
||||
.find( '.woocommerce_subscription_gifting_checkbox' );
|
||||
const email = $emailField.val().trim();
|
||||
|
||||
if ( ! $giftingCheckbox.is( ':checked' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email format is valid
|
||||
if ( ! isValidEmail( email ) ) {
|
||||
if ( showErrors ) {
|
||||
showValidationErrorForEmailField( $emailField );
|
||||
}
|
||||
allValid = false;
|
||||
}
|
||||
} );
|
||||
|
||||
// Control update cart button state
|
||||
const $updateCartButton = $(
|
||||
'.woocommerce-cart-form :input[type="submit"][name="update_cart"]'
|
||||
);
|
||||
|
||||
if ( $updateCartButton.length && ! allValid ) {
|
||||
$updateCartButton.prop( 'disabled', true );
|
||||
}
|
||||
|
||||
return allValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate recipient email and show error if invalid
|
||||
*
|
||||
* @param {jQuery} $emailField - The email input field jQuery object
|
||||
* @return {boolean} - Whether the email is valid
|
||||
*/
|
||||
function validateRecipientEmail( $emailField ) {
|
||||
const email = $emailField.val().trim();
|
||||
|
||||
hideValidationErrorForEmailField( $emailField );
|
||||
|
||||
// Check if email format is valid
|
||||
if ( ! isValidEmail( email ) ) {
|
||||
showValidationErrorForEmailField( $emailField );
|
||||
|
||||
// Only validate all emails and update button state on cart and checkout shortcode pages.
|
||||
if ( isShortcodeCartOrCheckoutPage() ) {
|
||||
validateAllRecipientEmails();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only validate all emails and update button state on cart and checkout shortcode pages.
|
||||
if ( isShortcodeCartOrCheckoutPage() ) {
|
||||
validateAllRecipientEmails();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle add to cart button click with email validation
|
||||
*/
|
||||
$( document ).on(
|
||||
'click',
|
||||
'.single_add_to_cart_button, .add_to_cart_button',
|
||||
function ( e ) {
|
||||
// Check if we're on a product page with gifting enabled
|
||||
const $giftingContainer = $(
|
||||
'.wcsg_add_recipient_fields_container'
|
||||
);
|
||||
if ( $giftingContainer.length === 0 ) {
|
||||
return; // No gifting on this page
|
||||
}
|
||||
|
||||
// Check if gifting checkbox is checked
|
||||
const $giftingCheckbox = $giftingContainer.find(
|
||||
'.woocommerce_subscription_gifting_checkbox'
|
||||
);
|
||||
if ( ! $giftingCheckbox.is( ':checked' ) ) {
|
||||
return; // Gifting not enabled for this item
|
||||
}
|
||||
|
||||
// Get the recipient email field
|
||||
const $emailField = $giftingContainer.find( '.recipient_email' );
|
||||
if ( $emailField.length === 0 ) {
|
||||
return; // No email field found
|
||||
}
|
||||
|
||||
// Validate the email
|
||||
if ( ! validateRecipientEmail( $emailField ) ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Focus on the email field
|
||||
$emailField.focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Real-time email validation on input
|
||||
*/
|
||||
$( document ).on( 'blur', '.recipient_email', function () {
|
||||
const $emailField = $( this );
|
||||
validateRecipientEmail( $emailField );
|
||||
} );
|
||||
|
||||
/**
|
||||
* Clear error styling when user starts typing
|
||||
*/
|
||||
$( document ).on( 'input', '.recipient_email', function () {
|
||||
const $emailField = $( this );
|
||||
|
||||
hideValidationErrorForEmailField( $emailField );
|
||||
} );
|
||||
|
||||
/*******************************************
|
||||
* Update checkout on input changed events *
|
||||
*******************************************/
|
||||
let updateTimer;
|
||||
|
||||
$( document ).on( 'change', '.recipient_email', function () {
|
||||
if ( $( 'form.checkout' ).length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( validateAllRecipientEmails() ) {
|
||||
updateCheckout();
|
||||
}
|
||||
} );
|
||||
|
||||
$( document ).on( 'keyup', '.recipient_email', function ( e ) {
|
||||
const code = e.keyCode || e.which || 0;
|
||||
|
||||
if ( $( 'form.checkout' ).length === 0 || code === 9 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentRecipient = $( this ).val();
|
||||
const originalRecipient = $( this ).attr( 'data-recipient' );
|
||||
resetCheckoutUpdateTimer();
|
||||
|
||||
// If the recipient has changed since last load, mark the element as needing an update.
|
||||
if ( currentRecipient !== originalRecipient ) {
|
||||
$( this ).addClass( 'wcsg_needs_update' );
|
||||
// Only set timer if all emails are valid
|
||||
if ( validateAllRecipientEmails( false ) ) {
|
||||
updateTimer = setTimeout( updateCheckout, 1500 );
|
||||
}
|
||||
} else {
|
||||
$( this ).removeClass( 'wcsg_needs_update' );
|
||||
}
|
||||
} );
|
||||
|
||||
function updateCheckout() {
|
||||
resetCheckoutUpdateTimer();
|
||||
$( '.recipient_email' ).removeClass( 'wcsg_needs_update' );
|
||||
$( document.body ).trigger( 'update_checkout' );
|
||||
}
|
||||
|
||||
function resetCheckoutUpdateTimer() {
|
||||
clearTimeout( updateTimer );
|
||||
}
|
||||
|
||||
function setShippingAddressNoticeVisibility( hide = true ) {
|
||||
const notice = $( 'form.checkout' )
|
||||
.find( '.woocommerce-shipping-fields' )
|
||||
.find( '.woocommerce-info' );
|
||||
|
||||
if ( ! notice.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( hide ) {
|
||||
notice.css( { display: 'none' } );
|
||||
} else {
|
||||
notice.css( { display: '' } );
|
||||
}
|
||||
}
|
||||
|
||||
function isShortcodeCartOrCheckoutPage() {
|
||||
return (
|
||||
$( 'form.woocommerce-cart-form' ).length > 0 ||
|
||||
$( 'form.woocommerce-checkout' ).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function showValidationErrorForEmailField( $emailField ) {
|
||||
$emailField.addClass( 'wcsg-email-error' );
|
||||
$emailField
|
||||
.closest( '.wcsg_add_recipient_fields' )
|
||||
.find( '.wc-shortcode-components-validation-error' )
|
||||
.show();
|
||||
}
|
||||
|
||||
function hideValidationErrorForEmailField( $emailField ) {
|
||||
$emailField.removeClass( 'wcsg-email-error' );
|
||||
$emailField
|
||||
.closest( '.wcsg_add_recipient_fields' )
|
||||
.find( '.wc-shortcode-components-validation-error' )
|
||||
.hide();
|
||||
}
|
||||
|
||||
// Triggers
|
||||
$( '.woocommerce_subscription_gifting_checkbox[type="checkbox"]' ).trigger(
|
||||
'change',
|
||||
'pageload'
|
||||
);
|
||||
|
||||
// Validate all recipient emails on page load to set initial button state
|
||||
$( document ).ready( function () {
|
||||
setTimeout( function () {
|
||||
// Only run validation on cart and checkout shortcode pages
|
||||
if ( isShortcodeCartOrCheckoutPage() ) {
|
||||
validateAllRecipientEmails();
|
||||
}
|
||||
}, 1000 );
|
||||
} );
|
||||
} );
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
jQuery( document ).ready( function ( $ ) {
|
||||
// Remove WC's revoke handler to make sure that only our handler is called (to make sure only the correct permissions are revoked not all permissions matching the product/order ID)
|
||||
$( '.order_download_permissions' ).off( 'click', 'button.revoke_access' );
|
||||
|
||||
$( '.order_download_permissions' ).on(
|
||||
'click',
|
||||
'button.revoke_access',
|
||||
function () {
|
||||
if (
|
||||
window.confirm(
|
||||
woocommerce_admin_meta_boxes.i18n_permission_revoke
|
||||
)
|
||||
) {
|
||||
var el = $( this ).parent().parent();
|
||||
var permission_id = $( this )
|
||||
.siblings()
|
||||
.find( '.wcsg_download_permission_id' )
|
||||
.val();
|
||||
var post_id = $( '#post_ID' ).val();
|
||||
|
||||
if ( 0 < permission_id ) {
|
||||
$( el ).block( {
|
||||
message: null,
|
||||
overlayCSS: {
|
||||
background: '#fff',
|
||||
opacity: 0.6,
|
||||
},
|
||||
} );
|
||||
|
||||
var data = {
|
||||
action: 'wcsg_revoke_access_to_download',
|
||||
post_id: post_id,
|
||||
download_permission_id: permission_id,
|
||||
nonce: wcs_gifting.revoke_download_permission_nonce,
|
||||
};
|
||||
|
||||
$.ajax( {
|
||||
url: wcs_gifting.ajax_url,
|
||||
data: data,
|
||||
type: 'POST',
|
||||
success: function () {
|
||||
// Success
|
||||
$( el ).fadeOut( '300', function () {
|
||||
$( el ).remove();
|
||||
} );
|
||||
},
|
||||
} );
|
||||
} else {
|
||||
$( el ).fadeOut( '300', function () {
|
||||
$( el ).remove();
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [ $# -lt 3 ] && [ -z $WCPAY_DIR ]; then
|
||||
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [wc-version] [skip-database-creation]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_NAME=${1-wcpay_tests}
|
||||
DB_USER=${2-root}
|
||||
DB_PASS=${3-$MYSQL_ROOT_PASSWORD}
|
||||
DB_HOST=${4-$WORDPRESS_DB_HOST}
|
||||
WP_VERSION=${5-latest}
|
||||
WC_VERSION=${6-latest}
|
||||
SKIP_DB_CREATE=${7-false}
|
||||
|
||||
TMPDIR=${TMPDIR-/tmp}
|
||||
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
|
||||
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
|
||||
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
|
||||
|
||||
download() {
|
||||
if [ `which curl` ]; then
|
||||
curl -s "$1" > "$2";
|
||||
elif [ `which wget` ]; then
|
||||
wget -nv -O "$2" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
wp() {
|
||||
WORKING_DIR="$PWD"
|
||||
cd "$WP_CORE_DIR"
|
||||
|
||||
if [ ! -f $TMPDIR/wp-cli.phar ]; then
|
||||
download https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar "$TMPDIR/wp-cli.phar"
|
||||
fi
|
||||
php "$TMPDIR/wp-cli.phar" $@
|
||||
|
||||
cd "$WORKING_DIR"
|
||||
}
|
||||
|
||||
get_db_connection_flags() {
|
||||
# parse DB_HOST for port or socket references
|
||||
local DB_HOST_PARTS=(${DB_HOST//\:/ })
|
||||
local DB_HOSTNAME=${DB_HOST_PARTS[0]};
|
||||
local DB_SOCK_OR_PORT=${DB_HOST_PARTS[1]};
|
||||
local EXTRA_FLAGS=""
|
||||
|
||||
if ! [ -z $DB_HOSTNAME ] ; then
|
||||
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
|
||||
EXTRA_FLAGS=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
|
||||
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
|
||||
EXTRA_FLAGS=" --socket=$DB_SOCK_OR_PORT"
|
||||
elif ! [ -z $DB_HOSTNAME ] ; then
|
||||
EXTRA_FLAGS=" --host=$DB_HOSTNAME --protocol=tcp"
|
||||
fi
|
||||
fi
|
||||
echo "--user=$DB_USER --password=$DB_PASS $EXTRA_FLAGS";
|
||||
}
|
||||
|
||||
wait_db() {
|
||||
local MYSQLADMIN_FLAGS=$(get_db_connection_flags)
|
||||
local WAITS=0
|
||||
|
||||
set +e
|
||||
mysqladmin status $MYSQLADMIN_FLAGS > /dev/null
|
||||
while [[ $? -ne 0 ]]; do
|
||||
((WAITS++))
|
||||
if [ $WAITS -ge 6 ]; then
|
||||
echo "Maximum database wait time exceeded"
|
||||
exit 1
|
||||
fi;
|
||||
echo "Waiting until the database is available..."
|
||||
sleep 5s
|
||||
mysqladmin status $MYSQLADMIN_FLAGS > /dev/null
|
||||
done
|
||||
set -e
|
||||
}
|
||||
|
||||
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
|
||||
WP_BRANCH=${WP_VERSION%\-*}
|
||||
WP_TESTS_TAG="branches/$WP_BRANCH"
|
||||
|
||||
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
WP_TESTS_TAG="branches/$WP_VERSION"
|
||||
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
|
||||
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
|
||||
WP_TESTS_TAG="tags/${WP_VERSION%??}"
|
||||
else
|
||||
WP_TESTS_TAG="tags/$WP_VERSION"
|
||||
fi
|
||||
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
|
||||
WP_TESTS_TAG="trunk"
|
||||
else
|
||||
# http serves a single offer, whereas https serves multiple. we only want one
|
||||
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
|
||||
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
|
||||
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
echo "Latest WordPress version could not be found"
|
||||
exit 1
|
||||
fi
|
||||
WP_TESTS_TAG="tags/$LATEST_VERSION"
|
||||
fi
|
||||
set -e
|
||||
|
||||
install_wp() {
|
||||
if [ -d $WP_CORE_DIR ]; then
|
||||
return;
|
||||
fi
|
||||
|
||||
mkdir -p $WP_CORE_DIR
|
||||
|
||||
wp core download --version=$WP_VERSION
|
||||
|
||||
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
|
||||
}
|
||||
|
||||
configure_wp() {
|
||||
WP_SITE_URL="http://local.wordpress.test"
|
||||
wait_db
|
||||
|
||||
if [[ ! -f "$WP_CORE_DIR/wp-config.php" ]]; then
|
||||
wp core config --dbname=$DB_NAME --dbuser=$DB_USER --dbpass=$DB_PASS --dbhost=$DB_HOST --dbprefix=wptests_
|
||||
fi
|
||||
wp core install --url="$WP_SITE_URL" --title="Example" --admin_user=admin --admin_password=password --admin_email=info@example.com --skip-email
|
||||
}
|
||||
|
||||
install_test_suite() {
|
||||
# portable in-place argument for both GNU sed and Mac OSX sed
|
||||
if [[ $(uname -s) == 'Darwin' ]]; then
|
||||
local ioption='-i.bak'
|
||||
else
|
||||
local ioption='-i'
|
||||
fi
|
||||
|
||||
# set up testing suite if it doesn't yet exist
|
||||
if [ ! -d $WP_TESTS_DIR ]; then
|
||||
# set up testing suite
|
||||
mkdir -p $WP_TESTS_DIR
|
||||
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
|
||||
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
|
||||
fi
|
||||
|
||||
if [ ! -f wp-tests-config.php ]; then
|
||||
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
# remove all forward slashes in the end
|
||||
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
|
||||
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/example.org/woocommerce.com/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
sed $ioption "s/admin@example.org/tests@woocommerce.com/" "$WP_TESTS_DIR"/wp-tests-config.php
|
||||
fi
|
||||
}
|
||||
|
||||
install_db() {
|
||||
if [ ${SKIP_DB_CREATE} = "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
wait_db
|
||||
local MYSQLADMIN_FLAGS=$(get_db_connection_flags)
|
||||
|
||||
# drop database if exists
|
||||
set +e
|
||||
mysqladmin drop --force $DB_NAME $MYSQLADMIN_FLAGS &> /dev/null
|
||||
set -e
|
||||
|
||||
# create database
|
||||
mysqladmin create $DB_NAME $MYSQLADMIN_FLAGS
|
||||
}
|
||||
|
||||
install_woocommerce() {
|
||||
WC_INSTALL_EXTRA=''
|
||||
INSTALLED_WC_VERSION=$(wp plugin get woocommerce --field=version)
|
||||
|
||||
if [[ $WC_VERSION == 'beta' ]]; then
|
||||
# Get the latest non-trunk version number from the .org repo. This will usually be the latest release, beta, or rc.
|
||||
WC_VERSION=$(curl https://api.wordpress.org/plugins/info/1.0/woocommerce.json | jq -r '.versions | with_entries(select(.key|match("beta";"i"))) | keys[-1]' --sort-keys)
|
||||
fi
|
||||
|
||||
if [[ -n $INSTALLED_WC_VERSION ]] && [[ $WC_VERSION == 'latest' ]]; then
|
||||
# WooCommerce is already installed, we just must update it to the latest stable version
|
||||
wp plugin update woocommerce
|
||||
wp plugin activate woocommerce
|
||||
else
|
||||
if [[ $INSTALLED_WC_VERSION != $WC_VERSION ]]; then
|
||||
# WooCommerce is installed but it's the wrong version, overwrite the installed version
|
||||
WC_INSTALL_EXTRA+=" --force"
|
||||
fi
|
||||
if [[ $WC_VERSION != 'latest' ]] && [[ $WC_VERSION != 'beta' ]]; then
|
||||
WC_INSTALL_EXTRA+=" --version=$WC_VERSION"
|
||||
fi
|
||||
wp plugin install woocommerce --activate$WC_INSTALL_EXTRA
|
||||
fi
|
||||
}
|
||||
|
||||
install_wp
|
||||
install_db
|
||||
configure_wp
|
||||
install_test_suite
|
||||
install_woocommerce
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-i18n', 'wp-primitives'), 'version' => '8bad0ce18409676b15d1');
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +0,0 @@
|
|||
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;right:0;opacity:.3;pointer-events:none;position:absolute;left:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-right:0;padding-left:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:left}
|
||||
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '32bfa486d902830f29c7');
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;left:0;opacity:.3;pointer-events:none;position:absolute;right:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-left:0;padding-right:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:right}
|
||||
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;left:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end!important}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;right:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end!important}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'cf4dd71c0c7418b591fd');
|
||||
|
|
@ -1 +0,0 @@
|
|||
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}
|
||||
File diff suppressed because one or more lines are too long
215
changelog.txt
215
changelog.txt
|
|
@ -1,220 +1,5 @@
|
|||
*** WooCommerce Subscriptions Changelog ***
|
||||
|
||||
2026-01-06 - version 8.3.0
|
||||
* Add: Support for ajax add-to-cart flows from the single product page, when upgrading or downgrading subscriptions.
|
||||
* Add: Enhanced functionality from the WooCommerce Subscription Downloads plugin has now been integrated directly into WooCommerce Subscriptions.
|
||||
* Fix: Ensured the subject line for the "Customer Renewal Invoice" email correctly updates.
|
||||
|
||||
2025-12-22 - version 8.2.1
|
||||
* Fix: Subscription pricing strings now correctly display singular and plural billing intervals like "$10 / month" and "$10 every 3 months" in cart and checkout totals.
|
||||
* Fix: Display all recurring coupon names in the totals section of classic cart and checkout when multiple recurring coupons are applied.
|
||||
* Fix: Payment method selector options on Subscriptions list page are now properly displaying the payment method name for gateways providing multiple payment methods.
|
||||
* Fix: PayPal Standard plugin IPN renewal orders not being created for failed payment retries.
|
||||
* Fix: Prevent fatal errors on shop pages when special characters like % are used in a product name.
|
||||
* Fix: Prevent an email validation error from briefly and inadvertently flashing on the screen when the "this is a gift" checkbox is used.
|
||||
|
||||
2025-12-10 - version 8.2.0
|
||||
* Fix: Various accessibility issues.
|
||||
* Added button roles.
|
||||
* Added labels to remove product links.
|
||||
* Adding missing header labels on related orders table.
|
||||
* Prevented various hidden elements from being exposed to screen readers.
|
||||
* Fixed focus problems for the renewal dialog.
|
||||
* Fixed focus when closing modals.
|
||||
* Fixed toggle state change using spacebar.
|
||||
* Fixed label for sign up now button.
|
||||
* Fixed toggle aria-label text to reflect state.
|
||||
* Added aria-haspopup attribute to buttons that open modals.
|
||||
* Fixed link for view order aria-label.
|
||||
* Fixed renew now button text and aria-label.
|
||||
* Added aria-modal and role attributes to modals and dialogs.
|
||||
* Fix: Resubscribe button now appears correctly for cancelled limited subscriptions.
|
||||
* Fix: Missing translation for pricing string on languages without plural form.
|
||||
* Fix: Customized subscriptions links in WooCommerce emails now work correctly.
|
||||
* Fix: Possible fatal errors when switching grouped subscriptions.
|
||||
* Fix: Prevent unnecessary log entries related to gifted subscriptions.
|
||||
* Fix: Make it easier for translation plugins to respect individual customer language preferences when sending subscription emails.
|
||||
* Fix: Prevent recurring local pickup options from displaying for virtual subscriptions when the cart contains physical products.
|
||||
* Fix: Prevent shipping summary from displaying on Blocks Checkout for virtual subscriptions.
|
||||
* Fix: Item price in blocks checkout is correctly displayed without "due today" words for items with zero sign up fee.
|
||||
|
||||
2025-11-13 - version 8.1.0
|
||||
* Fix: Prevent a fatal error that can occur when previewing emails in WooCommerce email settings.
|
||||
* Fix: Prevent technical subscription-specific discount types from appearing in the coupon edit UI.
|
||||
* Fix: Add admin notice and debug tool to recreate the payment retry database table when it's missing.
|
||||
* Fix: Prevent resubscribe and reactivate buttons from showing for subscriptions containing disabled, deleted, draft, or limited products.
|
||||
* Fix: Prevent error that blocks the subscriptions list from loading when gifted subscriptions are purchased with certain payment methods.
|
||||
* Fix: Ensure gift recipient email address is displayed in the order confirmation email sent to the purchaser.
|
||||
* Fix: Prevent pickup location options from displaying incorrectly alongside shipping methods on the Blocks Checkout page.
|
||||
* Fix: Fix invalid HTML tags that cause fatal errors when using themes with WordPress Interactivity API.
|
||||
* Fix: Add password reset links to the new account email template for gifted subscription recipients.
|
||||
* Tweak: Improve the gift recipient email input field on Blocks Cart and Mini Cart to match the style of other checkout fields.
|
||||
* Dev: Improve housekeeping of output buffers when handling requests to change subscription payment methods.
|
||||
* Dev: Improve type safety in relation to the `wcs_get_subscription()` function.
|
||||
|
||||
2025-10-15 - version 8.0.0
|
||||
* Add: Blocks Checkout now displays a single shipping method selection for both initial and recurring carts, similar to Classic Checkout. This can be disabled using the `wcs_cart_totals_shipping_html_price_only` filter.
|
||||
* Fix: Resubscribing to a limited subscription from the product page will now reactivate the existing subscription.
|
||||
* Fix: "New initial order - Recipient" email template preview no longer causes critical error.
|
||||
* Fix: Shipping value on blocks checkout no longer shows as "Free" when no shipping method is available.
|
||||
* Dev: Improved data telemetry which is sent to WooCommerce:
|
||||
* WooCommerce Subscriptions setting values
|
||||
* Order totals of subscription-related orders by order type and payment gateway, aggregated by month
|
||||
* Number of subscription-related orders by order type and payment gateway, aggregated by month
|
||||
* Number of products in parent subscription orders, aggregated by month
|
||||
* Number of non-zero value parent subscription orders, aggregated by month
|
||||
* Number of subscription products by billing interval
|
||||
* Number of giftable subscription products
|
||||
* Number of subscriptions by payment method, billing period, and renewal type
|
||||
* Number of gifted subscriptions
|
||||
* Number of subscribers
|
||||
* Purchase event for subscription products
|
||||
* Exclude orders with a "processing" status from the "subscription_orders" telemetry data.
|
||||
|
||||
2025-09-16 - version 7.9.0
|
||||
* Add: REST API endpoint to retrieve subscriptions associated with a specific order ID.
|
||||
* Fix: Keep subscription with successful renewals active when parent order fails.
|
||||
* Fix: Error when renewing subscriptions with downloadable gifts.
|
||||
* Fix: Improve performance by loading assets only when necessary.
|
||||
* Fix: "Update cart" button being always enabled.
|
||||
* Fix: Gifting recipient email layout issues when using a long e-mail address.
|
||||
* Fix: Gifting checkbox not showing up on blocks cart when using WooCommerce 10.2+.
|
||||
* Fix: Deleting a gifted subscription recipient will not delete their subscriptions, they will be moved to the subscription purchaser instead.
|
||||
* Dev: The `wcs_get_subscriptions()` and `wcs_get_subscriptions_for_order()` functions are now stricter, and only return instances of `WC_Subscription`.
|
||||
|
||||
2025-09-02 - version 7.8.2
|
||||
* Fix: Error when updating/renewing subscriptions containing a pseudo coupon.
|
||||
* Fix: Fix gifting recipient notice being shown for non gifting orders.
|
||||
|
||||
2025-08-25 - version 7.8.1
|
||||
* Fix: Fix checkout button default label.
|
||||
* Fix: Fix error when WP CLI is enabled in some environments.
|
||||
|
||||
2025-08-19 - version 7.8.0
|
||||
* Add: Native support for subscriptions gifting. Gifting for WooCommerce Subscriptions extension is no longer required.
|
||||
* Add: Enable subscriptions gifting storewide and per product.
|
||||
* Add: Blocks support for gifting on product, cart, and checkout pages.
|
||||
* Update: Make WooCommerce Subscriptions reports compatible with High Performance Order Storage.
|
||||
* Update: Rename Subscribe now button to Add to Cart to follow WooCommerce convention.
|
||||
* Fix: Fix integration with WooCommerce dashboard widget.
|
||||
* Fix: Cancel pending related orders when a subscription is cancelled to prevent orphaned orders that need payment.
|
||||
* Fix: Allow manual payments for pending renewal orders of Product Bundles or Composite Products when Mixed Checkout options is disabled.
|
||||
* Dev: Update moment.js package to the latest version 2.30.1
|
||||
|
||||
2025-07-09 - version 7.7.0
|
||||
* Fix: Restores normal behavior for the report caching updates scheduled action, which was failing due to a bad filepath.
|
||||
* Fix: Fix error when placing an order with a valid card after using a declined one.
|
||||
* Fix: Fix order renewal errors when using some plugins.
|
||||
* Fix: Prevent fatal errors when loading or deleting subscriptions with corrupted date values stored in a database.
|
||||
* Fix: Prevent fatal errors when trying to save invalid date values and display better error messages instead.
|
||||
* Fix: Fix broken blocks and javascript translations.
|
||||
* Fix: Keep newly created subscriptions in pending status when initial payment fails instead of putting them on hold.
|
||||
* Dev: Improve test suite bootstrap process by postponing interactions with Action Scheduler until it has fully initialized.
|
||||
* Dev: Plugin upgrade routines will now be triggered by changes in the main plugin version (and not the core library version).
|
||||
* Dev: Update wcs_is_paypal_profile_a() so it handles non-string parameters more gracefully. This is principally to reduce noise levels when running the test suite.
|
||||
* Dev: Proactively reviewed and hardened code to improve our security posture going forward.
|
||||
|
||||
2025-06-11 - version 7.6.0
|
||||
* Update: Allow updating billing info on existing subscriptions when customer changes account details.
|
||||
* Fix: Use floats instead of integers for tax calculation in subscription switching when prices include tax.
|
||||
* Fix: Enhance checks in email templates upgrade script for WooCommerce Subscriptions core 8.3.0.
|
||||
* Dev: Refactor argument processing in run-tests.sh to add a direct check for the -w flag.
|
||||
* Fix: Undefined array key `new`
|
||||
* Fix: Ensure subscription exists before updating its last order date.
|
||||
* Fix: Prevent fatal error when the line items no longer exist in the database.
|
||||
|
||||
2025-05-20 - version 7.5.0
|
||||
* Update: Add pagination to the related orders list in the My Account > View Subscription page.
|
||||
* Fix: Infinite loop when trying to load subscription related orders meta after cache busting.
|
||||
* Fix: Replace blogname with site_title on translation files.
|
||||
* Dev: Make the `wcs_get_subscriptions_for_order()` function more robust. It should not return any subscriptions if presented with an invalid order.
|
||||
|
||||
2025-04-14 - version 7.4.0
|
||||
* Update: Increase the number of args accepted by wcs_get_subscriptions(), to bring about parity with wc_get_orders().
|
||||
* Dev: Update wcs_maybe_prefix_key() and wcs_maybe_unprefix_key() to support an array of keys.
|
||||
* Fix: Prevent sending renewal reminders for orders with a 0 total.
|
||||
* Fix: Ensure the second parameter passed to the 'get_edit_post_link' filter is an integer.
|
||||
* Fix: Prevent WooCommerce Subscriptions buttons from overflowing in the View subscription page
|
||||
* Dev: Update subscriptions-core to 8.2.0
|
||||
|
||||
2025-03-31 - version 7.3.1
|
||||
* Update: Display the subscription parent order icon in the WooCommerce Orders list table by default.
|
||||
* Dev: Update subscriptions-core to 8.1.1
|
||||
|
||||
2025-03-24 - version 7.3.0
|
||||
* Tweak: Adjust the warning that appears on the reports page when HPOS is enabled but Compatibility Mode is disabled (provide a direct link to the relevant settings).
|
||||
* Update: Improved subscription search performance for WP Post stores by removing unnecessary _order_key and _billing_email meta queries.
|
||||
* Update: Make it possible to dispatch the Cancelled Subscription email more than once (when initially set to pending-cancellation, and again when it reaches final cancellation).
|
||||
* Update: Reduced duplicate queries when fetching multiple subscription related orders types.
|
||||
* Update: Removed unnecessary get_time() calls to reduce redundant get_last_order() queries in the Subscriptions list table.
|
||||
* Update: Improved performance on the Orders list table when rendering the Subscription Relationship column.
|
||||
* Update: Improved performance of the Generate Related Order Cache tool found under WooCommerce > Status > Tools.
|
||||
* Fix: Do not schedule updates to the report data cache if reporting is not available (ie, if HPOS is enabled but Compatibility Mode is not).
|
||||
* Fix: Resolved deprecated dynamic property warnings in WCS_Email_Payment_Retry and WCS_Email_Customer_Payment_Retry.
|
||||
* Fix: Added support for previewing payment retry emails in WooCommerce email settings.
|
||||
* Fix: Updated subscription email item table template to align with WooCommerce 9.7 email improvements.
|
||||
* Fix: Prevent PHP warning on cart page shipping method updates by removing unused method: maybe_restore_shipping_methods.
|
||||
* Fix: Removed unnecessary setting of renewal order paid date on status transition, relying on WooCommerce core behavior instead.
|
||||
* Fix: Ensure the order_awaiting_payment session arg is restored when loading a renewal cart from the session to prevent duplicate orders.
|
||||
* Fix: Ensure custom placeholders (time_until_renewal, customers_first_name) are included in customer notification email previews.
|
||||
* Fix: For stores with HPOS + compatibility mode enabled, using the bulk delete related orders cache tool was not correctly deleting the meta from the WP Posts table.
|
||||
* Fix: Prevent empty strings being saved in related orders cache ID meta when backfilling order data to the WP Posts table.
|
||||
* Fix: Correctly load product names with HTML on the cart and checkout shipping rates.
|
||||
* Dev: Fix Node version mismatch between package.json and .nvmrc (both are now set to v16.17.1).
|
||||
* Dev: Update subscriptions-core to 8.1.0
|
||||
|
||||
2025-02-13 - version 7.2.1
|
||||
* Fix: Revert a change released in 7.2.0 which triggered the "woocommerce_cart_item_name" filter with the wrong number of parameters.
|
||||
* Dev: Update subscriptions-core to 8.0.1.
|
||||
|
||||
2025-02-13 - version 7.2.0
|
||||
* Fix: In the system status report, show correct icon in the Cache Update Failures section when the total failures is 0.
|
||||
* Fix: Recommend WooPayments when there is no available payment gateway.
|
||||
* Fix: Safeguards added to the Subscriptions Totals template used in the My Account area, to guard against fatal errors that could arise in unusual conditions.
|
||||
* Fix: Correctly load product names with HTML on the cart and checkout shipping rates.
|
||||
* Fix: Improve our admin notice handling to ensure notices are displayed to the intended admin user.
|
||||
* Fix: Improve protections around the pending renewal order-creation process, to prevent uncaught exceptions and other errors from breaking the subscription editor.
|
||||
* Fix: Prevent unnecessary subscription object lookups on non-subscription admin pages when global $post is set.
|
||||
* Fix: After enabling the Customer Notification feature or changing the reminder timing setting, ensure notifications are correctly scheduled for all subscriptions with pending cancellation status.
|
||||
* Update: Improve performance of displaying recurring coupon limits in the admin coupon list table by optimizing coupon object retrieval.
|
||||
* Update: Display the subscription items table in all customer notification emails.
|
||||
* Update: Subscription notes for unsent customer notification emails now only occurs if WCS_DEBUG is enabled.
|
||||
* Dev: Introduces a new `woocommerce_subscription_valid_customer_notification_types` filter to modify which notification types are scheduled in Action Scheduler.
|
||||
* Dev: Update subscriptions-core to 8.0.0.
|
||||
|
||||
2025-01-10 - version 7.1.0
|
||||
* Add: Compatibility with WooCommerce's new preview email feature introduced in 9.6.
|
||||
* Fix: Prevents PHP fatal error when wcs_can_user_renew_early() is called with a non-existent subscription ID.
|
||||
* Fix: Prevent payment gateways that complete Change Payment requests asynchronously from blocking future attempts to update payment methods for all subscriptions.
|
||||
* Fix: Resolved an issue that could lead to undefined variable when attempting to send a customer notification email with no order.
|
||||
* Fix: Prevents the customer's postal code being used as their billing city in the current session, following a change to payment details.
|
||||
* Dev: Use the subscription's customer ID during the `wcs_can_user_renew_early()` function call when sending customer notification emails.
|
||||
* Dev: Fix PHP deprecated warnings caused by calling esc_url with null.
|
||||
* Dev: Update subscriptions-core to 7.9
|
||||
|
||||
2024-12-16 - version 7.0.0
|
||||
* Fix: Prevent failing non-recent renewal order from suspending the subscription and marking the most recent renewal order as failed.
|
||||
* Fix: Use block theme-styled buttons for subscription and related-orders actions on My Account pages.
|
||||
* Fix: Subscription totals not properly updating when customers remove items via the My Account > View Subscription page on some stores with caching enabled.
|
||||
* Fix: Resolved unexpected errors during the renewal process when a subscription contains metadata with key "id".
|
||||
* Update: Changed the link on the order thank-you page to take customers directly to their "My Account > Subscriptions" page.
|
||||
* Dev: Update subscriptions-core to 7.8.0
|
||||
|
||||
2024-11-27 - version 6.9.1
|
||||
* Fix: Resolved compatibility issues with WordPress 6.7 caused by translating strings too early.
|
||||
* Dev: Update subscriptions-core to 7.7.2
|
||||
|
||||
2024-11-14 - version 6.9.0
|
||||
* Add: New Customer Notification feature - sends reminder emails for upcoming subscription renewals, trials ending, and subscription expirations.
|
||||
* Fix: Prevent adding products to the cart if a subscription renewal is already present.
|
||||
* Update: Improved performance of wcs_get_subscription() when querying by product and customer or order.
|
||||
* Update: Improved performance when checking limited subscription product availability.
|
||||
* Update: Deprecate upgrading from versions of WooCommerce Subscriptions prior to 3.0.0 (released Jan 2020).
|
||||
* Dev: Minor refactoring of `init` method in `WC_Subscriptions_Upgrader` class.
|
||||
* Dev: Introduce the filter `woocommerce_subscriptions_synced_first_renewal_payment_timestamp` to enable plugins to modify the first renewal date of synced subscriptions.
|
||||
* Dev: Update `get_post_meta()` calls to fetch product meta using CRUD getters.
|
||||
* Dev: Update subscriptions-core to 7.7.1
|
||||
|
||||
2024-10-14 - version 6.8.0
|
||||
* Fix: Restore Retry icon in Orders table for HPOS-enabled stores.
|
||||
* Fix: Correctly updates a subscription status to `cancelled` during a payment failure call when the current status is `pending-cancel`.
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* WCS_Admin_Assets Class
|
||||
*
|
||||
* Handles admin assets (scripts and styles) for WooCommerce Subscriptions.
|
||||
*
|
||||
* @package WooCommerce Subscriptions/Admin
|
||||
*/
|
||||
class WCS_Admin_Assets {
|
||||
|
||||
/**
|
||||
* Initialize the tour handler
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue required scripts and styles
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/admin.asset.php' );
|
||||
$script_asset = file_exists( $script_asset_path )
|
||||
? require $script_asset_path
|
||||
: array(
|
||||
'dependencies' => array(
|
||||
'react',
|
||||
'wc-blocks-checkout',
|
||||
'wc-price-format',
|
||||
'wc-settings',
|
||||
'wp-element',
|
||||
'wp-i18n',
|
||||
'wp-plugins',
|
||||
),
|
||||
'version' => WC_Subscriptions::$version,
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wcs-admin',
|
||||
plugins_url( '/build/admin.js', WC_Subscriptions::$plugin_file ),
|
||||
$script_asset['dependencies'],
|
||||
$script_asset['version'],
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_style(
|
||||
'wcs-admin',
|
||||
plugins_url( '/build/style-admin.css', WC_Subscriptions::$plugin_file ),
|
||||
array( 'wp-components' ),
|
||||
$script_asset['version']
|
||||
);
|
||||
|
||||
wp_set_script_translations(
|
||||
'wcs-admin',
|
||||
'woocommerce-subscriptions',
|
||||
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,13 +23,10 @@ class WCS_Admin_Reports {
|
|||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// The subscription reports are compatible with HPOS since 7.8.0.
|
||||
// We can inform users running data sync mode, that it's no longer needed.
|
||||
if (
|
||||
wcs_is_custom_order_tables_usage_enabled() &&
|
||||
wcs_is_custom_order_tables_data_sync_enabled()
|
||||
) {
|
||||
add_action( 'admin_notices', [ __CLASS__, 'display_hpos_compatibility_notice' ] );
|
||||
// The subscription reports are incompatible with stores running HPOS with sycning disabled.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() && ! wcs_is_custom_order_tables_data_sync_enabled() ) {
|
||||
add_action( 'admin_notices', [ __CLASS__, 'display_hpos_incompatibility_notice' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the reports layout to the WooCommerce -> Reports admin section
|
||||
|
|
@ -40,18 +37,12 @@ class WCS_Admin_Reports {
|
|||
|
||||
// Add any actions we need based on the screen
|
||||
add_action( 'current_screen', __CLASS__ . '::conditional_reporting_includes' );
|
||||
|
||||
// Starting from WooCommerce 10.0 the dashboard widget is loaded asynchronously.
|
||||
// We also need to hook into AJAX request before WooCommerce so we can attach our hook to widget rendering flow.
|
||||
add_action( 'wp_ajax_woocommerce_load_status_widget', __CLASS__ . '::init_dashboard_report', 9 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an admin notice indicating subscription reports are compatible with HPOS.
|
||||
*
|
||||
* @since 7.8.0
|
||||
* Displays an admin notice indicating subscription reports are disabled on HPOS environments with no syncing.
|
||||
*/
|
||||
public static function display_hpos_compatibility_notice() {
|
||||
public static function display_hpos_incompatibility_notice() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
// Only display the admin notice on report admin screens.
|
||||
|
|
@ -59,41 +50,28 @@ class WCS_Admin_Reports {
|
|||
return;
|
||||
}
|
||||
|
||||
$nonce_name = 'wcs_reports_hpos_compatibility_notice';
|
||||
$option_name = 'woocommerce_subscriptions_reports_hpos_compatibility_notice_dismissed';
|
||||
$admin_notice = new WCS_Admin_Notice( 'error' );
|
||||
|
||||
$is_dismissed = get_option( $option_name );
|
||||
|
||||
if ( 'yes' === $is_dismissed ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['_wcsnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ), $nonce_name ) && ! empty( $_GET[ $nonce_name ] ) ) {
|
||||
update_option( $option_name, 'yes' );
|
||||
return;
|
||||
}
|
||||
|
||||
$dismiss_url = wp_nonce_url( add_query_arg( $nonce_name, '1' ), $nonce_name, '_wcsnonce' );
|
||||
|
||||
$admin_notice = new WCS_Admin_Notice( 'notice notice-info is-dismissible', array(), $dismiss_url );
|
||||
|
||||
$content = sprintf(
|
||||
// translators: placeholders $1 and $2 are opening <a> tags linking to the WooCommerce documentation on HPOS, and to the Advanced Features settings screen. Placeholder $3 is a closing link (</a>) tag.
|
||||
__( 'WooCommerce Subscriptions now supports %1$sHigh-Performance Order Storage (HPOS)%3$s - compatibility mode is no longer required to view subscriptions reports. You can disable compatibility mode in your %2$sstore settings%3$s.', 'woocommerce-subscriptions' ),
|
||||
'<a href="https://woocommerce.com/document/high-performance-order-storage/">',
|
||||
'<a href="' . esc_url( get_admin_url( null, 'admin.php?page=wc-settings&tab=advanced§ion=features' ) ) . '">',
|
||||
'</a>'
|
||||
$admin_notice->set_html_content(
|
||||
sprintf(
|
||||
'<p><strong>%s</strong></p><p>%s</p>',
|
||||
_x( 'WooCommerce Subscriptions - Reports Not Available', 'heading used in an admin notice', 'woocommerce-subscriptions' ),
|
||||
sprintf(
|
||||
// translators: placeholders $1 and $2 are opening <a> tags linking to the WooCommerce documentation on HPOS and data synchronization. Placeholder $3 is a closing link (<a>) tag.
|
||||
__( 'Subscription reports are incompatible with the %1$sWooCommerce data storage features%3$s enabled on your store. Please enable %2$stable synchronization%3$s if you wish to use subscription reports.', 'woocommerce-subscriptions' ),
|
||||
'<a href="https://woocommerce.com/document/high-performance-order-storage/">',
|
||||
'<a href="https://woocommerce.com/document/high-performance-order-storage/#synchronization">',
|
||||
'</a>'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$admin_notice->set_html_content( "<p>{$content}</p>" );
|
||||
|
||||
$admin_notice->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'Subscriptions' report type to the WooCommerce reports screen.
|
||||
*
|
||||
* @param array $reports Array of Report types & their labels, excluding the Subscription product type.
|
||||
* @param array Array of Report types & their labels, excluding the Subscription product type.
|
||||
* @return array Array of Report types & their labels, including the Subscription product type.
|
||||
* @since 2.1
|
||||
*/
|
||||
|
|
@ -191,19 +169,12 @@ class WCS_Admin_Reports {
|
|||
|
||||
$screen = get_current_screen();
|
||||
|
||||
// Before WooCommerce 10.0 the dashboard widget was loaded synchronously on the dashboard screen. Keep this for backward compatibility.
|
||||
if ( isset( $screen->id ) && 'dashboard' === $screen->id ) {
|
||||
self::init_dashboard_report();
|
||||
switch ( $screen->id ) {
|
||||
case 'dashboard':
|
||||
new WCS_Report_Dashboard();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the dashboard report.
|
||||
*
|
||||
* Used for loading the dashboard widget sync and async.
|
||||
*/
|
||||
public static function init_dashboard_report() {
|
||||
new WCS_Report_Dashboard();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -252,4 +223,32 @@ class WCS_Admin_Reports {
|
|||
WC_Tracks::record_event( $reports[ $name ], $properties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we hit one of our reports in the WC get_report function, change the path to our dir.
|
||||
*
|
||||
* @param string $report_path the parth to the report.
|
||||
* @param string $name the name of the report.
|
||||
* @param string $class the class of the report.
|
||||
*
|
||||
* @return string path to the report template.
|
||||
* @since 2.1
|
||||
* @deprecated in favor of autoloading
|
||||
* @access private
|
||||
*/
|
||||
public static function initialize_reports_path( $report_path, $name, $class ) {
|
||||
_deprecated_function( __METHOD__, '2.4.0' );
|
||||
if ( in_array( strtolower( $class ), array(
|
||||
'wc_report_subscription_events_by_date',
|
||||
'wc_report_upcoming_recurring_revenue',
|
||||
'wc_report_retention_rate',
|
||||
'wc_report_subscription_by_product',
|
||||
'wc_report_subscription_by_customer',
|
||||
'wc_report_subscription_payment_retry',
|
||||
) ) ) {
|
||||
$report_path = dirname( __FILE__ ) . '/reports/classwcsreport' . $name . '.php';
|
||||
}
|
||||
|
||||
return $report_path;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,10 @@ class WCS_Report_Cache_Manager {
|
|||
/**
|
||||
* Attach callbacks to manage cache updates
|
||||
*
|
||||
* @since 7.8.0 - Compatible with HPOS, originally introduced in 2.1
|
||||
* @since 2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// Use the old hooks
|
||||
if ( wcs_is_woocommerce_pre( '3.0' ) ) {
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ class WCS_Report_Cache_Manager {
|
|||
* This function is attached as a callback on the events in the $update_events_and_classes property.
|
||||
*
|
||||
* @since 2.1
|
||||
* @return void
|
||||
* @return null
|
||||
*/
|
||||
public function set_reports_to_update() {
|
||||
if ( isset( $this->update_events_and_classes[ current_filter() ] ) ) {
|
||||
|
|
@ -140,38 +141,37 @@ class WCS_Report_Cache_Manager {
|
|||
*/
|
||||
public function schedule_cache_updates() {
|
||||
|
||||
if ( empty( $this->reports_to_update ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! empty( $this->reports_to_update ) ) {
|
||||
|
||||
// On large sites, we want to run the cache update once at 4am in the site's timezone
|
||||
if ( $this->use_large_site_cache() ) {
|
||||
// On large sites, we want to run the cache update once at 4am in the site's timezone
|
||||
if ( $this->use_large_site_cache() ) {
|
||||
|
||||
$cache_update_timestamp = $this->get_large_site_cache_update_timestamp();
|
||||
$cache_update_timestamp = $this->get_large_site_cache_update_timestamp();
|
||||
|
||||
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
|
||||
foreach ( $this->reports_to_update as $index => $report_class ) {
|
||||
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
|
||||
foreach ( $this->reports_to_update as $index => $report_class ) {
|
||||
|
||||
$cron_args = array( 'report_class' => $report_class );
|
||||
$cron_args = array( 'report_class' => $report_class );
|
||||
|
||||
if ( false === as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
|
||||
// Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
|
||||
as_schedule_single_action( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
|
||||
if ( false === as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
|
||||
// Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
|
||||
as_schedule_single_action( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Otherwise, run it 10 minutes after the last cache invalidating event
|
||||
} else { // Otherwise, run it 10 minutes after the last cache invalidating event
|
||||
|
||||
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
|
||||
foreach ( $this->reports_to_update as $index => $report_class ) {
|
||||
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
|
||||
foreach ( $this->reports_to_update as $index => $report_class ) {
|
||||
|
||||
$cron_args = array( 'report_class' => $report_class );
|
||||
$cron_args = array( 'report_class' => $report_class );
|
||||
|
||||
if ( false !== as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
|
||||
as_unschedule_action( $this->cron_hook, $cron_args );
|
||||
if ( false !== as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
|
||||
as_unschedule_action( $this->cron_hook, $cron_args );
|
||||
}
|
||||
|
||||
// Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
|
||||
as_schedule_single_action( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
|
||||
}
|
||||
|
||||
// Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
|
||||
as_schedule_single_action( (int) gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,6 +180,7 @@ class WCS_Report_Cache_Manager {
|
|||
* Update the cache data for a given report, as specified with $report_class, by call it's get_data() method.
|
||||
*
|
||||
* @since 2.1
|
||||
* @return null
|
||||
*/
|
||||
public function update_cache( $report_class ) {
|
||||
/**
|
||||
|
|
@ -211,19 +212,7 @@ class WCS_Report_Cache_Manager {
|
|||
|
||||
// Load report class dependencies
|
||||
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
|
||||
|
||||
$within_ci_environment = getenv( 'CI' );
|
||||
$wc_core_dir_from_env = getenv( 'WC_CORE_DIR' );
|
||||
|
||||
if ( $within_ci_environment && ! empty( $wc_core_dir_from_env ) ) {
|
||||
$wc_core_dir = $wc_core_dir_from_env;
|
||||
} elseif ( $within_ci_environment ) {
|
||||
$wc_core_dir = '/tmp/woocommerce';
|
||||
} else {
|
||||
$wc_core_dir = WC()->plugin_path();
|
||||
}
|
||||
|
||||
require_once( $wc_core_dir . '/includes/admin/reports/class-wc-admin-report.php' );
|
||||
require_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
|
||||
|
||||
$reflector = new ReflectionMethod( $report_class, 'get_data' );
|
||||
|
||||
|
|
@ -332,7 +321,7 @@ class WCS_Report_Cache_Manager {
|
|||
'label' => 'Cache Update Failures',
|
||||
/* translators: %d refers to the number of times we have detected cache update failures */
|
||||
'note' => sprintf( _n( '%d failures', '%d failure', $failures, 'woocommerce-subscriptions' ), $failures ),
|
||||
'success' => 0 === (int)$failures,
|
||||
'success' => 0 === $failures,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,20 +16,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
class WCS_Report_Dashboard {
|
||||
/**
|
||||
* Tracks whether the cache should be updated after generating report data.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $should_update_cache = false;
|
||||
|
||||
/**
|
||||
* Cached report results for performance optimization.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $cached_report_results = array();
|
||||
|
||||
/**
|
||||
* Hook in additional reporting to WooCommerce dashboard widget
|
||||
|
|
@ -45,35 +31,175 @@ class WCS_Report_Dashboard {
|
|||
|
||||
/**
|
||||
* Get all data needed for this report and store in the class
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return object The report data.
|
||||
*/
|
||||
public static function get_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$default_args = array(
|
||||
'no_cache' => false,
|
||||
);
|
||||
|
||||
$args = apply_filters( 'wcs_reports_subscription_dashboard_args', $args );
|
||||
$args = wp_parse_args( $args, $default_args );
|
||||
$args = apply_filters( 'wcs_reports_subscription_dashboard_args', $args );
|
||||
$args = wp_parse_args( $args, $default_args );
|
||||
$offset = get_option( 'gmt_offset' );
|
||||
$update_cache = false;
|
||||
|
||||
self::init_cache();
|
||||
// Use this once it is merged - wcs_get_gmt_offset_string();
|
||||
// Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
|
||||
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
|
||||
|
||||
// Use current month as default date range.
|
||||
$start_date = $args['start_date'] ?? date( 'Y-m-01', current_time( 'timestamp' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date,WordPress.DateTime.CurrentTimeTimestamp.Requested -- Keep default date values for backward compatibility.
|
||||
$end_date = $args['end_date'] ?? date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date,WordPress.DateTime.CurrentTimeTimestamp.Requested -- Keep default date values for backward compatibility.
|
||||
$report_data = new stdClass;
|
||||
|
||||
$report_data = new stdClass();
|
||||
$report_data->signup_count = self::fetch_signup_count( $start_date, $end_date, $args['no_cache'] );
|
||||
$report_data->signup_revenue = self::fetch_signup_revenue( $start_date, $end_date, $args['no_cache'] );
|
||||
$report_data->renewal_count = self::fetch_renewal_count( $start_date, $end_date, $args['no_cache'] );
|
||||
$report_data->renewal_revenue = self::fetch_renewal_revenue( $start_date, $end_date, $args['no_cache'] );
|
||||
$report_data->cancel_count = self::fetch_cancel_count( $start_date, $end_date, $args['no_cache'] );
|
||||
$cached_results = get_transient( strtolower( __CLASS__ ) );
|
||||
|
||||
if ( self::$should_update_cache ) {
|
||||
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, HOUR_IN_SECONDS );
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( $cached_results ) ) {
|
||||
$cached_results = [];
|
||||
}
|
||||
|
||||
// Subscription signups this month
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
INNER JOIN {$wpdb->posts} AS wcorder
|
||||
ON wcsubs.post_parent = wcorder.ID
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcsubs.post_type IN ( 'shop_subscription' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= '%s'
|
||||
AND wcorder.post_date < '%s'",
|
||||
date( 'Y-m-01', current_time( 'timestamp' ) ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
|
||||
);
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! 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_query', $query ) );
|
||||
$update_cache = true;
|
||||
}
|
||||
|
||||
$report_data->signup_count = $cached_results[ $query_hash ];
|
||||
|
||||
// Signup revenue this month
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(order_total_meta.meta_value)
|
||||
FROM {$wpdb->postmeta} AS order_total_meta
|
||||
RIGHT JOIN
|
||||
(
|
||||
SELECT DISTINCT wcorder.ID
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
INNER JOIN {$wpdb->posts} AS wcorder
|
||||
ON wcsubs.post_parent = wcorder.ID
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcsubs.post_type IN ( 'shop_subscription' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= '%s'
|
||||
AND wcorder.post_date < '%s'
|
||||
) AS orders ON orders.ID = order_total_meta.post_id
|
||||
WHERE order_total_meta.meta_key = '_order_total'",
|
||||
date( 'Y-m-01', current_time( 'timestamp' ) ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
|
||||
);
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) );
|
||||
$update_cache = true;
|
||||
}
|
||||
|
||||
$report_data->signup_revenue = $cached_results[ $query_hash ];
|
||||
|
||||
// Subscription renewals this month
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcorder.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcorder
|
||||
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.post_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= '%s'
|
||||
AND wcorder.post_date < '%s'",
|
||||
date( 'Y-m-01', current_time( 'timestamp' ) ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
|
||||
);
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! 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 ) );
|
||||
$update_cache = true;
|
||||
}
|
||||
|
||||
$report_data->renewal_count = $cached_results[ $query_hash ];
|
||||
|
||||
// Renewal revenue this month
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(order_total_meta.meta_value)
|
||||
FROM {$wpdb->postmeta} as order_total_meta
|
||||
RIGHT JOIN
|
||||
(
|
||||
SELECT DISTINCT wcorder.ID
|
||||
FROM {$wpdb->posts} AS wcorder
|
||||
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.post_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= '%s'
|
||||
AND wcorder.post_date < '%s'
|
||||
) AS orders ON orders.ID = order_total_meta.post_id
|
||||
WHERE order_total_meta.meta_key = '_order_total'",
|
||||
date( 'Y-m-01', current_time( 'timestamp' ) ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
|
||||
);
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! 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 ) );
|
||||
$update_cache = true;
|
||||
}
|
||||
|
||||
$report_data->renewal_revenue = $cached_results[ $query_hash ];
|
||||
|
||||
// Cancellation count this month
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
JOIN {$wpdb->postmeta} AS wcsmeta_cancel
|
||||
ON wcsubs.ID = wcsmeta_cancel.post_id
|
||||
AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
|
||||
AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' )
|
||||
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) BETWEEN '%s' AND '%s'",
|
||||
date( 'Y-m-01', current_time( 'timestamp' ) ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
|
||||
);
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! 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 ) );
|
||||
$update_cache = true;
|
||||
}
|
||||
|
||||
$report_data->cancel_count = $cached_results[ $query_hash ];
|
||||
|
||||
if ( $update_cache ) {
|
||||
set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $report_data;
|
||||
|
|
@ -124,11 +250,11 @@ class WCS_Report_Dashboard {
|
|||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
|
||||
<?php
|
||||
// translators: 1$: count, 2$ and 3$ are opening and closing strong tags, respectively.
|
||||
echo wp_kses_post( sprintf( _n( '%2$s%1$s cancellation%3$s subscription cancellations this month', '%2$s%1$s cancellations%3$s subscription cancellations this month', $report_data->cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '<strong>', '</strong>' ) );
|
||||
?>
|
||||
echo wp_kses_post( sprintf( _n( '%2$s%1$s cancellation%3$s subscription cancellations this month', '%2$s%1$s cancellations%3$s subscription cancellations this month', $report_data->cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '<strong>', '</strong>' ) ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,374 +269,9 @@ class WCS_Report_Dashboard {
|
|||
/**
|
||||
* Clears the cached report data.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
public static function clear_cache() {
|
||||
delete_transient( strtolower( __CLASS__ ) );
|
||||
self::$should_update_cache = false;
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the signup count for the dashboard.
|
||||
*
|
||||
* @param string $start_date The start date.
|
||||
* @param string $end_date The end date.
|
||||
* @param bool $force_cache_update Whether to force update the cache.
|
||||
* @return int The signup count.
|
||||
*/
|
||||
private static function fetch_signup_count( $start_date, $end_date, $force_cache_update = false ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->prefix}wc_orders AS wcsubs
|
||||
INNER JOIN {$wpdb->prefix}wc_orders AS wcorder
|
||||
ON wcsubs.parent_order_id = wcorder.ID
|
||||
WHERE wcorder.type IN ( 'shop_order' )
|
||||
AND wcsubs.type IN ( 'shop_subscription' )
|
||||
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.date_created_gmt >= %s
|
||||
AND wcorder.date_created_gmt < %s",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
INNER JOIN {$wpdb->posts} AS wcorder
|
||||
ON wcsubs.post_parent = wcorder.ID
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcsubs.post_type IN ( 'shop_subscription' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= %s
|
||||
AND wcorder.post_date < %s",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
}
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
/**
|
||||
* Filter the query for the signup count.
|
||||
*
|
||||
* @param string $query The query to execute.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query );
|
||||
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the signup revenue for the dashboard.
|
||||
*
|
||||
* @param string $start_date The start date.
|
||||
* @param string $end_date The end date.
|
||||
* @param bool $force_cache_update Whether to force update the cache.
|
||||
* @return float The signup revenue.
|
||||
*/
|
||||
private static function fetch_signup_revenue( $start_date, $end_date, $force_cache_update = false ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(parent_orders.total_amount)
|
||||
FROM {$wpdb->prefix}wc_orders AS subscripitons
|
||||
INNER JOIN {$wpdb->prefix}wc_orders AS parent_orders
|
||||
ON subscripitons.parent_order_id = parent_orders.ID
|
||||
WHERE parent_orders.type IN ( 'shop_order' )
|
||||
AND subscripitons.type IN ( 'shop_subscription' )
|
||||
AND parent_orders.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND parent_orders.date_created_gmt >= %s
|
||||
AND parent_orders.date_created_gmt < %s
|
||||
",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(order_total_meta.meta_value)
|
||||
FROM {$wpdb->postmeta} AS order_total_meta
|
||||
RIGHT JOIN
|
||||
(
|
||||
SELECT DISTINCT wcorder.ID
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
INNER JOIN {$wpdb->posts} AS wcorder
|
||||
ON wcsubs.post_parent = wcorder.ID
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcsubs.post_type IN ( 'shop_subscription' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= %s
|
||||
AND wcorder.post_date < %s
|
||||
) AS orders ON orders.ID = order_total_meta.post_id
|
||||
WHERE order_total_meta.meta_key = '_order_total'",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
}
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
/**
|
||||
* Filter the query for the signup revenue.
|
||||
*
|
||||
* @param string $query The query to execute.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query );
|
||||
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the renewal count for the dashboard.
|
||||
*
|
||||
* @param string $start_date The start date.
|
||||
* @param string $end_date The end date.
|
||||
* @param bool $force_cache_update Whether to force update the cache.
|
||||
* @return int The renewal count.
|
||||
*/
|
||||
private static function fetch_renewal_count( $start_date, $end_date, $force_cache_update = false ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcorder.ID) AS count
|
||||
FROM {$wpdb->prefix}wc_orders AS wcorder
|
||||
INNER JOIN {$wpdb->prefix}wc_orders_meta AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.order_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.type IN ( 'shop_order' )
|
||||
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.date_created_gmt >= %s
|
||||
AND wcorder.date_created_gmt < %s",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcorder.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcorder
|
||||
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.post_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= %s
|
||||
AND wcorder.post_date < %s",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
}
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
/**
|
||||
* Filter the query for the renewal count.
|
||||
*
|
||||
* @param string $query The query to execute.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query );
|
||||
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the renewal revenue for the dashboard.
|
||||
*
|
||||
* @param string $start_date The start date.
|
||||
* @param string $end_date The end date.
|
||||
* @param bool $force_cache_update Whether to force update the cache.
|
||||
* @return float The renewal revenue.
|
||||
*/
|
||||
private static function fetch_renewal_revenue( $start_date, $end_date, $force_cache_update = false ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(wcorder.total_amount)
|
||||
FROM {$wpdb->prefix}wc_orders AS wcorder
|
||||
INNER JOIN {$wpdb->prefix}wc_orders_meta AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.order_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.type IN ( 'shop_order' )
|
||||
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.date_created_gmt >= %s
|
||||
AND wcorder.date_created_gmt < %s",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT SUM(order_total_meta.meta_value)
|
||||
FROM {$wpdb->postmeta} as order_total_meta
|
||||
RIGHT JOIN
|
||||
(
|
||||
SELECT DISTINCT wcorder.ID
|
||||
FROM {$wpdb->posts} AS wcorder
|
||||
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
|
||||
ON (
|
||||
wcorder.id = meta__subscription_renewal.post_id
|
||||
AND
|
||||
meta__subscription_renewal.meta_key = '_subscription_renewal'
|
||||
)
|
||||
WHERE wcorder.post_type IN ( 'shop_order' )
|
||||
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
|
||||
AND wcorder.post_date >= %s
|
||||
AND wcorder.post_date < %s
|
||||
) AS orders ON orders.ID = order_total_meta.post_id
|
||||
WHERE order_total_meta.meta_key = '_order_total'",
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
}
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
/**
|
||||
* Filter the query for the renewal revenue.
|
||||
*
|
||||
* @param string $query The query to execute.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query );
|
||||
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the cancellation count for the dashboard.
|
||||
*
|
||||
* @param string $start_date The start date.
|
||||
* @param string $end_date The end date.
|
||||
* @param bool $force_cache_update Whether to force update the cache.
|
||||
* @return int The cancellation count.
|
||||
*/
|
||||
private static function fetch_cancel_count( $start_date, $end_date, $force_cache_update = false ) {
|
||||
global $wpdb;
|
||||
|
||||
$offset = get_option( 'gmt_offset' );
|
||||
|
||||
// Use this once it is merged - wcs_get_gmt_offset_string();
|
||||
// Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
|
||||
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->prefix}wc_orders AS wcsubs
|
||||
JOIN {$wpdb->prefix}wc_orders_meta AS wcsmeta_cancel
|
||||
ON wcsubs.ID = wcsmeta_cancel.order_id
|
||||
AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
|
||||
AND wcsubs.status NOT IN ( 'trash', 'auto-draft' )
|
||||
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', %s ) BETWEEN %s AND %s",
|
||||
$site_timezone,
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
|
||||
FROM {$wpdb->posts} AS wcsubs
|
||||
JOIN {$wpdb->postmeta} AS wcsmeta_cancel
|
||||
ON wcsubs.ID = wcsmeta_cancel.post_id
|
||||
AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
|
||||
AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' )
|
||||
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', %s ) BETWEEN %s AND %s",
|
||||
$site_timezone,
|
||||
$start_date,
|
||||
$end_date
|
||||
);
|
||||
}
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
/**
|
||||
* Filter the query for the cancellation count.
|
||||
*
|
||||
* @param string $query The query to execute.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query );
|
||||
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cache for report results.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function init_cache() {
|
||||
self::$should_update_cache = false;
|
||||
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
|
||||
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( self::$cached_report_results ) ) {
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache report results for performance optimization.
|
||||
*
|
||||
* @param string $query_hash The hash of the query for caching.
|
||||
* @param array $report_data The report data to cache.
|
||||
* @return void
|
||||
*/
|
||||
private static function cache_report_results( $query_hash, $report_data ) {
|
||||
self::$cached_report_results[ $query_hash ] = $report_data;
|
||||
self::$should_update_cache = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
* Get report data
|
||||
*
|
||||
* @since 2.1
|
||||
* @return stdClass
|
||||
* @return array
|
||||
*/
|
||||
public function get_report_data() {
|
||||
if ( empty( $this->report_data ) ) {
|
||||
|
|
@ -38,13 +38,25 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
* subscription may not have been active all of that time. Instead, it may have been on-hold for part of it.
|
||||
*
|
||||
* @since 2.1
|
||||
* @return void
|
||||
* @return null
|
||||
*/
|
||||
private function query_report_data() {
|
||||
global $wpdb;
|
||||
|
||||
$this->report_data = new stdClass;
|
||||
|
||||
// First, let's find the age of the longest living subscription in days
|
||||
$oldest_subscription_age_in_days = $this->get_max_subscription_age_in_days();
|
||||
$oldest_subscription_age_in_days = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
|
||||
FROM {$wpdb->prefix}posts posts
|
||||
LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
|
||||
WHERE posts.post_type = 'shop_subscription'
|
||||
AND postmeta.meta_key = %s
|
||||
AND postmeta.meta_value <> '0'
|
||||
ORDER BY age_in_days DESC
|
||||
LIMIT 1",
|
||||
wcs_get_date_meta_key( 'end' )
|
||||
) );
|
||||
|
||||
// Now determine what interval to use based on that length
|
||||
if ( $oldest_subscription_age_in_days > 365 ) {
|
||||
|
|
@ -62,12 +74,32 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
$oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period );
|
||||
|
||||
// Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription)
|
||||
$subscription_ages = $this->fetch_subscriptions_ages( $days_in_interval_period, $oldest_subscription_age );
|
||||
$base_query = $wpdb->prepare(
|
||||
"SELECT
|
||||
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
|
||||
COUNT(posts.ID) as count
|
||||
FROM {$wpdb->prefix}posts posts
|
||||
LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
|
||||
ON posts.ID = cancelled_date.post_id
|
||||
AND cancelled_date.meta_key = %s
|
||||
AND cancelled_date.meta_value <> '0'
|
||||
LEFT JOIN {$wpdb->prefix}postmeta end_date
|
||||
ON posts.ID = end_date.post_id
|
||||
AND end_date.meta_key = %s
|
||||
WHERE posts.post_type = 'shop_subscription'
|
||||
AND posts.post_status NOT IN( 'wc-pending', 'trash' )
|
||||
GROUP BY periods_active
|
||||
ORDER BY periods_active ASC",
|
||||
$days_in_interval_period,
|
||||
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subsription
|
||||
wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime
|
||||
wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscritions that have expired
|
||||
);
|
||||
|
||||
// Set initial values for the report data.
|
||||
$this->report_data->total_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
|
||||
$this->report_data->unended_subscriptions = $this->report_data->total_subscriptions;
|
||||
$this->report_data->living_subscriptions = array();
|
||||
$subscription_ages = $wpdb->get_results( $base_query, OBJECT_K );
|
||||
|
||||
$this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
|
||||
$this->report_data->living_subscriptions = array();
|
||||
|
||||
// At day zero, no subscriptions have ended
|
||||
$this->report_data->living_subscriptions[0] = $this->report_data->total_subscriptions;
|
||||
|
|
@ -88,113 +120,13 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the age of the longest living subscription in days.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_max_subscription_age_in_days() {
|
||||
global $wpdb;
|
||||
|
||||
$end_date_meta_key = wcs_get_date_meta_key( 'end' );
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT MAX(DATEDIFF(CAST(meta.meta_value AS DATETIME),orders.date_created_gmt)) as age_in_days
|
||||
FROM {$wpdb->prefix}wc_orders orders
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta ON orders.ID = meta.order_id
|
||||
WHERE orders.type = 'shop_subscription'
|
||||
AND meta.meta_key = %s
|
||||
AND meta.meta_value <> '0'
|
||||
ORDER BY age_in_days DESC
|
||||
LIMIT 1",
|
||||
$end_date_meta_key
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
|
||||
FROM {$wpdb->prefix}posts posts
|
||||
LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
|
||||
WHERE posts.post_type = 'shop_subscription'
|
||||
AND postmeta.meta_key = %s
|
||||
AND postmeta.meta_value <> '0'
|
||||
ORDER BY age_in_days DESC
|
||||
LIMIT 1",
|
||||
$end_date_meta_key
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared above.
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the number of periods each subscription has between creating and ending.
|
||||
*
|
||||
* @param int $days_in_interval_period
|
||||
* @param int $oldest_subscription_age
|
||||
* @return array
|
||||
*/
|
||||
private function fetch_subscriptions_ages( $days_in_interval_period, $oldest_subscription_age ) {
|
||||
global $wpdb;
|
||||
|
||||
$end_date_meta_key = wcs_get_date_meta_key( 'end' );
|
||||
$cancelled_date_meta_key = wcs_get_date_meta_key( 'cancelled' );
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),orders.date_created_gmt)/%d),%d) as periods_active,
|
||||
COUNT(orders.ID) as count
|
||||
FROM {$wpdb->prefix}wc_orders orders
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta cancelled_date
|
||||
ON orders.ID = cancelled_date.order_id
|
||||
AND cancelled_date.meta_key = %s
|
||||
AND cancelled_date.meta_value <> '0'
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta end_date
|
||||
ON orders.ID = end_date.order_id
|
||||
AND end_date.meta_key = %s
|
||||
WHERE orders.type = 'shop_subscription'
|
||||
AND orders.status NOT IN( 'wc-pending', 'trash' )
|
||||
GROUP BY periods_active
|
||||
ORDER BY periods_active ASC",
|
||||
$days_in_interval_period,
|
||||
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
|
||||
$cancelled_date_meta_key, // If a subscription has a cancelled date, use that to determine a more accurate lifetime
|
||||
$end_date_meta_key // Otherwise, we want to use the end date for subscriptions that have expired
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
|
||||
COUNT(posts.ID) as count
|
||||
FROM {$wpdb->prefix}posts posts
|
||||
LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
|
||||
ON posts.ID = cancelled_date.post_id
|
||||
AND cancelled_date.meta_key = %s
|
||||
AND cancelled_date.meta_value <> '0'
|
||||
LEFT JOIN {$wpdb->prefix}postmeta end_date
|
||||
ON posts.ID = end_date.post_id
|
||||
AND end_date.meta_key = %s
|
||||
WHERE posts.post_type = 'shop_subscription'
|
||||
AND posts.post_status NOT IN( 'wc-pending', 'trash' )
|
||||
GROUP BY periods_active
|
||||
ORDER BY periods_active ASC",
|
||||
$days_in_interval_period,
|
||||
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
|
||||
$cancelled_date_meta_key, // If a subscription has a cancelled date, use that to determine a more accurate lifetime
|
||||
$end_date_meta_key // Otherwise, we want to use the end date for subscriptions that have expired
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared above.
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the report
|
||||
*
|
||||
* Use a custom report as we don't need the date filters provided by the WooCommerce html-report-by-date.php template.
|
||||
*
|
||||
* @since 2.1
|
||||
* @return null
|
||||
*/
|
||||
public function output_report() {
|
||||
include( WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'includes/admin/views/html-report-by-period.php' ) );
|
||||
|
|
@ -204,6 +136,7 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
* Output the HTML and JavaScript to plot the chart
|
||||
*
|
||||
* @since 2.1
|
||||
* @return null
|
||||
*/
|
||||
public function get_main_chart() {
|
||||
|
||||
|
|
@ -218,8 +151,6 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
);
|
||||
}
|
||||
|
||||
$x_axes_label = '';
|
||||
|
||||
switch ( $this->report_data->interval_period ) {
|
||||
case 'day':
|
||||
$x_axes_label = _x( 'Number of days after sign-up', 'X axis label on retention rate graph', 'woocommerce-subscriptions' );
|
||||
|
|
@ -281,7 +212,7 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
|
|||
color: '#aaa',
|
||||
position: "bottom",
|
||||
tickDecimals: 0,
|
||||
axisLabel: "<?php esc_js( $x_axes_label ); ?>",
|
||||
axisLabel: "<?php echo esc_js( $x_axes_label ); ?>",
|
||||
axisLabelPadding: 18,
|
||||
font: {
|
||||
color: "#aaa"
|
||||
|
|
|
|||
|
|
@ -11,12 +11,6 @@
|
|||
* @since 2.1
|
||||
*/
|
||||
class WCS_Report_Subscription_By_Customer extends WP_List_Table {
|
||||
/**
|
||||
* Cached report results.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $cached_report_results = array();
|
||||
|
||||
private $totals;
|
||||
|
||||
|
|
@ -31,15 +25,6 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
|
|||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the totals.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_totals() {
|
||||
return $this->totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* No subscription products found text.
|
||||
*/
|
||||
|
|
@ -127,6 +112,8 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
|
|||
* Prepare subscription list items.
|
||||
*/
|
||||
public function prepare_items() {
|
||||
global $wpdb;
|
||||
|
||||
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
|
||||
$current_page = absint( $this->get_pagenum() );
|
||||
$per_page = absint( apply_filters( 'wcs_reports_customers_per_page', 20 ) );
|
||||
|
|
@ -134,489 +121,177 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
|
|||
|
||||
$this->totals = self::get_data();
|
||||
|
||||
$active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
|
||||
$paid_statuses = wcs_maybe_prefix_key( apply_filters( 'woocommerce_reports_paid_order_statuses', [ 'completed', 'processing' ] ), 'wc-' );
|
||||
$query_options = array(
|
||||
'active_statuses' => $active_statuses,
|
||||
'paid_statuses' => $paid_statuses,
|
||||
'offset' => $offset,
|
||||
'per_page' => $per_page,
|
||||
$customer_query = apply_filters( 'wcs_reports_current_customer_query',
|
||||
"SELECT customer_ids.meta_value as customer_id,
|
||||
COUNT(subscription_posts.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_order.ID) as initial_order_count,
|
||||
SUM(CASE
|
||||
WHEN subscription_posts.post_status
|
||||
IN ( 'wc-" . implode( "','wc-", apply_filters( 'wcs_reports_active_statuses', array( 'active', 'pending-cancel' ) ) ) . "' ) THEN 1
|
||||
ELSE 0
|
||||
END) AS active_subscriptions
|
||||
FROM {$wpdb->posts} subscription_posts
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON customer_ids.post_id = subscription_posts.ID
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
LEFT JOIN {$wpdb->posts} parent_order
|
||||
ON parent_order.ID = subscription_posts.post_parent
|
||||
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' )
|
||||
LEFT JOIN {$wpdb->postmeta} parent_total
|
||||
ON parent_total.post_id = parent_order.ID
|
||||
AND parent_total.meta_key = '_order_total'
|
||||
WHERE subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
|
||||
GROUP BY customer_ids.meta_value
|
||||
ORDER BY customer_id DESC
|
||||
LIMIT {$offset}, {$per_page}" );
|
||||
|
||||
$this->items = $wpdb->get_results( $customer_query );
|
||||
|
||||
// Now get each customer's renewal and switch total
|
||||
$customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query',
|
||||
"SELECT
|
||||
customer_ids.meta_value as customer_id,
|
||||
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
|
||||
FROM {$wpdb->postmeta} renewal_order_ids
|
||||
INNER JOIN {$wpdb->posts} subscription_posts
|
||||
ON renewal_order_ids.meta_value = subscription_posts.ID
|
||||
AND subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON renewal_order_ids.meta_value = customer_ids.post_id
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
AND customer_ids.meta_value IN ('" . implode( "','", wp_list_pluck( $this->items, 'customer_id' ) ) . "' )
|
||||
INNER JOIN {$wpdb->posts} renewal_order_posts
|
||||
ON renewal_order_ids.post_id = renewal_order_posts.ID
|
||||
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ) ) . "' )
|
||||
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
|
||||
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
|
||||
AND renewal_switch_totals.meta_key = '_order_total'
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'
|
||||
GROUP BY customer_id
|
||||
ORDER BY customer_id"
|
||||
);
|
||||
|
||||
$this->items = self::fetch_subscriptions_by_customer( $query_options );
|
||||
$customer_ids = wp_list_pluck( $this->items, 'customer_id' );
|
||||
|
||||
$related_orders_query_options = array(
|
||||
'order_status' => $paid_statuses,
|
||||
'customer_ids' => $customer_ids,
|
||||
);
|
||||
|
||||
$related_orders_totals_by_customer = self::fetch_subscriptions_related_orders_totals_by_customer( $related_orders_query_options );
|
||||
$customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K );
|
||||
|
||||
foreach ( $this->items as $index => $item ) {
|
||||
if ( isset( $related_orders_totals_by_customer[ $item->customer_id ] ) ) {
|
||||
$this->items[ $index ]->renewal_switch_total = $related_orders_totals_by_customer[ $item->customer_id ]->renewal_switch_total;
|
||||
$this->items[ $index ]->renewal_switch_count = $related_orders_totals_by_customer[ $item->customer_id ]->renewal_switch_count;
|
||||
if ( isset( $customer_renewal_switch_totals[ $item->customer_id ] ) ) {
|
||||
$this->items[ $index ]->renewal_switch_total = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_total;
|
||||
$this->items[ $index ]->renewal_switch_count = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_count;
|
||||
} else {
|
||||
$this->items[ $index ]->renewal_switch_total = 0;
|
||||
$this->items[ $index ]->renewal_switch_count = 0;
|
||||
$this->items[ $index ]->renewal_switch_total = $this->items[ $index ]->renewal_switch_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination.
|
||||
*/
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $this->totals->total_customers,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => ceil( $this->totals->total_customers / $per_page ),
|
||||
)
|
||||
);
|
||||
$this->set_pagination_args( array(
|
||||
'total_items' => $this->totals->total_customers,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => ceil( $this->totals->total_customers / $per_page ),
|
||||
) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather totals for customers.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return object The totals for customers.
|
||||
*/
|
||||
* Gather totals for customers
|
||||
*/
|
||||
public static function get_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$default_args = array(
|
||||
'no_cache' => false,
|
||||
/**
|
||||
* Filter the order statuses considered as "paid" for the report.
|
||||
*
|
||||
* @param array $order_statuses The default paid order statuses: completed, processing.
|
||||
* @return array The filtered order statuses.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the arguments for the totals of subscriptions by customer report.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return array The filtered arguments.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$args = apply_filters( 'wcs_reports_customer_total_args', $args );
|
||||
$args = wp_parse_args( $args, $default_args );
|
||||
|
||||
self::init_cache();
|
||||
$subscriptions_totals = self::fetch_customer_subscription_totals( $args );
|
||||
$related_orders_totals = self::fetch_customer_subscription_related_orders_totals( $args );
|
||||
$total_query = apply_filters( 'wcs_reports_customer_total_query',
|
||||
"SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers,
|
||||
COUNT(subscription_posts.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_order.ID) as initial_order_count,
|
||||
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), 0) AS active_subscriptions
|
||||
FROM {$wpdb->posts} subscription_posts
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON customer_ids.post_id = subscription_posts.ID
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
LEFT JOIN {$wpdb->posts} parent_order
|
||||
ON parent_order.ID = subscription_posts.post_parent
|
||||
AND parent_order.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
|
||||
LEFT JOIN {$wpdb->postmeta} parent_total
|
||||
ON parent_total.post_id = parent_order.ID
|
||||
AND parent_total.meta_key = '_order_total'
|
||||
WHERE subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
|
||||
");
|
||||
|
||||
$subscriptions_totals->renewal_switch_total = $related_orders_totals->renewal_switch_total;
|
||||
$subscriptions_totals->renewal_switch_count = $related_orders_totals->renewal_switch_count;
|
||||
$cached_results = get_transient( strtolower( __CLASS__ ) );
|
||||
$query_hash = md5( $total_query );
|
||||
|
||||
return $subscriptions_totals;
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( $cached_results ) ) {
|
||||
$cached_results = [];
|
||||
}
|
||||
|
||||
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
// Enable big selects for reports
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) );
|
||||
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
|
||||
$customer_totals = $cached_results[ $query_hash ];
|
||||
|
||||
$renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query',
|
||||
"SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
|
||||
FROM {$wpdb->postmeta} renewal_order_ids
|
||||
INNER JOIN {$wpdb->posts} subscription_posts
|
||||
ON renewal_order_ids.meta_value = subscription_posts.ID
|
||||
AND subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
|
||||
INNER JOIN {$wpdb->posts} renewal_order_posts
|
||||
ON renewal_order_ids.post_id = renewal_order_posts.ID
|
||||
AND renewal_order_posts.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
|
||||
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
|
||||
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
|
||||
AND renewal_switch_totals.meta_key = '_order_total'
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'"
|
||||
);
|
||||
|
||||
$query_hash = md5( $renewal_switch_total_query );
|
||||
|
||||
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
// Enable big selects for reports
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) );
|
||||
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
|
||||
$customer_totals->renewal_switch_total = $cached_results[ $query_hash ]->renewal_switch_total;
|
||||
$customer_totals->renewal_switch_count = $cached_results[ $query_hash ]->renewal_switch_count;
|
||||
|
||||
return $customer_totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached report data.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
public static function clear_cache() {
|
||||
delete_transient( strtolower( __CLASS__ ) );
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch totals by customer for subscriptions.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return object The totals by customer for subscriptions.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function fetch_customer_subscription_totals( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
/**
|
||||
* Filter the active subscription statuses used for reporting.
|
||||
*
|
||||
* @param array $active_statuses The default active subscription statuses: active, pending-cancel.
|
||||
* @return array The filtered active statuses.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
|
||||
$order_statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
|
||||
|
||||
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
|
||||
$order_statuses_placeholders = implode( ',', array_fill( 0, count( $order_statuses ), '%s' ) );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT( DISTINCT subscriptions.customer_id) as total_customers,
|
||||
COUNT(subscriptions.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_orders.total_amount), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_orders.ID) as initial_order_count,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN subscriptions.status
|
||||
IN ( {$active_statuses_placeholders} ) THEN 1
|
||||
ELSE 0
|
||||
END), 0) AS active_subscriptions
|
||||
FROM {$wpdb->prefix}wc_orders subscriptions
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders parent_orders
|
||||
ON parent_orders.ID = subscriptions.parent_order_id
|
||||
AND parent_orders.status IN ( {$order_statuses_placeholders} )
|
||||
WHERE subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
|
||||
array_merge( $active_statuses, $order_statuses )
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers,
|
||||
COUNT(subscription_posts.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_order.ID) as initial_order_count,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN subscription_posts.post_status
|
||||
IN ( {$active_statuses_placeholders} ) THEN 1
|
||||
ELSE 0
|
||||
END), 0) AS active_subscriptions
|
||||
FROM {$wpdb->posts} subscription_posts
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON customer_ids.post_id = subscription_posts.ID
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
LEFT JOIN {$wpdb->posts} parent_order
|
||||
ON parent_order.ID = subscription_posts.post_parent
|
||||
AND parent_order.post_status IN ( {$order_statuses_placeholders} )
|
||||
LEFT JOIN {$wpdb->postmeta} parent_total
|
||||
ON parent_total.post_id = parent_order.ID
|
||||
AND parent_total.meta_key = '_order_total'
|
||||
WHERE subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
|
||||
array_merge( $active_statuses, $order_statuses )
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
|
||||
|
||||
/**
|
||||
* Filter the query used to fetch the customer subscription totals.
|
||||
*
|
||||
* @param string $query The query to fetch the customer subscription totals.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_customer_total_query', $query );
|
||||
$query_hash = md5( $query );
|
||||
|
||||
// We expect that cache was initialized before calling this method.
|
||||
// Skip running the query if cache is available.
|
||||
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$query_results = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
|
||||
/**
|
||||
* Filter the query results for customer totals.
|
||||
*
|
||||
* @param object $query_results The query results.
|
||||
* @return object The filtered query results.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query_results = apply_filters( 'wcs_reports_customer_total_data', $query_results );
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch totals by customer for related renewal and switch orders.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return object The totals by customer for related renewal and switch orders.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function fetch_customer_subscription_related_orders_totals( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$status_placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
|
||||
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COALESCE( SUM(renewal_orders.total_amount), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_orders.ID) as renewal_switch_count
|
||||
FROM {$wpdb->prefix}wc_orders_meta renewal_order_ids
|
||||
INNER JOIN {$wpdb->prefix}wc_orders subscriptions
|
||||
ON renewal_order_ids.meta_value = subscriptions.ID
|
||||
AND subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
INNER JOIN {$wpdb->prefix}wc_orders renewal_orders
|
||||
ON renewal_order_ids.order_id = renewal_orders.ID
|
||||
AND renewal_orders.status IN ( {$status_placeholders} )
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'
|
||||
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
|
||||
$statuses
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
|
||||
FROM {$wpdb->postmeta} renewal_order_ids
|
||||
INNER JOIN {$wpdb->posts} subscription_posts
|
||||
ON renewal_order_ids.meta_value = subscription_posts.ID
|
||||
AND subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
INNER JOIN {$wpdb->posts} renewal_order_posts
|
||||
ON renewal_order_ids.post_id = renewal_order_posts.ID
|
||||
AND renewal_order_posts.post_status IN ( {$status_placeholders} )
|
||||
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
|
||||
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
|
||||
AND renewal_switch_totals.meta_key = '_order_total'
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'
|
||||
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
|
||||
$statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
|
||||
|
||||
/**
|
||||
* Filter the query used to fetch the customer subscription related orders totals.
|
||||
*
|
||||
* @param string $query The query to fetch the customer subscription related orders totals.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query', $query );
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
// Enable big selects for reports
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$query_results = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
|
||||
/**
|
||||
* Filter the query results for customer subscription related orders totals.
|
||||
*
|
||||
* @param object $query_results The query results.
|
||||
* @return object The filtered query results.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query_results = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $query_results );
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch subscriptions by customer.
|
||||
*
|
||||
* @param array $query_options The query options.
|
||||
* @return array The subscriptions by customer.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
private static function fetch_subscriptions_by_customer( $query_options = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$active_statuses = $query_options['active_statuses'] ?? array();
|
||||
$paid_statuses = $query_options['paid_statuses'] ?? array();
|
||||
$offset = $query_options['offset'] ?? 0;
|
||||
$per_page = $query_options['per_page'] ?? 20;
|
||||
|
||||
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
|
||||
$paid_statuses_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
|
||||
|
||||
// Ignored for allowing interpolation in the IN statements.
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT subscriptions.customer_id as customer_id,
|
||||
COUNT(subscriptions.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_order.total_amount), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_order.ID) as initial_order_count,
|
||||
SUM(CASE
|
||||
WHEN subscriptions.status
|
||||
IN ( {$active_statuses_placeholders} ) THEN 1
|
||||
ELSE 0
|
||||
END) AS active_subscriptions
|
||||
FROM {$wpdb->prefix}wc_orders subscriptions
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders parent_order
|
||||
ON parent_order.ID = subscriptions.parent_order_id
|
||||
AND parent_order.status IN ( {$paid_statuses_placeholders} )
|
||||
WHERE subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status NOT IN ('wc-pending','auto-draft', 'wc-checkout-draft', 'trash')
|
||||
GROUP BY subscriptions.customer_id
|
||||
ORDER BY customer_id DESC
|
||||
LIMIT %d, %d
|
||||
",
|
||||
array_merge( $active_statuses, $paid_statuses, array( $offset, $per_page ) )
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT customer_ids.meta_value as customer_id,
|
||||
COUNT(subscription_posts.ID) as total_subscriptions,
|
||||
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
|
||||
COUNT(DISTINCT parent_order.ID) as initial_order_count,
|
||||
SUM(CASE
|
||||
WHEN subscription_posts.post_status
|
||||
IN ( {$active_statuses_placeholders} ) THEN 1
|
||||
ELSE 0
|
||||
END) AS active_subscriptions
|
||||
FROM {$wpdb->posts} subscription_posts
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON customer_ids.post_id = subscription_posts.ID
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
LEFT JOIN {$wpdb->posts} parent_order
|
||||
ON parent_order.ID = subscription_posts.post_parent
|
||||
AND parent_order.post_status IN ( {$paid_statuses_placeholders} )
|
||||
LEFT JOIN {$wpdb->postmeta} parent_total
|
||||
ON parent_total.post_id = parent_order.ID
|
||||
AND parent_total.meta_key = '_order_total'
|
||||
WHERE subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
GROUP BY customer_ids.meta_value
|
||||
ORDER BY customer_id DESC
|
||||
LIMIT %d, %d
|
||||
",
|
||||
array_merge( $active_statuses, $paid_statuses, array( $offset, $per_page ) )
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
|
||||
/**
|
||||
* Filter the query used to fetch the subscriptions by customer.
|
||||
*
|
||||
* @param string $query The query to fetch the subscriptions by customer.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_current_customer_query', $query );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
return $wpdb->get_results( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch totals by customer for related renewal and switch orders.
|
||||
*
|
||||
* @param array $query_options The query options.
|
||||
* @return array The totals by customer for related renewal and switch orders.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
private static function fetch_subscriptions_related_orders_totals_by_customer( $query_options = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$paid_statuses = $query_options['order_status'] ?? array();
|
||||
$customer_ids = $query_options['customer_ids'] ?? array();
|
||||
|
||||
$customer_placeholders = implode( ',', array_fill( 0, count( $customer_ids ), '%s' ) );
|
||||
$status_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
renewal_orders.customer_id as customer_id,
|
||||
COALESCE( SUM(renewal_orders.total_amount), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_orders.ID) as renewal_switch_count
|
||||
FROM {$wpdb->prefix}wc_orders_meta renewal_order_ids
|
||||
INNER JOIN {$wpdb->prefix}wc_orders subscriptions
|
||||
ON renewal_order_ids.meta_value = subscriptions.ID
|
||||
AND subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
INNER JOIN {$wpdb->prefix}wc_orders renewal_orders
|
||||
ON renewal_order_ids.order_id = renewal_orders.ID
|
||||
AND renewal_orders.status IN ( {$status_placeholders} )
|
||||
AND renewal_orders.customer_id IN ( {$customer_placeholders} )
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'
|
||||
GROUP BY renewal_orders.customer_id
|
||||
ORDER BY renewal_orders.customer_id
|
||||
",
|
||||
array_merge( $paid_statuses, $customer_ids )
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
customer_ids.meta_value as customer_id,
|
||||
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
|
||||
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
|
||||
FROM {$wpdb->postmeta} renewal_order_ids
|
||||
INNER JOIN {$wpdb->posts} subscription_posts
|
||||
ON renewal_order_ids.meta_value = subscription_posts.ID
|
||||
AND subscription_posts.post_type = 'shop_subscription'
|
||||
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
|
||||
INNER JOIN {$wpdb->postmeta} customer_ids
|
||||
ON renewal_order_ids.meta_value = customer_ids.post_id
|
||||
AND customer_ids.meta_key = '_customer_user'
|
||||
AND customer_ids.meta_value IN ( {$customer_placeholders} )
|
||||
INNER JOIN {$wpdb->posts} renewal_order_posts
|
||||
ON renewal_order_ids.post_id = renewal_order_posts.ID
|
||||
AND renewal_order_posts.post_status IN ( {$status_placeholders} )
|
||||
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
|
||||
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
|
||||
AND renewal_switch_totals.meta_key = '_order_total'
|
||||
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
|
||||
OR renewal_order_ids.meta_key = '_subscription_switch'
|
||||
GROUP BY customer_id
|
||||
ORDER BY customer_id
|
||||
",
|
||||
array_merge( $customer_ids, $paid_statuses )
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare.
|
||||
|
||||
/**
|
||||
* Filter the query used to fetch the totals by customer for related renewal and switch orders.
|
||||
*
|
||||
* @param string $query The query to fetch the totals by customer for related renewal and switch orders.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query', $query );
|
||||
|
||||
return $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cache for report results.
|
||||
*/
|
||||
private static function init_cache() {
|
||||
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
|
||||
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( self::$cached_report_results ) ) {
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache report results.
|
||||
*
|
||||
* @param string $query_hash The query hash.
|
||||
* @param array $report_data The report data.
|
||||
*/
|
||||
private static function cache_report_results( $query_hash, $report_data ) {
|
||||
self::$cached_report_results[ $query_hash ] = $report_data;
|
||||
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,24 +12,15 @@
|
|||
*/
|
||||
class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
||||
|
||||
/**
|
||||
* Cached report results.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $cached_report_results = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => __( 'Product', 'woocommerce-subscriptions' ),
|
||||
'plural' => __( 'Products', 'woocommerce-subscriptions' ),
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
parent::__construct( array(
|
||||
'singular' => __( 'Product', 'woocommerce-subscriptions' ),
|
||||
'plural' => __( 'Products', 'woocommerce-subscriptions' ),
|
||||
'ajax' => false,
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,11 +57,11 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
|||
case 'product_name':
|
||||
// If the product is a subscription variation, use the parent product's edit post link
|
||||
if ( $report_item->parent_product_id > 0 ) {
|
||||
edit_post_link( $report_item->product_name, ' - ', null, $report_item->parent_product_id );
|
||||
return edit_post_link( $report_item->product_name, ' - ', null, $report_item->parent_product_id );
|
||||
} else {
|
||||
edit_post_link( $report_item->product_name, null, null, $report_item->product_id );
|
||||
return edit_post_link( $report_item->product_name, null, null, $report_item->product_id );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'subscription_count':
|
||||
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_subscription&_wcs_product=' ), $report_item->product_id, $report_item->subscription_count );
|
||||
|
||||
|
|
@ -117,37 +108,126 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
|||
|
||||
/**
|
||||
* Get subscription product data, either from the cache or the database.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return array The subscription product data.
|
||||
*/
|
||||
public static function get_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$default_args = array(
|
||||
'no_cache' => false,
|
||||
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the arguments for the subscription by product report.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return array The filtered arguments.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$args = apply_filters( 'wcs_reports_product_args', $args );
|
||||
$args = wp_parse_args( $args, $default_args );
|
||||
|
||||
self::init_cache();
|
||||
$subscriptions_by_product = self::fetch_subscription_products_data( $args );
|
||||
$subscription_product_totals = self::fetch_product_totals_data( $args );
|
||||
$ordered_report_data = self::organize_subscription_products_data( $subscriptions_by_product );
|
||||
$query = apply_filters( 'wcs_reports_product_query',
|
||||
"SELECT product.id as product_id,
|
||||
product.post_parent as parent_product_id,
|
||||
product.post_title as product_name,
|
||||
mo.product_type,
|
||||
COUNT(subscription_line_items.subscription_id) as subscription_count,
|
||||
SUM(subscription_line_items.product_total) as recurring_total
|
||||
FROM {$wpdb->posts} AS product
|
||||
LEFT JOIN (
|
||||
SELECT tr.object_id AS product_id, t.slug AS product_type
|
||||
FROM {$wpdb->prefix}term_relationships AS tr
|
||||
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
|
||||
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
|
||||
INNER JOIN {$wpdb->prefix}terms AS t
|
||||
ON t.term_id = x.term_id
|
||||
) AS mo
|
||||
ON product.id = mo.product_id
|
||||
LEFT JOIN (
|
||||
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE wcoitems.order_item_type = 'line_item'
|
||||
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
) as subscription_line_items
|
||||
ON product.id = subscription_line_items.product_id
|
||||
LEFT JOIN {$wpdb->posts} as subscriptions
|
||||
ON subscriptions.ID = subscription_line_items.subscription_id
|
||||
WHERE product.post_status = 'publish'
|
||||
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
|
||||
AND subscriptions.post_type = 'shop_subscription'
|
||||
AND subscriptions.post_status NOT IN( 'wc-pending', 'trash' )
|
||||
GROUP BY product.id
|
||||
ORDER BY COUNT(subscription_line_items.subscription_id) DESC" );
|
||||
|
||||
$cached_results = get_transient( strtolower( __CLASS__ ) );
|
||||
$query_hash = md5( $query );
|
||||
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( $cached_results ) ) {
|
||||
$cached_results = [];
|
||||
}
|
||||
|
||||
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_data', $wpdb->get_results( $query, OBJECT_K ), $args );
|
||||
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
|
||||
$report_data = $cached_results[ $query_hash ];
|
||||
|
||||
// Organize subscription variations under the parent product in a tree structure
|
||||
$tree = array();
|
||||
foreach ( $report_data as $product_id => $product ) {
|
||||
if ( ! $product->parent_product_id ) {
|
||||
if ( isset( $tree[ $product_id ] ) ) {
|
||||
array_unshift( $tree[ $product_id ], $product_id );
|
||||
} else {
|
||||
$tree[ $product_id ][] = $product_id;
|
||||
}
|
||||
} else {
|
||||
$tree[ $product->parent_product_id ][] = $product_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an array with all the report data in the correct order
|
||||
$ordered_report_data = array();
|
||||
foreach ( $tree as $parent_id => $children ) {
|
||||
foreach ( $children as $child_id ) {
|
||||
$ordered_report_data[ $child_id ] = $report_data[ $child_id ];
|
||||
|
||||
// When there are variations, store the variation ids.
|
||||
if ( 'variable-subscription' === $report_data[ $child_id ]->product_type ) {
|
||||
$ordered_report_data[ $child_id ]->variations = array_diff( $children, array( $parent_id ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now let's get the total revenue for each product so we can provide an average lifetime value for that product
|
||||
$query = apply_filters( 'wcs_reports_product_lifetime_value_query',
|
||||
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->posts} AS wcorders
|
||||
ON wcoitems.order_id = wcorders.ID
|
||||
AND wcorders.post_type = 'shop_order'
|
||||
AND wcorders.post_status IN ( 'wc-" . implode( "','wc-", $args['order_status'] ) . "' )
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
GROUP BY product_id" );
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_lifetime_value_data', $wpdb->get_results( $query, OBJECT_K ), $args );
|
||||
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
|
||||
// Add the product total to each item
|
||||
foreach ( array_keys( $ordered_report_data ) as $product_id ) {
|
||||
$ordered_report_data[ $product_id ]->product_total = isset( $subscription_product_totals[ $product_id ] ) ? $subscription_product_totals[ $product_id ]->product_total : 0;
|
||||
$ordered_report_data[ $product_id ]->product_total = isset( $cached_results[ $query_hash ][ $product_id ] ) ? $cached_results[ $query_hash ][ $product_id ]->product_total : 0;
|
||||
}
|
||||
|
||||
return $ordered_report_data;
|
||||
|
|
@ -189,11 +269,11 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
|||
jQuery('.chart-placeholder.variation_breakdown_chart'),
|
||||
[
|
||||
<?php
|
||||
$colorindex = -1;
|
||||
$colorindex = -1;
|
||||
$last_parent_id = -1;
|
||||
foreach ( $variations as $product ) {
|
||||
if ( '0' === $product->parent_product_id || $last_parent_id !== $product->parent_product_id ) {
|
||||
++$colorindex;
|
||||
$colorindex++;
|
||||
$last_parent_id = $product->parent_product_id;
|
||||
}
|
||||
?>
|
||||
|
|
@ -243,7 +323,7 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
|||
color: '<?php echo esc_js( $chart_colors[ $i ] ); ?>'
|
||||
},
|
||||
<?php
|
||||
++$i;
|
||||
$i++;
|
||||
}
|
||||
?>
|
||||
],
|
||||
|
|
@ -280,261 +360,9 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
|
|||
/**
|
||||
* Clears the cached report data.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
public static function clear_cache() {
|
||||
delete_transient( strtolower( __CLASS__ ) );
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
|
||||
private static function fetch_subscription_products_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = "SELECT product.id as product_id,
|
||||
product.post_parent as parent_product_id,
|
||||
product.post_title as product_name,
|
||||
mo.product_type,
|
||||
COUNT(subscription_line_items.subscription_id) as subscription_count,
|
||||
SUM(subscription_line_items.product_total) as recurring_total
|
||||
FROM {$wpdb->posts} AS product
|
||||
LEFT JOIN (
|
||||
SELECT tr.object_id AS product_id, t.slug AS product_type
|
||||
FROM {$wpdb->prefix}term_relationships AS tr
|
||||
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
|
||||
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
|
||||
INNER JOIN {$wpdb->prefix}terms AS t
|
||||
ON t.term_id = x.term_id
|
||||
) AS mo
|
||||
ON product.id = mo.product_id
|
||||
LEFT JOIN (
|
||||
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE wcoitems.order_item_type = 'line_item'
|
||||
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
) as subscription_line_items
|
||||
ON product.id = subscription_line_items.product_id
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders as subscriptions
|
||||
ON subscriptions.ID = subscription_line_items.subscription_id
|
||||
WHERE product.post_status = 'publish'
|
||||
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
|
||||
AND subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status NOT IN( 'wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash' )
|
||||
GROUP BY product.id
|
||||
ORDER BY COUNT(subscription_line_items.subscription_id) DESC";
|
||||
} else {
|
||||
$query = "SELECT product.id as product_id,
|
||||
product.post_parent as parent_product_id,
|
||||
product.post_title as product_name,
|
||||
mo.product_type,
|
||||
COUNT(subscription_line_items.subscription_id) as subscription_count,
|
||||
SUM(subscription_line_items.product_total) as recurring_total
|
||||
FROM {$wpdb->posts} AS product
|
||||
LEFT JOIN (
|
||||
SELECT tr.object_id AS product_id, t.slug AS product_type
|
||||
FROM {$wpdb->prefix}term_relationships AS tr
|
||||
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
|
||||
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
|
||||
INNER JOIN {$wpdb->prefix}terms AS t
|
||||
ON t.term_id = x.term_id
|
||||
) AS mo
|
||||
ON product.id = mo.product_id
|
||||
LEFT JOIN (
|
||||
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE wcoitems.order_item_type = 'line_item'
|
||||
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
) as subscription_line_items
|
||||
ON product.id = subscription_line_items.product_id
|
||||
LEFT JOIN {$wpdb->posts} as subscriptions
|
||||
ON subscriptions.ID = subscription_line_items.subscription_id
|
||||
WHERE product.post_status = 'publish'
|
||||
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
|
||||
AND subscriptions.post_type = 'shop_subscription'
|
||||
AND subscriptions.post_status NOT IN( 'wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash' )
|
||||
GROUP BY product.id
|
||||
ORDER BY COUNT(subscription_line_items.subscription_id) DESC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the query to get the subscription products data.
|
||||
*
|
||||
* @param string $query The query to get the subscription products data.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_product_query', $query );
|
||||
$query_hash = md5( $query );
|
||||
|
||||
// We expect that cache was initialized before calling this method.
|
||||
// Skip running the query if cache is available.
|
||||
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$query_results = (array) $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
/**
|
||||
* Filter the query results for subscription products.
|
||||
*
|
||||
* @param array $query_results The query results.
|
||||
* @param array $args The arguments for the report.
|
||||
* @return array The filtered query results.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query_results = apply_filters( 'wcs_reports_product_data', $query_results, $args );
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize subscription products data for futher reporting.
|
||||
*
|
||||
* Group subscription product variations under variable subscription products.
|
||||
*
|
||||
* @param array $report_data The report data.
|
||||
* @return array The organized report data.
|
||||
*/
|
||||
private static function organize_subscription_products_data( $report_data ) {
|
||||
$tree = array();
|
||||
foreach ( $report_data as $product_id => $product ) {
|
||||
if ( ! $product->parent_product_id ) {
|
||||
if ( isset( $tree[ $product_id ] ) ) {
|
||||
array_unshift( $tree[ $product_id ], $product_id );
|
||||
} else {
|
||||
$tree[ $product_id ][] = $product_id;
|
||||
}
|
||||
} else {
|
||||
$tree[ $product->parent_product_id ][] = $product_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an array with all the report data in the correct order
|
||||
$ordered_report_data = array();
|
||||
foreach ( $tree as $parent_id => $children ) {
|
||||
foreach ( $children as $child_id ) {
|
||||
$ordered_report_data[ $child_id ] = $report_data[ $child_id ];
|
||||
|
||||
// When there are variations, store the variation ids.
|
||||
if ( 'variable-subscription' === $report_data[ $child_id ]->product_type ) {
|
||||
$ordered_report_data[ $child_id ]->variations = array_diff( $children, array( $parent_id ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered_report_data;
|
||||
}
|
||||
|
||||
private static function fetch_product_totals_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
$placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
|
||||
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->prefix}wc_orders AS wcorders
|
||||
ON wcoitems.order_id = wcorders.ID
|
||||
AND wcorders.type = 'shop_order'
|
||||
AND wcorders.status IN ( {$placeholders} )
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
GROUP BY product_id",
|
||||
$statuses
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
|
||||
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
|
||||
INNER JOIN {$wpdb->posts} AS wcorders
|
||||
ON wcoitems.order_id = wcorders.ID
|
||||
AND wcorders.post_type = 'shop_order'
|
||||
AND wcorders.post_status IN ( {$placeholders} )
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
|
||||
ON wcoimeta.order_item_id = wcoitems.order_item_id
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
|
||||
ON wcoimeta2.order_item_id = wcoitems.order_item_id
|
||||
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
|
||||
AND wcoimeta2.meta_key = '_line_total'
|
||||
GROUP BY product_id",
|
||||
$statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
||||
/**
|
||||
* Filter the query to get the product totals data.
|
||||
*
|
||||
* @param string $query The query to get the product totals data.
|
||||
* @return string The filtered query.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query = apply_filters( 'wcs_reports_product_lifetime_value_query', $query );
|
||||
|
||||
$query_hash = md5( $query );
|
||||
|
||||
// We expect that cache was initialized before calling this method.
|
||||
// Skip running the query if cache is available.
|
||||
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$query_results = (array) $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
/**
|
||||
* Filter the query results for product totals.
|
||||
*
|
||||
* @param array $query_results The query results.
|
||||
* @param array $args The arguments for the report.
|
||||
* @return array The filtered query results.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$query_results = apply_filters( 'wcs_reports_product_lifetime_value_data', $query_results, $args );
|
||||
self::cache_report_results( $query_hash, $query_results );
|
||||
}
|
||||
|
||||
return self::$cached_report_results[ $query_hash ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cache for report results.
|
||||
*/
|
||||
private static function init_cache() {
|
||||
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
|
||||
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( self::$cached_report_results ) ) {
|
||||
self::$cached_report_results = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache report results.
|
||||
*
|
||||
* @param string $query_hash The query hash.
|
||||
* @param array $report_data The report data.
|
||||
*/
|
||||
private static function cache_report_results( $query_hash, $report_data ) {
|
||||
self::$cached_report_results[ $query_hash ] = $report_data;
|
||||
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,7 +18,7 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
|
|||
|
||||
/**
|
||||
* Get report data
|
||||
* @return stdClass
|
||||
* @return array
|
||||
*/
|
||||
public function get_report_data() {
|
||||
|
||||
|
|
@ -36,35 +36,65 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
|
|||
global $wpdb;
|
||||
|
||||
// Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
|
||||
$offset = get_option( 'gmt_offset' );
|
||||
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
|
||||
$retry_date_in_local_time_query = $wpdb->prepare( "CONVERT_TZ(retries.date_gmt, '+00:00', %s)", $site_timezone );
|
||||
$offset = get_option( 'gmt_offset' );
|
||||
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
|
||||
$retry_date_in_local_time = $wpdb->prepare( "CONVERT_TZ(retries.date_gmt, '+00:00', %s)", $site_timezone );
|
||||
|
||||
// We need to compute this on our own since 'group_by_query' from the parent class uses posts table column names.
|
||||
switch ( $this->chart_groupby ) {
|
||||
case 'day':
|
||||
$this->group_by_query = "YEAR({$retry_date_in_local_time_query}), MONTH({$retry_date_in_local_time_query}), DAY({$retry_date_in_local_time_query})";
|
||||
$this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time}), DAY({$retry_date_in_local_time})";
|
||||
break;
|
||||
case 'month':
|
||||
$this->group_by_query = "YEAR({$retry_date_in_local_time_query}), MONTH({$retry_date_in_local_time_query})";
|
||||
$this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time})";
|
||||
break;
|
||||
}
|
||||
|
||||
$this->report_data = new stdClass;
|
||||
|
||||
$query_options = array(
|
||||
'site_timezone' => $site_timezone,
|
||||
'query_start_date' => get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
'query_end_date' => get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
$query_start_date = get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) );
|
||||
$query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
|
||||
|
||||
// Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
|
||||
$renewal_query = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals
|
||||
FROM {$wpdb->posts} AS orders
|
||||
INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id )
|
||||
LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
|
||||
WHERE retries.status = 'complete'
|
||||
AND retries.date_gmt >= %s
|
||||
AND retries.date_gmt < %s
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY retry_date_gmt ASC
|
||||
",
|
||||
$query_start_date,
|
||||
$query_end_date
|
||||
);
|
||||
|
||||
$this->fetch_renewal_data( $query_options );
|
||||
$this->fetch_retry_data( $query_options );
|
||||
$this->report_data->renewal_data = $wpdb->get_results( $renewal_query );
|
||||
|
||||
// Get the counts for all retries, grouped by day or month and status
|
||||
$retry_query = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date
|
||||
FROM {$wpdb->prefix}wcs_payment_retries AS retries
|
||||
WHERE retries.status IN ( 'complete', 'failed', 'pending' )
|
||||
AND retries.date_gmt >= %s
|
||||
AND retries.date_gmt < %s
|
||||
GROUP BY {$this->group_by_query}, status
|
||||
ORDER BY retry_date_gmt ASC
|
||||
",
|
||||
$query_start_date,
|
||||
$query_end_date
|
||||
);
|
||||
|
||||
$this->report_data->retry_data = $wpdb->get_results( $retry_query );
|
||||
|
||||
// Total up the query data
|
||||
$this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );
|
||||
$this->report_data->retry_success_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'count' ) ) );
|
||||
$this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) );
|
||||
$this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );
|
||||
$this->report_data->retry_success_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'count' ) ) );
|
||||
$this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) );
|
||||
|
||||
$this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
|
||||
$this->report_data->renewal_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
|
||||
|
|
@ -174,7 +204,7 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
|
|||
/**
|
||||
* Get the main chart
|
||||
*
|
||||
* @return void
|
||||
* @return string
|
||||
*/
|
||||
public function get_main_chart() {
|
||||
global $wp_locale;
|
||||
|
|
@ -378,86 +408,4 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
|
|||
return wc_format_decimal( $amount, wc_get_price_decimals() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
|
||||
*
|
||||
* @param array $query_options Query options.
|
||||
*/
|
||||
private function fetch_renewal_data( $query_options ) {
|
||||
global $wpdb;
|
||||
$site_timezone = $query_options['site_timezone'];
|
||||
$query_start_date = $query_options['query_start_date'];
|
||||
$query_end_date = $query_options['query_end_date'];
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) AS retry_date, SUM(orders.total_amount) AS renewal_totals
|
||||
FROM {$wpdb->prefix}wcs_payment_retries AS retries
|
||||
INNER JOIN {$wpdb->prefix}wc_orders AS orders ON ( orders.id = retries.order_id )
|
||||
WHERE retries.status = 'complete'
|
||||
AND retries.date_gmt >= %s
|
||||
AND retries.date_gmt < %s
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY retry_date_gmt ASC
|
||||
",
|
||||
$site_timezone,
|
||||
$query_start_date,
|
||||
$query_end_date
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals
|
||||
FROM {$wpdb->posts} AS orders
|
||||
INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id )
|
||||
LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
|
||||
WHERE retries.status = 'complete'
|
||||
AND retries.date_gmt >= %s
|
||||
AND retries.date_gmt < %s
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY retry_date_gmt ASC
|
||||
",
|
||||
$site_timezone,
|
||||
$query_start_date,
|
||||
$query_end_date
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
$this->report_data->renewal_data = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the counts for all retries, grouped by day or month and status
|
||||
*
|
||||
* @param array $query_options Query options.
|
||||
*/
|
||||
private function fetch_retry_data( $query_options ) {
|
||||
global $wpdb;
|
||||
$site_timezone = $query_options['site_timezone'];
|
||||
$query_start_date = $query_options['query_start_date'];
|
||||
$query_end_date = $query_options['query_end_date'];
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
|
||||
// We don't use HPOS tables here, so we can use it for both CPT and HPOS data stores.
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) AS retry_date
|
||||
FROM {$wpdb->prefix}wcs_payment_retries AS retries
|
||||
WHERE retries.status IN ( 'complete', 'failed', 'pending' )
|
||||
AND retries.date_gmt >= %s
|
||||
AND retries.date_gmt < %s
|
||||
GROUP BY {$this->group_by_query}, status
|
||||
ORDER BY retry_date_gmt ASC
|
||||
",
|
||||
$site_timezone,
|
||||
$query_start_date,
|
||||
$query_end_date
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$this->report_data->retry_data = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
}
|
||||
}
|
||||
} while ( $next_payment_timestamp > 0 && $next_payment_timestamp <= $this->end_date
|
||||
&& isset( $scheduled_ends[ $key ] )
|
||||
&& isset( $key, $scheduled_ends[ $key ] )
|
||||
&& ( 0 == $scheduled_ends[ $key ] || $next_payment_timestamp < strtotime( $scheduled_ends[ $key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -117,11 +117,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
|
||||
/**
|
||||
* Get report data.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
|
||||
*
|
||||
* @param array $args The arguments for the report.
|
||||
* @return stdClass[] - Upcoming renewal data grouped by scheduled date.
|
||||
* @return stdClass
|
||||
*/
|
||||
public function get_data( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
|
@ -134,100 +130,53 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
$args = wp_parse_args( $args, $default_args );
|
||||
|
||||
// Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
DATE(meta_next_payment.meta_value) as scheduled_date,
|
||||
SUM(subscriptions.total_amount) as recurring_total,
|
||||
COUNT(subscriptions.total_amount) as total_renewals,
|
||||
group_concat(subscriptions.ID) as subscription_ids,
|
||||
group_concat(meta_billing_interval.meta_value) as billing_intervals,
|
||||
group_concat(meta_billing_period.meta_value) as billing_periods,
|
||||
group_concat(meta_schedule_end.meta_value) as scheduled_ends,
|
||||
group_concat(subscriptions.total_amount) as subscription_totals
|
||||
FROM {$wpdb->prefix}wc_orders subscriptions
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_next_payment
|
||||
ON subscriptions.ID = meta_next_payment.order_id
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_billing_interval
|
||||
ON subscriptions.ID = meta_billing_interval.order_id
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_billing_period
|
||||
ON subscriptions.ID = meta_billing_period.order_id
|
||||
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_schedule_end
|
||||
ON subscriptions.ID = meta_schedule_end.order_id
|
||||
WHERE subscriptions.type = 'shop_subscription'
|
||||
AND subscriptions.status = 'wc-active'
|
||||
AND meta_next_payment.meta_key = '_schedule_next_payment'
|
||||
AND ( ( meta_next_payment.meta_value < %s AND meta_schedule_end.meta_value = 0 ) OR ( meta_schedule_end.meta_value > %s AND meta_next_payment.meta_value < %s ) )
|
||||
AND meta_billing_interval.meta_key = '_billing_interval'
|
||||
AND meta_billing_period.meta_key = '_billing_period'
|
||||
AND meta_schedule_end.meta_key = '_schedule_end'
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY meta_next_payment.meta_value ASC",
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
date( 'Y-m-d', $this->start_date ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
);
|
||||
} else {
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
DATE(meta_next_payment.meta_value) as scheduled_date,
|
||||
SUM(meta_order_total.meta_value) as recurring_total,
|
||||
COUNT(meta_order_total.meta_value) as total_renewals,
|
||||
group_concat(posts.ID) as subscription_ids,
|
||||
group_concat(meta_billing_interval.meta_value) as billing_intervals,
|
||||
group_concat(meta_billing_period.meta_value) as billing_periods,
|
||||
group_concat(meta_schedule_end.meta_value) as scheduled_ends,
|
||||
group_concat(meta_order_total.meta_value) as subscription_totals
|
||||
FROM {$wpdb->prefix}posts posts
|
||||
LEFT JOIN {$wpdb->prefix}postmeta meta_next_payment
|
||||
ON posts.ID = meta_next_payment.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta meta_order_total
|
||||
ON posts.ID = meta_order_total.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta meta_billing_interval
|
||||
ON posts.ID = meta_billing_interval.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta meta_billing_period
|
||||
ON posts.ID = meta_billing_period.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta meta_schedule_end
|
||||
ON posts.ID = meta_schedule_end.post_id
|
||||
WHERE posts.post_type = 'shop_subscription'
|
||||
AND posts.post_status = 'wc-active'
|
||||
AND meta_order_total.meta_key = '_order_total'
|
||||
AND meta_next_payment.meta_key = '_schedule_next_payment'
|
||||
AND ( ( meta_next_payment.meta_value < %s AND meta_schedule_end.meta_value = 0 ) OR ( meta_schedule_end.meta_value > %s AND meta_next_payment.meta_value < %s ) )
|
||||
AND meta_billing_interval.meta_key = '_billing_interval'
|
||||
AND meta_billing_period.meta_key = '_billing_period'
|
||||
AND meta_schedule_end.meta_key = '_schedule_end'
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY meta_next_payment.meta_value ASC",
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
date( 'Y-m-d', $this->start_date ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$base_query = $wpdb->prepare(
|
||||
"SELECT
|
||||
DATE(ms.meta_value) as scheduled_date,
|
||||
SUM(mo.meta_value) as recurring_total,
|
||||
COUNT(mo.meta_value) as total_renewals,
|
||||
group_concat(p.ID) as subscription_ids,
|
||||
group_concat(mi.meta_value) as billing_intervals,
|
||||
group_concat(mp.meta_value) as billing_periods,
|
||||
group_concat(me.meta_value) as scheduled_ends,
|
||||
group_concat(mo.meta_value) as subscription_totals
|
||||
FROM {$wpdb->prefix}posts p
|
||||
LEFT JOIN {$wpdb->prefix}postmeta ms
|
||||
ON p.ID = ms.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta mo
|
||||
ON p.ID = mo.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta mi
|
||||
ON p.ID = mi.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta mp
|
||||
ON p.ID = mp.post_id
|
||||
LEFT JOIN {$wpdb->prefix}postmeta me
|
||||
ON p.ID = me.post_id
|
||||
WHERE p.post_type = 'shop_subscription'
|
||||
AND p.post_status = 'wc-active'
|
||||
AND mo.meta_key = '_order_total'
|
||||
AND ms.meta_key = '_schedule_next_payment'
|
||||
AND ( ( ms.meta_value < '%s' AND me.meta_value = 0 ) OR ( me.meta_value > '%s' AND ms.meta_value < '%s' ) )
|
||||
AND mi.meta_key = '_billing_interval'
|
||||
AND mp.meta_key = '_billing_period'
|
||||
AND me.meta_key = '_schedule_end '
|
||||
GROUP BY {$this->group_by_query}
|
||||
ORDER BY ms.meta_value ASC",
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ),
|
||||
date( 'Y-m-d', $this->start_date ),
|
||||
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) )
|
||||
);
|
||||
|
||||
$cached_results = get_transient( strtolower( get_class( $this ) ) );
|
||||
$query_hash = md5( $query );
|
||||
$query_hash = md5( $base_query );
|
||||
|
||||
// Set a default value for cached results for PHP 8.2+ compatibility.
|
||||
if ( empty( $cached_results ) ) {
|
||||
$cached_results = array();
|
||||
$cached_results = [];
|
||||
}
|
||||
|
||||
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
|
||||
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
|
||||
$results = $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
|
||||
/**
|
||||
* Filter the upcoming recurring revenue data.
|
||||
*
|
||||
* @param array $results The upcoming recurring revenue data.
|
||||
* @param array $args The arguments for the report.
|
||||
* @since 2.1.0
|
||||
*/
|
||||
$results = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $results, $args );
|
||||
|
||||
$cached_results[ $query_hash ] = $results;
|
||||
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $wpdb->get_results( $base_query, OBJECT_K ), $args );
|
||||
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +230,7 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
|
||||
/**
|
||||
* Get the main chart
|
||||
* @return void
|
||||
* @return string
|
||||
*/
|
||||
public function get_main_chart() {
|
||||
global $wp_locale;
|
||||
|
|
@ -466,12 +415,12 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
// Group by
|
||||
switch ( $this->chart_groupby ) {
|
||||
case 'day':
|
||||
$this->group_by_query = 'YEAR(meta_next_payment.meta_value), MONTH(meta_next_payment.meta_value), DAY(meta_next_payment.meta_value)';
|
||||
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value), DAY(ms.meta_value)';
|
||||
$this->chart_interval = ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) );
|
||||
$this->barwidth = 60 * 60 * 24 * 1000;
|
||||
break;
|
||||
case 'month':
|
||||
$this->group_by_query = 'YEAR(meta_next_payment.meta_value), MONTH(meta_next_payment.meta_value), DAY(meta_next_payment.meta_value)';
|
||||
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value), DAY(ms.meta_value)';
|
||||
$this->chart_interval = 0;
|
||||
$min_date = $this->start_date;
|
||||
while ( ( $min_date = wcs_add_months( $min_date, '1' ) ) <= $this->end_date ) {
|
||||
|
|
@ -498,8 +447,6 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
|
|||
/**
|
||||
* Clears the cached query results.
|
||||
*
|
||||
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
|
||||
*
|
||||
* @since 3.0.10
|
||||
*/
|
||||
public function clear_cache() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Subscriptions Admin Report - Retention Rate
|
||||
*
|
||||
* Find the number of periods between when each subscription is created and ends or ended
|
||||
* then plot all subscriptions using this data to provide a curve of retention rates.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated in favor of WCS_Report_Retention_Rate
|
||||
*/
|
||||
class WC_Report_Retention_Rate extends WCS_Report_Retention_Rate {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Subscriptions Admin Report - Subscriptions by customer
|
||||
*
|
||||
* Creates the subscription admin reports area.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated in favor of WCS_Report_Subscription_By_Customer
|
||||
*/
|
||||
class WC_Report_Subscription_By_Customer extends WCS_Report_Subscription_By_Customer {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Subscriptions Admin Report - Subscriptions by product
|
||||
*
|
||||
* Creates the subscription admin reports area.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated In favor of WCS_Report_Subscription_By_Product
|
||||
*/
|
||||
class WC_Report_Subscription_By_Product extends WCS_Report_Subscription_By_Product {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Subscriptions Admin Report - Subscription Events by Date
|
||||
*
|
||||
* Display important historical data for subscription revenue and events, like switches and cancellations.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated In favor of WCS_Report_Subscription_Events_By_Date
|
||||
*/
|
||||
class WC_Report_Subscription_Events_By_Date extends WCS_Report_Subscription_Events_By_Date {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
/**
|
||||
* Subscriptions Admin Report - Subscription Events by Date
|
||||
*
|
||||
* Creates the subscription admin reports area.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated In favor of WCS_Report_Subscription_Payment_Retry
|
||||
*/
|
||||
class WC_Report_Subscription_Payment_Retry extends WCS_Report_Subscription_Payment_Retry {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* Subscriptions Admin Report - Upcoming Recurring Revenue
|
||||
*
|
||||
* Display the renewal order count and revenue that will be processed for all currently active subscriptions
|
||||
* for a given period of time in the future.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Admin_Reports
|
||||
* @category Class
|
||||
* @author Prospress
|
||||
* @since 2.1
|
||||
* @deprecated In favor of WCS_Report_Upcoming_Recurring_Revenue
|
||||
*/
|
||||
class WC_Report_Upcoming_Recurring_Revenue extends WCS_Report_Upcoming_Recurring_Revenue {
|
||||
public function __construct() {
|
||||
wcs_deprecated_function( __CLASS__, '2.4.0', get_parent_class( __CLASS__ ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ class WC_REST_Subscription_System_Status_Manager {
|
|||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function add_subscription_fields_to_response( $response ) {
|
||||
$count_by_status = WCS_Admin_System_Status::get_subscription_status_counts();
|
||||
$count_by_status = array_filter( (array) WC_Data_Store::load( 'subscription' )->get_subscriptions_count_by_status() );
|
||||
|
||||
$response->data['subscriptions'] = array(
|
||||
'wcs_debug' => defined( 'WCS_DEBUG' ) ? WCS_DEBUG : false,
|
||||
|
|
@ -79,7 +79,6 @@ class WC_REST_Subscription_System_Status_Manager {
|
|||
private static function get_payment_gateway_feature_support() {
|
||||
$gateway_features = array();
|
||||
|
||||
// @phpstan-ignore property.notFound
|
||||
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
|
||||
// Some gateways include array keys. For consistency, only send the values.
|
||||
$gateway_features[ $gateway_id ] = array_values( (array) apply_filters( 'woocommerce_subscriptions_payment_gateway_features_list', $gateway->supports, $gateway ) );
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
* -- Subscription specific --
|
||||
* GET /subscriptions/status
|
||||
* GET /subscriptions/<subscription_id>/orders
|
||||
* GET /orders/<order_id>/subscriptions
|
||||
* POST /orders/<order_id>/subscriptions
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
|
@ -63,25 +61,15 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
) );
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
"/orders/(?P<id>[\d]+)/{$this->rest_base}",
|
||||
register_rest_route( $this->namespace, "/orders/(?P<id>[\d]+)/{$this->rest_base}", array(
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_order_subscriptions' ),
|
||||
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_subscriptions_from_order' ),
|
||||
'permission_callback' => array( $this, 'create_item_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_subscriptions_from_order' ),
|
||||
'permission_callback' => array( $this, 'create_item_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,7 +94,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
*
|
||||
* @since 3.1.0
|
||||
*
|
||||
* @param WC_Subscription $object Subscription object.
|
||||
* @param WC_Data $object Subscription object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
|
|
@ -218,76 +206,6 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
return apply_filters( 'wcs_rest_subscription_orders_response', $response, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the /orders/[id]/subscriptions response.
|
||||
*
|
||||
* @since 7.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_Error|WP_REST_Response $response The response or an error if one occurs.
|
||||
*/
|
||||
public function get_order_subscriptions( $request ) {
|
||||
$order_id = absint( $request['id'] );
|
||||
|
||||
if ( empty( $order_id ) ) {
|
||||
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return new WP_Error(
|
||||
'woocommerce_rest_invalid_order_id',
|
||||
// translators: %d is the order ID.
|
||||
sprintf( __( 'Failed to load order object with the ID %d.', 'woocommerce-subscriptions' ), $order_id ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Build arguments for wcs_get_subscriptions_for_order()x
|
||||
$args = array(
|
||||
'order_type' => 'any', // Return all subscriptions for the order.
|
||||
);
|
||||
|
||||
// Set arguments from request.
|
||||
if ( ! empty( $request['orderby'] ) ) {
|
||||
$args['orderby'] = $request['orderby'];
|
||||
}
|
||||
|
||||
if ( ! empty( $request['order'] ) ) {
|
||||
$args['order'] = $request['order'];
|
||||
}
|
||||
|
||||
// Map standard request parameters to wcs_get_subscriptions_for_order() arguments.
|
||||
if ( ! empty( $request['customer'] ) ) {
|
||||
$args['customer_id'] = $request['customer'];
|
||||
}
|
||||
|
||||
if ( ! empty( $request['status'] ) ) {
|
||||
$args['subscription_status'] = $request['status'];
|
||||
}
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_order( $order, $args );
|
||||
|
||||
$response_data = array();
|
||||
|
||||
foreach ( $subscriptions as $subscription ) {
|
||||
if ( is_a( $subscription, 'WC_Subscription' ) && ! wc_rest_check_post_permissions( 'shop_subscription', 'read', $subscription->get_id() ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response = $this->prepare_object_for_response( $subscription, $request );
|
||||
$response_data[] = $this->prepare_response_for_collection( $response );
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $response_data );
|
||||
// Pagination is not fully supported, so we manually set the total.
|
||||
$response->header( 'X-WP-Total', count( $response_data ) );
|
||||
$response->header( 'X-WP-TotalPages', 1 );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides WC_REST_Orders_Controller::get_order_statuses() so that subscription statuses are
|
||||
* validated correctly.
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Class WC_REST_Subscriptions_Settings_Option_Controller
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for settings options.
|
||||
*/
|
||||
class WC_REST_Subscriptions_Settings_Option_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v3';
|
||||
|
||||
/**
|
||||
* List of allowed option names that can be updated via the REST API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const ALLOWED_OPTIONS = [
|
||||
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
|
||||
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'subscriptions/settings';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<option_name>[a-zA-Z0-9_-]+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'update_option' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'option_name' => [
|
||||
'required' => true,
|
||||
'validate_callback' => [ $this, 'validate_option_name' ],
|
||||
],
|
||||
'value' => [
|
||||
'required' => true,
|
||||
'validate_callback' => [ $this, 'validate_value' ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the option name.
|
||||
*
|
||||
* @param string $option_name The option name to validate.
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_option_name( string $option_name ): bool {
|
||||
return in_array( $option_name, self::ALLOWED_OPTIONS, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the value parameter.
|
||||
*
|
||||
* @param mixed $value The value to validate.
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid.
|
||||
*/
|
||||
public function validate_value( $value ) {
|
||||
if ( is_bool( $value ) || is_array( $value ) ) {
|
||||
return true;
|
||||
}
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__( 'Invalid value type; must be either boolean or array', 'woocommerce-subscriptions' ),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the option value.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function update_option( WP_REST_Request $request ) {
|
||||
$option_name = $request->get_param( 'option_name' );
|
||||
$value = $request->get_param( 'value' );
|
||||
|
||||
update_option( $option_name, $value );
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'success' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify access.
|
||||
*
|
||||
* Override this method if custom permissions required.
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* REST API Subscription Notes controller class.
|
||||
*
|
||||
* @package WooCommerce_Subscriptions/API
|
||||
* @extends WC_REST_Order_Notes_Controller
|
||||
*/
|
||||
class WC_REST_Subscription_Notes_Controller extends WC_REST_Order_Notes_Controller {
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* REST API Subscriptions controller class.
|
||||
*
|
||||
* @package WooCommerce_Subscriptions/API
|
||||
* @extends WC_REST_Orders_Controller
|
||||
*/
|
||||
class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
*
|
||||
* @since 2.1
|
||||
* @param WP_REST_Response $response
|
||||
* @param WP_Post $post
|
||||
* @param WP_POST $post
|
||||
* @param WP_REST_Request $request
|
||||
*/
|
||||
public function filter_get_subscription_response( $response, $post, $request ) {
|
||||
|
|
@ -122,7 +123,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
*
|
||||
* @since 2.1
|
||||
* @param WP_REST_Request $request
|
||||
* @param WP_Post $post
|
||||
* @param WP_POST $post
|
||||
*/
|
||||
protected function update_order( $request, $post ) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* REST API Subscription Notes controller class.
|
||||
*
|
||||
* @package WooCommerce_Subscriptions/API
|
||||
* @extends WC_REST_Order_Notes_Controller
|
||||
*/
|
||||
class WC_REST_Subscription_Notes_V1_Controller extends WC_REST_Order_Notes_V1_Controller {
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* REST API Subscriptions controller class.
|
||||
*
|
||||
* @package WooCommerce_Subscriptions/API
|
||||
* @extends WC_REST_Orders_Controller
|
||||
*/
|
||||
class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
||||
|
||||
|
|
@ -74,7 +75,7 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
*
|
||||
* @since 2.1
|
||||
* @param WP_REST_Response $response
|
||||
* @param WP_Post $post
|
||||
* @param WP_POST $post
|
||||
* @param WP_REST_Request $request
|
||||
*/
|
||||
public function filter_get_subscription_response( $response, $post, $request ) {
|
||||
|
|
@ -83,10 +84,6 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) {
|
||||
$subscription = wcs_get_subscription( $post->ID );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->data['billing_period'] = $subscription->get_billing_period();
|
||||
$response->data['billing_interval'] = $subscription->get_billing_interval();
|
||||
|
||||
|
|
@ -235,6 +232,7 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
*
|
||||
* @since 2.1
|
||||
* @param WP_REST_Request $request
|
||||
* @param WP_POST $post
|
||||
*/
|
||||
protected function update_order( $request ) {
|
||||
try {
|
||||
|
|
@ -296,13 +294,8 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->post_type = 'shop_order';
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->post_type = 'shop_order';
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
$subscription_orders = $subscription->get_related_orders();
|
||||
|
||||
$orders = array();
|
||||
|
|
@ -380,10 +373,6 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
// In particular, the update_post_meta() called while _stripe_card_id is updated to _stripe_source_id
|
||||
$subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
throw new WC_REST_Exception( 'woocommerce_rest_payment_update_failed', __( 'Subscription payment method could not be set updated due to technical issues.', 'woocommerce-subscriptions' ), 500 );
|
||||
}
|
||||
|
||||
if ( isset( $payment_method_meta[ $payment_method ] ) ) {
|
||||
$payment_method_meta = $payment_method_meta[ $payment_method ];
|
||||
|
||||
|
|
@ -718,4 +707,78 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
$schema['properties'] += $subscriptions_schema;
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prepare subscription data for create.
|
||||
*
|
||||
* Now that we override WC_REST_Orders_V1_Controller::prepare_item_for_database() function,
|
||||
* we no longer need to prepare these args
|
||||
*
|
||||
* @since 2.1
|
||||
* @param stdClass $data
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return stdClass
|
||||
* @deprecated 2.2
|
||||
*/
|
||||
public function prepare_subscription_args( $data, $request ) {
|
||||
wcs_deprecated_function( __METHOD__, '2.2' );
|
||||
|
||||
$data->billing_interval = $request['billing_interval'];
|
||||
$data->billing_period = $request['billing_period'];
|
||||
|
||||
foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
|
||||
if ( ! empty( $request[ $date_type . '_date' ] ) ) {
|
||||
$date_type_key = ( 'start' === $date_type ) ? 'date_created' : $date_type . '_date';
|
||||
$data->{$date_type_key} = $request[ $date_type . '_date' ];
|
||||
}
|
||||
}
|
||||
|
||||
$data->payment_details = ! empty( $request['payment_details'] ) ? $request['payment_details'] : '';
|
||||
$data->payment_method = ! empty( $request['payment_method'] ) ? $request['payment_method'] : '';
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or set the subscription schedule with the request data.
|
||||
*
|
||||
*
|
||||
* @since 2.1
|
||||
* @param WC_Subscription $subscription
|
||||
* @param array $data
|
||||
* @deprecated 2.2
|
||||
*/
|
||||
public function update_schedule( $subscription, $data ) {
|
||||
wcs_deprecated_function( __METHOD__, '2.2', 'WC_REST_Subscriptions_Controller::prepare_item_for_database() now prepares the billing interval/period and dates' );
|
||||
|
||||
if ( isset( $data['billing_interval'] ) ) {
|
||||
$subscription->set_billing_interval( absint( $data['billing_interval'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $data['billing_period'] ) ) {
|
||||
$subscription->set_billing_period( $data['billing_period'] );
|
||||
}
|
||||
|
||||
try {
|
||||
$dates_to_update = array();
|
||||
|
||||
foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
|
||||
if ( isset( $data[ $date_type . '_date' ] ) ) {
|
||||
$date_type_key = ( 'start' === $date_type ) ? 'date_created' : $date_type;
|
||||
$dates_to_update[ $date_type_key ] = $data[ $date_type . '_date' ];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $dates_to_update ) ) {
|
||||
$subscription->update_dates( $dates_to_update );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
// translators: placeholder is an error message.
|
||||
throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_subscription_dates', sprintf( __( 'Updating subscription dates errored with message: %s', 'woocommerce-subscriptions' ), $e->getMessage() ), 400 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* REST API Subscription Notes controller class.
|
||||
*
|
||||
* @package WooCommerce_Subscriptions/API
|
||||
* @extends WC_REST_Order_Notes_Controller
|
||||
*/
|
||||
class WC_REST_Subscription_Notes_V2_Controller extends WC_REST_Order_Notes_V2_Controller {
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
*
|
||||
* @since 6.4.0
|
||||
*
|
||||
* @param WC_Subscription $object Subscription object.
|
||||
* @param WC_Data $object Subscription object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
|
|
@ -702,16 +702,12 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
$subscription->add_item( $item );
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Fetch a fresh instance of the subscription because the current instance has an empty line item cache generated before we had copied the line items.
|
||||
* Fetching a new instance will ensure the line items are used when calculating totals.
|
||||
*/
|
||||
$subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
$subscription->calculate_totals();
|
||||
|
||||
/**
|
||||
|
|
@ -723,13 +719,7 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
*/
|
||||
do_action( "woocommerce_rest_insert_{$this->post_type}_object", $subscription, $request, true );
|
||||
|
||||
$fresh_subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $fresh_subscription ) {
|
||||
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
$response = $this->prepare_object_for_response( $fresh_subscription, $request );
|
||||
$response = $this->prepare_object_for_response( wcs_get_subscription( $subscription->get_id() ), $request );
|
||||
$subscriptions[] = $this->prepare_response_for_collection( $response );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
|
|
|
|||
|
|
@ -27,28 +27,11 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
*/
|
||||
private $wc_version_cached = false;
|
||||
|
||||
/**
|
||||
* @var boolean Whether to skip the class_exists and WC_VERSION constant checks.
|
||||
*/
|
||||
private $skip_class_exists_and_wc_version_constant_checks = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct( $minimum_supported_wc_version ) {
|
||||
$this->minimum_supported_wc_version = $minimum_supported_wc_version;
|
||||
/**
|
||||
* Filter allows to skip the class_exists and WC_VERSION constant checks.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param bool $use_class_exists Whether to use the class_exists and WC_VERSION constant checks.
|
||||
*
|
||||
* @return bool false to use the class_exists and WC_VERSION checks, true to skip them.
|
||||
*/
|
||||
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && WCS_ENVIRONMENT_TYPE === 'tests' && apply_filters( 'woocommerce_subscriptions_skip_class_exists_and_wc_version_constant_checks', false ) ) {
|
||||
$this->skip_class_exists_and_wc_version_constant_checks = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,7 +52,7 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
* @return bool True if the plugin is active, false otherwise.
|
||||
*/
|
||||
public function is_woocommerce_active() {
|
||||
if ( class_exists( 'WooCommerce' ) && ! $this->skip_class_exists_and_wc_version_constant_checks ) {
|
||||
if ( class_exists( 'WooCommerce' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +89,7 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
* @return string|null The active WooCommerce version, or null if WooCommerce is not active.
|
||||
*/
|
||||
private function get_woocommerce_active_version() {
|
||||
if ( defined( 'WC_VERSION' ) && ! $this->skip_class_exists_and_wc_version_constant_checks ) {
|
||||
if ( defined( 'WC_VERSION' ) ) {
|
||||
return WC_VERSION;
|
||||
}
|
||||
|
||||
|
|
@ -117,13 +100,6 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
|
||||
$this->wc_version_cached = true;
|
||||
|
||||
// Try to get version from transient first
|
||||
$this->wc_active_version = get_transient( 'wcs_woocommerce_active_version' );
|
||||
|
||||
if ( false !== $this->wc_active_version ) {
|
||||
return $this->wc_active_version;
|
||||
}
|
||||
|
||||
// Load plugin.php if it's not already loaded.
|
||||
if ( ! function_exists( 'is_plugin_active' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
|
|
@ -146,15 +122,9 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
|
||||
if ( $is_woocommerce && is_plugin_active( $plugin_slug ) ) {
|
||||
$this->wc_active_version = $plugin_data['Version'];
|
||||
break; // Found it, no need to continue looping
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result in a transient for 1 hour
|
||||
if ( ! empty( $this->wc_active_version ) ) {
|
||||
set_transient( 'wcs_woocommerce_active_version', $this->wc_active_version, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $this->wc_active_version;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Events as WC_Tracks_Events;
|
||||
|
||||
/**
|
||||
* @method static WC_Subscriptions_Plugin instance()
|
||||
*/
|
||||
|
|
@ -34,19 +32,12 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
|
|||
WCS_Call_To_Action_Button_Text_Manager::init();
|
||||
WCS_Subscriber_Role_Manager::init();
|
||||
WCS_Upgrade_Notice_Manager::init();
|
||||
WCS_Admin_Assets::init();
|
||||
|
||||
$tracks_events = new WC_Tracks_Events();
|
||||
$tracks_events->setup();
|
||||
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
new WC_Subscriptions_CLI();
|
||||
}
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_welcome_message' ) );
|
||||
add_action( 'plugins_loaded', array( $this, 'init_gifting' ) );
|
||||
add_action( 'plugins_loaded', array( $this, 'init_downloads' ) );
|
||||
add_action( 'admin_notices', array( WC_Subscription_Downloads_Settings::class, 'add_notice_about_bundled_feature' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -241,142 +232,4 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
|
|||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to initialize gifting functionality.
|
||||
*
|
||||
* Before doing this, the method tries to determine if the standalone WooCommerce Gifting plugin is active and has
|
||||
* already loaded (if the standalone plugin is active, we do not proceed). To accomplish this, this method expects
|
||||
* to run during plugins_loaded at priority 20 (whereas the equivalent code from the standalone plugin will run at
|
||||
* priority 11).
|
||||
*/
|
||||
public function init_gifting() {
|
||||
if ( ! WCSG_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
|
||||
WCSG_Admin_Welcome_Announcement::init();
|
||||
}
|
||||
|
||||
if (
|
||||
$this->is_plugin_being_activated( 'woocommerce-subscriptions-gifting' )
|
||||
|| function_exists( 'wcsg_load' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gifting_includes = trailingslashit( $this->get_plugin_directory( 'includes/gifting' ) );
|
||||
|
||||
require_once $gifting_includes . 'wcsg-compatibility-functions.php';
|
||||
|
||||
WCSG_Admin_Order::init();
|
||||
WCSG_Product::init();
|
||||
WCSG_Cart::init();
|
||||
WCSG_Checkout::init();
|
||||
WCSG_Recipient_Management::init();
|
||||
WCSG_Recipient_Details::init();
|
||||
WCSG_Email::init();
|
||||
WCSG_Download_Handler::init();
|
||||
WCSG_Admin::init();
|
||||
WCSG_Recipient_Addresses::init();
|
||||
WCSG_Template_Loader::init();
|
||||
WCSG_Admin_System_Status::init();
|
||||
|
||||
add_action(
|
||||
'init',
|
||||
function () {
|
||||
new WCSG_Privacy();
|
||||
}
|
||||
);
|
||||
|
||||
WCS_Gifting::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to initialize additional downloads functionality.
|
||||
*
|
||||
* This functionality makes it possible to link downloadable products with a subscription
|
||||
* product. Purchasers of the subscription product then automatically get access to the files associated with downloadable product.
|
||||
*
|
||||
* Previously, this functionality existed as a standalone plugin (WooCommerce Subscription Downnloads) and so,
|
||||
* before initializing, we try to determine if the standalone plugin is active and has already loaded (if it is
|
||||
* active, we do not proceed).
|
||||
*/
|
||||
public function init_downloads() {
|
||||
if (
|
||||
$this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
|
||||
|| class_exists( WC_Subscription_Downloads::class, false )
|
||||
) {
|
||||
if ( class_exists( WC_Subscription_Downloads::class, false ) ) {
|
||||
// Will show the welcome announcement if the standalone plugin is active and the welcome announcement has not been dismissed.
|
||||
if ( ! WC_Subscription_Downloads_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
|
||||
WC_Subscription_Downloads_Admin_Welcome_Announcement::init();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WC_Subscription_Downloads::setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine if the specified plugin is being activated.
|
||||
*
|
||||
* The provided plugin slug can be either the complete relative plugin path (ie, 'plugin-slug/plugin-slug.php') or
|
||||
* just a part of the path (ie, 'plugin-slug'). So long as the plugin which is actually being activated contains
|
||||
* that string, then we consider ourselves to have a match and will return true.
|
||||
*
|
||||
* Therefore, consider with care how precise you need to be: something highly specific like our first example will
|
||||
* fail if the plugin directory has been renamed. A shorter fragment, on the other hand, will potentially match the
|
||||
* wrong plugin.
|
||||
*
|
||||
* This method is only useful as a means of detecting when a plugin is activated through 'conventional' means (via
|
||||
* the plugin admin screen, or via WP CLI), but it will not provide protection if, for example, third party code
|
||||
* makes its own arbitrary calls to activate_plugin().
|
||||
*
|
||||
* @param string $plugin_slug Plugin slug.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_plugin_being_activated( string $plugin_slug ): bool {
|
||||
// Try to determine if a plugin is in the process of being activated via the plugin admin screen.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$action = isset( $_REQUEST['action'] ) ? wc_clean( wp_unslash( $_REQUEST['action'] ) ) : '';
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$plugin = isset( $_REQUEST['plugin'] ) ? wc_clean( wp_unslash( $_REQUEST['plugin'] ) ) : '';
|
||||
|
||||
if (
|
||||
'activate' === $action
|
||||
&& str_contains( $plugin, $plugin_slug )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to determine if a plugin is being activated via WP CLI.
|
||||
if ( class_exists( WP_CLI::class ) ) {
|
||||
// Note that flags such as `--no-color` are filtered out of this array.
|
||||
$args = WP_CLI::get_runner()->arguments;
|
||||
|
||||
if ( ! is_countable( $args ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
( count( $args ) < 3
|
||||
|| 'plugin' !== $args[0]
|
||||
|| 'activate' !== $args[1] )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The remaining arguments are the list of plugin slugs to be activated.
|
||||
$plugins_to_be_activated = array_slice( $args, 2 );
|
||||
|
||||
foreach ( $plugins_to_be_activated as $plugin ) {
|
||||
if ( str_contains( $plugin, $plugin_slug ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
* @since 2.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
|
@ -20,7 +18,7 @@ class WCS_API {
|
|||
add_filter( 'woocommerce_api_classes', array( __CLASS__, 'includes' ) );
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ), 15 );
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_route_overrides' ), 15 );
|
||||
add_action( 'woocommerce_rest_set_order_item', array( __CLASS__, 'add_sign_up_fee_to_order_item' ), 15, 2 );
|
||||
add_action( 'woocommerce_rest_set_order_item', [ __CLASS__, 'add_sign_up_fee_to_order_item' ], 15, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,11 +60,9 @@ class WCS_API {
|
|||
// V3 (latest)
|
||||
'WC_REST_Subscriptions_Controller',
|
||||
'WC_REST_Subscription_notes_Controller',
|
||||
'WC_REST_Subscriptions_Settings_Option_Controller',
|
||||
);
|
||||
|
||||
foreach ( $endpoint_classes as $class ) {
|
||||
// @phpstan-ignore class.nameCase
|
||||
$controller = new $class();
|
||||
$controller->register_routes();
|
||||
}
|
||||
|
|
@ -91,8 +87,8 @@ class WCS_API {
|
|||
*
|
||||
* @since 6.3.0
|
||||
*
|
||||
* @param WC_Order_Item_Product $item Order item object.
|
||||
* @param array $item_request_data Data posted to the API about the order item.
|
||||
* @param WC_Order_Item $item Order item object.
|
||||
* @param array $item_request_data Data posted to the API about the order item.
|
||||
*/
|
||||
public static function add_sign_up_fee_to_order_item( $item, $item_request_data = array() ) {
|
||||
if ( 'line_item' !== $item->get_type() || ! self::is_orders_api_request() ) {
|
||||
|
|
@ -123,13 +119,7 @@ class WCS_API {
|
|||
$price = (float) $product->get_price() + $sign_up_fee;
|
||||
}
|
||||
|
||||
$total = wc_get_price_excluding_tax(
|
||||
$product,
|
||||
array(
|
||||
'qty' => $item->get_quantity(),
|
||||
'price' => $price,
|
||||
)
|
||||
);
|
||||
$total = wc_get_price_excluding_tax( $product, [ 'qty' => $item->get_quantity(), 'price' => $price ] );
|
||||
|
||||
$item->set_total( $total );
|
||||
$item->set_subtotal( $total );
|
||||
|
|
@ -155,7 +145,7 @@ class WCS_API {
|
|||
* @return boolean
|
||||
*/
|
||||
protected static function is_orders_api_request() {
|
||||
if ( ! Constants::is_true( 'REST_REQUEST' ) || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +165,6 @@ class WCS_API {
|
|||
*/
|
||||
public static function get_wc_api_endpoint_data( $endpoint ) {
|
||||
if ( wcs_is_woocommerce_pre( '9.0.0' ) ) {
|
||||
// @phpstan-ignore-next-line Call to deprecated method.
|
||||
return WC()->api->get_endpoint_data( $endpoint );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
|
|||
'wcs_webhooks' => true,
|
||||
'wcs_auth' => true,
|
||||
'wcs_upgrade_notice_manager' => true,
|
||||
'wcs_admin_assets' => true,
|
||||
'wc_subscriptions_cli' => true,
|
||||
);
|
||||
|
||||
|
|
@ -94,6 +93,8 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
|
|||
|
||||
if ( stripos( $class, 'switch' ) !== false || 'wcs_add_cart_item' === $class ) {
|
||||
$path .= '/switching';
|
||||
} elseif ( false !== strpos( $class, 'wc_report' ) ) {
|
||||
$path .= '/admin/reports/deprecated';
|
||||
} elseif ( false !== strpos( $class, 'wcs_report' ) ) {
|
||||
$path .= '/admin/reports';
|
||||
} elseif ( false !== strpos( $class, 'retry' ) || false !== strpos( $class, 'retries' ) ) {
|
||||
|
|
@ -121,10 +122,13 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
|
|||
*/
|
||||
protected function should_autoload( $class ) {
|
||||
static $legacy = array(
|
||||
'wc_order_item_pending_switch' => 1,
|
||||
'wc_order_item_pending_switch' => 1,
|
||||
'wc_report_retention_rate' => 1,
|
||||
'wc_report_upcoming_recurring_revenue' => 1,
|
||||
);
|
||||
|
||||
return isset( $legacy[ $class ] ) ? true : parent::should_autoload( $class );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class WCS_Call_To_Action_Button_Text_Manager {
|
|||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_subscription_settings', array( __CLASS__, 'add_settings' ), 5 );
|
||||
add_filter( 'wc_subscription_product_add_to_cart_text', array( __CLASS__, 'filter_add_to_cart_text' ) );
|
||||
add_filter( 'wc_subscription_product_add_to_cart_text', array( __CLASS__, 'filter_add_to_cart_text' ), 10, 2 );
|
||||
add_filter( 'wcs_place_subscription_order_text', array( __CLASS__, 'filter_place_subscription_order_text' ) );
|
||||
}
|
||||
|
||||
|
|
@ -30,32 +30,32 @@ class WCS_Call_To_Action_Button_Text_Manager {
|
|||
public static function add_settings( $settings ) {
|
||||
$button_text_settings = array(
|
||||
array(
|
||||
'name' => __( 'Button text', 'woocommerce-subscriptions' ),
|
||||
'name' => __( 'Button Text', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'desc' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_button_text',
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Add to Cart Button Text', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'A product displays a button with the text "Add to cart". 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' => WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text',
|
||||
'css' => 'min-width:150px;',
|
||||
'default' => __( 'Add to cart', 'woocommerce-subscriptions' ),
|
||||
'default' => __( 'Sign up now', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Add to cart', '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.', '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' => WC_Subscriptions_Admin::$option_prefix . '_order_button_text',
|
||||
'css' => 'min-width:150px;',
|
||||
'default' => __( 'Place order', 'woocommerce-subscriptions' ),
|
||||
'default' => __( 'Sign up now', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Place order', 'woocommerce-subscriptions' ),
|
||||
'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ),
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
|
|
@ -71,7 +71,8 @@ class WCS_Call_To_Action_Button_Text_Manager {
|
|||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $add_to_cart_text The product's add to cart text.
|
||||
* @param string $add_to_cart_text The product's add to cart text.
|
||||
* @param WC_Abstract_Product $product The product.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -77,33 +77,28 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
* Get the number of renewals for a limited coupon.
|
||||
*
|
||||
* @since 4.0.0
|
||||
* @param string|WC_Coupon $coupon The coupon or coupon code.
|
||||
* @param string $code The coupon code.
|
||||
* @return false|int False for non-recurring coupons, or the limit number for recurring coupons.
|
||||
* A value of 0 is for unlimited usage.
|
||||
*/
|
||||
public static function get_coupon_limit( $coupon ) {
|
||||
// If we have a coupon code, attempt to get the coupon object.
|
||||
if ( is_string( $coupon ) ) {
|
||||
$coupon = new WC_Coupon( $coupon );
|
||||
}
|
||||
|
||||
if ( ! $coupon instanceof WC_Coupon ) {
|
||||
public static function get_coupon_limit( $code ) {
|
||||
if ( wcs_is_woocommerce_pre( '3.2' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve the coupon data.
|
||||
$coupon = new WC_Coupon( $code );
|
||||
$coupon_type = $coupon->get_discount_type();
|
||||
|
||||
// If we have a virtual coupon, attempt to get the original coupon.
|
||||
if ( WC_Subscriptions_Coupon::is_renewal_cart_coupon( $coupon_type ) ) {
|
||||
$coupon = WC_Subscriptions_Coupon::map_virtual_coupon( $coupon->get_code() );
|
||||
$coupon = WC_Subscriptions_Coupon::map_virtual_coupon( $code );
|
||||
$coupon_type = $coupon->get_discount_type();
|
||||
}
|
||||
|
||||
if ( ! WC_Subscriptions_Coupon::is_recurring_coupon( $coupon_type ) ) {
|
||||
return false;
|
||||
}
|
||||
$limited = $coupon->get_meta( self::$coupons_renewals );
|
||||
|
||||
return intval( $coupon->get_meta( self::$coupons_renewals ) );
|
||||
return WC_Subscriptions_Coupon::is_recurring_coupon( $coupon_type ) ? intval( $limited ) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -271,16 +266,11 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
* @param int $id The coupon ID.
|
||||
*/
|
||||
public static function add_limit_to_list_table( $column_name, $id ) {
|
||||
global $the_coupon;
|
||||
|
||||
if ( 'usage' !== $column_name ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm the global coupon object is the one we're looking for, otherwise fetch it.
|
||||
$coupon = empty( $the_coupon ) || $the_coupon->get_id() !== $id ? new WC_Coupon( $id ) : $the_coupon;
|
||||
$limit = self::get_coupon_limit( $coupon );
|
||||
|
||||
$limit = self::get_coupon_limit( wc_get_coupon_code_by_id( $id ) );
|
||||
if ( false === $limit ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -313,14 +303,13 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
}
|
||||
|
||||
// Bail early if there are no limited coupons applied to the recurring cart or if there is no discount provided.
|
||||
// @phpstan-ignore property.notFound
|
||||
if ( empty( $limited_recurring_coupons ) || ! $recurring_cart->discount_cart ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$has_expiring_coupon = false;
|
||||
$subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' );
|
||||
$subscription_payments = (int) $subscription_length / (int) wcs_cart_pluck( $recurring_cart, 'subscription_period_interval' );
|
||||
$subscription_payments = $subscription_length / wcs_cart_pluck( $recurring_cart, 'subscription_period_interval' );
|
||||
|
||||
// Limited recurring coupons will always expire at some point on subscriptions with no length.
|
||||
if ( empty( $subscription_length ) ) {
|
||||
|
|
@ -357,12 +346,7 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
$has_limited_coupon = false;
|
||||
|
||||
if ( $change_payment && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
|
||||
$subscription = wcs_get_subscription( $change_payment );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
$subscription = wcs_get_subscription( $change_payment );
|
||||
$has_limited_coupon = self::order_has_limited_recurring_coupon( $subscription );
|
||||
}
|
||||
|
||||
|
|
@ -387,6 +371,7 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $message The current message indicating there are no payment methods available..
|
||||
* @return string The filtered message indicating there are no payment methods available.
|
||||
*/
|
||||
public static function no_available_payment_methods_message() {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class WCS_Manual_Renewal_Manager {
|
|||
*
|
||||
* @since 4.0.0
|
||||
* @param $settings The full subscription settings array.
|
||||
* @return array
|
||||
* @return $settings.
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,8 @@ class WCS_Subscriber_Role_Manager {
|
|||
* @return array Subscriptions settings.
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
$roles_options = array();
|
||||
|
||||
if ( ! function_exists( 'get_editable_roles' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
require_once( ABSPATH . 'wp-admin/includes/user.php' );
|
||||
}
|
||||
|
||||
foreach ( get_editable_roles() as $role => $details ) {
|
||||
|
|
@ -74,10 +72,7 @@ class WCS_Subscriber_Role_Manager {
|
|||
),
|
||||
);
|
||||
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' ) ) {
|
||||
$settings = array_merge( $settings, $role_settings );
|
||||
}
|
||||
|
||||
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' );
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class WCS_Upgrade_Notice_Manager {
|
|||
|
||||
// translators: placeholder is Subscription version string ('3.1')
|
||||
$notice->set_heading( sprintf( __( 'Welcome to WooCommerce Subscriptions %s!', 'woocommerce-subscriptions' ), $version ) );
|
||||
$notice->set_content_template( 'update-welcome-notice.php', WC_Subscriptions_Plugin::instance()->get_plugin_directory() . '/includes/upgrades/templates/', array(
|
||||
$notice->set_content_template( 'update-welcome-notice.php', WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory() . '/includes/upgrades/templates/', array(
|
||||
'version' => $version,
|
||||
'features' => $features,
|
||||
) );
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ class WCS_Webhooks {
|
|||
switch ( $webhook->get_api_version() ) {
|
||||
case 'legacy_v3':
|
||||
|
||||
// @phpstan-ignore-next-line Ignore legacy referencies.
|
||||
if ( is_null( wc()->api ) ) {
|
||||
throw new \Exception( 'The Legacy REST API plugin is not installed on this site. More information: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ ' );
|
||||
}
|
||||
|
|
@ -145,7 +144,6 @@ class WCS_Webhooks {
|
|||
case 'wp_api_v2':
|
||||
// There is no v2 subscritpion endpoint support so they fall back to v1.
|
||||
$request = new WP_REST_Request( 'GET' );
|
||||
// @phpstan-ignore class.nameCase
|
||||
$controller = new WC_REST_Subscriptions_v1_Controller();
|
||||
|
||||
$request->set_param( 'id', $resource_id );
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class WCS_Zero_Initial_Payment_Checkout_Manager {
|
|||
*/
|
||||
public static function cart_needs_payment( $cart_needs_payment ) {
|
||||
if ( ! self::zero_initial_checkout_requires_payment() ) {
|
||||
remove_filter( 'woocommerce_cart_needs_payment', 'WC_Subscriptions_Cart::cart_needs_payment' );
|
||||
remove_filter( 'woocommerce_cart_needs_payment', 'WC_Subscriptions_Cart::cart_needs_payment', 10, 2 );
|
||||
}
|
||||
|
||||
return $cart_needs_payment;
|
||||
|
|
@ -77,7 +77,7 @@ class WCS_Zero_Initial_Payment_Checkout_Manager {
|
|||
*/
|
||||
public static function order_needs_payment( $needs_payment ) {
|
||||
if ( ! self::zero_initial_checkout_requires_payment() ) {
|
||||
remove_filter( 'woocommerce_order_needs_payment', 'WC_Subscriptions_Order::order_needs_payment' );
|
||||
remove_filter( 'woocommerce_order_needs_payment', 'WC_Subscriptions_Order::order_needs_payment', 10, 3 );
|
||||
}
|
||||
|
||||
return $needs_payment;
|
||||
|
|
|
|||
|
|
@ -1,350 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* WooCommerce Subscriptions Notifications Debug Tool Processor.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @category Class
|
||||
* @since 7.7.0
|
||||
*/
|
||||
class WCS_Notifications_Debug_Tool_Processor implements WCS_Batch_Processor {
|
||||
|
||||
/**
|
||||
* Option name for the tool state.
|
||||
* This is used to pass the state of the tool between requests.
|
||||
*/
|
||||
const TOOL_STATE_OPTION_NAME = 'wcs_notifications_debug_tool_state';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_debug_tools', array( $this, 'handle_woocommerce_debug_tools' ), 999, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state of the tool.
|
||||
*
|
||||
* @return array {
|
||||
* @last_offset Last offset processed.
|
||||
* }
|
||||
*/
|
||||
private function get_tool_state(): array {
|
||||
return (array) get_option( self::TOOL_STATE_OPTION_NAME, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of the tool.
|
||||
*
|
||||
* @param array $state New state of the tool.
|
||||
*/
|
||||
private function update_tool_state( $state ) {
|
||||
update_option( self::TOOL_STATE_OPTION_NAME, $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the state of the tool.
|
||||
*/
|
||||
private function delete_tool_state() {
|
||||
delete_option( self::TOOL_STATE_OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly name for this processor.
|
||||
*
|
||||
* @return string Name of the processor.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'wcs_notifications_debug_tool_processor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly description for this processor.
|
||||
*
|
||||
* @return string Description of what this processor does.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return 'WooCommerce Notifications Debug Tool Processor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed subscription statuses to process.
|
||||
*/
|
||||
protected function get_subscription_statuses(): array {
|
||||
$allowed_statuses = array(
|
||||
'active',
|
||||
'pending',
|
||||
'on-hold',
|
||||
);
|
||||
|
||||
return array_map( 'wcs_sanitize_subscription_status_key', $allowed_statuses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of pending items that require processing.
|
||||
* Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.
|
||||
*
|
||||
* Note that the once the processor is enqueued the batch processor controller will keep
|
||||
* invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.
|
||||
*
|
||||
* In this case, this means total number of subscriptions in allowed statuses - number of processed subscriptions.
|
||||
*
|
||||
* @return int Number of items pending processing.
|
||||
*/
|
||||
public function get_total_pending_count(): int {
|
||||
global $wpdb;
|
||||
|
||||
$allowed_statuses = $this->get_subscription_statuses();
|
||||
$placeholders = implode( ', ', array_fill( 0, count( $allowed_statuses ), '%s' ) );
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$total_subscriptions = $wpdb->get_var(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(id)
|
||||
FROM {$wpdb->prefix}wc_orders
|
||||
WHERE type='shop_subscription'
|
||||
AND status IN ($placeholders)
|
||||
",
|
||||
...$allowed_statuses
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$total_subscriptions = $wpdb->get_var(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(ID)
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE post_type='shop_subscription'
|
||||
AND post_status IN ($placeholders)
|
||||
",
|
||||
...$allowed_statuses
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$state = $this->get_tool_state();
|
||||
if ( isset( $state['last_offset'] ) ) {
|
||||
$total_subscriptions -= (int) $state['last_offset'];
|
||||
}
|
||||
|
||||
return $total_subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next batch of items that need to be processed.
|
||||
*
|
||||
* A batch item can be anything needed to identify the actual processing to be done,
|
||||
* but whenever possible items should be numbers (e.g. database record ids)
|
||||
* or at least strings, to ease troubleshooting and logging in case of problems.
|
||||
*
|
||||
* The size of the batch returned can be less than $size if there aren't that
|
||||
* many items pending processing (and it can be zero if there isn't anything to process),
|
||||
* but the size should always be consistent with what 'get_total_pending_count' returns
|
||||
* (i.e. the size of the returned batch shouldn't be larger than the pending items count).
|
||||
*
|
||||
* @param int $size Maximum size of the batch to be returned.
|
||||
*
|
||||
* @return array Batch of items to process, containing $size or less items.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ): array {
|
||||
global $wpdb;
|
||||
|
||||
$allowed_statuses = $this->get_subscription_statuses();
|
||||
$placeholders = implode( ', ', array_fill( 0, count( $allowed_statuses ), '%s' ) );
|
||||
$state = $this->get_tool_state();
|
||||
$offset = isset( $state['last_offset'] ) ? (int) $state['last_offset'] : 0;
|
||||
|
||||
$args = array_merge(
|
||||
$allowed_statuses,
|
||||
array( $size ),
|
||||
array( $offset )
|
||||
);
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
$subscriptions_to_process = $wpdb->get_col(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
id
|
||||
FROM {$wpdb->prefix}wc_orders
|
||||
WHERE type='shop_subscription'
|
||||
AND status IN ($placeholders)
|
||||
ORDER BY id ASC
|
||||
LIMIT %d
|
||||
OFFSET %d",
|
||||
...$args
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$subscriptions_to_process = $wpdb->get_col(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
ID
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE post_type='shop_subscription'
|
||||
AND post_status IN ($placeholders)
|
||||
ORDER BY ID ASC
|
||||
LIMIT %d
|
||||
OFFSET %d",
|
||||
...$args
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the tool state if there are no more subscriptions to process.
|
||||
if ( empty( $subscriptions_to_process ) ) {
|
||||
$this->delete_tool_state();
|
||||
}
|
||||
|
||||
return $subscriptions_to_process;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data for the supplied batch.
|
||||
*
|
||||
* This method should be prepared to receive items that don't actually need processing
|
||||
* (because they have been processed before) and ignore them, but if at least
|
||||
* one of the batch items that actually need processing can't be processed, an exception should be thrown.
|
||||
*
|
||||
* Once an item has been processed it shouldn't be counted in 'get_total_pending_count'
|
||||
* nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it
|
||||
* to actually require further processing).
|
||||
*
|
||||
* @throw \Exception Something went wrong while processing the batch.
|
||||
*
|
||||
* @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
|
||||
*/
|
||||
public function process_batch( array $batch ): void {
|
||||
|
||||
$subscriptions_notifications = WC_Subscriptions_Core_Plugin::instance()->notifications_scheduler;
|
||||
|
||||
foreach ( $batch as $subscription_id ) {
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( WC_Subscriptions_Email_Notifications::notifications_globally_enabled() ) {
|
||||
$subscriptions_notifications->update_status( $subscription, $subscription->get_status(), null );
|
||||
} else {
|
||||
$subscriptions_notifications->unschedule_all_notifications( $subscription );
|
||||
}
|
||||
|
||||
// Update the subscription's update time to mark it as updated.
|
||||
$subscription->set_date_modified( time() );
|
||||
$subscription->save();
|
||||
}
|
||||
|
||||
// Update tool state.
|
||||
$state = $this->get_tool_state();
|
||||
$state['last_offset'] = isset( $state['last_offset'] ) ? absint( $state['last_offset'] ) + count( $batch ) : count( $batch );
|
||||
$this->update_tool_state( $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default (preferred) batch size to pass to 'get_next_batch_to_process'.
|
||||
* The controller will pass this size unless it's externally configured
|
||||
* to use a different size.
|
||||
*
|
||||
* @return int Default batch size.
|
||||
*/
|
||||
public function get_default_batch_size(): int {
|
||||
return 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background process for batch processing subscription notifications updates.
|
||||
*
|
||||
* @return string Informative string to show after the tool is triggered in UI.
|
||||
*/
|
||||
public function enqueue(): string {
|
||||
$batch_processor = WCS_Batch_Processing_Controller::instance();
|
||||
if ( $batch_processor->is_enqueued( self::class ) ) {
|
||||
return __( 'Background process for updating subscription notifications already started, nothing done.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
$batch_processor->enqueue_processor( self::class );
|
||||
|
||||
return __( 'Background process for updating subscription notifications started', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the background process for batch processing subscription notifications updates.
|
||||
*
|
||||
* @return string Informative string to show after the tool is triggered in UI.
|
||||
*/
|
||||
public function dequeue(): string {
|
||||
$batch_processor = WCS_Batch_Processing_Controller::instance();
|
||||
if ( ! $batch_processor->is_enqueued( self::class ) ) {
|
||||
return __( 'Background process for updating subscription notifications not started, nothing done.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
$batch_processor->remove_processor( self::class );
|
||||
return __( 'Background process for updating subscription notifications stopped', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the tool to start or stop the background process that manages notification batch processing.
|
||||
*
|
||||
* @param array $tools Old tools array.
|
||||
* @return array Updated tools array.
|
||||
*/
|
||||
public function handle_woocommerce_debug_tools( array $tools ): array {
|
||||
|
||||
if ( ! WC_Subscriptions_Email_Notifications::notifications_globally_enabled() ) {
|
||||
|
||||
$tools['start_add_subscription_notifications'] = array(
|
||||
'name' => __( 'Regenerate subscription notifications', 'woocommerce-subscriptions' ),
|
||||
'button' => __( 'Regenerate notifications', 'woocommerce-subscriptions' ),
|
||||
'disabled' => true,
|
||||
'desc' => sprintf(
|
||||
'%1$s<br/><strong class="red">%2$s</strong> %3$s <a href="%4$s">%5$s</a>',
|
||||
__( 'This tool will add notifications to pending, active, and on-hold subscriptions. These updates will occur gradually in the background using Action Scheduler.', 'woocommerce-subscriptions' ),
|
||||
__( 'Note:', 'woocommerce-subscriptions' ),
|
||||
__( 'Notifications are currently turned off. To activate them, check the "Enable customer renewal reminder notification emails." option (via WooCommerce > Settings > Subscriptions > Customer Notifications).', 'woocommerce-subscriptions' ),
|
||||
esc_url( admin_url( 'admin.php?page=wc-settings&tab=subscriptions' ) ),
|
||||
__( 'Manage settings.', 'woocommerce-subscriptions' )
|
||||
),
|
||||
'requires_refresh' => true,
|
||||
);
|
||||
return $tools;
|
||||
}
|
||||
|
||||
$batch_processor = WCS_Batch_Processing_Controller::instance();
|
||||
|
||||
if ( $batch_processor->is_enqueued( self::class ) ) {
|
||||
|
||||
$pending_count = $this->get_total_pending_count();
|
||||
$tools['stop_add_subscription_notifications'] = array(
|
||||
'name' => __( 'Regenerate subscription notifications', 'woocommerce-subscriptions' ),
|
||||
'button' => __( 'Stop regenerating notifications', 'woocommerce-subscriptions' ),
|
||||
'desc' =>
|
||||
/* translators: %1$d=count of total entries needing conversion */
|
||||
sprintf( __( 'Stopping this will halt the background process that adds notifications to pending, active, and on-hold subscriptions. %1$d subscriptions remain to be processed.', 'woocommerce-subscriptions' ), $pending_count ),
|
||||
'callback' => array( $this, 'dequeue' ),
|
||||
'requires_refresh' => true,
|
||||
);
|
||||
} else {
|
||||
$tools['start_add_subscription_notifications'] = array(
|
||||
'name' => __( 'Regenerate subscription notifications', 'woocommerce-subscriptions' ),
|
||||
'button' => __( 'Regenerate notifications', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'This tool will regenerate notifications to pending, active, and on-hold subscriptions. These updates will occur gradually in the background using Action Scheduler.', 'woocommerce-subscriptions' ),
|
||||
'callback' => array( $this, 'enqueue' ),
|
||||
'requires_refresh' => true,
|
||||
);
|
||||
}
|
||||
|
||||
return $tools;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Display a row in the related orders table for a subscription or order
|
||||
*
|
||||
* @var WC_Order|WC_Subscription $order A WC_Order or WC_Subscription order object to display.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
// Order number column
|
||||
// translators: placeholder is an order number.
|
||||
$order_number = '<a href="' . esc_url( $order->get_edit_order_url() ) . '" aria-label="' . esc_attr( sprintf( __( 'Edit order number %s', 'woocommerce-subscriptions' ), $order->get_order_number() ) ) . '">' .
|
||||
// translators: placeholder is an order number.
|
||||
sprintf( esc_html_x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), esc_html( $order->get_order_number() ) ) .
|
||||
'</a>';
|
||||
|
||||
// Relationship column
|
||||
$relationship = esc_html( $order->get_meta( '_relationship' ) );
|
||||
|
||||
// Date created column
|
||||
$date_created = $order->get_date_created();
|
||||
|
||||
if ( $date_created ) {
|
||||
$t_time = $order->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );
|
||||
$date_to_display = ucfirst( wcs_get_human_time_diff( $date_created->getTimestamp() ) );
|
||||
} else {
|
||||
$t_time = __( 'Unpublished', 'woocommerce-subscriptions' );
|
||||
$date_to_display = $t_time;
|
||||
}
|
||||
|
||||
if ( ! wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
// Backwards compatibility for third-parties using the generic WP post time filter.
|
||||
// Only apply this filter if HPOS is not enabled, as the filter is not compatible with HPOS.
|
||||
$date_to_display = apply_filters( 'post_date_column_time', $date_to_display, get_post( $order->get_id() ) );
|
||||
}
|
||||
|
||||
$date_created = '<abbr title="' . esc_attr( $t_time ) . '">' . esc_html( apply_filters( 'wc_subscriptions_related_order_date_column', $date_to_display, $order ) ) . '</abbr>';
|
||||
|
||||
// Status column
|
||||
$classes = array(
|
||||
'order-status',
|
||||
sanitize_html_class( 'status-' . $order->get_status() ),
|
||||
);
|
||||
|
||||
if ( wcs_is_subscription( $order ) ) {
|
||||
$status_name = wcs_get_subscription_status_name( $order->get_status() );
|
||||
$classes[] = 'subscription-status';
|
||||
} else {
|
||||
$status_name = wc_get_order_status_name( $order->get_status() );
|
||||
}
|
||||
|
||||
$status_html = '<mark class="' . esc_attr( implode( ' ', $classes ) ) . '"><span>' . esc_html( $status_name ) . '</span></mark>';
|
||||
|
||||
// Total column
|
||||
$total = '<span class="amount">' . wp_kses(
|
||||
$order->get_formatted_order_total(),
|
||||
array(
|
||||
'small' => array(),
|
||||
'span' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'del' => array(),
|
||||
'ins' => array(),
|
||||
)
|
||||
) . '</span>';
|
||||
|
||||
$columns = array(
|
||||
$order_number,
|
||||
$relationship,
|
||||
$date_created,
|
||||
$status_html,
|
||||
$total,
|
||||
);
|
||||
|
||||
$columns = apply_filters( 'wcs_related_orders_table_row_columns', $columns );
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<?php foreach ( $columns as $column ) { ?>
|
||||
<td>
|
||||
<?php echo wp_kses_post( $column ); ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce Subscriptions Admin Functions
|
||||
*
|
||||
* @author Prospress
|
||||
* @category Core
|
||||
* @package WooCommerce Subscriptions/Functions
|
||||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an admin notice to be displayed once to the current (or other specified) user.
|
||||
*
|
||||
* @see wcs_display_admin_notices()
|
||||
* @since 1.0.0 Migrated from WooCommerce Subscriptions v2.0.
|
||||
* @since 7.2.0 Added support for specifying the target user and context.
|
||||
*
|
||||
* @param string $message The message to display.
|
||||
* @param string $notice_type Either 'success' or 'error'.
|
||||
* @param int|null $user_id The specific user who should see this message. If not specified, defaults to the current user.
|
||||
* @param string|null $screen_id The screen ID for which the message should be displayed. If not specified, it will show on the next admin page load.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wcs_add_admin_notice( $message, $notice_type = 'success', $user_id = null, $screen_id = null ) {
|
||||
$user_id = (int) ( null === $user_id ? get_current_user_id() : $user_id );
|
||||
|
||||
if ( $user_id < 1 ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
/* Translators: %1$s: notice type ('success' or 'error'), %2$s: notice text. */
|
||||
'Admin notices can only be added if a user is currently logged in. Attempted (%1$s) notice: "%2$s"',
|
||||
$notice_type,
|
||||
$message
|
||||
),
|
||||
array(
|
||||
'backtrace' => true,
|
||||
'user_id' => $user_id,
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$notices = get_transient( '_wcs_admin_notices_' . $user_id );
|
||||
|
||||
if ( ! is_array( $notices ) ) {
|
||||
$notices = array();
|
||||
}
|
||||
|
||||
$notices[ $notice_type ][] = array(
|
||||
'message' => $message,
|
||||
'screen_id' => $screen_id,
|
||||
);
|
||||
|
||||
set_transient( '_wcs_admin_notices_' . $user_id, $notices, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display any admin notices added with wcs_add_admin_notice().
|
||||
*
|
||||
* @see wcs_add_admin_notice()
|
||||
* @since 1.0.0 Migrated from WooCommerce Subscriptions v2.0.
|
||||
* @since 7.2.0 Supports contextual awareness of the user and screen.
|
||||
*
|
||||
* @param bool $clear If the message queue should be cleared after rendering the message(s). Defaults to true.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wcs_display_admin_notices( $clear = true ) {
|
||||
$user_id = get_current_user_id();
|
||||
$notices = get_transient( '_wcs_admin_notices_' . $user_id );
|
||||
|
||||
if ( ! is_array( $notices ) || empty( $notices ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes, sanitizes and outputs the provided notices.
|
||||
*
|
||||
* @param array &$notices The notice data.
|
||||
* @param string $class The CSS notice class to be applied (typically 'updated' or 'error').
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
$handle_notices = static function ( &$notices, $class ) {
|
||||
$notice_output = array();
|
||||
$screen_id = false;
|
||||
|
||||
foreach ( $notices as $index => $notice ) {
|
||||
// Ensure the notice data now has the expected shape. If it does not, remove it.
|
||||
if ( ! is_array( $notice ) || ! isset( $notice['message'] ) || ! array_key_exists( 'screen_id', $notice ) ) {
|
||||
unset( $notices[ $index ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
// We only need to determine the current screen ID once.
|
||||
if ( false === $screen_id ) {
|
||||
$screen = get_current_screen();
|
||||
$screen_id = $screen instanceof WP_Screen ? $screen->id : '';
|
||||
}
|
||||
|
||||
// Should the notice display in the current screen context?
|
||||
if ( is_string( $notice['screen_id'] ) && $screen_id !== $notice['screen_id'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$notice_output[] = $notice['message'];
|
||||
unset( $notices[ $index ] );
|
||||
}
|
||||
|
||||
// $notice_output may be empty if some notices were withheld, due to not matching the screen context.
|
||||
if ( ! empty( $notice_output ) ) {
|
||||
echo '<div id="moderated" class="' . esc_attr( $class ) . '"><p>' . wp_kses_post( implode( "</p>\n<p>", $notice_output ) ) . '</p></div>';
|
||||
}
|
||||
};
|
||||
|
||||
if ( ! empty( $notices['success'] ) ) {
|
||||
$handle_notices( $notices['success'], 'updated' );
|
||||
}
|
||||
|
||||
if ( ! empty( $notices['error'] ) ) {
|
||||
$handle_notices( $notices['error'], 'error' );
|
||||
}
|
||||
|
||||
// Under certain circumstances, the caller may not wish for the rendered messages to be cleared from the queue.
|
||||
if ( false === $clear ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If all notices were rendered, clear the queue. If only some were rendered, clear what we can.
|
||||
if ( empty( $notices['success'] ) && empty( $notices['error'] ) ) {
|
||||
wcs_clear_admin_notices();
|
||||
} else {
|
||||
set_transient( '_wcs_admin_notices_' . $user_id, $notices, HOUR_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'admin_notices', 'wcs_display_admin_notices' );
|
||||
|
||||
/**
|
||||
* Delete any admin notices we stored for display later.
|
||||
*
|
||||
* @since 1.0.0 Migrated from WooCommerce Subscriptions v2.0.
|
||||
* @since 7.2.0 Became user aware.
|
||||
*/
|
||||
function wcs_clear_admin_notices() {
|
||||
delete_transient( '_wcs_admin_notices_' . get_current_user_id() );
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Subscription Query Controller
|
||||
*
|
||||
* This class is used to assist the wcs_get_subscriptions() query for something.
|
||||
* It currently supports 1 feature to determine if the query should be filtered by product ID or variation ID after the query has been run.
|
||||
*
|
||||
* QUERYING SUBSCRIPTIONS BY PRODUCT
|
||||
*
|
||||
* Querying subscriptions by product or variation ID is an expensive database operation. This class provides methods to determine if a wcs_get_subscriptions()
|
||||
* set of args would be better served by filtering the query results by product ID or variation ID after the query has been run, rather than querying for
|
||||
* subscriptions by products.
|
||||
*
|
||||
* If the wcs_get_subscriptions() args are already limited by customer ID or order ID we know that the results will be sufficiently limited. In these cases, we can
|
||||
* filter the results by product ID or variation ID after the query has been run.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage Component
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Subscription_Query_Controller class.
|
||||
*/
|
||||
class WC_Subscription_Query_Controller {
|
||||
|
||||
/**
|
||||
* The wcs_get_subscriptions() query variables.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $query_vars = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $query_vars The wcs_get_subscriptions() query variables.
|
||||
*/
|
||||
public function __construct( $query_vars ) {
|
||||
$this->query_vars = $query_vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the query is for a specific product or variation.
|
||||
*
|
||||
* @return bool True if the query is for a specific product or variation, otherwise false.
|
||||
*/
|
||||
public function has_product_query() {
|
||||
return ( 0 !== $this->query_vars['product_id'] && is_numeric( $this->query_vars['product_id'] ) ) || ( 0 !== $this->query_vars['variation_id'] && is_numeric( $this->query_vars['variation_id'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the wcs_get_subscription() query should filter the results by product ID or variation ID after the query has been run.
|
||||
*
|
||||
* If the wcs_get_subscriptions() query is substantially limited (eg to a customer or order) we know that the results will be small. In these cases, we can
|
||||
* filter the results by product ID or variation ID after the query has been run for better performance.
|
||||
*
|
||||
* @return bool True if the subscriptions should be queried by product ID, otherwise false.
|
||||
*/
|
||||
public function should_filter_query_results() {
|
||||
$can_filter_results = false;
|
||||
$order_id = $this->query_vars['order_id'] ?? 0;
|
||||
$customer_id = $this->query_vars['customer_id'] ?? 0;
|
||||
|
||||
// If we're querying by order ID or customer ID, we can filter the results by product ID after the query has been run.
|
||||
if ( ! empty( $order_id ) || ! empty( $customer_id ) ) {
|
||||
$can_filter_results = apply_filters( 'wcs_should_filter_subscriptions_results_by_product_id', true, $this->query_vars );
|
||||
}
|
||||
|
||||
return $can_filter_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the subscription query results by product ID or variation ID.
|
||||
*
|
||||
* @param WC_Subscription[] $subscriptions
|
||||
* @return WC_Subscription[] The filtered subscriptions.
|
||||
*/
|
||||
public function filter_subscriptions( $subscriptions ) {
|
||||
$filtered_subscriptions = [];
|
||||
$product_id = $this->query_vars['product_id'] ?? 0;
|
||||
$variation_id = $this->query_vars['variation_id'] ?? 0;
|
||||
|
||||
if ( empty( $product_id ) && empty( $variation_id ) ) {
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
// Filter the subscriptions by product ID or variation ID.
|
||||
foreach ( $subscriptions as $subscription_id => $subscription ) {
|
||||
if (
|
||||
( $variation_id && $subscription->has_product( $variation_id ) ) ||
|
||||
( $product_id && $subscription->has_product( $product_id ) )
|
||||
) {
|
||||
$filtered_subscriptions[ $subscription_id ] = $subscription;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered_subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies pagination to the subscriptions array.
|
||||
*
|
||||
* @param WC_Subscriptions[] $subscriptions
|
||||
* @return WC_Subscriptions[] The subscriptions array with pagination applied.
|
||||
*/
|
||||
public function paginate_results( $subscriptions ) {
|
||||
$per_page = $this->query_vars['subscriptions_per_page'];
|
||||
$page = $this->query_vars['paged'];
|
||||
$offset = $this->query_vars['offset'];
|
||||
|
||||
// If the limit is -1, return all subscriptions.
|
||||
if ( -1 === $per_page ) {
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
if ( $offset ) {
|
||||
$start_index = $offset;
|
||||
} else {
|
||||
// Calculate the starting index for the slice.
|
||||
$start_index = ( $page - 1 ) * $per_page;
|
||||
}
|
||||
|
||||
// Slice the subscriptions array to get the required items.
|
||||
return array_slice( $subscriptions, $start_index, $per_page, true );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Subscriptions Email Notifications Class
|
||||
*
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @subpackage WC_Subscriptions_Email
|
||||
* @category Class
|
||||
*/
|
||||
class WC_Subscriptions_Email_Notifications {
|
||||
|
||||
/**
|
||||
* @var string Offset setting option identifier.
|
||||
*/
|
||||
public static $offset_setting_string = '_customer_notifications_offset';
|
||||
|
||||
/**
|
||||
* @var string Enabled/disabled setting option identifier.
|
||||
*/
|
||||
public static $switch_setting_string = '_customer_notifications_enabled';
|
||||
|
||||
/**
|
||||
* List of subscription notification email classes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $email_classes = [
|
||||
'WCS_Email_Customer_Notification_Manual_Trial_Expiration' => true,
|
||||
'WCS_Email_Customer_Notification_Auto_Trial_Expiration' => true,
|
||||
'WCS_Email_Customer_Notification_Manual_Renewal' => true,
|
||||
'WCS_Email_Customer_Notification_Auto_Renewal' => true,
|
||||
'WCS_Email_Customer_Notification_Subscription_Expiration' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public static function init() {
|
||||
|
||||
add_action( 'woocommerce_email_classes', [ __CLASS__, 'add_emails' ], 10, 1 );
|
||||
|
||||
add_action( 'woocommerce_init', [ __CLASS__, 'hook_notification_emails' ] );
|
||||
|
||||
// Add notification actions to the admin edit subscriptions page.
|
||||
add_filter( 'woocommerce_order_actions', [ __CLASS__, 'add_notification_actions' ], 10, 1 );
|
||||
|
||||
// Trigger actions from Edit order screen.
|
||||
add_action( 'woocommerce_order_action_wcs_customer_notification_free_trial_expiration', [ __CLASS__, 'forward_action' ], 10, 1 );
|
||||
add_action( 'woocommerce_order_action_wcs_customer_notification_subscription_expiration', [ __CLASS__, 'forward_action' ], 10, 1 );
|
||||
add_action( 'woocommerce_order_action_wcs_customer_notification_renewal', [ __CLASS__, 'forward_action' ], 10, 1 );
|
||||
|
||||
// Add settings UI.
|
||||
add_filter( 'woocommerce_subscription_settings', [ __CLASS__, 'add_settings' ], 20 );
|
||||
|
||||
// Bump settings update time whenever related options change.
|
||||
add_action( 'update_option_' . WC_Subscriptions_Admin::$option_prefix . self::$offset_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10 );
|
||||
add_action( 'update_option_' . WC_Subscriptions_Admin::$option_prefix . self::$switch_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10 );
|
||||
add_action( 'add_option_' . WC_Subscriptions_Admin::$option_prefix . self::$offset_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10 );
|
||||
add_action( 'add_option_' . WC_Subscriptions_Admin::$option_prefix . self::$switch_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Map and forward Edit order screen action to the correct reminder.
|
||||
*
|
||||
* @param $order
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function forward_action( $order ) {
|
||||
$trigger_action = '';
|
||||
$current_action = current_action();
|
||||
switch ( $current_action ) {
|
||||
case 'woocommerce_order_action_wcs_customer_notification_free_trial_expiration':
|
||||
$trigger_action = 'woocommerce_scheduled_subscription_customer_notification_trial_expiration';
|
||||
break;
|
||||
case 'woocommerce_order_action_wcs_customer_notification_subscription_expiration':
|
||||
$trigger_action = 'woocommerce_scheduled_subscription_customer_notification_expiration';
|
||||
break;
|
||||
case 'woocommerce_order_action_wcs_customer_notification_renewal':
|
||||
$trigger_action = 'woocommerce_scheduled_subscription_customer_notification_renewal';
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $trigger_action ) {
|
||||
do_action( $trigger_action, $order->get_id() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the update time when any of the settings that affect notifications change and triggers update of subscriptions.
|
||||
*
|
||||
* When time offset or global on/off switch change values, this method gets triggered and it:
|
||||
* 1. Updates the wcs_notification_settings_update_time option so that the code knows which subscriptions to update
|
||||
* 2. Triggers rescheduling/unscheduling of existing notifications.
|
||||
* 3. Adds a notice with info about the actions that got triggered to the store manager.
|
||||
*
|
||||
* Side note: offset gets updated in WCS_Action_Scheduler_Customer_Notifications::set_time_offset_from_option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_notification_settings_update_time() {
|
||||
update_option( 'wcs_notification_settings_update_time', time() );
|
||||
|
||||
// Shortcut to unschedule all notifications more efficiently instead of processing them subscription by subscription.
|
||||
if ( ! self::notifications_globally_enabled() ) {
|
||||
as_unschedule_all_actions( null, [], 'wcs_customer_notifications' );
|
||||
} else {
|
||||
$message = WCS_Notifications_Batch_Processor::enqueue();
|
||||
$admin_notice = new WCS_Admin_Notice( 'updated' );
|
||||
$admin_notice->set_simple_content( $message );
|
||||
$admin_notice->display();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Subscriptions notifications' email classes.
|
||||
*/
|
||||
public static function add_emails( $email_classes ) {
|
||||
foreach ( self::$email_classes as $email_class => $_ ) {
|
||||
$email_classes[ $email_class ] = new $email_class();
|
||||
}
|
||||
|
||||
return $email_classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook the notification emails with our custom trigger.
|
||||
*/
|
||||
public static function hook_notification_emails() {
|
||||
add_action( 'woocommerce_scheduled_subscription_customer_notification_renewal', [ __CLASS__, 'send_notification' ] );
|
||||
add_action( 'woocommerce_scheduled_subscription_customer_notification_trial_expiration', [ __CLASS__, 'send_notification' ] );
|
||||
add_action( 'woocommerce_scheduled_subscription_customer_notification_expiration', [ __CLASS__, 'send_notification' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification emails.
|
||||
*
|
||||
* @param int $subscription_id Subscription ID.
|
||||
*/
|
||||
public static function send_notification( $subscription_id ) {
|
||||
|
||||
// Init email classes.
|
||||
$emails = WC()->mailer()->get_emails();
|
||||
|
||||
if ( ! ( $emails['WCS_Email_Customer_Notification_Auto_Renewal'] instanceof WCS_Email_Customer_Notification_Auto_Renewal
|
||||
&& $emails['WCS_Email_Customer_Notification_Manual_Renewal'] instanceof WCS_Email_Customer_Notification_Manual_Renewal
|
||||
&& $emails['WCS_Email_Customer_Notification_Subscription_Expiration'] instanceof WCS_Email_Customer_Notification_Subscription_Expiration
|
||||
&& $emails['WCS_Email_Customer_Notification_Manual_Trial_Expiration'] instanceof WCS_Email_Customer_Notification_Manual_Trial_Expiration
|
||||
&& $emails['WCS_Email_Customer_Notification_Auto_Trial_Expiration'] instanceof WCS_Email_Customer_Notification_Auto_Trial_Expiration
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$notification = null;
|
||||
switch ( current_action() ) {
|
||||
case 'woocommerce_scheduled_subscription_customer_notification_renewal':
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $subscription->get_total() <= 0 ) {
|
||||
break;
|
||||
}
|
||||
if ( $subscription->is_manual() ) {
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Manual_Renewal'];
|
||||
} else {
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Auto_Renewal'];
|
||||
}
|
||||
break;
|
||||
case 'woocommerce_scheduled_subscription_customer_notification_trial_expiration':
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $subscription->is_manual() ) {
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Manual_Trial_Expiration'];
|
||||
} else {
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Auto_Trial_Expiration'];
|
||||
}
|
||||
break;
|
||||
case 'woocommerce_scheduled_subscription_customer_notification_expiration':
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Subscription_Expiration'];
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $notification && $notification->is_enabled() ) {
|
||||
$notification->trigger( $subscription_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the notifications feature enabled?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function notifications_globally_enabled() {
|
||||
return ( 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . self::$switch_setting_string )
|
||||
&& get_option( WC_Subscriptions_Admin::$option_prefix . self::$offset_setting_string ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the emails be sent out?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_send_notification() {
|
||||
$notification_enabled = true;
|
||||
|
||||
if ( WCS_Staging::is_duplicate_site() ) {
|
||||
$notification_enabled = false;
|
||||
}
|
||||
|
||||
$allowed_env_types = [
|
||||
'production',
|
||||
];
|
||||
if ( ! in_array( wp_get_environment_type(), $allowed_env_types, true ) ) {
|
||||
$notification_enabled = false;
|
||||
}
|
||||
|
||||
// If Customer notifications are disabled in the settings by a global switch, or there is no offset set, don't send notifications.
|
||||
if ( ! self::notifications_globally_enabled() ) {
|
||||
$notification_enabled = false;
|
||||
}
|
||||
|
||||
return $notification_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds actions to the admin edit subscriptions page.
|
||||
*
|
||||
* @param array $actions An array of available actions
|
||||
* @return array An array of updated actions
|
||||
*/
|
||||
public static function add_notification_actions( $actions ) {
|
||||
global $theorder;
|
||||
|
||||
if ( ! self::notifications_globally_enabled() ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
if ( ! wcs_is_subscription( $theorder ) ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
if ( ! $theorder->has_status( [ 'active', 'on-hold', 'pending-cancel' ] ) ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$valid_notifications = WCS_Action_Scheduler_Customer_Notifications::get_valid_notifications( $theorder );
|
||||
|
||||
if ( in_array( 'trial_end', $valid_notifications, true ) ) {
|
||||
$actions['wcs_customer_notification_free_trial_expiration'] = esc_html__( 'Send trial is ending notification', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
if ( in_array( 'end', $valid_notifications, true ) ) {
|
||||
$actions['wcs_customer_notification_subscription_expiration'] = esc_html__( 'Send upcoming subscription expiration notification', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
if ( in_array( 'next_payment', $valid_notifications, true ) && $theorder->get_total() > 0 ) {
|
||||
$actions['wcs_customer_notification_renewal'] = esc_html__( 'Send upcoming renewal notification', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the subscription notification setting.
|
||||
*
|
||||
* @param array $settings Subscriptions settings.
|
||||
* @return array Subscriptions settings.
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
|
||||
$notification_settings = [
|
||||
[
|
||||
'name' => __( 'Customer notifications', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_customer_notifications',
|
||||
/* translators: Link to WC Settings > Email. */
|
||||
'desc' => sprintf( __( 'To enable/disable individual notifications and customize templates, visit the <a href="%s">Email settings</a>.', 'woocommerce-subscriptions' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Enable Reminders', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'Send notification emails to customers for subscription renewals and expirations.', 'woocommerce-subscriptions' ),
|
||||
'tip' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . self::$switch_setting_string,
|
||||
'desc_tip' => false,
|
||||
'type' => 'checkbox',
|
||||
'default' => 'no',
|
||||
'autoload' => false,
|
||||
],
|
||||
[
|
||||
'name' => __( 'Reminder Timing', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'How long before the event should the notification be sent.', 'woocommerce-subscriptions' ),
|
||||
'tip' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . self::$offset_setting_string,
|
||||
'desc_tip' => true,
|
||||
'type' => 'relative_date_selector',
|
||||
'placeholder' => __( 'N/A', 'woocommerce-subscriptions' ),
|
||||
'default' => [
|
||||
'number' => '3',
|
||||
'unit' => 'days',
|
||||
],
|
||||
'autoload' => false,
|
||||
],
|
||||
[
|
||||
'type' => 'sectionend',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_customer_notifications',
|
||||
],
|
||||
];
|
||||
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' ) ) {
|
||||
$settings = array_merge( $settings, $notification_settings );
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Tracker for Subscriptions usage.
|
||||
*
|
||||
* @class WC_Subscriptions_Tracker
|
||||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.4
|
||||
* @package WooCommerce Subscriptions/Classes
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Collector;
|
||||
|
||||
class WC_Subscriptions_Tracker {
|
||||
/**
|
||||
* Handles the collection of additional pieces of data.
|
||||
*/
|
||||
private static Collector $telemetry_collector;
|
||||
|
||||
/**
|
||||
* Initialize the Tracker.
|
||||
*/
|
||||
public static function init() {
|
||||
// Only add data if Tracker enabled
|
||||
if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
|
||||
self::$telemetry_collector = new Collector();
|
||||
self::$telemetry_collector->setup();
|
||||
add_filter( 'woocommerce_tracker_data', [ __CLASS__, 'add_subscriptions_tracking_data' ], 10, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Subscriptions data to the WC tracked data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array all the tracking data.
|
||||
*/
|
||||
public static function add_subscriptions_tracking_data( $data ) {
|
||||
$data['extensions']['wc_subscriptions']['settings'] = self::get_subscriptions_options();
|
||||
$data['extensions']['wc_subscriptions']['subscriptions'] = self::get_subscriptions();
|
||||
$data['extensions']['wc_subscriptions']['subscription_orders'] = self::get_subscription_orders();
|
||||
|
||||
// Insert any additional telemetry that we have been collecting.
|
||||
$additional_telemetry = self::$telemetry_collector->get_telemetry_data();
|
||||
$data['extensions']['wc_subscriptions'] = array_merge_recursive( $data['extensions']['wc_subscriptions'], $additional_telemetry );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tracked Subscriptions options data.
|
||||
*
|
||||
* @return array Subscriptions options data.
|
||||
*/
|
||||
private static function get_subscriptions_options() {
|
||||
$customer_notifications_offset = get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string, [] );
|
||||
|
||||
return [
|
||||
// Staging and live site
|
||||
'wc_subscriptions_staging' => WCS_Staging::is_duplicate_site() ? 'staging' : 'live',
|
||||
'wc_subscriptions_live_url' => esc_url( WCS_Staging::get_site_url_from_source( 'subscriptions_install' ) ),
|
||||
|
||||
// Button text
|
||||
// Add to Cart Button Text
|
||||
'add_to_cart_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text' ),
|
||||
// Place Order Button Text
|
||||
'order_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_order_button_text' ),
|
||||
|
||||
// Roles
|
||||
// Subscriber Default Role
|
||||
'subscriber_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_subscriber_role' ),
|
||||
// Inactive Subscriber Role
|
||||
'cancelled_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_cancelled_role' ),
|
||||
|
||||
// Renewals
|
||||
// Accept Manual Renewals
|
||||
'accept_manual_renewals' => get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ),
|
||||
// Turn off Automatic Payments
|
||||
'turn_off_automatic_payments' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'none' ),
|
||||
// Auto Renewal Toggle
|
||||
'enable_auto_renewal_toggle' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_auto_renewal_toggle' ),
|
||||
|
||||
// Early renewal
|
||||
// Accept Early Renewal Payments
|
||||
'enable_early_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ),
|
||||
// Accept Early Renewal Payments via a Modal
|
||||
'enable_early_renewal_via_modal' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal_via_modal', 'none' ),
|
||||
|
||||
// Switching
|
||||
// Between Subscription Variations and Between Grouped Subscriptions are condensed into this setting.
|
||||
'allow_switching' => get_option( WC_Subscriptions_Admin::$option_prefix . '_allow_switching' ),
|
||||
// Prorate Recurring Payment
|
||||
'apportion_recurring_price' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'none' ),
|
||||
// Prorate Sign up Fee
|
||||
'apportion_sign_up_fee' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'none' ),
|
||||
// Prorate Subscription Length
|
||||
'apportion_length' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'none' ),
|
||||
// Switch Button Text
|
||||
'switch_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_switch_button_text', 'none' ),
|
||||
|
||||
// Gifting
|
||||
// Enable gifting
|
||||
'gifting_enable_gifting' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_enable_gifting' ),
|
||||
'gifting_default_option' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_default_option' ),
|
||||
// Gifting Checkbox Text
|
||||
'gifting_gifting_checkbox_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_gifting_checkbox_text' ),
|
||||
// Downloadable Products
|
||||
'gifting_downloadable_products' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_downloadable_products' ),
|
||||
|
||||
// Synchronization
|
||||
// Synchronise renewals
|
||||
'sync_payments' => get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ),
|
||||
// Prorate First Renewal
|
||||
'prorate_synced_payments' => $prorate_synced_payments = ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_prorate_synced_payments', 'none' ) ),
|
||||
// Sign-up grace period
|
||||
'days_no_fee' => 'recurring' === $prorate_synced_payments ? get_option( WC_Subscriptions_Admin::$option_prefix . '_days_no_fee', 'none' ) : 'none',
|
||||
|
||||
// Miscellaneous
|
||||
// Customer Suspensions
|
||||
'max_customer_suspensions' => get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions' ),
|
||||
// Mixed Checkout
|
||||
'multiple_purchase' => get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase' ),
|
||||
// $0 Initial Checkout
|
||||
'allow_zero_initial_order_without_payment_method' => get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment' ),
|
||||
// Drip Downloadable Content
|
||||
'drip_downloadable_content_on_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_drip_downloadable_content_on_renewal' ),
|
||||
// Retry Failed Payments
|
||||
'enable_retry' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_retry' ),
|
||||
|
||||
// Notifications
|
||||
// Enable Reminders
|
||||
'enable_notification_reminders' => get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string ),
|
||||
// Reminder Timing
|
||||
'customer_notifications_offset_number' => $customer_notifications_offset['number'] ?? 'none',
|
||||
'customer_notifications_offset_unit' => $customer_notifications_offset['unit'] ?? 'none',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the combined subscription dates, count, and totals data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_subscriptions() {
|
||||
$subscription_dates = self::get_subscription_dates();
|
||||
$subscription_counts = self::get_subscription_counts();
|
||||
|
||||
return array_merge( $subscription_dates, $subscription_counts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets subscription counts.
|
||||
*
|
||||
* @return array Subscription count by status. Keys are subscription status slugs, values are subscription counts (string).
|
||||
*/
|
||||
private static function get_subscription_counts() {
|
||||
$subscription_counts = [];
|
||||
$count_by_status = WC_Data_Store::load( 'subscription' )->get_subscriptions_count_by_status();
|
||||
foreach ( wcs_get_subscription_statuses() as $status_slug => $status_name ) {
|
||||
$subscription_counts[ $status_slug ] = $count_by_status[ $status_slug ] ?? 0;
|
||||
}
|
||||
// Ensure all values are strings.
|
||||
$subscription_counts = array_map( 'strval', $subscription_counts );
|
||||
return $subscription_counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets subscription order counts and totals.
|
||||
*
|
||||
* @return array Subscription order counts and totals by type (initial, switch, renewal, resubscribe). Values are returned as strings.
|
||||
*/
|
||||
private static function get_subscription_orders() {
|
||||
$order_totals = array();
|
||||
|
||||
// Get the subtotal and count for all subscription types in one query
|
||||
$counts_and_totals = self::get_order_count_and_total_by_meta_key();
|
||||
|
||||
foreach ( $counts_and_totals as $type => $data ) {
|
||||
$order_totals[ $type . '_gross' ] = $data['total'];
|
||||
$order_totals[ $type . '_count' ] = $data['count'];
|
||||
}
|
||||
|
||||
// Get initial orders (orders without switch, renewal, or resubscribe meta keys).
|
||||
$count_and_total = self::get_initial_order_count_and_total();
|
||||
|
||||
$order_totals['initial_gross'] = $count_and_total['total'];
|
||||
$order_totals['initial_count'] = $count_and_total['count'];
|
||||
|
||||
// Ensure all values are strings.
|
||||
$order_totals = array_map( 'strval', $order_totals );
|
||||
|
||||
return $order_totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets order count and total for subscription-related orders.
|
||||
*
|
||||
* @return array Array with counts and totals for switch, renewal, and resubscribe orders.
|
||||
*/
|
||||
private static function get_order_count_and_total_by_meta_key() {
|
||||
global $wpdb;
|
||||
|
||||
$order_statuses = array( 'wc-completed', 'wc-refunded' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
// HPOS: Use wc_orders and wc_orders_meta tables.
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
$meta_table = $wpdb->prefix . 'wc_orders_meta';
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
order_relation.meta_key as 'type',
|
||||
SUM( orders.total_amount ) AS 'total',
|
||||
COUNT( orders.id ) as 'count'
|
||||
FROM {$orders_table} AS orders
|
||||
RIGHT JOIN {$meta_table} AS order_relation ON order_relation.order_id = orders.id
|
||||
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
|
||||
AND orders.status in (%s, %s)
|
||||
AND orders.type = 'shop_order'
|
||||
GROUP BY order_relation.meta_key",
|
||||
$order_statuses
|
||||
);
|
||||
} else {
|
||||
// CPT: Use posts and postmeta tables.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
order_relation.meta_key as 'type',
|
||||
SUM( order_total.meta_value ) AS 'total',
|
||||
COUNT( orders.ID ) as 'count'
|
||||
FROM {$wpdb->prefix}posts AS orders
|
||||
RIGHT JOIN {$wpdb->prefix}postmeta AS order_relation ON order_relation.post_id = orders.ID
|
||||
RIGHT JOIN {$wpdb->prefix}postmeta AS order_total ON order_total.post_id = orders.ID
|
||||
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
|
||||
AND orders.post_status in (%s, %s)
|
||||
AND order_total.meta_key = '_order_total'
|
||||
GROUP BY order_relation.meta_key",
|
||||
$order_statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
|
||||
$results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$totals = array(
|
||||
'switch' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
'renewal' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
'resubscribe' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
);
|
||||
|
||||
if ( $results ) {
|
||||
foreach ( $results as $result ) {
|
||||
$type = str_replace( '_subscription_', '', $result['type'] );
|
||||
if ( isset( $totals[ $type ] ) ) {
|
||||
$totals[ $type ] = array(
|
||||
'count' => (int) $result['count'],
|
||||
'total' => (float) $result['total'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log if any type has no data
|
||||
foreach ( $totals as $type => $data ) {
|
||||
if ( 0 === $data['count'] && 0.0 === $data['total'] ) {
|
||||
wc_get_logger()->warning( "WC_Subscriptions_Tracker::get_order_count_and_total_by_meta_key() returned 0 count and total for {$type} orders" );
|
||||
}
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets count and total for initial orders (orders without subscription relation meta keys).
|
||||
*
|
||||
* @return array Array with 'count' and 'total' keys.
|
||||
*/
|
||||
private static function get_initial_order_count_and_total() {
|
||||
global $wpdb;
|
||||
|
||||
$order_statuses = array( 'wc-completed', 'wc-refunded' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
// HPOS: Use wc_orders table
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
SUM( orders.total_amount ) AS 'total', COUNT( DISTINCT orders.id ) as 'count'
|
||||
FROM {$orders_table} AS orders
|
||||
RIGHT JOIN {$orders_table} AS subscriptions ON subscriptions.parent_order_id = orders.id
|
||||
WHERE orders.status in ( %s, %s )
|
||||
AND subscriptions.type = 'shop_subscription'
|
||||
AND orders.type = 'shop_order'",
|
||||
$order_statuses
|
||||
);
|
||||
} else {
|
||||
// CPT: Use posts and postmeta tables.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
SUM( order_total.meta_value ) AS 'total', COUNT( * ) as 'count'
|
||||
FROM {$wpdb->posts} AS orders
|
||||
RIGHT JOIN {$wpdb->posts} AS subscriptions ON subscriptions.post_parent = orders.ID
|
||||
RIGHT JOIN {$wpdb->postmeta} AS order_total ON order_total.post_id = orders.ID
|
||||
WHERE orders.post_status in ( %s, %s )
|
||||
AND subscriptions.post_type = 'shop_subscription'
|
||||
AND orders.post_type = 'shop_order'
|
||||
AND order_total.meta_key = '_order_total'",
|
||||
$order_statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
|
||||
$result = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$totals = array(
|
||||
'count' => (int) ( $result ? $result['count'] : 0 ),
|
||||
'total' => (float) ( $result ? $result['total'] : 0 ),
|
||||
);
|
||||
|
||||
if ( 0 === $totals['count'] && 0.0 === $totals['total'] ) {
|
||||
wc_get_logger()->warning( 'WC_Subscriptions_Tracker::get_initial_order_count_and_total() returned 0 count and total' );
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first and last subscription created dates.
|
||||
*
|
||||
* @return array 'first' and 'last' created subscription dates as a string in the date format 'Y-m-d H:i:s' or '-'.
|
||||
*/
|
||||
private static function get_subscription_dates() {
|
||||
// Ignore subscriptions with status 'trash'.
|
||||
$first = wcs_get_subscriptions(
|
||||
[
|
||||
'subscriptions_per_page' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'subscription_status' => [ 'active', 'on-hold', 'pending', 'cancelled', 'expired' ],
|
||||
]
|
||||
);
|
||||
$last = wcs_get_subscriptions(
|
||||
[
|
||||
'subscriptions_per_page' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'subscription_status' => [ 'active', 'on-hold', 'pending', 'cancelled', 'expired' ],
|
||||
]
|
||||
);
|
||||
|
||||
// Return each date in 'Y-m-d H:i:s' format or '-' if no subscriptions found.
|
||||
$min_max = [
|
||||
'first' => count( $first ) ? array_shift( $first )->get_date( 'date_created' ) : '-',
|
||||
'last' => count( $last ) ? array_shift( $last )->get_date( 'date_created' ) : '-',
|
||||
];
|
||||
|
||||
return $min_max;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,549 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Scheduler for subscription notifications that uses the Action Scheduler
|
||||
*
|
||||
* @class WCS_Action_Scheduler_Customer_Notifications
|
||||
* @version 7.7.0
|
||||
* @package WooCommerce Subscriptions/Classes
|
||||
* @category Class
|
||||
*/
|
||||
class WCS_Action_Scheduler_Customer_Notifications extends WCS_Scheduler {
|
||||
|
||||
/**
|
||||
* @var int Time offset (in whole seconds) between the notification and the action it's notifying about.
|
||||
*/
|
||||
protected $time_offset;
|
||||
|
||||
/**
|
||||
* @var array|string[] Notifications scheduled by this class.
|
||||
*
|
||||
* Just for reference.
|
||||
*/
|
||||
protected static $notification_actions = [
|
||||
'woocommerce_scheduled_subscription_customer_notification_trial_expiration',
|
||||
'woocommerce_scheduled_subscription_customer_notification_expiration',
|
||||
'woocommerce_scheduled_subscription_customer_notification_renewal',
|
||||
];
|
||||
|
||||
/**
|
||||
* Name of Action Scheduler group used for customer notification actions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $notifications_as_group = 'wcs_customer_notifications';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$setting_option = get_option(
|
||||
WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string,
|
||||
[
|
||||
'number' => 3,
|
||||
'unit' => 'days',
|
||||
]
|
||||
);
|
||||
$this->set_time_offset( self::convert_offset_to_seconds( $setting_option ) );
|
||||
|
||||
add_action( 'woocommerce_before_subscription_object_save', [ $this, 'update_notifications' ], 10, 2 );
|
||||
|
||||
add_action( 'update_option_' . WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string, [ $this, 'set_time_offset_from_option' ], 5, 2 );
|
||||
add_action( 'add_option_' . WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string, [ $this, 'set_time_offset_from_option' ], 5, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the subscription period is too short to send a renewal notification.
|
||||
*
|
||||
* @param $subscription
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_subscription_period_too_short( $subscription ) {
|
||||
$period = $subscription->get_billing_period();
|
||||
$interval = $subscription->get_billing_interval();
|
||||
|
||||
// By default, there are no shorter periods than days in WCS, so we ignore hours, minutes, etc.
|
||||
if ( $interval <= 2 && 'day' === $period ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return time offset for notifications for given subscription.
|
||||
*
|
||||
* Generally, there is one offset for all subscriptions, but there's a filter.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param string $notification_type
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get_time_offset( $subscription, $notification_type ) {
|
||||
/**
|
||||
* Offset between a subscription event and related notification.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param int $time_offset In seconds
|
||||
* @param WC_Subscription $subscription
|
||||
* @param string $notification_type Can be 'trial_end', 'next_payment' or 'end'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
return apply_filters( 'woocommerce_subscription_customer_notification_time_offset', $this->time_offset, $subscription, $notification_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* General time offset setter.
|
||||
*
|
||||
* @param int $time_offset In seconds
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_time_offset( $time_offset ) {
|
||||
$this->time_offset = $time_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the offset based on new value set in the option.
|
||||
*
|
||||
* @param $_ Unused parameter.
|
||||
* @param $new_option_value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_time_offset_from_option( $_, $new_option_value ) {
|
||||
$this->time_offset = self::convert_offset_to_seconds( $new_option_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time offset in seconds from the settings array.
|
||||
*
|
||||
* @param array $offset Format: [ 'number' => 3, 'unit' => 'days' ]
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected static function convert_offset_to_seconds( $offset ) {
|
||||
$default_offset = 3 * DAY_IN_SECONDS;
|
||||
|
||||
if ( ! isset( $offset['unit'] ) || ! isset( $offset['number'] ) ) {
|
||||
return $default_offset;
|
||||
}
|
||||
|
||||
switch ( $offset['unit'] ) {
|
||||
case 'days':
|
||||
return ( $offset['number'] * DAY_IN_SECONDS );
|
||||
case 'weeks':
|
||||
return ( $offset['number'] * WEEK_IN_SECONDS );
|
||||
case 'months':
|
||||
return ( $offset['number'] * MONTH_IN_SECONDS );
|
||||
case 'years':
|
||||
return ( $offset['number'] * YEAR_IN_SECONDS );
|
||||
default:
|
||||
return $default_offset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe schedule a notification action for given subscription and timestamp.
|
||||
*
|
||||
* Will *not* schedule notification if:
|
||||
* - the notifications are globally disabled,
|
||||
* - the subscription isn't active/pending-cancel,
|
||||
* - the subscription's billing cycle is less than 3 days,
|
||||
* - there is already the same action scheduled for the same subscription and time.
|
||||
*
|
||||
* If only the time differs, the previous scheduled action will be unscheduled and a new one will replace it.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription to schedule the action for.
|
||||
* @param string $action Action ID to schedule.
|
||||
* @param int $timestamp Time to schedule the notification for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function maybe_schedule_notification( $subscription, $action, $timestamp ) {
|
||||
if ( ! WC_Subscriptions_Email_Notifications::notifications_globally_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $subscription->has_status( [ 'active', 'pending-cancel' ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self::is_subscription_period_too_short( $subscription ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action_args = self::get_action_args( $subscription );
|
||||
|
||||
$next_scheduled = as_next_scheduled_action( $action, $action_args, self::$notifications_as_group );
|
||||
|
||||
if ( $timestamp === $next_scheduled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unschedule_actions( $action, $action_args );
|
||||
|
||||
// Only reschedule if it's in the future
|
||||
if ( $timestamp <= time() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
as_schedule_single_action( $timestamp, $action, $action_args, self::$notifications_as_group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract time offset from given datetime based on the settings and subscription properties and return resulting timestamp.
|
||||
*
|
||||
* @param string $datetime
|
||||
* @param WC_Subscription $subscription
|
||||
* @param string $notification_type Can be 'trial_end', 'next_payment' or 'end'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function subtract_time_offset( $datetime, $subscription, $notification_type ) {
|
||||
$dt = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
|
||||
|
||||
return $dt->getTimestamp() - $this->get_time_offset( $subscription, $notification_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification action name based on the date type.
|
||||
*
|
||||
* @param string $date_type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_action_from_date_type( $date_type ) {
|
||||
$action = '';
|
||||
|
||||
switch ( $date_type ) {
|
||||
case 'trial_end':
|
||||
$action = 'woocommerce_scheduled_subscription_customer_notification_trial_expiration';
|
||||
break;
|
||||
case 'next_payment':
|
||||
$action = 'woocommerce_scheduled_subscription_customer_notification_renewal';
|
||||
break;
|
||||
case 'end':
|
||||
$action = 'woocommerce_scheduled_subscription_customer_notification_expiration';
|
||||
break;
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notifications when subscription gets updated.
|
||||
*
|
||||
* To make batch processing easier, we need to handle the following use case:
|
||||
* 1. Subscription S1 gets updated.
|
||||
* 2. Notification config gets updated, a batch to fix all subscriptions is started and processes all subscriptions
|
||||
* with update time before the config got updated.
|
||||
* 3. Subscription S1 gets updated before it gets processed by the batch process.
|
||||
*
|
||||
* Thus, we update notifications for all subscriptions that are being updated after notification config change time
|
||||
* and which have their update time before that.
|
||||
*
|
||||
* As this gets called on Subscription save, the modification timestamp should be updated, too, and thus
|
||||
* the currently updated subscription no longer needs to be processed by the batch process.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param $subscription_data_store
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update_notifications( $subscription, $subscription_data_store ) {
|
||||
if ( ! $subscription->has_status( 'active' ) && ! $subscription->has_status( 'pending-cancel' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Here, we need the 'old' update timestamp for comparison, so can't use get_date_modified() method.
|
||||
$subscription_update_time_raw = array_key_exists( 'date_modified', $subscription->get_data() ) ? $subscription->get_data()['date_modified'] : $subscription->get_date_created();
|
||||
if ( ! $subscription_update_time_raw ) {
|
||||
$subscription_update_utc_timestamp = 0;
|
||||
} else {
|
||||
$subscription_update_time_raw->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
$subscription_update_utc_timestamp = $subscription_update_time_raw->getTimestamp();
|
||||
}
|
||||
|
||||
$notification_settings_update_utc_timestamp = get_option( 'wcs_notification_settings_update_time', 0 );
|
||||
|
||||
if ( $subscription_update_utc_timestamp < $notification_settings_update_utc_timestamp ) {
|
||||
$this->schedule_all_notifications( $subscription );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a notification with given type for given subscription.
|
||||
*
|
||||
* Date/time is determined automatically based on notification type, dates stored on the subscription,
|
||||
* and offset WCS_Action_Scheduler_Customer_Notifications::$time_offset.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param string $notification_type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule_notification( $subscription, $notification_type ) {
|
||||
$action_name = self::get_action_from_date_type( $notification_type );
|
||||
|
||||
$event_date = $subscription->get_date( $notification_type );
|
||||
$timestamp = $this->subtract_time_offset( $event_date, $subscription, $notification_type );
|
||||
|
||||
$this->maybe_schedule_notification(
|
||||
$subscription,
|
||||
$action_name,
|
||||
$timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule all notifications for a subscription based on the dates defined on the subscription.
|
||||
*
|
||||
* Which notifications are needed for the subscription is determined by \WCS_Action_Scheduler_Customer_Notifications::get_valid_notifications.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule_all_notifications( $subscription ) {
|
||||
$valid_notifications = self::get_valid_notifications( $subscription );
|
||||
$actual_notifications = $this->get_notifications( $subscription );
|
||||
|
||||
// Unschedule notifications that aren't valid for this subscription.
|
||||
$notifications_to_unschedule = array_diff( $actual_notifications, $valid_notifications );
|
||||
foreach ( $notifications_to_unschedule as $notification_type ) {
|
||||
$this->unschedule_actions( self::get_action_from_date_type( $notification_type ), self::get_action_args( $subscription ) );
|
||||
}
|
||||
|
||||
// Schedule/check scheduling for valid notifications.
|
||||
foreach ( $valid_notifications as $notification_type ) {
|
||||
$this->schedule_notification( $subscription, $notification_type );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set which date types are affecting the notifications.
|
||||
*
|
||||
* Currently, only trial_end, end and next_payment are being used.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_date_types_to_schedule() {
|
||||
$this->date_types_to_schedule = [
|
||||
'trial_end',
|
||||
'next_payment',
|
||||
'end',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule notifications if the date has changed.
|
||||
*
|
||||
* @param object $subscription An instance of a WC_Subscription object
|
||||
* @param string $date_type Can be 'trial_end', 'next_payment', 'payment_retry', 'end', 'end_of_prepaid_term' or a custom date type
|
||||
* @param string $datetime A MySQL formatted date/time string in the GMT/UTC timezone.
|
||||
*/
|
||||
public function update_date( $subscription, $date_type, $datetime ) {
|
||||
if ( in_array( $date_type, $this->get_date_types_to_schedule(), true ) ) {
|
||||
$this->schedule_all_notifications( $subscription );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule notifications if the date has been deleted.
|
||||
*
|
||||
* @param WC_Subscription $subscription An instance of a WC_Subscription object
|
||||
* @param string $date_type Can be 'trial_end', 'next_payment', 'end', 'end_of_prepaid_term' or a custom date type
|
||||
*/
|
||||
public function delete_date( $subscription, $date_type ) {
|
||||
$action = $this->get_action_from_date_type( $date_type );
|
||||
if ( $action ) {
|
||||
$this->unschedule_actions( $action, self::get_action_args( $subscription ) );
|
||||
}
|
||||
|
||||
$this->schedule_all_notifications( $subscription );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule all notifications for a subscription.
|
||||
*
|
||||
* @param object $subscription An instance of a WC_Subscription object
|
||||
* @param array $exceptions Array of notification actions to not unschedule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unschedule_all_notifications( $subscription = null, $exceptions = [] ) {
|
||||
foreach ( self::$notification_actions as $action ) {
|
||||
if ( in_array( $action, $exceptions, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->unschedule_actions( $action, self::get_action_args( $subscription ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a subscription's status is updated, maybe schedule an event
|
||||
*
|
||||
* @param object $subscription An instance of a WC_Subscription object
|
||||
* @param string $new_status New subscription status
|
||||
* @param string $old_status Previous subscription status
|
||||
*/
|
||||
public function update_status( $subscription, $new_status, $old_status ) {
|
||||
|
||||
switch ( $new_status ) {
|
||||
case 'active':
|
||||
// Schedule new notifications (will also unschedule unneeded ones).
|
||||
$this->schedule_all_notifications( $subscription );
|
||||
break;
|
||||
case 'pending-cancel':
|
||||
// Unschedule all except expiration notification.
|
||||
$this->unschedule_all_notifications( $subscription, [ 'woocommerce_scheduled_subscription_customer_notification_expiration' ] );
|
||||
break;
|
||||
case 'on-hold':
|
||||
case 'cancelled':
|
||||
case 'switched':
|
||||
case 'expired':
|
||||
case 'trash':
|
||||
$this->unschedule_all_notifications( $subscription );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the args to set on the scheduled action.
|
||||
*
|
||||
* @param WC_Subscription|null $subscription An instance of WC_Subscription to get the hook for
|
||||
*
|
||||
* @return array Array of name => value pairs stored against the scheduled action.
|
||||
*/
|
||||
public static function get_action_args( $subscription ) {
|
||||
if ( ! $subscription ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$action_args = [ 'subscription_id' => $subscription->get_id() ];
|
||||
|
||||
return $action_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the args to set on the scheduled action.
|
||||
*
|
||||
* @param string $action_hook Name of event used as the hook for the scheduled action.
|
||||
* @param array $action_args Array of name => value pairs stored against the scheduled action.
|
||||
*/
|
||||
protected function unschedule_actions( $action_hook, $action_args = [] ) {
|
||||
as_unschedule_all_actions( $action_hook, $action_args, self::$notifications_as_group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given date for subscription is now or in the future.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription whose date is examined.
|
||||
* @param string $date_type Date type to evaluate.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_date_in_the_future_or_now( $subscription, $date_type ) {
|
||||
$dt = new DateTime( $subscription->get_date( $date_type ), new DateTimeZone( 'UTC' ) );
|
||||
$timestamp = $dt->getTimestamp();
|
||||
|
||||
return $timestamp >= time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of notifications valid for given subscription based on the dates set on the subscription.
|
||||
*
|
||||
* This method doesn't take status into account. That's done in \WCS_Action_Scheduler_Customer_Notifications::update_status.
|
||||
*
|
||||
* Possible values in the array: 'end', 'trial_end', 'next_payment'.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function get_valid_notifications( $subscription ) {
|
||||
$notifications = [];
|
||||
|
||||
if ( $subscription->get_date( 'end' ) && self::is_date_in_the_future_or_now( $subscription, 'end' ) ) {
|
||||
$notifications[] = 'end';
|
||||
}
|
||||
|
||||
if ( $subscription->get_date( 'trial_end' ) && self::is_date_in_the_future_or_now( $subscription, 'trial_end' ) ) {
|
||||
$notifications[] = 'trial_end';
|
||||
}
|
||||
|
||||
if ( $subscription->get_date( 'next_payment' ) ) {
|
||||
|
||||
// Renewal notification is only valid after the trial ended.
|
||||
$trial_end = $subscription->get_date( 'trial_end' );
|
||||
if ( $trial_end ) {
|
||||
$trial_end_dt = new DateTime( $trial_end, new DateTimeZone( 'UTC' ) );
|
||||
$trial_end_timestamp = $trial_end_dt->getTimestamp();
|
||||
|
||||
if ( $trial_end_timestamp < time() && self::is_date_in_the_future_or_now( $subscription, 'next_payment' ) ) {
|
||||
$notifications[] = 'next_payment';
|
||||
}
|
||||
} elseif ( self::is_date_in_the_future_or_now( $subscription, 'next_payment' ) ) {
|
||||
$notifications[] = 'next_payment';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: `woocommerce_subscription_valid_customer_notification_types`.
|
||||
*
|
||||
* Allows filtering the list of notification types that will be scheduled for a particular subscription.
|
||||
*
|
||||
* Default array format returned:
|
||||
*
|
||||
* array(
|
||||
* 'next_payment', // Exists if the subscription contains a next payment date in the future.
|
||||
* 'trial_end', // Exists if the subscription contains a trial end date in the future.
|
||||
* 'end' // Exists if the subscription contains an end date in the future.
|
||||
* )
|
||||
*
|
||||
* @since 7.2.0
|
||||
*
|
||||
* @param array $notifications Array of valid notification types.
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*/
|
||||
return (array) apply_filters( 'woocommerce_subscription_valid_customer_notification_types', $notifications, $subscription );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of currently scheduled notifications for a subscription.
|
||||
*
|
||||
* Notifications are identified by the date type of the subscription.
|
||||
* I.e. possible values are: 'end', 'trial_end' and 'next_payment'.
|
||||
*
|
||||
* @param $subscription
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_notifications( $subscription ) {
|
||||
$notifications = [];
|
||||
|
||||
$date_types = $this->get_date_types_to_schedule();
|
||||
|
||||
foreach ( $date_types as $date_type ) {
|
||||
$next_scheduled = as_next_scheduled_action(
|
||||
self::get_action_from_date_type( $date_type ),
|
||||
self::get_action_args( $subscription ),
|
||||
self::$notifications_as_group
|
||||
);
|
||||
if ( $next_scheduled ) {
|
||||
$notifications[] = $date_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,572 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This class is a helper intended to handle data processings that need to happen in batches in a deferred way.
|
||||
* It abstracts away the nuances of (re)scheduling actions and dealing with errors.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* 1. Create a class that implements WCS_Batch_Processor.
|
||||
*
|
||||
* 2. Whenever there's data to be processed invoke the 'enqueue_processor' method in this class,
|
||||
* passing the class name of the processor.
|
||||
*
|
||||
* That's it, processing will be performed in batches inside scheduled actions; enqueued processors will only
|
||||
* be dequeued once they notify that no more items are left to process (or when `force_clear_all_processes` is invoked).
|
||||
* Failed batches will be retried after a while.
|
||||
*
|
||||
* This is heavily inspired by core's version at Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @category Class
|
||||
* @since 7.7.0
|
||||
*/
|
||||
class WCS_Batch_Processing_Controller {
|
||||
/*
|
||||
* Identifier of a "watchdog" action that will schedule a processing action
|
||||
* for any processor that is enqueued but not yet scheduled
|
||||
* (because it's been just enqueued or because it threw an error while processing a batch),
|
||||
* that's one single action that reschedules itself continuously.
|
||||
*/
|
||||
const WATCHDOG_ACTION_NAME = 'wcs_schedule_pending_batch_processes';
|
||||
|
||||
/*
|
||||
* Identifier of the action that will do the actual batch processing.
|
||||
* There's one action per enqueued processor that will keep rescheduling itself
|
||||
* as long as there are still pending items to process
|
||||
* (except if there's an error that caused no items to be processed at all).
|
||||
*/
|
||||
const PROCESS_SINGLE_BATCH_ACTION_NAME = 'wcs_run_batch_process';
|
||||
|
||||
const ENQUEUED_PROCESSORS_OPTION_NAME = 'wcs_pending_batch_processes';
|
||||
const ACTION_GROUP = 'wcs_batch_processes';
|
||||
const LOGS_CONTEXT = 'wcs-batch-processing';
|
||||
|
||||
/**
|
||||
* Maximum number of failures per processor before it gets dequeued.
|
||||
*/
|
||||
const FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT = 5;
|
||||
|
||||
/**
|
||||
* Instance of WC_Logger class.
|
||||
*
|
||||
* @var \WC_Logger_Interface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var WCS_Batch_Processing_Controller
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Schedules the necessary actions to process batches.
|
||||
*/
|
||||
private function __construct() {
|
||||
add_action(
|
||||
self::WATCHDOG_ACTION_NAME,
|
||||
function () {
|
||||
$this->handle_watchdog_action();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
self::PROCESS_SINGLE_BATCH_ACTION_NAME,
|
||||
function ( $batch_process ) {
|
||||
$this->process_next_batch_for_single_processor( $batch_process );
|
||||
},
|
||||
10
|
||||
);
|
||||
|
||||
add_action(
|
||||
'shutdown',
|
||||
function () {
|
||||
$this->remove_or_retry_failed_processors();
|
||||
}
|
||||
);
|
||||
|
||||
$this->logger = wc_get_logger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of this class.
|
||||
*
|
||||
* @return WCS_Batch_Processing_Controller
|
||||
*/
|
||||
final public static function instance(): WCS_Batch_Processing_Controller {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a processor so that it will get batch processing requests from within scheduled actions.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor, must implement `WCS_Batch_Processor`.
|
||||
*/
|
||||
public function enqueue_processor( string $processor_class_name ): void {
|
||||
$pending_updates = $this->get_enqueued_processors();
|
||||
if ( ! in_array( $processor_class_name, array_keys( $pending_updates ), true ) ) {
|
||||
$pending_updates[] = $processor_class_name;
|
||||
$this->set_enqueued_processors( $pending_updates );
|
||||
}
|
||||
$this->schedule_watchdog_action( false, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the watchdog action.
|
||||
*
|
||||
* @param bool $with_delay Whether to delay the action execution. Should be true when rescheduling, false when enqueueing.
|
||||
* @param bool $unique Whether to make the action unique.
|
||||
*/
|
||||
private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void {
|
||||
$time = time();
|
||||
if ( $with_delay ) {
|
||||
/**
|
||||
* Modify the delay interval for the batch processor's watchdog events.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param int $delay Time, in seconds, before the watchdog process will run. Defaults to 3600 (1 hour).
|
||||
*/
|
||||
$time += apply_filters( 'wcs_batch_processor_watchdog_delay_seconds', HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
if ( ! as_has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
|
||||
as_schedule_single_action(
|
||||
$time,
|
||||
self::WATCHDOG_ACTION_NAME,
|
||||
array(),
|
||||
self::ACTION_GROUP,
|
||||
$unique
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a processing action for all the processors that are enqueued but not scheduled
|
||||
* (because they have just been enqueued, or because the processing for a batch failed).
|
||||
*/
|
||||
private function handle_watchdog_action(): void {
|
||||
$pending_processes = $this->get_enqueued_processors();
|
||||
if ( empty( $pending_processes ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $pending_processes as $process_name ) {
|
||||
if ( ! $this->is_scheduled( $process_name ) ) {
|
||||
$this->schedule_batch_processing( $process_name );
|
||||
}
|
||||
}
|
||||
$this->schedule_watchdog_action( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch for a single processor, and handle any required rescheduling or state cleanup.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
*
|
||||
* @throws \Exception If error occurred during batch processing.
|
||||
*/
|
||||
private function process_next_batch_for_single_processor( string $processor_class_name ): void {
|
||||
if ( ! $this->is_enqueued( $processor_class_name ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch_processor = $this->get_processor_instance( $processor_class_name );
|
||||
$error = $this->process_next_batch_for_single_processor_core( $batch_processor );
|
||||
$still_pending = count( $batch_processor->get_next_batch_to_process( 1 ) ) > 0;
|
||||
if ( ( $error instanceof \Exception ) ) {
|
||||
// The batch processing failed and no items were processed:
|
||||
// reschedule the processing with a delay, unless this is a repeatead failure.
|
||||
if ( $this->is_consistently_failing( $batch_processor ) ) {
|
||||
$this->log_consistent_failure( $batch_processor, $this->get_process_details( $batch_processor ) );
|
||||
$this->remove_processor( $processor_class_name );
|
||||
} else {
|
||||
$this->schedule_batch_processing( $processor_class_name, true );
|
||||
}
|
||||
|
||||
throw $error;
|
||||
}
|
||||
if ( $still_pending ) {
|
||||
$this->schedule_batch_processing( $processor_class_name );
|
||||
} else {
|
||||
$this->dequeue_processor( $processor_class_name );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch for a single processor, updating state and logging any error.
|
||||
*
|
||||
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
||||
*
|
||||
* @return null|\Exception Exception if error occurred, null otherwise.
|
||||
*/
|
||||
private function process_next_batch_for_single_processor_core( WCS_Batch_Processor $batch_processor ): ?\Exception {
|
||||
$details = $this->get_process_details( $batch_processor );
|
||||
$time_start = microtime( true );
|
||||
$batch = $batch_processor->get_next_batch_to_process( $details['current_batch_size'] );
|
||||
if ( empty( $batch ) ) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$batch_processor->process_batch( $batch );
|
||||
$time_taken = microtime( true ) - $time_start;
|
||||
$this->update_processor_state( $batch_processor, $time_taken );
|
||||
} catch ( \Exception $exception ) {
|
||||
$time_taken = microtime( true ) - $time_start;
|
||||
$this->log_error( $exception, $batch_processor, $batch );
|
||||
$this->update_processor_state( $batch_processor, $time_taken, $exception );
|
||||
return $exception;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state for a given enqueued processor.
|
||||
*
|
||||
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
||||
*
|
||||
* @return array Current state for the processor, or a "blank" state if none exists yet.
|
||||
*/
|
||||
private function get_process_details( WCS_Batch_Processor $batch_processor ): array {
|
||||
$defaults = array(
|
||||
'total_time_spent' => 0,
|
||||
'current_batch_size' => $batch_processor->get_default_batch_size(),
|
||||
'last_error' => null,
|
||||
'recent_failures' => 0,
|
||||
'batch_first_failure' => null,
|
||||
'batch_last_failure' => null,
|
||||
);
|
||||
|
||||
$process_details = get_option( $this->get_processor_state_option_name( $batch_processor ) );
|
||||
$process_details = wp_parse_args( is_array( $process_details ) ? $process_details : array(), $defaults );
|
||||
|
||||
return $process_details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the option where we will be saving state for a given processor.
|
||||
*
|
||||
* @param WCS_Batch_Processor|string $batch_processor Batch processor instance or class name.
|
||||
*
|
||||
* @return string Option name.
|
||||
*/
|
||||
private function get_processor_state_option_name( $batch_processor ): string {
|
||||
$class_name = is_a( $batch_processor, WCS_Batch_Processor::class ) ? get_class( $batch_processor ) : $batch_processor;
|
||||
$class_md5 = md5( $class_name );
|
||||
// truncate the class name so we know that it will fit in the option name column along with md5 hash and prefix.
|
||||
$class_name = substr( $class_name, 0, 140 );
|
||||
return 'wcs_batch_' . $class_name . '_' . $class_md5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state for a processor after a batch has completed processing.
|
||||
*
|
||||
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
||||
* @param float $time_taken Time take by the batch to complete processing.
|
||||
* @param \Exception|null $last_error Exception object in processing the batch, if there was one.
|
||||
*/
|
||||
private function update_processor_state( WCS_Batch_Processor $batch_processor, float $time_taken, ?\Exception $last_error = null ): void {
|
||||
$current_status = $this->get_process_details( $batch_processor );
|
||||
$current_status['total_time_spent'] += $time_taken;
|
||||
$current_status['last_error'] = null !== $last_error ? $last_error->getMessage() : null;
|
||||
|
||||
if ( null !== $last_error ) {
|
||||
$current_status['recent_failures'] = ( $current_status['recent_failures'] ?? 0 ) + 1;
|
||||
$current_status['batch_last_failure'] = current_time( 'mysql' );
|
||||
|
||||
if ( is_null( $current_status['batch_first_failure'] ) ) {
|
||||
$current_status['batch_first_failure'] = $current_status['batch_last_failure'];
|
||||
}
|
||||
} else {
|
||||
$current_status['recent_failures'] = 0;
|
||||
$current_status['batch_first_failure'] = null;
|
||||
$current_status['batch_last_failure'] = null;
|
||||
}
|
||||
|
||||
update_option( $this->get_processor_state_option_name( $batch_processor ), $current_status, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the option where we store state for a given processor.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
*/
|
||||
private function clear_processor_state( string $processor_class_name ): void {
|
||||
delete_option( $this->get_processor_state_option_name( $processor_class_name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a processing action for a single processor.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
* @param bool $with_delay Whether to schedule the action for immediate execution or for later.
|
||||
*/
|
||||
private function schedule_batch_processing( string $processor_class_name, bool $with_delay = false ): void {
|
||||
$time = $with_delay ? time() + MINUTE_IN_SECONDS : time();
|
||||
as_schedule_single_action( $time, self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a batch processing action is already scheduled for a given processor.
|
||||
* Differs from `as_has_scheduled_action` in that this excludes actions in progress.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the batch processor.
|
||||
*
|
||||
* @return bool True if a batch processing action is already scheduled for the processor.
|
||||
*/
|
||||
public function is_scheduled( string $processor_class_name ): bool {
|
||||
return as_has_scheduled_action( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of a processor given its class name.
|
||||
*
|
||||
* @param string $processor_class_name Full class name of the batch processor.
|
||||
*
|
||||
* @return WCS_Batch_Processor Instance of batch processor for the given class.
|
||||
* @throws \Exception If it's not possible to get an instance of the class.
|
||||
*/
|
||||
private function get_processor_instance( string $processor_class_name ): WCS_Batch_Processor {
|
||||
$processor = null;
|
||||
if ( class_exists( $processor_class_name ) ) {
|
||||
// This is a fallback for when the batch processor is not registered in the container.
|
||||
$processor = new $processor_class_name();
|
||||
}
|
||||
if ( ! is_a( $processor, WCS_Batch_Processor::class ) ) {
|
||||
throw new \Exception( "Unable to initialize batch processor instance for $processor_class_name" );
|
||||
}
|
||||
return $processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get list of all the enqueued processors.
|
||||
*
|
||||
* @return array List (of string) of the class names of the enqueued processors.
|
||||
*/
|
||||
public function get_enqueued_processors(): array {
|
||||
$enqueued_processors = get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
|
||||
if ( ! is_array( $enqueued_processors ) ) {
|
||||
$this->logger->error( 'Could not fetch list of processors. Clearing up queue.', array( 'source' => self::LOGS_CONTEXT ) );
|
||||
delete_option( self::ENQUEUED_PROCESSORS_OPTION_NAME );
|
||||
$enqueued_processors = array();
|
||||
}
|
||||
|
||||
return $enqueued_processors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeue a processor once it has no more items pending processing.
|
||||
*
|
||||
* @param string $processor_class_name Full processor class name.
|
||||
*/
|
||||
private function dequeue_processor( string $processor_class_name ): void {
|
||||
$pending_processes = $this->get_enqueued_processors();
|
||||
if ( in_array( $processor_class_name, $pending_processes, true ) ) {
|
||||
$this->clear_processor_state( $processor_class_name );
|
||||
$pending_processes = array_diff( $pending_processes, array( $processor_class_name ) );
|
||||
$this->set_enqueued_processors( $pending_processes );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set the enqueued processor class names.
|
||||
*
|
||||
* @param array $processors List of full processor class names.
|
||||
*/
|
||||
private function set_enqueued_processors( array $processors ): void {
|
||||
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $processors, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a particular processor is enqueued.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
*
|
||||
* @return bool True if the processor is enqueued.
|
||||
*/
|
||||
public function is_enqueued( string $processor_class_name ): bool {
|
||||
return in_array( $processor_class_name, $this->get_enqueued_processors(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeue and de-schedule a processor instance so that it won't be processed anymore.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
* @return bool True if the processor has been dequeued, false if the processor wasn't enqueued (so nothing has been done).
|
||||
*/
|
||||
public function remove_processor( string $processor_class_name ): bool {
|
||||
$enqueued_processors = $this->get_enqueued_processors();
|
||||
if ( ! in_array( $processor_class_name, $enqueued_processors, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enqueued_processors = array_diff( $enqueued_processors, array( $processor_class_name ) );
|
||||
if ( empty( $enqueued_processors ) ) {
|
||||
$this->force_clear_all_processes();
|
||||
} else {
|
||||
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $enqueued_processors, false );
|
||||
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
||||
$this->clear_processor_state( $processor_class_name );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeues and de-schedules all the processors.
|
||||
*/
|
||||
public function force_clear_all_processes(): void {
|
||||
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME );
|
||||
as_unschedule_all_actions( self::WATCHDOG_ACTION_NAME );
|
||||
|
||||
foreach ( $this->get_enqueued_processors() as $processor ) {
|
||||
$this->clear_processor_state( $processor );
|
||||
}
|
||||
|
||||
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array(), false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error that happened while processing a batch.
|
||||
*
|
||||
* @param \Exception $error Exception object to log.
|
||||
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
||||
* @param array $batch Batch that was being processed.
|
||||
*/
|
||||
protected function log_error( \Exception $error, WCS_Batch_Processor $batch_processor, array $batch ): void {
|
||||
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}";
|
||||
$error_context = array(
|
||||
'exception' => $error,
|
||||
'source' => self::LOGS_CONTEXT,
|
||||
);
|
||||
|
||||
// Log only first and last, as the entire batch may be too big.
|
||||
if ( count( $batch ) > 0 ) {
|
||||
$error_context = array_merge(
|
||||
$error_context,
|
||||
array(
|
||||
'batch_start' => $batch[0],
|
||||
'batch_end' => end( $batch ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the error message for a batch processing.
|
||||
*
|
||||
* @param string $error_message The error message that will be logged.
|
||||
* @param \Exception $error The exception that was thrown by the processor.
|
||||
* @param WCS_Batch_Processor $batch_processor The processor that threw the exception.
|
||||
* @param array $batch The batch that was being processed.
|
||||
* @param array $error_context Context to be passed to the logging function.
|
||||
* @return string The actual error message that will be logged.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*/
|
||||
$error_message = apply_filters( 'wcs_batch_processing_log_message', $error_message, $error, $batch_processor, $batch, $error_context );
|
||||
|
||||
$this->logger->error( $error_message, $error_context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given processor is consistently failing based on how many recent consecutive failures it has had.
|
||||
*
|
||||
* @param WCS_Batch_Processor $batch_processor The processor that we want to check.
|
||||
* @return boolean TRUE if processor is consistently failing. FALSE otherwise.
|
||||
*/
|
||||
private function is_consistently_failing( WCS_Batch_Processor $batch_processor ): bool {
|
||||
$process_details = $this->get_process_details( $batch_processor );
|
||||
$max_attempts = absint(
|
||||
/**
|
||||
* Controls the failure threshold for batch processors. That is, the number of times we'll attempt to
|
||||
* process a batch that has resulted in a failure. Once above this threshold, the processor won't be
|
||||
* re-scheduled and will be removed from the queue.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param int $failure_threshold Maximum number of times for the processor to try processing a given batch.
|
||||
* @param WCS_Batch_Processor $batch_processor The processor instance.
|
||||
* @param array $process_details Array with batch processor state.
|
||||
*/
|
||||
apply_filters(
|
||||
'wcs_batch_processing_max_attempts',
|
||||
self::FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT,
|
||||
$batch_processor,
|
||||
$process_details
|
||||
)
|
||||
);
|
||||
|
||||
return absint( $process_details['recent_failures'] ?? 0 ) >= max( $max_attempts, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates log entry with details about a batch processor that is consistently failing.
|
||||
*
|
||||
* @param WCS_Batch_Processor $batch_processor The batch processor instance.
|
||||
* @param array $process_details Failing process details.
|
||||
*/
|
||||
private function log_consistent_failure( WCS_Batch_Processor $batch_processor, array $process_details ): void {
|
||||
$this->logger->error(
|
||||
"Batch processor {$batch_processor->get_name()} appears to be failing consistently: {$process_details['recent_failures']} unsuccessful attempt(s). No further attempts will be made.",
|
||||
array(
|
||||
'source' => self::LOGS_CONTEXT,
|
||||
'failures' => $process_details['recent_failures'],
|
||||
'first_failure' => $process_details['batch_first_failure'],
|
||||
'last_failure' => $process_details['batch_last_failure'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'shutdown'. This cleanup routine checks enqueued processors and whether they are scheduled or not to
|
||||
* either re-eschedule them or remove them from the queue.
|
||||
* This prevents stale states where Action Scheduler won't schedule any more attempts but we still report the
|
||||
* processor as enqueued.
|
||||
*
|
||||
*/
|
||||
private function remove_or_retry_failed_processors(): void {
|
||||
if ( ! did_action( 'wp_loaded' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$last_error = error_get_last();
|
||||
if ( ! is_null( $last_error ) && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
|
||||
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
|
||||
$has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
|
||||
|
||||
if ( call_user_func( $has_scheduled_action, self::WATCHDOG_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enqueued_processors = $this->get_enqueued_processors();
|
||||
$unscheduled_processors = array_diff( $enqueued_processors, array_filter( $enqueued_processors, array( $this, 'is_scheduled' ) ) );
|
||||
|
||||
foreach ( $unscheduled_processors as $processor ) {
|
||||
try {
|
||||
$instance = $this->get_processor_instance( $processor );
|
||||
} catch ( \Exception $e ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exception = new \Exception( 'Processor is enqueued but not scheduled. Background job was probably killed or marked as failed. Reattempting execution.' );
|
||||
$this->update_processor_state( $instance, 0, $exception );
|
||||
$this->log_error( $exception, $instance, array() );
|
||||
|
||||
if ( $this->is_consistently_failing( $instance ) ) {
|
||||
$this->log_consistent_failure( $instance, $this->get_process_details( $instance ) );
|
||||
$this->remove_processor( $processor );
|
||||
} else {
|
||||
$this->schedule_batch_processing( $processor, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,275 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* WooCommerce Subscriptions Notifications Batch Processor.
|
||||
*
|
||||
* This batch processor is used to process subscriptions whenever global settings get updated
|
||||
* (global on/off for notifications or time offset).
|
||||
*
|
||||
* It will only process subscriptions whose update time is before the time when the settings got updated.
|
||||
* To ensure all subscription end up having correct notifications, the hook
|
||||
* WCS_Action_Scheduler_Customer_Notifications::update_notifications will update any notifications
|
||||
* whose update time is before the settings got updated. The rest of subscriptions should be updated by this
|
||||
* batch processor.
|
||||
*
|
||||
* In addition to this batch processor which runs ad-hoc, there's also a debug tool to regenerate notifications for
|
||||
* all subscriptions: WCS_Notifications_Debug_Tool_Processor.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @category Class
|
||||
* @since 7.7.0
|
||||
*/
|
||||
class WCS_Notifications_Batch_Processor implements WCS_Batch_Processor {
|
||||
|
||||
/**
|
||||
* Get a user-friendly name for this processor.
|
||||
*
|
||||
* @return string Name of the processor.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'wcs_notifications_batch_processor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly description for this processor.
|
||||
*
|
||||
* @return string Description of what this processor does.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return 'WooCommerce Notifications Batch Processor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription statuses that should be processed.
|
||||
*
|
||||
* @return array Subscription statuses that should be processed.
|
||||
*/
|
||||
protected function get_subscription_statuses() {
|
||||
$allowed_statuses = array(
|
||||
'active',
|
||||
'pending',
|
||||
'on-hold',
|
||||
'pending-cancel',
|
||||
);
|
||||
|
||||
return array_map( 'wcs_sanitize_subscription_status_key', $allowed_statuses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the last time the notification settings were updated.
|
||||
*
|
||||
* @return string Datetime of the last time the notification settings were updated.
|
||||
*/
|
||||
public function get_notification_settings_update_time() {
|
||||
$notification_settings_update_timestamp = get_option( 'wcs_notification_settings_update_time', 0 );
|
||||
if ( 0 === $notification_settings_update_timestamp ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$notification_settings_update_time = new DateTime( "@$notification_settings_update_timestamp", new DateTimeZone( 'UTC' ) );
|
||||
return $notification_settings_update_time->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of pending items that require processing.
|
||||
* Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.
|
||||
*
|
||||
* Note that once the processor is enqueued the batch processor controller will keep
|
||||
* invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.
|
||||
*
|
||||
* Since this batch processor updates only subscriptions older than the settings update,
|
||||
* it only selects subscriptions updated before the settings update time.
|
||||
*
|
||||
* @return int Number of items pending processing.
|
||||
*/
|
||||
public function get_total_pending_count(): int {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $this->get_notification_settings_update_time() ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$allowed_statuses = $this->get_subscription_statuses();
|
||||
$placeholders = implode( ', ', array_fill( 0, count( $allowed_statuses ), '%s' ) );
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
return $wpdb->get_var(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(*)
|
||||
FROM {$wpdb->prefix}wc_orders
|
||||
WHERE type='shop_subscription'
|
||||
AND date_updated_gmt < %s
|
||||
AND status IN ($placeholders)
|
||||
",
|
||||
$this->get_notification_settings_update_time(),
|
||||
...$allowed_statuses
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return $wpdb->get_var(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(*)
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE post_type='shop_subscription'
|
||||
AND post_modified_gmt < %s
|
||||
AND post_status IN ($placeholders)
|
||||
",
|
||||
$this->get_notification_settings_update_time(),
|
||||
...$allowed_statuses
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next batch of items that need to be processed.
|
||||
*
|
||||
* A batch item can be anything needed to identify the actual processing to be done,
|
||||
* but whenever possible items should be numbers (e.g. database record ids)
|
||||
* or at least strings, to ease troubleshooting and logging in case of problems.
|
||||
*
|
||||
* The size of the batch returned can be less than $size if there aren't that
|
||||
* many items pending processing (and it can be zero if there isn't anything to process),
|
||||
* but the size should always be consistent with what 'get_total_pending_count' returns
|
||||
* (i.e. the size of the returned batch shouldn't be larger than the pending items count).
|
||||
*
|
||||
* @param int $size Maximum size of the batch to be returned.
|
||||
*
|
||||
* @return array Batch of items to process, containing $size or less items.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ): array {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $this->get_notification_settings_update_time() ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$allowed_statuses = $this->get_subscription_statuses();
|
||||
$placeholders = implode( ', ', array_fill( 0, count( $allowed_statuses ), '%s' ) );
|
||||
|
||||
$args = array_merge(
|
||||
array( $this->get_notification_settings_update_time() ),
|
||||
$allowed_statuses,
|
||||
array( $size )
|
||||
);
|
||||
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
return $wpdb->get_col(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
id
|
||||
FROM {$wpdb->prefix}wc_orders
|
||||
WHERE type='shop_subscription'
|
||||
AND date_updated_gmt < %s
|
||||
AND status IN ($placeholders)
|
||||
ORDER BY id ASC
|
||||
LIMIT %d",
|
||||
...$args
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return $wpdb->get_col(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
ID
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE post_type='shop_subscription'
|
||||
AND post_modified_gmt < %s
|
||||
AND post_status IN ($placeholders)
|
||||
ORDER BY ID ASC
|
||||
LIMIT %d",
|
||||
...$args
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data for the supplied batch: update all notifications for given batch of subscriptions.
|
||||
*
|
||||
* This method should be prepared to receive items that don't actually need processing
|
||||
* (because they have been processed before) and ignore them, but if at least
|
||||
* one of the batch items that actually need processing can't be processed, an exception should be thrown.
|
||||
*
|
||||
* Once an item has been processed it shouldn't be counted in 'get_total_pending_count'
|
||||
* nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it
|
||||
* to actually require further processing).
|
||||
*
|
||||
* @throw \Exception Something went wrong while processing the batch.
|
||||
*
|
||||
* @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
|
||||
*/
|
||||
public function process_batch( array $batch ): void {
|
||||
// Instantiating this again would hook another set of hooks for update_status and update_date. No bueno.
|
||||
$subscriptions_notifications = WC_Subscriptions_Core_Plugin::instance()->notifications_scheduler;
|
||||
|
||||
foreach ( $batch as $subscription_id ) {
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( WC_Subscriptions_Email_Notifications::notifications_globally_enabled() ) {
|
||||
$subscriptions_notifications->update_status( $subscription, $subscription->get_status(), null );
|
||||
} else {
|
||||
$subscriptions_notifications->unschedule_all_notifications( $subscription );
|
||||
}
|
||||
|
||||
// Update the subscription's update time to mark it as updated.
|
||||
$subscription->set_date_modified( time() );
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default (preferred) batch size to pass to 'get_next_batch_to_process'.
|
||||
* The controller will pass this size unless it's externally configured
|
||||
* to use a different size.
|
||||
*
|
||||
* @return int Default batch size.
|
||||
*/
|
||||
public function get_default_batch_size(): int {
|
||||
return 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background process for updating notifications.
|
||||
*
|
||||
* @return string Informative string to show after the tool is triggered in UI.
|
||||
*/
|
||||
public static function enqueue(): string {
|
||||
$batch_processor = WCS_Batch_Processing_Controller::instance();
|
||||
if ( $batch_processor->is_enqueued( self::class ) ) {
|
||||
return __( 'Background process for updating subscription notifications already started, nothing done.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
$batch_processor->enqueue_processor( self::class );
|
||||
return __( 'Background process for updating subscription notifications started.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the background process for updating notifications.
|
||||
*
|
||||
* @return string Informative string to show after the tool is triggered in UI.
|
||||
*/
|
||||
public static function dequeue(): string {
|
||||
$batch_processor = WCS_Batch_Processing_Controller::instance();
|
||||
if ( ! $batch_processor->is_enqueued( self::class ) ) {
|
||||
return __( 'Background process for updating subscription notifications not started, nothing done.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
$batch_processor->remove_processor( self::class );
|
||||
return __( 'Background process for updating subscription notifications stopped.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Subscriptions Email Preview Class
|
||||
*/
|
||||
class WC_Subscriptions_Email_Preview {
|
||||
|
||||
/**
|
||||
* The email being previewed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $email_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_prepare_email_for_preview', [ $this, 'prepare_email_for_preview' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare subscription email dummy data for preview.
|
||||
*
|
||||
* @param WC_Email $email The email object.
|
||||
*
|
||||
* @return WC_Email
|
||||
*/
|
||||
public function prepare_email_for_preview( $email ) {
|
||||
$this->email_type = get_class( $email );
|
||||
|
||||
if ( ! $this->is_subscription_email() ) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
$this->set_up_filters();
|
||||
|
||||
switch ( $this->email_type ) {
|
||||
case 'WCS_Email_New_Switch_Order':
|
||||
case 'WCS_Email_Completed_Switch_Order':
|
||||
$email->subscriptions = [ $this->get_dummy_subscription() ];
|
||||
break;
|
||||
case 'WCS_Email_Cancelled_Subscription':
|
||||
case 'WCS_Email_Expired_Subscription':
|
||||
case 'WCS_Email_On_Hold_Subscription':
|
||||
case 'WCS_Email_Customer_Notification_Auto_Trial_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Manual_Trial_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Subscription_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Manual_Renewal':
|
||||
case 'WCS_Email_Customer_Notification_Auto_Renewal':
|
||||
$email->set_object( $this->get_dummy_subscription() );
|
||||
break;
|
||||
case 'WCSG_Email_Recipient_New_Initial_Order':
|
||||
$email->set_object( $this->get_dummy_subscription() );
|
||||
$email->subscriptions = [ $this->get_dummy_subscription() ];
|
||||
break;
|
||||
case 'WCS_Email_Customer_Payment_Retry':
|
||||
case 'WCS_Email_Payment_Retry':
|
||||
$email->retry = $this->get_dummy_retry( $email->object );
|
||||
break;
|
||||
}
|
||||
|
||||
$this->add_placeholders( $email );
|
||||
|
||||
add_filter( 'woocommerce_mail_content', [ $this, 'clean_up_filters' ] );
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dummy subscription for use in preview emails.
|
||||
*
|
||||
* @return WC_Subscription
|
||||
*/
|
||||
private function get_dummy_subscription() {
|
||||
$subscription = new WC_Subscription();
|
||||
$product = $this->get_dummy_product();
|
||||
|
||||
$subscription->add_product( $product, 2 );
|
||||
$subscription->set_id( 12346 );
|
||||
$subscription->set_customer_id( 1 );
|
||||
$subscription->set_date_created( gmdate( 'Y-m-d H:i:s', strtotime( '-1 month' ) ) );
|
||||
$subscription->set_currency( 'USD' );
|
||||
$subscription->set_total( 100 );
|
||||
$subscription->set_billing_period( 'month' );
|
||||
$subscription->set_billing_interval( 1 );
|
||||
$subscription->set_start_date( gmdate( 'Y-m-d H:i:s', strtotime( '-1 month' ) ) );
|
||||
$subscription->set_trial_end_date( gmdate( 'Y-m-d H:i:s', strtotime( '+1 week' ) ) );
|
||||
$subscription->set_next_payment_date( gmdate( 'Y-m-d H:i:s', strtotime( '+1 week' ) ) );
|
||||
$subscription->set_end_date( gmdate( 'Y-m-d H:i:s', strtotime( '+1 month' ) ) );
|
||||
|
||||
$address = self::get_dummy_address();
|
||||
|
||||
$subscription->set_billing_address( $address );
|
||||
$subscription->set_shipping_address( $address );
|
||||
|
||||
/**
|
||||
* Filter the dummy subscription object used in email previews.
|
||||
*
|
||||
* @param WC_Subscription $subscription The dummy subscription object.
|
||||
* @param string $email_type The email type being previewed.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_subscriptions_email_preview_dummy_subscription', $subscription, $this->email_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dummy product for use when previewing subscription emails.
|
||||
*
|
||||
* @return WC_Product
|
||||
*/
|
||||
private function get_dummy_product() {
|
||||
$product = new WC_Product();
|
||||
$product->set_name( 'Dummy Subscription' );
|
||||
$product->set_price( 25 );
|
||||
|
||||
/**
|
||||
* Filter the dummy subscription product object used in email previews.
|
||||
*
|
||||
* @param WC_Product $product The dummy product object.
|
||||
* @param string $email_type The email type being previewed.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_subscriptions_email_preview_dummy_product', $product, $this->email_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dummy address used when previewing subscription emails.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_dummy_address() {
|
||||
$address = [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'company' => 'Company',
|
||||
'email' => 'john@company.com',
|
||||
'phone' => '555-555-5555',
|
||||
'address_1' => '123 Fake Street',
|
||||
'city' => 'Faketown',
|
||||
'postcode' => '12345',
|
||||
'country' => 'US',
|
||||
'state' => 'CA',
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter the dummy address used in email previews.
|
||||
*
|
||||
* @param array $address The dummy address.
|
||||
* @param string $email_type The email type being previewed.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_subscriptions_email_preview_dummy_address', $address, $this->email_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dummy retry for use when previewing failed subscription payment retry emails.
|
||||
*
|
||||
* @param WC_Order $order The order object to create a dummy retry for.
|
||||
* @return WCS_Retry The dummy retry object.
|
||||
*/
|
||||
private function get_dummy_retry( $order ) {
|
||||
|
||||
if ( ! class_exists( 'WCS_Retry_Manager' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$order_id = is_a( $order, 'WC_Order' ) ? $order->get_id() : 12345;
|
||||
$retry_rule = WCS_Retry_Manager::rules()->get_rule( 1, $order_id );
|
||||
|
||||
if ( is_a( $retry_rule, 'WCS_Retry_Rule' ) ) {
|
||||
$interval = $retry_rule->get_retry_interval();
|
||||
$raw_retry_rule = $retry_rule->get_raw_data();
|
||||
} else {
|
||||
// If the retry rule is not found, use a default interval of 12 hours and an empty raw rule.
|
||||
$interval = 12 * HOUR_IN_SECONDS;
|
||||
$raw_retry_rule = [];
|
||||
}
|
||||
|
||||
return new WCS_Retry(
|
||||
[
|
||||
'status' => 'pending',
|
||||
'order_id' => $order_id,
|
||||
'date_gmt' => gmdate( 'Y-m-d H:i:s', time() + $interval ),
|
||||
'rule_raw' => $raw_retry_rule,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the email being previewed is a subscription email.
|
||||
*
|
||||
* Subscription emails include:
|
||||
* - WC_Subscriptions_Email::$email_classes - core subscription emails.
|
||||
* - WC_Subscriptions_Email_Notifications::$email_classes - subscription notification emails (pre-renewal emails).
|
||||
* - WCS_Email_Customer_Payment_Retry - customer payment retry emails.
|
||||
* - WCS_Email_Payment_Retry - admin payment retry emails.
|
||||
*
|
||||
* @return bool Whether the email being previewed is a subscription email.
|
||||
*/
|
||||
private function is_subscription_email() {
|
||||
return isset( apply_filters( 'wcs_email_classes', array_merge( WC_Subscriptions_Email::$email_classes, WC_Subscriptions_Email_Notifications::$email_classes ) )[ $this->email_type ] )
|
||||
|| in_array( $this->email_type, [ 'WCS_Email_Customer_Payment_Retry', 'WCS_Email_Payment_Retry' ], true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up filters for previewing emails.
|
||||
*/
|
||||
private function set_up_filters() {
|
||||
// Filter the last order date created for a subscription to be displayed in the Cancelled Subscription email.
|
||||
add_filter( 'woocommerce_subscription_get_last_order_date_created_date', [ $this, 'mock_last_order_date_created' ], 10, 2 );
|
||||
// For the purpose of previewing an email, force the subscription to be early renewable if the feature is enabled.
|
||||
add_filter( 'woocommerce_subscriptions_can_user_renew_early', [ $this, 'allow_early_renewals_during_preview' ], 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up filters at the end of previewing emails.
|
||||
*
|
||||
* @param string $preview_content The email content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function clean_up_filters( $preview_content ) {
|
||||
remove_filter( 'woocommerce_subscription_get_last_order_date_created_date', [ $this, 'mock_last_order_date_created' ] );
|
||||
remove_filter( 'woocommerce_subscriptions_can_user_renew_early', [ $this, 'allow_early_renewals_during_preview' ] );
|
||||
|
||||
return $preview_content;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the last order date created for a subscription to the current date.
|
||||
*
|
||||
* @param string $date The date.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mock_last_order_date_created( $date, $subscription ) {
|
||||
if ( is_a( $subscription, 'WC_Subscription' ) && 12346 === $subscription->get_id() && 1 === $subscription->get_customer_id() ) {
|
||||
return gmdate( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow early renewals for previewing emails.
|
||||
*
|
||||
* @param bool $can_renew_early Whether the subscription can be renewed early.
|
||||
* @param WC_Subscription $subscription The subscription.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function allow_early_renewals_during_preview( $can_renew_early, $subscription, $user_id ) {
|
||||
if ( 1 === $user_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $can_renew_early;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom placeholders for subscription emails.
|
||||
*
|
||||
* @param WC_Email $email The email object.
|
||||
*/
|
||||
private function add_placeholders( $email ) {
|
||||
if ( ! isset( $email->placeholders ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$placeholders = [];
|
||||
|
||||
switch ( $this->email_type ) {
|
||||
case 'WCS_Email_Customer_Notification_Subscription_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Manual_Trial_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Auto_Trial_Expiration':
|
||||
case 'WCS_Email_Customer_Notification_Manual_Renewal':
|
||||
case 'WCS_Email_Customer_Notification_Auto_Renewal':
|
||||
// Pull the real values from the email object (Order or Subscription) if available.
|
||||
if ( is_a( $email->object, 'WC_Subscription' ) ) {
|
||||
$time_until_renewal = $email->get_time_until_date( $email->object, 'next_payment' );
|
||||
$customer_first_name = $email->object->get_billing_first_name();
|
||||
} else {
|
||||
$time_until_renewal = human_time_diff( time(), time() + WEEK_IN_SECONDS );
|
||||
$customer_first_name = 'John';
|
||||
}
|
||||
|
||||
$placeholders['{time_until_renewal}'] = $time_until_renewal;
|
||||
$placeholders['{customers_first_name}'] = $customer_first_name;
|
||||
break;
|
||||
case 'WCS_Email_Customer_Payment_Retry':
|
||||
case 'WCS_Email_Payment_Retry':
|
||||
$retry_time = is_a( $email->retry, 'WCS_Retry' )
|
||||
? $email->retry->get_time()
|
||||
: time() + ( 12 * HOUR_IN_SECONDS );
|
||||
|
||||
$placeholders['{retry_time}'] = wcs_get_human_time_diff( $retry_time );
|
||||
break;
|
||||
}
|
||||
|
||||
// Merge placeholders without overriding existing ones, and only adding those in the email.
|
||||
$email->placeholders = wp_parse_args(
|
||||
$placeholders,
|
||||
$email->placeholders
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Notification: Automated Subscription Renewal.
|
||||
*
|
||||
* An email sent to the customer when a subscription will be renewed automatically.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification_Auto_Renewal
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Auto_Renewal extends WCS_Email_Customer_Notification {
|
||||
|
||||
/**
|
||||
* Create an instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin_id = 'woocommerce-subscriptions_';
|
||||
|
||||
$this->id = 'customer_notification_auto_renewal';
|
||||
$this->title = __( 'Customer Notification: Automatic renewal notice', 'woocommerce-subscriptions' );
|
||||
$this->description = __( 'Customer Notification: Automatic renewal notice emails are sent when customer\'s subscription is about to be renewed automatically.', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->heading = __( 'Automatic renewal notice', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->subject = sprintf(
|
||||
// translators: $1: {site_title}, $2: {customers_first_name}, $3: {time_until_renewal}, variables that will be substituted when email is sent out
|
||||
_x( '[%1$s] %2$s, your subscription automatically renews in %3$s!', 'default email subject for subscription\'s automatic renewal notice', 'woocommerce-subscriptions' ),
|
||||
'{site_title}',
|
||||
'{customers_first_name}',
|
||||
'{time_until_renewal}'
|
||||
);
|
||||
|
||||
$this->template_html = 'emails/customer-notification-auto-renewal.php';
|
||||
$this->template_plain = 'emails/plain/customer-notification-auto-renewal.php';
|
||||
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
|
||||
|
||||
$this->customer_email = true;
|
||||
|
||||
// Constructor in parent uses the values above in the initialization.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function get_relevant_date_type() {
|
||||
return 'next_payment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content to show below main email content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_additional_content() {
|
||||
return __( 'Thank you for being a loyal customer, {customers_first_name} — we appreciate your business.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Notification: Free Trial Expiring Subscription Email
|
||||
*
|
||||
* An email sent to the customer when a free trial is about to end.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Auto_Trial_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
/**
|
||||
* Create an instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin_id = 'woocommerce-subscriptions_';
|
||||
|
||||
$this->id = 'customer_notification_auto_trial_expiry';
|
||||
$this->title = __( 'Customer Notification: Free trial expiration: automatic payment notice', 'woocommerce-subscriptions' );
|
||||
$this->description = __( 'Free trial expiry notification emails are sent when customer\'s free trial for an automatically renewd subscription is about to expire.', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->heading = __( 'Free trial expiration: automatic payment notice', 'woocommerce-subscriptions' );
|
||||
// translators: $1: {site_title}, $2: {customers_first_name}, variables that will be substituted when email is sent out
|
||||
$this->subject = sprintf( _x( '[%1$s] %2$s, your paid subscription starts soon!', 'default email subject for free trial expiry notification emails sent to the customer', 'woocommerce-subscriptions' ), '{site_title}', '{customers_first_name}' );
|
||||
|
||||
$this->template_html = 'emails/customer-notification-auto-trial-ending.php';
|
||||
$this->template_plain = 'emails/plain/customer-notification-auto-trial-ending.php';
|
||||
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
|
||||
|
||||
$this->customer_email = true;
|
||||
|
||||
// Constructor in parent uses the values above in the initialization.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function get_relevant_date_type() {
|
||||
return 'trial_end';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Notification: Manual Subscription Renewal.
|
||||
*
|
||||
* An email sent to the customer when a subscription needs to be renewed manually.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification_Manual_Renewal
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Manual_Renewal extends WCS_Email_Customer_Notification {
|
||||
|
||||
/**
|
||||
* Create an instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin_id = 'woocommerce-subscriptions_';
|
||||
|
||||
$this->id = 'customer_notification_manual_renewal';
|
||||
$this->title = __( 'Customer Notification: Manual renewal notice', 'woocommerce-subscriptions' );
|
||||
$this->description = __( 'Customer Notification: Manual renewal notice are sent when customer\'s subscription needs to be manually renewed.', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->heading = __( 'Manual renewal notice', 'woocommerce-subscriptions' );
|
||||
// translators: $1: {site_title}, $2: {customers_first_name}, variables that will be substituted when email is sent out
|
||||
$this->subject = sprintf( _x( '[%1$s] %2$s, your subscription is ready to be renewed!', 'default email subject for notification for a manually renewed subscription sent to the customer', 'woocommerce-subscriptions' ), '{site_title}', '{customers_first_name}' );
|
||||
|
||||
$this->template_html = 'emails/customer-notification-manual-renewal.php';
|
||||
$this->template_plain = 'emails/plain/customer-notification-manual-renewal.php';
|
||||
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
|
||||
|
||||
$this->customer_email = true;
|
||||
|
||||
// Constructor in parent uses the values above in the initialization.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function get_relevant_date_type() {
|
||||
return 'next_payment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content to show below main email content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_additional_content() {
|
||||
return __( 'Thanks again for choosing {site_title}.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Notification: Free Trial Expiring Subscription Email
|
||||
*
|
||||
* An email sent to the customer when a free trial is about to end.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Manual_Trial_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
/**
|
||||
* Create an instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
$this->plugin_id = 'woocommerce-subscriptions_';
|
||||
|
||||
$this->id = 'customer_notification_manual_trial_expiry';
|
||||
$this->title = __( 'Customer Notification: Free trial expiration: manual payment required', 'woocommerce-subscriptions' );
|
||||
$this->description = __( 'Free trial expiry notification emails are sent when customer\'s free trial for a manually renewed subscription is about to expire.', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->heading = __( 'Free trial expiration: manual payment required', 'woocommerce-subscriptions' );
|
||||
// translators: $1: {site_title}, $2: {customers_first_name}, variables that will be substituted when email is sent out.
|
||||
$this->subject = sprintf( _x( '[%1$s] %2$s, your free trial is almost up!', 'default email subject for an email notification for a manually renewed subscription with free trial expiry emails sent to the customer', 'woocommerce-subscriptions' ), '{site_title}', '{customers_first_name}' );
|
||||
|
||||
$this->template_html = 'emails/customer-notification-manual-trial-ending.php';
|
||||
$this->template_plain = 'emails/plain/customer-notification-manual-trial-ending.php';
|
||||
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
|
||||
|
||||
$this->customer_email = true;
|
||||
|
||||
// Constructor in parent uses the values above in the initialization.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function get_relevant_date_type() {
|
||||
return 'trial_end';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Notification: Subscription Expiring email
|
||||
*
|
||||
* An email sent to the customer when a subscription is about to expire.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification_Subscription_Expiring
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Subscription_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
/**
|
||||
* Create an instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin_id = 'woocommerce-subscriptions_';
|
||||
|
||||
$this->id = 'customer_notification_subscription_expiry';
|
||||
$this->title = __( 'Customer Notification: Subscription expiration notice', 'woocommerce-subscriptions' );
|
||||
$this->description = __( 'Subscription expiration notification emails are sent when customer\'s subscription is about to expire.', 'woocommerce-subscriptions' );
|
||||
|
||||
$this->heading = __( 'Subscription expiration notice', 'woocommerce-subscriptions' );
|
||||
// translators: $1: {site_title}, $2: {customers_first_name}, variables that will be substituted when email is sent out
|
||||
$this->subject = sprintf( _x( '[%1$s] %2$s, your subscription is about to expire!', 'default email subject for subscription expiry notification email sent to the customer', 'woocommerce-subscriptions' ), '{site_title}', '{customers_first_name}' );
|
||||
|
||||
$this->template_html = 'emails/customer-notification-expiring-subscription.php';
|
||||
$this->template_plain = 'emails/plain/customer-notification-expiring-subscription.php';
|
||||
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
|
||||
|
||||
$this->customer_email = true;
|
||||
|
||||
// Constructor in parent uses the values above in the initialization.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function get_relevant_date_type() {
|
||||
return 'end';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content to show below main email content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_additional_content() {
|
||||
return __( 'Thank you for choosing {site_title}, {customers_first_name}.', 'woocommerce-subscriptions' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Customer notification email
|
||||
*
|
||||
* Customer notification email sent to customer when a there's an upcoming payment/expity/free trial expiry.
|
||||
*
|
||||
* @class WCS_Email_Customer_Notification
|
||||
* @version 7.7.0
|
||||
* @package WooCommerce/Classes/Emails
|
||||
*/
|
||||
class WCS_Email_Customer_Notification extends WC_Email {
|
||||
|
||||
public function __construct() {
|
||||
// These values are only available later, but it's an available placeholder.
|
||||
$this->placeholders = array_merge(
|
||||
[
|
||||
'{customers_first_name}' => '',
|
||||
'{time_until_renewal}' => '',
|
||||
],
|
||||
$this->placeholders
|
||||
);
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Settings Form Fields - these are generic email options most will use.
|
||||
*/
|
||||
public function init_form_fields() {
|
||||
/* translators: %s: list of placeholders */
|
||||
$placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce-subscriptions' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
|
||||
$this->form_fields = array(
|
||||
'enabled' => array(
|
||||
'title' => __( 'Enable/Disable', 'woocommerce-subscriptions' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable this email notification. Disabled automatically on staging sites.', 'woocommerce-subscriptions' ),
|
||||
'default' => 'yes',
|
||||
),
|
||||
'subject' => array(
|
||||
'title' => __( 'Subject', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => $placeholder_text,
|
||||
'placeholder' => $this->get_default_subject(),
|
||||
'default' => '',
|
||||
),
|
||||
'heading' => array(
|
||||
'title' => __( 'Email heading', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => $placeholder_text,
|
||||
'placeholder' => $this->get_default_heading(),
|
||||
'default' => '',
|
||||
),
|
||||
'additional_content' => array(
|
||||
'title' => __( 'Additional content', 'woocommerce-subscriptions' ),
|
||||
'description' => __( 'Text to appear below the main email content.', 'woocommerce-subscriptions' ) . ' ' . $placeholder_text,
|
||||
'css' => 'width:400px; height: 75px;',
|
||||
'placeholder' => __( 'N/A', 'woocommerce-subscriptions' ),
|
||||
'type' => 'textarea',
|
||||
'default' => $this->get_default_additional_content(),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'email_type' => array(
|
||||
'title' => __( 'Email type', 'woocommerce-subscriptions' ),
|
||||
'type' => 'select',
|
||||
'description' => __( 'Choose which format of email to send.', 'woocommerce-subscriptions' ),
|
||||
'default' => 'html',
|
||||
'class' => 'email_type wc-enhanced-select',
|
||||
'options' => $this->get_email_type_options(),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function trigger( $subscription_id ) {
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
$this->object = $subscription;
|
||||
$this->recipient = $subscription->get_billing_email();
|
||||
|
||||
if ( ! $this->should_send_reminder_email( $subscription ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setup_locale();
|
||||
|
||||
try {
|
||||
$this->placeholders['{customers_first_name}'] = $subscription->get_billing_first_name();
|
||||
$this->placeholders['{time_until_renewal}'] = $this->get_time_until_date( $subscription, 'next_payment' );
|
||||
|
||||
$result = $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
|
||||
|
||||
if ( $result ) {
|
||||
/* translators: 1: Notification type, 2: customer's email. */
|
||||
$order_note_msg = sprintf( __( '%1$s was successfully sent to %2$s.', 'woocommerce-subscriptions' ), $this->title, $this->recipient );
|
||||
} else {
|
||||
/* translators: 1: Notification type, 2: customer's email. */
|
||||
$order_note_msg = sprintf( __( 'Attempt to send %1$s to %2$s failed.', 'woocommerce-subscriptions' ), $this->title, $this->recipient );
|
||||
}
|
||||
|
||||
$subscription->add_order_note( $order_note_msg );
|
||||
} finally {
|
||||
$this->restore_locale();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content for the HTML-version of the email.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_html() {
|
||||
$subscription = $this->object;
|
||||
|
||||
if ( wcs_can_user_renew_early( $subscription, $subscription->get_customer_id() )
|
||||
&& $subscription->payment_method_supports( 'subscription_date_changes' )
|
||||
&& WCS_Early_Renewal_Manager::is_early_renewal_enabled()
|
||||
&& WCS_Manual_Renewal_Manager::is_manual_renewal_enabled()
|
||||
) {
|
||||
$url_for_renewal = wcs_get_early_renewal_url( $subscription );
|
||||
$can_renew_early = true;
|
||||
} else {
|
||||
$url_for_renewal = $subscription->get_view_order_url();
|
||||
$can_renew_early = false;
|
||||
}
|
||||
|
||||
return wc_get_template_html(
|
||||
$this->template_html,
|
||||
[
|
||||
'subscription' => $subscription,
|
||||
'order' => $subscription->get_parent(),
|
||||
'email_heading' => $this->get_heading(),
|
||||
'subscription_time_til_event' => $this->get_time_until_date( $subscription, $this->get_relevant_date_type() ),
|
||||
'subscription_event_date' => $this->get_formatted_date( $subscription, $this->get_relevant_date_type() ),
|
||||
'url_for_renewal' => $url_for_renewal,
|
||||
'can_renew_early' => $can_renew_early,
|
||||
'additional_content' => is_callable(
|
||||
[
|
||||
$this,
|
||||
'get_additional_content',
|
||||
]
|
||||
) ? $this->get_additional_content() : '',
|
||||
// WC 3.7 introduced an additional content field for all emails.
|
||||
'sent_to_admin' => false,
|
||||
'plain_text' => false,
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content for the plain (text, non-HTML) version of the email.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_plain() {
|
||||
$subscription = $this->object;
|
||||
|
||||
if ( wcs_can_user_renew_early( $subscription, $subscription->get_customer_id() )
|
||||
&& $subscription->payment_method_supports( 'subscription_date_changes' )
|
||||
&& WCS_Early_Renewal_Manager::is_early_renewal_enabled()
|
||||
&& WCS_Manual_Renewal_Manager::is_manual_renewal_enabled()
|
||||
) {
|
||||
$url_for_renewal = wcs_get_early_renewal_url( $subscription );
|
||||
$can_renew_early = true;
|
||||
} else {
|
||||
$url_for_renewal = $subscription->get_view_order_url();
|
||||
$can_renew_early = false;
|
||||
}
|
||||
|
||||
return wc_get_template_html(
|
||||
$this->template_plain,
|
||||
[
|
||||
'subscription' => $subscription,
|
||||
'order' => $subscription->get_parent(),
|
||||
'email_heading' => $this->get_heading(),
|
||||
'subscription_time_til_event' => $this->get_time_until_date( $subscription, $this->get_relevant_date_type() ),
|
||||
'subscription_event_date' => $this->get_formatted_date( $subscription, $this->get_relevant_date_type() ),
|
||||
'url_for_renewal' => $url_for_renewal,
|
||||
'can_renew_early' => $can_renew_early,
|
||||
'additional_content' => is_callable(
|
||||
[
|
||||
$this,
|
||||
'get_additional_content',
|
||||
]
|
||||
) ? $this->get_additional_content() : '',
|
||||
// WC 3.7 introduced an additional content field for all emails.
|
||||
'sent_to_admin' => false,
|
||||
'plain_text' => true,
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of days until date_type for subscription.
|
||||
*
|
||||
* This method is needed when sending out the emails as the email queue might be delayed, in which case the email
|
||||
* should state the correct number of days until the date_type.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription to check.
|
||||
* @param string $date_type Date type to count days to.
|
||||
*
|
||||
* @return false|int|string Number of days from now until the date type event's time. Empty string if subscription doesn't have the date_type defined. False if DateTime can't process the data.
|
||||
*/
|
||||
public function get_time_until_date( $subscription, $date_type ) {
|
||||
$next_event = $subscription->get_date( $date_type );
|
||||
|
||||
if ( ! $next_event ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$next_event_dt = new DateTime( $next_event, new DateTimeZone( 'UTC' ) );
|
||||
$now = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
|
||||
|
||||
// Both dates to midnight so we only compare days, not hours.
|
||||
$next_event_dt->setTime( 0, 0 );
|
||||
$now->setTime( 0, 0 );
|
||||
|
||||
// Add some buffer, otherwise it will claim that only 2 full days are left when in reality it's 2 days, 23 hours and 59 minutes.
|
||||
$now->modify( '-1 hour' );
|
||||
return human_time_diff( $now->getTimestamp(), $next_event_dt->getTimestamp() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return subscription's date of date type in localized format.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param string $date_type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_date( $subscription, $date_type ) {
|
||||
return date_i18n( wc_date_format(), $subscription->get_time( $date_type, 'site' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content to show below main email content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_additional_content() {
|
||||
return __( 'Thank you for choosing {site_title}!', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the customer reminder email should be sent.
|
||||
*
|
||||
* Reminder emails are not sent if:
|
||||
* - The Customer Notification feature is disabled.
|
||||
* - The store is a staging or development site.
|
||||
* - The recipient email address is missing.
|
||||
* - The subscription's billing cycle is too short.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_send_reminder_email( $subscription ) {
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return $this->log_reminder_email_not_sent( $subscription, __( 'Reminder emails disabled.', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
$skipped_reasons = [];
|
||||
|
||||
if ( ! WC_Subscriptions_Email_Notifications::should_send_notification() ) {
|
||||
$skipped_reasons[] = __( 'Not a production site, or notifications have been globally disabled', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
if ( ! $this->get_recipient() ) {
|
||||
$skipped_reasons[] = __( 'Recipient not found', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
if ( WCS_Action_Scheduler_Customer_Notifications::is_subscription_period_too_short( $subscription ) ) {
|
||||
$skipped_reasons[] = __( 'Subscription billing cycle too short', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
return empty( $skipped_reasons ) || $this->log_reminder_email_not_sent( $subscription, $skipped_reasons );
|
||||
}
|
||||
|
||||
/**
|
||||
* If WCS_DEBUG or WP_DEBUG is enabled, attach a note to the subscription to detail why a reminder email was not sent.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param array|string $reasons
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
private function log_reminder_email_not_sent( $subscription, $reasons ) {
|
||||
if ( ( defined( 'WCS_DEBUG' ) && WCS_DEBUG ) || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
|
||||
$reasons = (array) $reasons;
|
||||
|
||||
// translators: %1$s: email title, %2$s: list of reasons why email was skipped.
|
||||
$subscription->add_order_note( sprintf( __( 'Skipped sending "%1$s": %2$s', 'woocommerce-subscriptions' ), $this->title, '<br>- ' . implode( '<br>- ', $reasons ) ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* WCS_Batch_Processor Interface
|
||||
*
|
||||
* Interface for batch data processors. See the WCS_Batch_Processing_Controller class for usage details.
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @version 7.7.0
|
||||
* @since 7.7.0
|
||||
*/
|
||||
interface WCS_Batch_Processor {
|
||||
|
||||
/**
|
||||
* Get a user-friendly name for this processor.
|
||||
*
|
||||
* @return string Name of the processor.
|
||||
*/
|
||||
public function get_name() : string;
|
||||
|
||||
/**
|
||||
* Get a user-friendly description for this processor.
|
||||
*
|
||||
* @return string Description of what this processor does.
|
||||
*/
|
||||
public function get_description() : string;
|
||||
|
||||
/**
|
||||
* Get the total number of pending items that require processing.
|
||||
* Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.
|
||||
*
|
||||
* Note that the once the processor is enqueued the batch processor controller will keep
|
||||
* invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.
|
||||
*
|
||||
* @return int Number of items pending processing.
|
||||
*/
|
||||
public function get_total_pending_count() : int;
|
||||
|
||||
/**
|
||||
* Returns the next batch of items that need to be processed.
|
||||
*
|
||||
* A batch item can be anything needed to identify the actual processing to be done,
|
||||
* but whenever possible items should be numbers (e.g. database record ids)
|
||||
* or at least strings, to ease troubleshooting and logging in case of problems.
|
||||
*
|
||||
* The size of the batch returned can be less than $size if there aren't that
|
||||
* many items pending processing (and it can be zero if there isn't anything to process),
|
||||
* but the size should always be consistent with what 'get_total_pending_count' returns
|
||||
* (i.e. the size of the returned batch shouldn't be larger than the pending items count).
|
||||
*
|
||||
* @param int $size Maximum size of the batch to be returned.
|
||||
*
|
||||
* @return array Batch of items to process, containing $size or less items.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ) : array;
|
||||
|
||||
/**
|
||||
* Process data for the supplied batch.
|
||||
*
|
||||
* This method should be prepared to receive items that don't actually need processing
|
||||
* (because they have been processed before) and ignore them, but if at least
|
||||
* one of the batch items that actually need processing can't be processed, an exception should be thrown.
|
||||
*
|
||||
* Once an item has been processed it shouldn't be counted in 'get_total_pending_count'
|
||||
* nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it
|
||||
* to actually require further processing).
|
||||
*
|
||||
* @throw \Exception Something went wrong while processing the batch.
|
||||
*
|
||||
* @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
|
||||
*/
|
||||
public function process_batch( array $batch ): void;
|
||||
|
||||
/**
|
||||
* Default (preferred) batch size to pass to 'get_next_batch_to_process'.
|
||||
* The controller will pass this size unless it's externally configured
|
||||
* to use a different size.
|
||||
*
|
||||
* @return int Default batch size.
|
||||
*/
|
||||
public function get_default_batch_size() : int;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Upgrade script for version 7.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WCS_Plugin_Upgrade_7_8_0 {
|
||||
|
||||
/**
|
||||
* Check if the Gifting plugin is enabled and update the settings.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*/
|
||||
public static function check_gifting_plugin_is_enabled() {
|
||||
|
||||
WCS_Upgrade_Logger::add( 'Checking if the Gifting plugin is enabled...' );
|
||||
|
||||
if ( ! is_plugin_active( 'woocommerce-subscriptions-gifting/woocommerce-subscriptions-gifting.php' ) ) {
|
||||
WCS_Upgrade_Logger::add( 'Gifting plugin is not enabled, skipping...' );
|
||||
return;
|
||||
}
|
||||
|
||||
WCS_Upgrade_Logger::add( 'Gifting plugin is enabled, updating Gifting settings...' );
|
||||
|
||||
update_option( 'woocommerce_subscriptions_gifting_enable_gifting', 'yes' );
|
||||
update_option( 'woocommerce_subscriptions_gifting_default_option', 'enabled' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Upgrade script for version 8.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WCS_Plugin_Upgrade_8_3_0 {
|
||||
|
||||
/**
|
||||
* Check if the Gifting plugin is enabled and update the settings.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
public static function check_downloads_plugin_is_enabled() {
|
||||
|
||||
WCS_Upgrade_Logger::add( 'Checking if the Downloads plugin is enabled...' );
|
||||
|
||||
$active_plugins = get_option( 'active_plugins', array() );
|
||||
$is_downloads_plugin_active = false;
|
||||
|
||||
foreach ( $active_plugins as $plugin ) {
|
||||
if ( strpos( $plugin, 'woocommerce-subscription-downloads.php' ) !== false ) {
|
||||
$is_downloads_plugin_active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $is_downloads_plugin_active ) {
|
||||
WCS_Upgrade_Logger::add( 'Downloads plugin is not enabled via active_plugins, skipping...' );
|
||||
return;
|
||||
}
|
||||
|
||||
WCS_Upgrade_Logger::add( 'Downloads plugin is enabled, updating Downloads settings...' );
|
||||
|
||||
update_option( WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking', 'yes' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Upgrade script for version 8.3.0
|
||||
*
|
||||
* @version 8.3.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WCS_Upgrade_8_3_0 {
|
||||
|
||||
/**
|
||||
* Update Subscription email templates Subject and Heading replacing blogname with site_title.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
public static function migrate_subscription_email_templates() {
|
||||
|
||||
$settings_names = array(
|
||||
'woocommerce_cancelled_subscription_settings',
|
||||
'woocommerce_customer_completed_renewal_order_settings',
|
||||
'woocommerce_customer_completed_switch_order_settings',
|
||||
'woocommerce-subscriptions_customer_notification_auto_renewal_settings',
|
||||
'woocommerce-subscriptions_customer_notification_auto_trial_expiry_settings',
|
||||
'woocommerce-subscriptions_customer_notification_manual_renewal_settings',
|
||||
'woocommerce-subscriptions_customer_notification_manual_trial_expiry_settings',
|
||||
'woocommerce-subscriptions_customer_notification_auto_trial_expiry_settings',
|
||||
'woocommerce_customer_on_hold_renewal_order_settings',
|
||||
'woocommerce_customer_renewal_invoice_settings',
|
||||
'woocommerce_expired_subscription_settings',
|
||||
'woocommerce_new_renewal_order_settings',
|
||||
'woocommerce_new_switch_order_settings',
|
||||
'woocommerce_customer_processing_renewal_order_settings',
|
||||
'woocommerce_suspended_subscription_settings',
|
||||
);
|
||||
|
||||
WCS_Upgrade_Logger::add( '8.3.0 - Updating subscription email settings.' );
|
||||
|
||||
foreach ( $settings_names as $settings_name ) {
|
||||
// Cast to array helps us to avoid issues with further checks or leave the option unchanged if its format is modified by other plugins.
|
||||
$option = (array) get_option( $settings_name );
|
||||
|
||||
if ( empty( $option['subject'] ) && empty( $option['heading'] ) ) {
|
||||
WCS_Upgrade_Logger::add( sprintf( 'Subscription email settings not found: %s.', $settings_name ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
$option['subject'] = str_replace( '{blogname}', '{site_title}', $option['subject'] );
|
||||
$option['heading'] = str_replace( '{blogname}', '{site_title}', $option['heading'] );
|
||||
|
||||
WCS_Upgrade_Logger::add(
|
||||
update_option( $settings_name, $option )
|
||||
? sprintf( 'Updated subscription email settings: %s.', $settings_name )
|
||||
: sprintf( 'Subscription email settings for %s were not changed.', $settings_name )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Downloads Admin Announcement Handler Class
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WC_Subscription_Downloads_Admin_Welcome_Announcement {
|
||||
|
||||
/**
|
||||
* Initialize the tour handler
|
||||
*/
|
||||
public static function init() {
|
||||
// Register scripts and styles
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
|
||||
// Add the tour HTML to the admin footer
|
||||
add_action( 'admin_footer', array( __CLASS__, 'output_tour' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue required scripts and styles
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
wp_localize_script(
|
||||
'wcs-admin',
|
||||
'wcsDownloadsSettings',
|
||||
array(
|
||||
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
|
||||
'pluginsUrl' => admin_url( 'plugins.php' ),
|
||||
'subscriptionsUrl' => WC_Subscriptions_Admin::settings_tab_url() . '#woocommerce_subscriptions_downloads_enable',
|
||||
'isStandaloneDownloadsEnabled' => is_plugin_active( 'woocommerce-subscription-downloads/woocommerce-subscription-downloads.php' ),
|
||||
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the tour HTML in the admin footer
|
||||
*/
|
||||
public static function output_tour() {
|
||||
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a div for the tour to be rendered into
|
||||
echo '<div id="wcs-downloads-welcome-announcement-root" class="woocommerce-tour-kit"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the welcome tour has been dismissed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_welcome_announcement_dismissed() {
|
||||
return '1' === get_option(
|
||||
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current screen is WooCommerce Admin or subscriptions listing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_woocommerce_admin_or_subscriptions_listing() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$action_param = isset( $_GET['action'] ) ? wc_clean( wp_unslash( $_GET['action'] ) ) : '';
|
||||
|
||||
$is_woocommerce_admin = 'woocommerce_page_wc-admin' === $screen->id;
|
||||
$is_subscriptions_listing = 'woocommerce_page_wc-orders--shop_subscription' === $screen->id && empty( $action_param );
|
||||
|
||||
return $is_woocommerce_admin || $is_subscriptions_listing;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Subscription Downloads Ajax.
|
||||
*
|
||||
* @package WC_Subscription_Downloads_Ajax
|
||||
* @category Ajax
|
||||
* @author WooThemes
|
||||
*/
|
||||
class WC_Subscription_Downloads_Ajax {
|
||||
|
||||
/**
|
||||
* Ajax actions.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'wp_ajax_wc_subscription_downloads_search', array( $this, 'search_subscriptions' ) );
|
||||
add_action( 'wp_ajax_wc_subscription_linked_downloadable_products_search', array( $this, 'search_downloadable_products' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search subscription products.
|
||||
*/
|
||||
public function search_subscriptions() {
|
||||
ob_start();
|
||||
|
||||
global $wpdb;
|
||||
|
||||
check_ajax_referer( 'search-products', 'security' );
|
||||
|
||||
$term = wc_clean( stripslashes( $_GET['term'] ) );
|
||||
|
||||
if ( empty( $term ) ) {
|
||||
die();
|
||||
}
|
||||
|
||||
$found_subscriptions = array();
|
||||
|
||||
$term = apply_filters( 'woocommerce_subscription_downloads_json_search_order_number', $term );
|
||||
|
||||
// Find subscription products by title.
|
||||
$query_subscriptions = $wpdb->get_results( $wpdb->prepare( "
|
||||
SELECT ID
|
||||
FROM $wpdb->posts AS posts
|
||||
LEFT JOIN $wpdb->term_relationships AS t_relationships ON(posts.ID = t_relationships.object_id)
|
||||
LEFT JOIN $wpdb->term_taxonomy AS t_taxonomy ON(t_relationships.term_taxonomy_id = t_taxonomy.term_taxonomy_id)
|
||||
LEFT JOIN $wpdb->terms AS terms ON(t_taxonomy.term_id = terms.term_id)
|
||||
WHERE posts.post_type = 'product'
|
||||
AND posts.post_status = 'publish'
|
||||
AND posts.post_title LIKE %s
|
||||
AND t_taxonomy.taxonomy = 'product_type'
|
||||
AND (terms.slug = 'subscription' OR terms.slug = 'variable-subscription')
|
||||
ORDER BY posts.post_date DESC
|
||||
", '%' . $term . '%' ) );
|
||||
|
||||
if ( $query_subscriptions ) {
|
||||
foreach ( $query_subscriptions as $item ) {
|
||||
$_product = wc_get_product( $item->ID );
|
||||
$found_subscriptions[ $item->ID ] = sanitize_text_field( $_product->get_formatted_name() );
|
||||
|
||||
if ( 'variable-subscription' == $_product->get_type() ) {
|
||||
$chindren = get_children( array( 'post_parent' => $_product->get_id(), 'post_type' => 'product_variation' ) );
|
||||
|
||||
foreach ( $chindren as $child ) {
|
||||
$_child_product = wc_get_product( $child );
|
||||
$found_subscriptions[ $child->ID ] = sanitize_text_field( $_child_product->get_formatted_name() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json( $found_subscriptions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for downloadable products that are simple or variants.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function search_downloadable_products(): void {
|
||||
$results = array();
|
||||
|
||||
// Prevent error noise from leaking.
|
||||
ob_start();
|
||||
|
||||
if ( isset( $_GET['term'] ) && check_ajax_referer( 'search-products', 'security' ) ) {
|
||||
$term = wc_clean( wp_unslash( $_GET['term'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $term ) ) {
|
||||
$products = wc_get_products(
|
||||
array(
|
||||
'downloadable' => true,
|
||||
'limit' => 100,
|
||||
's' => $term,
|
||||
'type' => array( 'simple', 'variation' ),
|
||||
'status' => 'any',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $products as $product ) {
|
||||
$results[ $product->get_id() ] = sanitize_text_field( $product->get_formatted_name() );
|
||||
}
|
||||
}
|
||||
|
||||
ob_clean();
|
||||
wp_send_json( $results );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Subscription Downloads Install.
|
||||
*
|
||||
* @package WC_Subscription_Downloads_Install
|
||||
* @category Install
|
||||
* @author WooThemes
|
||||
*/
|
||||
class WC_Subscription_Downloads_Install {
|
||||
|
||||
/**
|
||||
* Run the install.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->create_table();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the plugin table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function create_table() {
|
||||
global $wpdb;
|
||||
|
||||
$version = get_option( 'woocommerce_subscription_downloads_version' );
|
||||
|
||||
if ( ! $version ) {
|
||||
add_option( 'woocommerce_subscription_downloads_version', WC_Subscriptions::$version );
|
||||
|
||||
$collate = '';
|
||||
|
||||
if ( $wpdb->has_cap( 'collation' ) ) {
|
||||
if ( ! empty( $wpdb->charset ) ) {
|
||||
$collate .= "DEFAULT CHARACTER SET $wpdb->charset";
|
||||
}
|
||||
if ( ! empty( $wpdb->collate ) ) {
|
||||
$collate .= " COLLATE $wpdb->collate";
|
||||
}
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$create = "
|
||||
CREATE TABLE {$wpdb->prefix}woocommerce_subscription_downloads (
|
||||
id bigint(20) NOT NULL auto_increment,
|
||||
product_id bigint(20) NOT NULL,
|
||||
subscription_id bigint(20) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) $collate;
|
||||
";
|
||||
|
||||
dbDelta( $create );
|
||||
} else {
|
||||
update_option( 'woocommerce_subscription_downloads_version', WC_Subscriptions::$version );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Subscription Downloads Order.
|
||||
*
|
||||
* @package WC_Subscription_Downloads_Order
|
||||
* @category Order
|
||||
* @author WooThemes
|
||||
*/
|
||||
class WC_Subscription_Downloads_Order {
|
||||
|
||||
/**
|
||||
* Order actions.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_subscription_status_changed', array( $this, 'download_permissions' ), 10, 4 );
|
||||
add_action( 'woocommerce_email_after_order_table', array( $this, 'email_list_downloads' ), 10, 3 );
|
||||
add_action( 'woocommerce_subscriptions_switched_item', array( $this, 'handle_download_switch' ), 10, 3 );
|
||||
add_filter( 'woocommerce_order_get_downloadable_items', array( $this, 'remove_subscription_download_duplicates' ), 1, 2 );
|
||||
add_filter( 'woocommerce_customer_available_downloads', array( $this, 'remove_customer_download_duplicates' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the download permissions in the subscription depending on the status.
|
||||
*
|
||||
* @param int $subscription_id Subscription ID.
|
||||
* @param string $old_status Old status.
|
||||
* @param string $new_status New status.
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function download_permissions( $subscription_id, $old_status, $new_status, $subscription ) {
|
||||
if ( ! in_array( $new_status, array( 'active', 'expired', 'cancelled' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_item_ids = array_map( function( $item ) {
|
||||
return $item['product_id'];
|
||||
}, $subscription->get_items() );
|
||||
|
||||
foreach ( $subscription->get_items() as $item ) {
|
||||
|
||||
// Gets the downloadable products.
|
||||
$downloadable_products = WC_Subscription_Downloads::get_downloadable_products( $item['product_id'], $item['variation_id'] );
|
||||
|
||||
if ( $downloadable_products ) {
|
||||
|
||||
foreach ( $downloadable_products as $product_id ) {
|
||||
$_product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $_product ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @phpstan-ignore property.notFound
|
||||
$product_status = version_compare( WC_VERSION, '3.0', '<' ) ? $_product->post->post_status : $_product->get_status();
|
||||
|
||||
if ( 'expired' === $new_status || 'cancelled' === $new_status ) {
|
||||
WCS_Download_Handler::revoke_downloadable_file_permission( $product_id, $subscription_id, $subscription->get_user_id() );
|
||||
}
|
||||
// Adds the downloadable files to the subscription.
|
||||
else if ( $_product && $_product->exists() && $_product->is_downloadable() && 'publish' === $product_status ) {
|
||||
WCS_Download_Handler::revoke_downloadable_file_permission( $product_id, $subscription_id, $subscription->get_user_id() );
|
||||
$downloads = version_compare( WC_VERSION, '3.0', '<' ) ? $_product->get_files() : $_product->get_downloads();
|
||||
|
||||
foreach ( array_keys( $downloads ) as $download_id ) {
|
||||
wc_downloadable_file_permission( $download_id, $product_id, $subscription );
|
||||
}
|
||||
|
||||
if ( ! in_array( $_product->get_id(), $product_item_ids ) ) {
|
||||
// Skip wrong recalculation of totals by adding a 0 amount Subscriptions.
|
||||
$totals = array(
|
||||
'subtotal' => wc_format_decimal( 0 ),
|
||||
'total' => wc_format_decimal( 0 ),
|
||||
'subtotal_tax' => wc_format_decimal( 0 ),
|
||||
'tax' => wc_format_decimal( 0 ),
|
||||
);
|
||||
|
||||
$subscription->add_product( $_product, 1, array( 'totals' => $totals ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove downloads duplicates on subscriptions.
|
||||
*
|
||||
* @since 1.1.29
|
||||
*
|
||||
* @param array $downloads List of downloads.
|
||||
* @param WC_Order $order The order.
|
||||
*
|
||||
* @return array Array of downloads.
|
||||
*/
|
||||
public static function remove_subscription_download_duplicates( $downloads, $order ) {
|
||||
if ( is_a( $order, 'WC_Subscription' ) ) {
|
||||
$downloads = array_unique( $downloads, SORT_REGULAR );
|
||||
}
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove customer download duplicates that were added to the same order.
|
||||
*
|
||||
* @since 1.1.30
|
||||
*
|
||||
* @param array $downloads List of downloads.
|
||||
* @param int $customer_id The customer id.
|
||||
*
|
||||
* @return array Array of downloads.
|
||||
*/
|
||||
public static function remove_customer_download_duplicates( $downloads, $customer_id ) {
|
||||
// As downloads have an order_id, the following only removes download duplicates from the same order.
|
||||
$downloads = array_unique( $downloads, SORT_REGULAR );
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the downloads in order emails.
|
||||
*
|
||||
* @param WC_Order $order Order data
|
||||
* @param bool $sent_to_admin Sent or not to admin.
|
||||
* @param bool $plain_text Plain or HTML email.
|
||||
*/
|
||||
public function email_list_downloads( $order, $sent_to_admin = false, $plain_text = false ) {
|
||||
// @phpstan-ignore property.notFound
|
||||
$order_status = version_compare( WC_VERSION, '3.0', '<' ) ? $order->status : $order->get_status();
|
||||
|
||||
if ( $sent_to_admin && ! in_array( $order_status, array( 'processing', 'completed' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloads = WC_Subscription_Downloads::get_order_downloads( $order );
|
||||
|
||||
if ( $downloads && $plain_text ) {
|
||||
$html = apply_filters( 'woocommerce_subscription_downloads_my_downloads_title', __( 'Available downloads', 'woocommerce-subscriptions' ) );
|
||||
$html .= PHP_EOL . PHP_EOL;
|
||||
|
||||
foreach ( $downloads as $download ) {
|
||||
$html .= $download['name'] . ': ' . $download['download_url'] . PHP_EOL;
|
||||
}
|
||||
|
||||
$html .= PHP_EOL;
|
||||
$html .= '****************************************************';
|
||||
$html .= PHP_EOL . PHP_EOL;
|
||||
|
||||
echo esc_html( wp_strip_all_tags( $html ) );
|
||||
|
||||
} elseif ( $downloads && ! $plain_text ) {
|
||||
$html = '<h2>' . esc_html( apply_filters( 'woocommerce_subscription_downloads_my_downloads_title', __( 'Available downloads', 'woocommerce-subscriptions' ) ) ) . '</h2>';
|
||||
|
||||
$html .= '<table cellspacing="0" cellpadding="0" style="width: 100%; vertical-align: top;" border="0">';
|
||||
$html .= '<tr>';
|
||||
$html .= '<td valign="top">';
|
||||
$html .= '<ul class="digital-downloads">';
|
||||
foreach ( $downloads as $download ) {
|
||||
$html .= sprintf( '<li><a href="%1$s" title="%2$s" target="_blank">%2$s</a></li>', esc_url( $download['download_url'] ), esc_html( $download['name'] ) );
|
||||
}
|
||||
$html .= '</ul>';
|
||||
$html .= '</td>';
|
||||
$html .= '</tr>';
|
||||
$html .= '</table>';
|
||||
|
||||
/**
|
||||
* The following HTML output consists of both static content and variable elements.
|
||||
* The variable elements, such as user-generated content or database values, are properly escaped to prevent security vulnerabilities.
|
||||
*
|
||||
* If this HTML is ever exposed externally via a filter or if more variable elements are added, additional security measures should be taken into account.
|
||||
* Consider using the necessary escaping functions/methods to ensure the continued safety of the output.
|
||||
*
|
||||
* Note: The warning 'WordPress.Security.EscapeOutput.OutputNotEscaped' has been suppressed intentionally, but ensure that the code adheres to the required security standards.
|
||||
*/
|
||||
echo $html; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke download permissions granted on the old switch item.
|
||||
*
|
||||
* @param WC_Subscription $subscription
|
||||
* @param array $new_item
|
||||
* @param array $old_item
|
||||
*/
|
||||
public function handle_download_switch( $subscription, $new_item, $old_item ) {
|
||||
if ( ! is_object( $subscription ) ) {
|
||||
$subscription = wcs_get_subscription( $subscription );
|
||||
}
|
||||
|
||||
$subscription_id = $subscription->get_id();
|
||||
$downloadable_products = WC_Subscription_Downloads::get_downloadable_products( $old_item['product_id'], $old_item['variation_id'] );
|
||||
$subscription_items = $subscription->get_items();
|
||||
|
||||
// Remove old item attached to the subscription.
|
||||
foreach ( $subscription_items as $item ) {
|
||||
if ( in_array( $item['product_id'], $downloadable_products ) || in_array( $item['variation_id'], $downloadable_products ) ) {
|
||||
$item = $subscription->get_item( $item );
|
||||
if ( $item ) {
|
||||
$item->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Further, remove all attached downloadable products to the subscription.
|
||||
foreach ( $downloadable_products as $product_id ) {
|
||||
WCS_Download_Handler::revoke_downloadable_file_permission( $product_id, $subscription_id, $subscription->get_user_id() );
|
||||
}
|
||||
|
||||
// Re-trigger download permissions. It will automatically add permissions to the new items.
|
||||
$this->download_permissions( $subscription_id, '', 'active', $subscription );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,815 +0,0 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Subscription Downloads Products.
|
||||
*
|
||||
* @package WC_Subscription_Downloads_Products
|
||||
*/
|
||||
class WC_Subscription_Downloads_Products {
|
||||
public const EDITOR_UPDATE = 'wcsubs_subscription_download_relationships';
|
||||
public const RELATIONSHIP_DOWNLOAD_TO_SUB = 'download-to-sub';
|
||||
public const RELATIONSHIP_VAR_DOWNLOAD_TO_SUB = 'var-download-to-sub';
|
||||
public const RELATIONSHIP_SUB_TO_DOWNLOAD = 'sub-to-download';
|
||||
public const RELATIONSHIP_VAR_SUB_TO_DOWNLOAD = 'var-sub-to-download';
|
||||
|
||||
/**
|
||||
* Products actions.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_product_options_downloads', array( $this, 'simple_write_panel_options' ) );
|
||||
add_action( 'woocommerce_variation_options_download', array( $this, 'variable_write_panel_options' ), 10, 3 );
|
||||
add_action( 'woocommerce_product_options_pricing', array( $this, 'subscription_product_editor_ui' ) );
|
||||
add_action( 'woocommerce_variable_subscription_pricing', array( $this, 'variable_subscription_product_editor_ui' ), 10, 3 );
|
||||
|
||||
add_action( 'save_post_product', array( $this, 'handle_product_save' ) );
|
||||
add_action( 'save_post_product_variation', array( $this, 'handle_product_variation_save' ) );
|
||||
add_action( 'woocommerce_save_product_variation', array( $this, 'handle_product_variation_save' ) );
|
||||
add_action( 'woocommerce_update_product', array( $this, 'handle_product_save' ) );
|
||||
add_action( 'woocommerce_update_product_variation', array( $this, 'handle_product_variation_save' ) );
|
||||
|
||||
add_action( 'init', array( $this, 'init' ) );
|
||||
}
|
||||
|
||||
public function init() {
|
||||
add_action( 'woocommerce_product_duplicate', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle product save - generic handler for all product updates.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_product_save( $post_id ) {
|
||||
// Bail if this is an autosave or revision.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_is_downloadable = $product->is_downloadable();
|
||||
$product_is_subscription = $product->is_type( array( 'subscription', 'variable-subscription' ) );
|
||||
|
||||
// We do not allow downloadable subscription products to be linked with other subscription products; this is
|
||||
// principally to avoid confusion (though it would be technically feasible).
|
||||
if ( $product_is_subscription && ! $product_is_downloadable ) {
|
||||
$this->handle_subscription_product_save( $post_id );
|
||||
} elseif ( ! $product_is_subscription && $product_is_downloadable ) {
|
||||
$this->handle_downloadable_product_save( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle product variation save - generic handler for all variation updates.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_product_variation_save( $post_id ) {
|
||||
// Bail if this is an autosave or revision.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variation = wc_get_product( $post_id );
|
||||
if ( ! $variation || ! $variation->is_type( 'variation' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle downloadable variations (they link TO subscriptions).
|
||||
if ( $variation->is_downloadable() ) {
|
||||
$this->handle_downloadable_product_save( $post_id );
|
||||
}
|
||||
|
||||
// Handle subscription variations (they link TO downloadable products).
|
||||
$parent = wc_get_product( $variation->get_parent_id() );
|
||||
|
||||
if ( $parent && $parent->is_type( 'variable-subscription' ) ) {
|
||||
$this->handle_subscription_product_save( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save for downloadable products (simple or variation).
|
||||
* These products link TO subscription products.
|
||||
*
|
||||
* @param int $product_id Product or variation ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_downloadable_product_save( $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
if (
|
||||
isset( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ] )
|
||||
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ], self::EDITOR_UPDATE )
|
||||
) {
|
||||
$subscription_ids = wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $product_id ] ?? array() ) );
|
||||
$subscription_ids = array_filter( (array) $subscription_ids );
|
||||
$this->update_subscription_downloads( $product_id, $subscription_ids );
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ] )
|
||||
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ], self::EDITOR_UPDATE )
|
||||
) {
|
||||
$subscription_ids = wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ?? array() ) );
|
||||
$subscription_ids = array_filter( (array) $subscription_ids );
|
||||
$this->update_subscription_downloads( $product_id, $subscription_ids );
|
||||
}
|
||||
|
||||
// Observe and act on product status changes (regardless of whether they were made from within the product
|
||||
// editor, therefore we don't care about nonce checks here).
|
||||
$this->assess_downloadable_product_status( $product_id );
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save for subscription products (simple subscription or variation).
|
||||
* These products link TO downloadable products.
|
||||
*
|
||||
* @param int $product_id Subscription product or variation ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_subscription_product_save( $product_id ) {
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
if (
|
||||
isset( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ] )
|
||||
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ], self::EDITOR_UPDATE )
|
||||
) {
|
||||
$product_ids = wc_clean( wp_unslash( $_POST[ '_subscription_linked_downloadable_products_' . $product_id ] ?? array() ) );
|
||||
$product_ids = array_filter( (array) $product_ids );
|
||||
$this->update_subscription_products( $product_id, $product_ids );
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ] )
|
||||
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ], self::EDITOR_UPDATE )
|
||||
) {
|
||||
$product_ids = wc_clean( wp_unslash( (array) $_POST['_subscription_linked_downloadable_products'] ?? array() ) );
|
||||
$product_ids = array_filter( (array) $product_ids );
|
||||
$this->update_subscription_products( $product_id, $product_ids );
|
||||
}
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess downloadable product status and adjust permissions accordingly.
|
||||
* Called when no form data is available (e.g., status change, REST API update, file changes).
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function assess_downloadable_product_status( $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || ! $product->is_downloadable() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status_object = get_post_status_object( $product->get_status() );
|
||||
$is_public = $status_object && $status_object->public;
|
||||
|
||||
// Always revoke existing permissions first to ensure clean state.
|
||||
// This handles file changes and status transitions.
|
||||
$this->revoke_permissions_for_product( $product_id );
|
||||
|
||||
// Grant fresh permissions only if product is public.
|
||||
if ( $is_public ) {
|
||||
$this->grant_permissions_for_product( $product_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple product write panel options.
|
||||
*/
|
||||
public function simple_write_panel_options() {
|
||||
global $post;
|
||||
?>
|
||||
<p class="form-field _subscription_downloads_field hide_if_subscription">
|
||||
<label for="subscription-downloads-ids"><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?></label>
|
||||
|
||||
<select id="subscription-downloads-ids" multiple="multiple" data-action="wc_subscription_downloads_search" data-placeholder="<?php esc_attr_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_subscription_downloads_ids[]" style="width: 50%;">
|
||||
<?php
|
||||
$subscriptions_ids = WC_Subscription_Downloads::get_subscriptions( $post->ID );
|
||||
|
||||
if ( $subscriptions_ids ) {
|
||||
foreach ( $subscriptions_ids as $subscription_id ) {
|
||||
$subscription = wc_get_product( $subscription_id );
|
||||
|
||||
if ( $subscription ) {
|
||||
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<span class="description"><?php esc_html_e( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ); ?></span>
|
||||
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_DOWNLOAD_TO_SUB, false ); ?>
|
||||
</p>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Variable product write panel options.
|
||||
*/
|
||||
public function variable_write_panel_options( $loop, $variation_data, $variation ) {
|
||||
?>
|
||||
<tr class="show_if_variation_downloadable">
|
||||
<td colspan="2">
|
||||
<p class="form-field _subscription_downloads_field form-row form-row-full hide_if_variable-subscription">
|
||||
<label><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?>:</label>
|
||||
<?php echo wc_help_tip( wc_sanitize_tooltip( __( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ) ) ); ?>
|
||||
|
||||
<select multiple="multiple" data-placeholder="<?php esc_html_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_variable_subscription_downloads_ids[<?php echo esc_attr( $variation->ID ); ?>][]" style="width: 100%">
|
||||
<?php
|
||||
$subscriptions_ids = WC_Subscription_Downloads::get_subscriptions( $variation->ID );
|
||||
if ( $subscriptions_ids ) {
|
||||
foreach ( $subscriptions_ids as $subscription_id ) {
|
||||
$subscription = wc_get_product( $subscription_id );
|
||||
|
||||
if ( $subscription ) {
|
||||
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $variation->ID, false ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a field with which to link the subscription product (the product being edited) with zero-or-many
|
||||
* downloadable products.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function subscription_product_editor_ui(): void {
|
||||
global $post;
|
||||
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
wc_get_logger()->warning(
|
||||
'Unable to add the downloadable products selector to the product editor (global post object is unavailable).',
|
||||
array( 'backtrace' => true )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$description = esc_html__( 'Select simple and variable downloadable products that will be included with this subscription product.', 'woocommerce-subscriptions' );
|
||||
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
|
||||
$linked_products = '';
|
||||
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_SUB_TO_DOWNLOAD, false, false );
|
||||
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
|
||||
|
||||
foreach ( WC_Subscription_Downloads::get_downloadable_products( $post->ID ) as $product_id ) {
|
||||
$product_id = absint( $product_id );
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( $product ) {
|
||||
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
|
||||
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
|
||||
echo "
|
||||
<div class='options_group subscription_linked_downloadable_products_section'>
|
||||
<p class='form-field subscription_linked_downloadable_products'>
|
||||
<label for='subscription-linked-downloadable-products'>$label</label>
|
||||
<select
|
||||
class='wc-product-search subscription-downloads-ids'
|
||||
data-action='wc_subscription_linked_downloadable_products_search'
|
||||
data-placeholder='$placeholder'
|
||||
id='subscription-linked-downloadable-products'
|
||||
multiple='multiple'
|
||||
name='_subscription_linked_downloadable_products[]'
|
||||
style='width: 50%;'
|
||||
>
|
||||
$linked_products
|
||||
</select>
|
||||
<span class='description'>$description</span>
|
||||
$nonce_field
|
||||
</p>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $loop
|
||||
* @param array $variation_data
|
||||
* @param WP_Post $variation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function variable_subscription_product_editor_ui( $loop, $variation_data, $variation ): void {
|
||||
if ( ! $variation instanceof WP_Post ) {
|
||||
wc_get_logger()->warning(
|
||||
'Unable to add the downloadable products selector to the variation section of the product editor (we do not have a valid post object).',
|
||||
array( 'backtrace' => true )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$variation_id = (int) $variation->ID;
|
||||
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
|
||||
$linked_products = '';
|
||||
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $variation_id, false, false );
|
||||
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
|
||||
$tooltip = wc_help_tip( wc_sanitize_tooltip( __( 'Select simple and variable downloadable products that will be included with this subscription variation.', 'woocommerce-subscriptions' ) ) );
|
||||
|
||||
foreach ( WC_Subscription_Downloads::get_downloadable_products( $variation->ID ) as $product_id ) {
|
||||
$product_id = absint( $product_id );
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( $product ) {
|
||||
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
|
||||
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
|
||||
echo "
|
||||
<div class='variable_subscription_linked_downloadable_products show_if_variable-subscription' style='display: none'>
|
||||
<p class='form-row form-field subscription_linked_downloadable_products'>
|
||||
<label for='subscription-linked-downloadable-products'>$label</label>
|
||||
$tooltip
|
||||
<select
|
||||
class='wc-product-search subscription-downloads-ids'
|
||||
data-action='wc_subscription_linked_downloadable_products_search'
|
||||
data-placeholder='$placeholder'
|
||||
id='subscription-linked-downloadable-products'
|
||||
multiple='multiple'
|
||||
name='_subscription_linked_downloadable_products_{$variation_id}[]'
|
||||
style='width: 100%;'
|
||||
>
|
||||
$linked_products
|
||||
</select>
|
||||
$nonce_field
|
||||
</p>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Search orders from subscription product ID.
|
||||
*
|
||||
* @param int $subscription_product_id
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_orders( $subscription_product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$orders = array();
|
||||
$meta_key = '_product_id';
|
||||
|
||||
// Check if subscription product has parent (i.e. is a variable subscription product).
|
||||
$parent_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_parent AS parent_id
|
||||
FROM {$wpdb->prefix}posts
|
||||
WHERE ID = %d;
|
||||
",
|
||||
$subscription_product_id
|
||||
)
|
||||
);
|
||||
|
||||
// If the subscription product is a variation, use variation meta key to find related orders.
|
||||
if ( ! empty( $parent_id ) ) {
|
||||
$meta_key = '_variation_id';
|
||||
}
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT order_items.order_id AS id
|
||||
FROM {$wpdb->prefix}woocommerce_order_items as order_items
|
||||
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS itemmeta ON order_items.order_item_id = itemmeta.order_item_id
|
||||
WHERE itemmeta.meta_key = %s
|
||||
AND itemmeta.meta_value = %d;
|
||||
",
|
||||
$meta_key,
|
||||
$subscription_product_id
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $results as $order ) {
|
||||
$orders[] = $order->id;
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_subscription_downloads_get_orders', $orders, $subscription_product_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke access to download.
|
||||
*
|
||||
* @param bool $download_id
|
||||
* @param bool $product_id
|
||||
* @param bool $order_id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function revoke_access_to_download( $download_id, $product_id, $order_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||
WHERE order_id = %d AND product_id = %d AND download_id = %s;
|
||||
",
|
||||
$order_id,
|
||||
$product_id,
|
||||
$download_id
|
||||
)
|
||||
);
|
||||
|
||||
do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update subscription downloads table and orders according in respect to the described relationship between a
|
||||
* regular product and zero-to-many regular subscription products.
|
||||
*
|
||||
* @param int $product_id The downloadable product ID.
|
||||
* @param array $subscriptions Subscription product IDs.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function update_subscription_downloads( $product_id, $subscriptions ) {
|
||||
$current = array_map( 'intval', WC_Subscription_Downloads::get_subscriptions( $product_id ) );
|
||||
$subscriptions = array_map( 'intval', (array) $subscriptions );
|
||||
|
||||
sort( $current );
|
||||
sort( $subscriptions );
|
||||
|
||||
$to_delete = array_diff( $current, $subscriptions );
|
||||
$to_create = array_diff( $subscriptions, $current );
|
||||
|
||||
$this->delete_relationships( $to_delete, array( $product_id ) );
|
||||
$this->create_relationships( $to_create, array( $product_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update subscription downloads table and orders according in respect to the described relationship between a
|
||||
* subscription product and zero-to-many regular products.
|
||||
*
|
||||
* @param int $subscription_product_id Subscription product ID.
|
||||
* @param int[] $new_ids IDs for downloadable products that should be associated with the subscription product.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function update_subscription_products( int $subscription_product_id, array $new_ids ): void {
|
||||
$existing_ids = array_map( 'intval', WC_Subscription_Downloads::get_downloadable_products( $subscription_product_id ) );
|
||||
$new_ids = array_map( 'intval', $new_ids );
|
||||
|
||||
sort( $existing_ids );
|
||||
sort( $new_ids );
|
||||
|
||||
$to_delete = array_diff( $existing_ids, $new_ids );
|
||||
$to_create = array_diff( $new_ids, $existing_ids );
|
||||
|
||||
$this->delete_relationships( array( $subscription_product_id ), $to_delete );
|
||||
$this->create_relationships( array( $subscription_product_id ), $to_create );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes relationships that exist between any of the supplied subscription IDs and any of the supplied product
|
||||
* IDs.
|
||||
*
|
||||
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
|
||||
* inverse.
|
||||
*
|
||||
* @param int[] $subscription_ids
|
||||
* @param int[] $product_ids
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function delete_relationships( array $subscription_ids, array $product_ids ): void {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $product_ids as $product_id ) {
|
||||
$product_id = (int) $product_id;
|
||||
|
||||
foreach ( $subscription_ids as $subscription_id ) {
|
||||
$subscription_id = (int) $subscription_id;
|
||||
|
||||
$wpdb->delete(
|
||||
$wpdb->prefix . 'woocommerce_subscription_downloads',
|
||||
array(
|
||||
'product_id' => $product_id,
|
||||
'subscription_id' => $subscription_id,
|
||||
),
|
||||
array(
|
||||
'%d',
|
||||
'%d',
|
||||
)
|
||||
);
|
||||
|
||||
$orders = $this->get_orders( $subscription_id );
|
||||
foreach ( $orders as $order_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
$downloads = $product->get_downloads();
|
||||
|
||||
// Adds the downloadable files to the order/subscription.
|
||||
foreach ( array_keys( $downloads ) as $download_id ) {
|
||||
$this->revoke_access_to_download( $download_id, $product_id, $order_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke download permissions for a product across all related subscriptions.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function revoke_permissions_for_product( $product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$subscription_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
|
||||
|
||||
if ( empty( $subscription_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $subscription_ids as $subscription_id ) {
|
||||
$orders = $this->get_orders( $subscription_id );
|
||||
|
||||
foreach ( $orders as $order_id ) {
|
||||
// Delete ALL permissions for this product+order combination.
|
||||
// This ensures that when files change, old permissions with different download_ids are removed.
|
||||
$wpdb->delete(
|
||||
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
|
||||
array(
|
||||
'order_id' => $order_id,
|
||||
'product_id' => $product_id,
|
||||
),
|
||||
array(
|
||||
'%d',
|
||||
'%d',
|
||||
)
|
||||
);
|
||||
|
||||
do_action( 'woocommerce_revoke_access_to_product_download', $product_id, $order_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant download permissions for a product across all related subscriptions.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function grant_permissions_for_product( $product_id ) {
|
||||
$subscription_product_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( empty( $subscription_product_ids ) || ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloads = $product->get_downloads();
|
||||
|
||||
foreach ( $subscription_product_ids as $subscription_id ) {
|
||||
$orders = $this->get_orders( $subscription_id );
|
||||
|
||||
foreach ( $orders as $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! is_a( $order, 'WC_Subscription' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( array_keys( $downloads ) as $download_id ) {
|
||||
wc_downloadable_file_permission( $download_id, $product_id, $order );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relationships between the specified subscription and product IDs.
|
||||
*
|
||||
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
|
||||
* inverse.
|
||||
*
|
||||
* @param int[] $subscription_ids
|
||||
* @param int[] $product_ids
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function create_relationships( array $subscription_ids, array $product_ids ): void {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $product_ids as $product_id ) {
|
||||
$product_id = (int) $product_id;
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
// Check if product has public status.
|
||||
$has_public_status = false;
|
||||
if ( $product ) {
|
||||
$status_object = get_post_status_object( $product->get_status() );
|
||||
$has_public_status = $status_object && $status_object->public;
|
||||
}
|
||||
|
||||
foreach ( $subscription_ids as $subscription_id ) {
|
||||
$subscription_id = (int) $subscription_id;
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'woocommerce_subscription_downloads',
|
||||
array(
|
||||
'product_id' => $product_id,
|
||||
'subscription_id' => $subscription_id,
|
||||
),
|
||||
array(
|
||||
'%d',
|
||||
'%d',
|
||||
)
|
||||
);
|
||||
|
||||
// Only grant download permissions if product has public status.
|
||||
if ( $has_public_status ) {
|
||||
$orders = $this->get_orders( $subscription_id );
|
||||
foreach ( $orders as $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! is_a( $order, 'WC_Subscription' ) ) {
|
||||
// avoid adding permissions to orders and it's
|
||||
// subscription for the same user, causing duplicates
|
||||
// to show up
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = wc_get_product( $product_id );
|
||||
$downloads = $product->get_downloads();
|
||||
|
||||
// Adds the downloadable files to the order/subscription.
|
||||
foreach ( array_keys( $downloads ) as $download_id ) {
|
||||
wc_downloadable_file_permission( $download_id, $product_id, $order );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save simple product data.
|
||||
*
|
||||
* @param int $product_id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save_simple_product_data( $product_id ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$subscription_downloads_ids = ! empty( $_POST['_subscription_downloads_ids'] ) ? wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ) ) : '';
|
||||
|
||||
if ( empty( $subscription_downloads_ids ) ) {
|
||||
$subscription_downloads_ids = array();
|
||||
}
|
||||
|
||||
$this->update_subscription_downloads( $product_id, $subscription_downloads_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save subscriptions information when duplicating a product.
|
||||
*
|
||||
* @param int|WC_Product $id_or_product Duplicated product ID
|
||||
* @param WP_Post|WC_Product $post Product being duplicated
|
||||
*/
|
||||
public function save_subscriptions_when_duplicating_product( $id_or_product, $post ) {
|
||||
$post_id = is_a( $post, 'WC_Product' ) ? $post->get_parent_id() : $post->ID;
|
||||
$new_id = is_a( $id_or_product, 'WC_Product' ) ? $id_or_product->get_id() : $id_or_product;
|
||||
|
||||
$subscriptions = WC_Subscription_Downloads::get_subscriptions( $post_id );
|
||||
if ( ! empty( $subscriptions ) ) {
|
||||
$this->update_subscription_downloads( $new_id, $subscriptions );
|
||||
}
|
||||
|
||||
$children_products = get_children( 'post_parent=' . $post_id . '&post_type=product_variation' );
|
||||
if ( empty( $children_products ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create assoc array where keys are flatten variation attributes and values
|
||||
// are original product variations.
|
||||
$children_ids_by_variation_attributes = array();
|
||||
foreach ( $children_products as $child ) {
|
||||
$str_attributes = $this->get_str_variation_attributes( $child );
|
||||
if ( ! empty( $str_attributes ) ) {
|
||||
$children_ids_by_variation_attributes[ $str_attributes ] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy variations' subscriptions.
|
||||
$exclude = apply_filters( 'woocommerce_duplicate_product_exclude_children', false );
|
||||
$new_children_products = get_children( 'post_parent=' . $new_id . '&post_type=product_variation' );
|
||||
if ( ! $exclude && ! empty( $new_children_products ) ) {
|
||||
foreach ( $new_children_products as $child ) {
|
||||
$str_attributes = $this->get_str_variation_attributes( $child );
|
||||
if ( ! empty( $children_ids_by_variation_attributes[ $str_attributes ] ) ) {
|
||||
$this->save_subscriptions_when_duplicating_product(
|
||||
$child->ID,
|
||||
$children_ids_by_variation_attributes[ $str_attributes ]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string representation of variation attributes from a given product variation.
|
||||
*
|
||||
* @param mixed $product_variation Product variation
|
||||
*
|
||||
* @return string Variation attributes
|
||||
*/
|
||||
protected function get_str_variation_attributes( $product_variation ) {
|
||||
$product_variation = wc_get_product( $product_variation );
|
||||
if ( ! is_callable( array( $product_variation, 'get_formatted_variation_attributes' ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) wc_get_formatted_variation( $product_variation, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated, do not use. Previously took care of saving product data for variations.
|
||||
*
|
||||
* @deprecated 8.3.0
|
||||
*
|
||||
* @param int $variation_id
|
||||
* @param int $index
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function save_variation_product_data( $variation_id, $index ) {
|
||||
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_variation_save' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated, do not use. Previously took care of saving product data.
|
||||
*
|
||||
* @deprecated 8.3.0
|
||||
*
|
||||
* @param int $subscription_product_id
|
||||
* @param int|null $index
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function save_subscription_product_data( int $subscription_product_id, ?int $index = null ) {
|
||||
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_save' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated, do not use. Previously set up assets for the Subscription Downloads extension.
|
||||
*
|
||||
* @deprecated 8.3.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scripts() {
|
||||
wc_deprecated_function( __METHOD__, '8.3.0' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Registers and manages settings related to linked downloadable files functionality.
|
||||
*
|
||||
* @internal This class is used internally by WooCommerce Subscriptions. It is not intended for third party use, and may change at any time.
|
||||
*/
|
||||
class WC_Subscription_Downloads_Settings {
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_subscription_settings', array( $this, 'add_settings' ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if WooCommerce Subscription Downloads plugin is enabled and add a warning about the bundled feature if it is.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public static function add_notice_about_bundled_feature() {
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$is_subscriptions_settings_page = 'woocommerce_page_wc-settings' === $screen->id && isset( $_GET['tab'] ) && 'subscriptions' === sanitize_text_field( wp_unslash( $_GET['tab'] ) );
|
||||
|
||||
// Only show notice on plugins page or subscriptions settings page.
|
||||
if ( 'plugins' !== $screen->id && ! $is_subscriptions_settings_page ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Constants::get_constant( 'WC_SUBSCRIPTION_DOWNLOADS_VERSION' ) ) {
|
||||
$message = __( 'WooCommerce Subscription Downloads is now part of WooCommerce Subscriptions — no extra plugin needed. You can deactivate and uninstall WooCommerce Subscription Downloads via the plugin admin screen.', 'woocommerce-subscriptions' );
|
||||
|
||||
wp_admin_notice(
|
||||
$message,
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'dismissible' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds our settings to the main subscription settings page.
|
||||
*
|
||||
* @param array $settings The full subscription settings array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings( array $settings ): array {
|
||||
$download_settings = array(
|
||||
array(
|
||||
'name' => __( 'Downloads', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_downloads_settings',
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Enable downloadable file sharing', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'Allow downloadable files from simple and variable products to be shared with subscription products so they are available to active subscribers.', 'woocommerce-subscriptions' ),
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking',
|
||||
'default' => 'no',
|
||||
'type' => 'checkbox',
|
||||
'row_class' => 'enable-downloadable-file-linking',
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_downloads_settings',
|
||||
),
|
||||
);
|
||||
|
||||
// Insert the switch settings in after the synchronisation section otherwise add them to the end.
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_gifting', $download_settings, 'multiple-settings', 'sectionend' ) ) {
|
||||
$settings = array_merge( $settings, $download_settings );
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Subscriptions Downloads is enabled.
|
||||
* @since 8.1.0
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
return get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking', 'no' ) === 'yes';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Point of entry for our 'linked downloadable files' functionality.
|
||||
*
|
||||
* Along with other classes in this directory, this used to exist as a standalone plugin (important to note because, for
|
||||
* backwards compatibility reasons, that may place limits on future refactoring).
|
||||
*
|
||||
* The overall goal is to let merchants associate individual downloadable products with subscription products.
|
||||
* Customers who purchase the subscription product then automatically are granted acccess to the relevant downloadable
|
||||
* files.
|
||||
*
|
||||
* This class sets up the functionality, and also provides a high-level interface through methods such as
|
||||
* get_order_downloads( $order ) and get_subscriptions( $product_id ).
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
class WC_Subscription_Downloads {
|
||||
/**
|
||||
* Initialize the various subsystems that drive 'linked downloadable files' functionality.
|
||||
*/
|
||||
public static function setup(): void {
|
||||
|
||||
if ( WC_Subscription_Downloads_Settings::is_enabled() ) {
|
||||
new WC_Subscription_Downloads_Order();
|
||||
|
||||
if ( is_admin() ) {
|
||||
new WC_Subscription_Downloads_Products();
|
||||
new WC_Subscription_Downloads_Ajax();
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to load on admin even when downloads is disabled.
|
||||
// To ensure the settings are added to the subscription settings page.
|
||||
if ( is_admin() ) {
|
||||
new WC_Subscription_Downloads_Settings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install() {
|
||||
new WC_Subscription_Downloads_Install();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the ID of a downloadable product, returns an array of linked subscription product IDs.
|
||||
*
|
||||
* @param int $product_id
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_subscriptions( $product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$query = $wpdb->get_results( $wpdb->prepare( "SELECT subscription_id FROM {$wpdb->prefix}woocommerce_subscription_downloads WHERE product_id = %d", $product_id ), ARRAY_A );
|
||||
|
||||
$subscriptions = array();
|
||||
foreach ( $query as $item ) {
|
||||
$subscriptions[] = $item['subscription_id'];
|
||||
}
|
||||
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get downloadable products from a subscription.
|
||||
*
|
||||
* @param int $subscription_id
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_downloadable_products( $subscription_id, $subscription_variable_id = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
$query = $wpdb->get_results( $wpdb->prepare( "SELECT product_id FROM {$wpdb->prefix}woocommerce_subscription_downloads WHERE subscription_id = %d OR subscription_id = %d", $subscription_id, $subscription_variable_id ), ARRAY_A );
|
||||
|
||||
$products = array();
|
||||
foreach ( $query as $item ) {
|
||||
$products[] = $item['product_id'];
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order download files.
|
||||
*
|
||||
* @param WC_Order $order Order data.
|
||||
*
|
||||
* @return array Download data (name, file and download_url).
|
||||
*/
|
||||
public static function get_order_downloads( $order ) {
|
||||
$downloads = array();
|
||||
|
||||
if ( class_exists( 'WC_Subscriptions_Core_Plugin' ) || version_compare( WC_Subscriptions::$version, '2.0.0', '>=' ) ) {
|
||||
$contains_subscription = wcs_order_contains_subscription( $order );
|
||||
} else {
|
||||
$contains_subscription = WC_Subscriptions_Order::order_contains_subscription( $order );
|
||||
}
|
||||
|
||||
if ( 0 < count( $order->get_items() ) && $contains_subscription && $order->is_download_permitted() ) {
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
|
||||
// Gets the downloadable products.
|
||||
$downloadable_products = self::get_downloadable_products( $item['product_id'], $item['variation_id'] );
|
||||
|
||||
if ( $downloadable_products ) {
|
||||
foreach ( $downloadable_products as $product_id ) {
|
||||
$_item = array(
|
||||
'product_id' => $product_id,
|
||||
'variation_id' => '',
|
||||
);
|
||||
|
||||
// Get the download data.
|
||||
if ( version_compare( WC_VERSION, '3.0', '<' ) ) {
|
||||
$_downloads = $order->get_item_downloads( $_item );
|
||||
} else {
|
||||
$order_item = new WC_Order_Item_Product();
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( empty( $product ) ) {
|
||||
continue;
|
||||
}
|
||||
$order_item->set_product( $product );
|
||||
$order_item->set_order_id( $order->get_id() );
|
||||
$_downloads = $order_item->get_item_downloads();
|
||||
}
|
||||
|
||||
if ( empty( $_downloads ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $_downloads as $download ) {
|
||||
$downloads[] = $download;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
|
|||
|
||||
// Allow customers to cancel early renewal orders from their my account page.
|
||||
add_filter( 'woocommerce_my_account_my_orders_actions', array( $this, 'filter_early_renewal_order_actions' ), 15, 2 );
|
||||
add_action( 'wp_loaded', array( $this, 'allow_early_renewal_order_cancellation' ) );
|
||||
add_action( 'wp_loaded', array( $this, 'allow_early_renewal_order_cancellation' ), 10, 3 );
|
||||
|
||||
// Handles early renew of password-protected products.
|
||||
add_action( 'wcs_before_early_renewal_setup_cart_subscription', 'wcs_allow_protected_products_to_renew' );
|
||||
|
|
@ -69,20 +69,10 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
|
|||
|
||||
if ( wcs_can_user_renew_early( $subscription ) && $subscription->payment_method_supports( 'subscription_date_changes' ) && $subscription->has_status( 'active' ) ) {
|
||||
|
||||
$action = array(
|
||||
$actions['subscription_renewal_early'] = array(
|
||||
'url' => wcs_get_early_renewal_url( $subscription ),
|
||||
'name' => __( 'Renew now', 'woocommerce-subscriptions' ),
|
||||
'role' => 'link',
|
||||
);
|
||||
|
||||
// Set role to 'button' if renewal via modal is enabled (it opens a modal).
|
||||
// Modal ID is set to a predictable value containing the subscription ID for aria-controls.
|
||||
if ( WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled() ) {
|
||||
$action['role'] = 'button';
|
||||
$action['modal_id'] = 'wcs-early-renewal-modal-' . $subscription->get_id();
|
||||
}
|
||||
|
||||
$actions['subscription_renewal_early'] = $action;
|
||||
}
|
||||
|
||||
return $actions;
|
||||
|
|
@ -191,11 +181,11 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
|
|||
/**
|
||||
* Get the subscription object used to construct the early renewal cart.
|
||||
*
|
||||
* @param array $cart_item The resubscribe cart item.
|
||||
* @param array The resubscribe cart item.
|
||||
* @return WC_Subscription The subscription object.
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected function get_order( $cart_item = [] ) {
|
||||
protected function get_order( $cart_item = '' ) {
|
||||
|
||||
$subscription = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class WCS_Early_Renewal_Manager {
|
|||
* Add a setting to enable/disable the early renewal feature.
|
||||
*
|
||||
* @since 2.3.0
|
||||
* @param array $settings Settings array.
|
||||
* @param array Settings array.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
'text' => __( 'Pay now', 'woocommerce-subscriptions' ),
|
||||
'attributes' => array(
|
||||
'id' => 'early_renewal_modal_submit',
|
||||
'class' => 'button alt',
|
||||
'class' => 'button alt ',
|
||||
'href' => add_query_arg( array(
|
||||
'subscription_id' => $subscription->get_id(),
|
||||
'process_early_renewal' => true,
|
||||
|
|
@ -49,19 +49,12 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
),
|
||||
);
|
||||
|
||||
if ( wc_wp_theme_get_element_class_name( 'button' ) ) {
|
||||
$place_order_action['attributes']['class'] .= ' ' . wc_wp_theme_get_element_class_name( 'button' );
|
||||
$place_order_action['attributes']['role'] = 'button';
|
||||
}
|
||||
|
||||
$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' ) );
|
||||
// Set the modal ID to match the predictable value used for aria-controls in subscription-details.php
|
||||
$modal->set_id( 'wcs-early-renewal-modal-' . $subscription->get_id() );
|
||||
$modal->add_action( $place_order_action );
|
||||
$modal->print_html();
|
||||
}
|
||||
|
|
@ -151,7 +144,7 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
if ( $renewal_order->needs_payment() ) {
|
||||
$renewal_order->delete( true );
|
||||
wc_add_notice( __( 'Payment for the renewal order was unsuccessful with your payment method on file, please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
wp_safe_redirect( wcs_get_early_renewal_url( $subscription ) );
|
||||
wp_redirect( wcs_get_early_renewal_url( $subscription ) );
|
||||
exit();
|
||||
}
|
||||
|
||||
|
|
@ -181,10 +174,6 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
$user_id = ! empty( $user_id ) ? absint( $user_id ) : get_current_user_id();
|
||||
$subscription = wcs_get_subscription( $subscription );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled() || ! wcs_can_user_renew_early( $subscription, $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -198,7 +187,7 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
* @since 2.6.0
|
||||
*/
|
||||
private static function redirect() {
|
||||
wp_safe_redirect( remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ) );
|
||||
wp_redirect( remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ) );
|
||||
exit();
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +197,7 @@ class WCS_Early_Renewal_Modal_Handler {
|
|||
* @since 2.6.0
|
||||
*/
|
||||
private static function detach_renewal_callbacks() {
|
||||
remove_filter( 'wcs_renewal_order_created', 'WC_Subscriptions_Renewal_Order::add_order_note' );
|
||||
remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment' );
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,15 +66,15 @@ function wcs_can_user_renew_early( $subscription, $user_id = 0 ) {
|
|||
! boolval( apply_filters( 'wcs_allow_synced_product_early_renewal', false, $subscription ) )
|
||||
) {
|
||||
$reason = 'subscription_contains_synced_product';
|
||||
} else {
|
||||
// Make sure all line items still exist.
|
||||
foreach ( $subscription->get_items() as $line_item ) {
|
||||
$product = wc_get_product( wcs_get_canonical_product_id( $line_item ) );
|
||||
}
|
||||
|
||||
if ( false === $product ) {
|
||||
$reason = 'line_item_no_longer_exists';
|
||||
break;
|
||||
}
|
||||
// Make sure all line items still exist.
|
||||
foreach ( $subscription->get_items() as $line_item ) {
|
||||
$product = wc_get_product( wcs_get_canonical_product_id( $line_item ) );
|
||||
|
||||
if ( false === $product ) {
|
||||
$reason = 'line_item_no_longer_exists';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ class WC_Subscriptions_Payment_Gateways extends WC_Subscriptions_Core_Payment_Ga
|
|||
__( 'Payment processing of the renewal order %1$s was skipped because it is already paid (%2$s).', 'woocommerce_subscriptions' ),
|
||||
'<a href="' . esc_url( $latest_renewal_order->get_edit_order_url() ) . '">' . _x( '#', 'hash before order number', 'woocommerce' ) . $latest_renewal_order->get_order_number() . '</a>',
|
||||
wc_get_order_status_name( $latest_renewal_order->get_status() )
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ class WC_Subscriptions_Payment_Gateways extends WC_Subscriptions_Core_Payment_Ga
|
|||
/**
|
||||
* Fire a gateway specific hook for when a subscription renewal payment is due.
|
||||
*
|
||||
* @param WC_Order|false $renewal_order The renewal order to trigger the payment gateway hook for.
|
||||
* @param WC_Order $renewal_order The renewal order to trigger the payment gateway hook for.
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function trigger_gateway_renewal_payment_hook( $renewal_order ) {
|
||||
|
|
@ -136,7 +136,7 @@ class WC_Subscriptions_Payment_Gateways extends WC_Subscriptions_Core_Payment_Ga
|
|||
* Returns whether the gateway supports subscriptions and automatic renewals.
|
||||
*
|
||||
* @since 4.0.0
|
||||
* @param WC_Payment_Gateway $gateway Gateway to check if it supports subscriptions.
|
||||
* @param WC_Gateway $gateway Gateway to check if it supports subscriptions.
|
||||
* @return bool
|
||||
*/
|
||||
public static function gateway_supports_subscriptions( $gateway ) {
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Edit order page integration.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting/Admin
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for edit order page.
|
||||
*/
|
||||
class WCSG_Admin_Order {
|
||||
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_hidden_order_itemmeta', __CLASS__ . '::hide_gifting_meta' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the gifting meta from the order edit page.
|
||||
* @param array $item_meta_names The item meta names to hide.
|
||||
*/
|
||||
public static function hide_gifting_meta( $item_meta_names ) {
|
||||
$item_meta_names[] = '_cart_item_key_subscription_renewal';
|
||||
|
||||
return $item_meta_names;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,901 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Sets up and manages subscription gifting functionality.
|
||||
*/
|
||||
class WCS_Gifting {
|
||||
/**
|
||||
* Plugin's current version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $version = '2.9.0'; // WRCS: DEFINED_VERSION.
|
||||
|
||||
/**
|
||||
* Minimum WooCommerce version required.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $wc_minimum_supported_version = '3.0';
|
||||
|
||||
/**
|
||||
* Minimum WooCommerce Subscription version required.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $wcs_minimum_supported_version = '2.2';
|
||||
|
||||
/**
|
||||
* Minimum WooCommerce Memberships version required for integration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $wcm_minimum_supported_version = '1.4';
|
||||
|
||||
/**
|
||||
* Setup hooks & filters, when the class is initialised.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'wp_enqueue_scripts', __CLASS__ . '::gifting_scripts' );
|
||||
|
||||
// Needs to run after Subscriptions has loaded its dependant classes.
|
||||
self::load_dependant_classes();
|
||||
|
||||
add_action( 'woocommerce_subscription_before_actions', __CLASS__ . '::add_billing_period_table_row' );
|
||||
|
||||
add_filter( 'woocommerce_get_formatted_subscription_total', __CLASS__ . '::get_formatted_recipient_total', 10, 2 );
|
||||
|
||||
if ( ! class_exists( 'WC_Subscriptions_Data_Copier' ) ) {
|
||||
add_filter( 'wcs_renewal_order_meta_query', __CLASS__ . '::remove_renewal_order_meta_query', 11 );
|
||||
} else {
|
||||
add_filter( 'wc_subscriptions_renewal_order_data', __CLASS__ . '::remove_renewal_order_meta', 11 );
|
||||
}
|
||||
|
||||
// Handle "_is_gifted_subscription" argument in wc_get_orders().
|
||||
add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( __CLASS__, 'handle_is_gifted_subscription_query_var' ), 10, 2 );
|
||||
|
||||
add_action( 'woocommerce_blocks_loaded', __CLASS__ . '::setup_blocks_integration' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't carry the _recipient_user meta data to renewal orders.
|
||||
*
|
||||
* @param array $order_meta Renewal order meta.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function remove_renewal_order_meta( $order_meta ) {
|
||||
unset( $order_meta['_recipient_user'] );
|
||||
return $order_meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't carry recipient meta data to renewal orders.
|
||||
*
|
||||
* @param string $order_meta_query Renewal order meta-query.
|
||||
*/
|
||||
public static function remove_renewal_order_meta_query( $order_meta_query ) {
|
||||
$order_meta_query .= " AND `meta_key` NOT IN ('_recipient_user')";
|
||||
return $order_meta_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads classes after plugins for classes dependant on other plugin files.
|
||||
*/
|
||||
public static function load_dependant_classes() {
|
||||
require_once 'class-wcsg-query.php';
|
||||
|
||||
if ( function_exists( 'wc_memberships' ) ) {
|
||||
if ( version_compare( get_option( 'wc_memberships_version' ), self::$wcm_minimum_supported_version, '>=' ) ) {
|
||||
require_once 'class-wcsg-memberships-integration.php';
|
||||
} else {
|
||||
add_action( 'admin_notices', 'WCS_Gifting::plugin_dependency_notices' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/queue frontend scripts.
|
||||
*/
|
||||
public static function gifting_scripts() {
|
||||
global $post;
|
||||
|
||||
if ( ! WCSG_Admin::is_gifting_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We load this on all pages because of the mini-cart related styles.
|
||||
wp_enqueue_style(
|
||||
'woocommerce_subscriptions_gifting',
|
||||
plugins_url( '/assets/css/gifting/shortcode-checkout.css', WC_Subscriptions::$plugin_file ),
|
||||
array( 'wp-components' ),
|
||||
WC_VERSION,
|
||||
'all'
|
||||
);
|
||||
|
||||
if ( ! is_cart() && ! is_checkout() && ! is_product() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need to load the scripts on blocks pages.
|
||||
if ( WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/cart' ) || WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/checkout' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need to load the script on product pages that are not subscriptions or not giftable subscriptions.
|
||||
if ( is_product() && ( ! WC_Subscriptions_Product::is_subscription( $post->ID ) || ! WCSG_Product::is_giftable( $post->ID ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_register_script( 'woocommerce_subscriptions_gifting', plugins_url( '/assets/js/gifting/wcs-gifting.js', WC_Subscriptions::$plugin_file ), array( 'jquery' ), WC_Subscriptions::$version, true );
|
||||
wp_enqueue_script( 'woocommerce_subscriptions_gifting' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an email address belongs to the current user.
|
||||
*
|
||||
* @param string $email Email address.
|
||||
* @return bool Returns whether the email address belongs to the current user.
|
||||
*/
|
||||
public static function email_belongs_to_current_user( $email ) {
|
||||
$emails_to_try = array();
|
||||
|
||||
if ( is_user_logged_in() ) {
|
||||
/** @var WP_User $current_user */
|
||||
$current_user = wp_get_current_user();
|
||||
$emails_to_try[] = $current_user->user_email;
|
||||
}
|
||||
|
||||
if ( is_checkout() && ! empty( $_POST['billing_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.CSRF.NonceVerification.NoNonceVerification
|
||||
$emails_to_try[] = sanitize_email( wp_unslash( $_POST['billing_email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.CSRF.NonceVerification.NoNonceVerification
|
||||
}
|
||||
|
||||
return in_array( $email, $emails_to_try, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an array of recipient emails scheduling error notices if an error is found.
|
||||
*
|
||||
* @param array $recipients An array of recipient email addresses.
|
||||
* @return bool Returns whether any errors have occurred.
|
||||
*/
|
||||
public static function validate_recipient_emails( $recipients ) {
|
||||
$invalid_email_found = false;
|
||||
$self_gifting_found = false;
|
||||
|
||||
if ( is_array( $recipients ) ) {
|
||||
foreach ( $recipients as $key => $recipient ) {
|
||||
$cleaned_recipient = sanitize_email( $recipient );
|
||||
if ( $recipient === $cleaned_recipient && is_email( $cleaned_recipient ) ) {
|
||||
if ( ! $self_gifting_found && self::email_belongs_to_current_user( $cleaned_recipient ) ) {
|
||||
wc_add_notice( __( 'Please enter someone else\'s email address.', 'woocommerce-subscriptions' ), 'error' );
|
||||
$self_gifting_found = true;
|
||||
}
|
||||
} elseif ( ! empty( $recipient ) && ! $invalid_email_found ) {
|
||||
wc_add_notice( __( ' Invalid email address.', 'woocommerce-subscriptions' ), 'error' );
|
||||
$invalid_email_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ! ( $invalid_email_found || $self_gifting_found );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches recipient information to a subscription cart item.
|
||||
*
|
||||
* @param object $item The item in the cart to be updated.
|
||||
* @param string $key Cart item key.
|
||||
* @param array $new_recipient_data The new recipient information for the item.
|
||||
*/
|
||||
public static function update_cart_item_recipient( $item, $key, $new_recipient_data ) {
|
||||
if ( empty( $item['wcsg_gift_recipients_email'] ) || $item['wcsg_gift_recipients_email'] !== $new_recipient_data ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
WC()->cart->cart_contents[ $key ]['wcsg_gift_recipients_email'] = $new_recipient_data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the cart item data that will be used by WooCommerce to generate a unique ID for the cart item. That is to
|
||||
* avoid merging different products when they aren't the same. Previously the resubscribe status was ignored.
|
||||
*
|
||||
* @param array $item A cart item with all its data.
|
||||
* @param string $key A cart item key.
|
||||
* @param array $new_recipient_data Email address of the new recipient.
|
||||
* @return array New cart item data.
|
||||
*/
|
||||
private static function add_cart_item_data( $item, $key, $new_recipient_data ) {
|
||||
// start with a clean slate.
|
||||
$cart_item_data = array();
|
||||
|
||||
// Add the recipient email.
|
||||
if ( ! empty( $new_recipient_data ) ) {
|
||||
$cart_item_data = array( 'wcsg_gift_recipients_email' => $new_recipient_data );
|
||||
}
|
||||
|
||||
// Add resubscribe data.
|
||||
if ( array_key_exists( 'subscription_resubscribe', $item ) ) {
|
||||
$cart_item_data = array_merge( $cart_item_data, array( 'subscription_resubscribe' => $item['subscription_resubscribe'] ) );
|
||||
}
|
||||
|
||||
$cart_item_data = apply_filters( 'wcsg_cart_item_data', $cart_item_data, $item, $key, $new_recipient_data );
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks on each admin page load if Gifting plugin is activated.
|
||||
*
|
||||
* Apparently the official WP API is "lame" and it's far better to use an upgrade routine fired on admin_init: https://core.trac.wordpress.org/ticket/14170#comment:68
|
||||
*
|
||||
* @deprecated This is a hangover from the time when Subscriptions Gifting was a separate plugin.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.1.
|
||||
*/
|
||||
public static function maybe_activate() {
|
||||
wcs_deprecated_function( __METHOD__, '7.8.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is deactivated. Deletes the is active flag and fires an action.
|
||||
*
|
||||
* @deprecated This is a hangover from the time when Subscriptions Gifting was a separate plugin.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
wcs_deprecated_function( __METHOD__, '7.8.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the add recipient fields (including the checkbox and e-mail input).
|
||||
*
|
||||
* @param string $email E-mail address.
|
||||
* @param string $id ID, for uniqueness on page.
|
||||
* @param string $print_or_return Wether to print or return the HTML content. Optional. Default behaviour is to print the string. Pass 'return' to return the HTML content instead.
|
||||
* @return string Returns the HTML string if $print_or_return is set to 'return', otherwise prints the HTML and nothing is returned.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.
|
||||
*/
|
||||
public static function render_add_recipient_fields( $email = '', $id = '', $print_or_return = 'print' ) {
|
||||
$output = wc_get_template_html(
|
||||
'html-add-recipient.php',
|
||||
self::get_add_recipient_template_args( $email, $id ),
|
||||
'',
|
||||
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
|
||||
);
|
||||
|
||||
if ( 'return' === $print_or_return ) {
|
||||
return $output;
|
||||
} else {
|
||||
echo wp_kses(
|
||||
$output,
|
||||
array(
|
||||
'fieldset' => array(),
|
||||
'input' => array(
|
||||
'type' => array(),
|
||||
'id' => array(),
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
'value' => array(),
|
||||
'checked' => array(),
|
||||
'disabled' => array(),
|
||||
'data-recipient' => array(),
|
||||
'name' => array(),
|
||||
'placeholder' => array(),
|
||||
'aria-label' => array(),
|
||||
),
|
||||
'label' => array(
|
||||
'for' => array(),
|
||||
),
|
||||
'div' => array(
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
),
|
||||
'p' => array(
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
'id' => array(),
|
||||
),
|
||||
'svg' => array(
|
||||
'xmlns' => array(),
|
||||
'viewbox' => array(),
|
||||
'width' => array(),
|
||||
'height' => array(),
|
||||
'aria-hidden' => array(),
|
||||
'focusable' => array(),
|
||||
),
|
||||
'path' => array(
|
||||
'd' => array(),
|
||||
),
|
||||
'span' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the set of arguments to be passed to the "Add Recipient" template.
|
||||
*
|
||||
* @param string $email E-mail address.
|
||||
* @param string $id ID, for CSS uniqueness on page.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.
|
||||
*/
|
||||
public static function get_add_recipient_template_args( $email = '', $id = '' ) {
|
||||
$id = $id ? esc_attr( $id ) : '0';
|
||||
|
||||
// E-mail field.
|
||||
$email_field_args = array(
|
||||
'placeholder' => __( 'Recipient\'s Email Address', 'woocommerce-subscriptions' ),
|
||||
'class' => array( 'woocommerce_subscriptions_gifting_recipient_email' ),
|
||||
'style_attributes' => array(),
|
||||
);
|
||||
|
||||
if ( ! empty( $email ) && ( self::email_belongs_to_current_user( $email ) || ! is_email( $email ) ) ) {
|
||||
array_push( $email_field_args['class'], 'woocommerce-invalid' );
|
||||
}
|
||||
|
||||
// "This is a gift" checkbox.
|
||||
$checkbox_field_args = array(
|
||||
'class' => apply_filters( 'wcsg_recipient_checkbox_class', array() ),
|
||||
'style_attributes' => apply_filters( 'wcsg_recipient_checkbox_style_attributes', array() ),
|
||||
'disabled' => apply_filters( 'wcsg_recipient_checkbox_disabled', false ),
|
||||
'checked' => empty( $email ) ? apply_filters( 'wcsg_recipient_checkbox_checked', false ) : true,
|
||||
);
|
||||
|
||||
$nonce_field = '<input type="hidden" id="_wcsgnonce_' . $id . '" name="_wcsgnonce" value="' . wp_create_nonce( 'wcsg_add_recipient' ) . '" />';
|
||||
$nonce_field .= wp_referer_field( false );
|
||||
|
||||
$args = array(
|
||||
'email' => $email,
|
||||
'id' => $id,
|
||||
'container_style_attributes' => apply_filters( 'wcsg_recipient_fields_style_attributes', array(), $email ),
|
||||
'container_css_class' => apply_filters( 'wcsg_recipient_fields_css_class', empty( $email ) ? array( 'hidden' ) : array(), $email ),
|
||||
'email_field_args' => apply_filters( 'wcsg_recipient_email_field_args', $email_field_args, $email ),
|
||||
'checkbox_field_args' => apply_filters( 'wcsg_recipient_checkbox_field_args', $checkbox_field_args, $email ),
|
||||
'nonce_field' => $nonce_field,
|
||||
);
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds row to subscription details table that displays subscription period for recipients.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
|
||||
*/
|
||||
public static function add_billing_period_table_row( $subscription ) {
|
||||
if ( ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) && self::is_gifted_subscription( $subscription ) && get_current_user_id() === self::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$subscription_details = array(
|
||||
'recurring_amount' => '',
|
||||
'subscription_period' => $subscription->get_billing_period(),
|
||||
'subscription_interval' => $subscription->get_billing_interval(),
|
||||
'initial_amount' => '',
|
||||
'use_per_slash' => false,
|
||||
);
|
||||
$billing_period_string = apply_filters( 'woocommerce_subscription_price_string_details', $subscription_details, $subscription );
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html_x( 'Renewing', 'table heading', 'woocommerce-subscriptions' ); ?></td>
|
||||
<td><?php echo esc_html( wcs_price_string( $billing_period_string ) ); ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reformats the price of the subscription to hide it if the user is the recipient.
|
||||
*
|
||||
* @param string $formatted_order_total The order total formatted.
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
|
||||
*/
|
||||
public static function get_formatted_recipient_total( $formatted_order_total, $subscription ) {
|
||||
global $wp;
|
||||
|
||||
if ( ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) && is_account_page() && isset( $wp->query_vars['subscriptions'] ) && self::is_gifted_subscription( $subscription ) && get_current_user_id() === self::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$formatted_order_total = '-';
|
||||
}
|
||||
return $formatted_order_total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a combination of the customer's first name, last name and email depending on what the customer has set.
|
||||
*
|
||||
* @param int $user_id The ID of the customer user.
|
||||
* @param bool $strip_tags Whether to strip HTML tags in user name (defaulted to false).
|
||||
*/
|
||||
public static function get_user_display_name( $user_id, $strip_tags = false ) {
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
$name = '';
|
||||
|
||||
if ( ! empty( $user->first_name ) ) {
|
||||
$name = $user->first_name . ( ( ! empty( $user->last_name ) ) ? ' ' . $user->last_name : '' ) . ' (' . make_clickable( $user->user_email ) . ')';
|
||||
} else {
|
||||
$name = make_clickable( $user->user_email );
|
||||
}
|
||||
|
||||
if ( $strip_tags ) {
|
||||
$name = wp_strip_all_tags( $name );
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays plugin dependency notices if required plugins are inactive or the installed version is less than a
|
||||
* supported version.
|
||||
*/
|
||||
public static function plugin_dependency_notices() {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
self::output_plugin_dependency_notice( 'WooCommerce' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WC_Subscription' ) ) {
|
||||
self::output_plugin_dependency_notice( 'WooCommerce Subscriptions / WooCommerce Payments' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( version_compare( get_option( 'woocommerce_subscriptions_active_version' ), self::$wcs_minimum_supported_version, '<' ) ) {
|
||||
self::output_plugin_dependency_notice( 'WooCommerce Subscriptions', self::$wcs_minimum_supported_version );
|
||||
}
|
||||
|
||||
if ( version_compare( get_option( 'woocommerce_db_version' ), self::$wc_minimum_supported_version, '<' ) ) {
|
||||
self::output_plugin_dependency_notice( 'WooCommerce', self::$wc_minimum_supported_version );
|
||||
}
|
||||
|
||||
if ( class_exists( 'WC_Memberships' ) && version_compare( get_option( 'wc_memberships_version' ), self::$wcm_minimum_supported_version, '<' ) ) {
|
||||
self::output_plugin_dependency_notice( 'WooCommerce Memberships', self::$wcm_minimum_supported_version );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a plugin dependency admin notice. If a required version is supplied an invalid version notice is printed,
|
||||
* otherwise an inactive plugin notice is printed.
|
||||
*
|
||||
* @param string $plugin_name The plugin name.
|
||||
* @param string|bool $required_version The minimum supported version of the plugin.
|
||||
*/
|
||||
public static function output_plugin_dependency_notice( $plugin_name, $required_version = false ) {
|
||||
|
||||
if ( current_user_can( 'activate_plugins' ) ) {
|
||||
if ( $required_version ) {
|
||||
?>
|
||||
<div id="message" class="error">
|
||||
<p>
|
||||
<?php
|
||||
if ( 'WooCommerce Memberships' === $plugin_name ) {
|
||||
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$ required plugin version, 5$-6$: opening and closing link tags, leads to plugins.php in admin, 7$: line break, 8$-9$ Opening and closing small tags.
|
||||
printf( esc_html__( '%1$sWooCommerce Subscriptions Gifting Membership integration is inactive.%2$s In order to integrate with WooCommerce Memberships, WooCommerce Subscriptions Gifting requires %3$s %4$s or newer. %5$sPlease update »%6$s %7$s%8$sNote: All other WooCommerce Subscriptions Gifting features will remain available, however purchasing membership plans for recipients will fail to grant the membership to the gift recipient.%9$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), esc_html( $required_version ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>', '<br>', '<small>', '</small>' );
|
||||
} else {
|
||||
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$ required plugin version, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
|
||||
printf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s This version of WooCommerce Subscriptions Gifting requires %3$s %4$s or newer. %5$sPlease update »%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), esc_html( $required_version ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
} else {
|
||||
$message = null;
|
||||
|
||||
if ( 'WooCommerce Subscriptions / WooCommerce Payments' === $plugin_name ) {
|
||||
$wcs_plugin_url = 'http://www.woocommerce.com/products/woocommerce-subscriptions/';
|
||||
$wcpay_plugin_url = 'http://www.woocommerce.com/products/woocommerce-payments/';
|
||||
|
||||
// translators: 1$-2$: opening and closing <strong> tags, 3$:opening link tag, leads to WooCommerce Payments plugin product page, 4$:opening link tag, leads to WooCommerce Subscriptions plugin product page, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
|
||||
$message = sprintf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s WooCommerce Subscriptions Gifting requires either the %3$sWooCommerce Payments%6$s or %4$sWooCommerce Subscriptions%6$s plugin to be active to work correctly. Please %5$sinstall & activate either one »%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', '<a href="' . esc_url( $wcpay_plugin_url ) . '">', '<a href="' . esc_url( $wcs_plugin_url ) . '">', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
|
||||
} elseif ( 'WooCommerce' === $plugin_name ) {
|
||||
$plugin_url = 'http://wordpress.org/extend/plugins/woocommerce/';
|
||||
|
||||
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$:opening link tag, leads to plugin product page, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
|
||||
$message = sprintf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s WooCommerce Subscriptions Gifting requires the %4$s%3$s%6$s plugin to be active to work correctly. Please %5$sinstall & activate %3$s »%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), '<a href="' . esc_url( $plugin_url ) . '">', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
|
||||
}
|
||||
|
||||
if ( $message ) {
|
||||
?>
|
||||
<div id="message" class="error">
|
||||
<p>
|
||||
<?php
|
||||
echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a subscription is a gifted subscription.
|
||||
*
|
||||
* @param int|WC_Subscription $subscription either a subscription object or subscription's ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_gifted_subscription( $subscription ) {
|
||||
$is_gifted_subscription = false;
|
||||
|
||||
if ( is_int( $subscription ) ) {
|
||||
$subscription = wcs_get_subscription( $subscription );
|
||||
}
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( wcs_is_subscription( $subscription ) ) {
|
||||
$recipient_user_id = self::get_recipient_user( $subscription );
|
||||
$has_recipient_email = $subscription->get_meta( '_recipient_user_email_address' );
|
||||
$is_gifted_subscription = ( ! empty( $recipient_user_id ) && is_numeric( $recipient_user_id ) ) || $has_recipient_email;
|
||||
}
|
||||
|
||||
return $is_gifted_subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all order item ids and their containing order ids that have been purchased for a recipient.
|
||||
*
|
||||
* @param int $recipient_user_id User ID.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recipient_order_items( $recipient_user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT o.order_id, i.order_item_id
|
||||
FROM {$wpdb->prefix}woocommerce_order_itemmeta AS i
|
||||
INNER JOIN {$wpdb->prefix}woocommerce_order_items as o
|
||||
ON i.order_item_id=o.order_item_id
|
||||
WHERE meta_key = 'wcsg_recipient'
|
||||
AND meta_value = %s",
|
||||
'wcsg_recipient_id_' . $recipient_user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's shipping address.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_users_shipping_address( $user_id ) {
|
||||
return array(
|
||||
'first_name' => get_user_meta( $user_id, 'shipping_first_name', true ),
|
||||
'last_name' => get_user_meta( $user_id, 'shipping_last_name', true ),
|
||||
'company' => get_user_meta( $user_id, 'shipping_company', true ),
|
||||
'address_1' => get_user_meta( $user_id, 'shipping_address_1', true ),
|
||||
'address_2' => get_user_meta( $user_id, 'shipping_address_2', true ),
|
||||
'city' => get_user_meta( $user_id, 'shipping_city', true ),
|
||||
'state' => get_user_meta( $user_id, 'shipping_state', true ),
|
||||
'postcode' => get_user_meta( $user_id, 'shipping_postcode', true ),
|
||||
'country' => get_user_meta( $user_id, 'shipping_country', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an order contains a gifted subscription.
|
||||
*
|
||||
* @param mixed $order the order id or order object to check.
|
||||
* @return bool
|
||||
*/
|
||||
public static function order_contains_gifted_subscription( $order ) {
|
||||
|
||||
if ( ! is_object( $order ) ) {
|
||||
$order = wc_get_order( $order );
|
||||
}
|
||||
|
||||
if ( ! is_a( $order, 'WC_Order' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contains_gifted_subscription = false;
|
||||
|
||||
foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription_id => $subscription ) {
|
||||
|
||||
if ( self::is_gifted_subscription( $subscription ) ) {
|
||||
$contains_gifted_subscription = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $contains_gifted_subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the user id of the recipient stored in order item meta.
|
||||
*
|
||||
* @param mixed $order_item the order item to check.
|
||||
* @return mixed bool|int The recipient user id or false if the order item is not gifted.
|
||||
*/
|
||||
public static function get_order_item_recipient_user_id( $order_item ) {
|
||||
|
||||
if ( is_a( $order_item, 'WC_Order_Item' ) && $order_item->meta_exists( 'wcsg_recipient' ) ) {
|
||||
$raw_recipient_meta = $order_item->get_meta( 'wcsg_recipient' );
|
||||
} elseif ( isset( $order_item['item_meta']['wcsg_recipient'] ) ) {
|
||||
$raw_recipient_meta = $order_item['item_meta']['wcsg_recipient'][0];
|
||||
}
|
||||
|
||||
return isset( $raw_recipient_meta ) ? substr( $raw_recipient_meta, strlen( 'wcsg_recipient_id_' ) ) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a recipient user account.
|
||||
*
|
||||
* @param string $recipient_email Recipient's e-mail address.
|
||||
* @return int ID for newly created user.
|
||||
*/
|
||||
public static function create_recipient_user( $recipient_email ) {
|
||||
$username = explode( '@', $recipient_email );
|
||||
$username = sanitize_user( $username[0], true );
|
||||
$counter = 1;
|
||||
|
||||
$original_username = $username;
|
||||
|
||||
while ( username_exists( $username ) ) {
|
||||
$username = $original_username . $counter;
|
||||
++$counter;
|
||||
}
|
||||
|
||||
$password = wp_generate_password();
|
||||
|
||||
$recipient_user_id = wc_create_new_customer( $recipient_email, $username, $password );
|
||||
|
||||
// set a flag to force the user to update/set account information on login.
|
||||
update_user_meta( $recipient_user_id, 'wcsg_update_account', 'true' );
|
||||
return $recipient_user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the recipient user ID from a subscription.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*
|
||||
* @return string The recipient's user ID. Returns an empty string if there is no recipient set.
|
||||
*/
|
||||
public static function get_recipient_user( $subscription ) {
|
||||
// There may be cases, especially when emails are being previewed in the customizer, where we receive something
|
||||
// other than a WC_Subscription object.
|
||||
if ( $subscription instanceof WC_Subscription && $subscription->meta_exists( '_recipient_user' ) ) {
|
||||
return $subscription->get_meta( '_recipient_user' );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the recipient user ID on a subscription.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @param int $user_id The user ID of the user to set as the recipient on the subscription.
|
||||
* @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved.
|
||||
* @param int $meta_id The meta ID of existing meta data if you wish to overwrite an existing recipient meta value.
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
public static function set_recipient_user( &$subscription, $user_id, $save = 'save', $meta_id = 0, ?WC_Order $order = null ) {
|
||||
$current_user_id = absint( self::get_recipient_user( $subscription ) );
|
||||
$subscription->recipient_user = $user_id;
|
||||
|
||||
if ( 'save' === $save ) {
|
||||
$subscription->update_meta_data( '_recipient_user', $user_id, $meta_id );
|
||||
$subscription->save();
|
||||
|
||||
$gifting_subscription_items = $subscription->get_items();
|
||||
$gifting_subcription_item = reset( $gifting_subscription_items );
|
||||
|
||||
if ( ! empty( $gifting_subcription_item ) ) {
|
||||
if ( ! $order ) {
|
||||
$order = wc_get_order( $subscription->get_parent_id() );
|
||||
}
|
||||
foreach ( $order->get_items() as $order_item ) {
|
||||
if ( $order_item->get_meta( '_wcsg_cart_key' ) === $gifting_subcription_item->get_meta( '_wcsg_cart_key' ) ) {
|
||||
$order_item->add_meta_data( 'wcsg_recipient', 'wcsg_recipient_id_' . $user_id, true );
|
||||
$order_item->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_subscriptions_gifting_recipient_changed', $subscription, $user_id, $current_user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the recipient user ID on a subscription
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved.
|
||||
* @param int $meta_id The meta ID of existing recipient meta data if you wish to only delete a field specified by ID.
|
||||
*/
|
||||
public static function delete_recipient_user( &$subscription, $save = 'save', $meta_id = 0 ) {
|
||||
unset( $subscription->recipient_user );
|
||||
|
||||
// Save the data.
|
||||
if ( 'save' === $save ) {
|
||||
if ( ! empty( $meta_id ) ) {
|
||||
$subscription->delete_meta_data_by_mid( $meta_id );
|
||||
} else {
|
||||
$subscription->delete_meta_data( '_recipient_user' );
|
||||
}
|
||||
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a set of gifted subscriptions based on certain parameters.
|
||||
*
|
||||
* @see wc_get_orders()
|
||||
*
|
||||
* @param array $args Custom args for query, excluding 'type' and custom var 'is_gifted_subscription'.
|
||||
*
|
||||
* @return WC_Order[]
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
|
||||
*/
|
||||
public static function get_gifted_subscriptions( $args = array() ) {
|
||||
$query_args = wp_parse_args(
|
||||
$args,
|
||||
array(
|
||||
'limit' => -1,
|
||||
'status' => 'any',
|
||||
'orderby' => 'date',
|
||||
'order' => 'desc',
|
||||
)
|
||||
);
|
||||
|
||||
$query_args['type'] = 'shop_subscription';
|
||||
|
||||
if ( function_exists( 'wcs_get_orders_with_meta_query' ) ) {
|
||||
$query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_recipient_user',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
);
|
||||
|
||||
return wcs_get_orders_with_meta_query( $query_args );
|
||||
}
|
||||
|
||||
$query_args['is_gifted_subscription'] = true;
|
||||
|
||||
return wc_get_orders( $query_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle custom WCS Gifting query vars to get subscriptions with 'WCS Gifting' meta.
|
||||
*
|
||||
* @param array $query Args for WP_Query.
|
||||
* @param array $query_vars Query vars from WC_Order_Query.
|
||||
*
|
||||
* @return array modified $query
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
|
||||
*/
|
||||
public static function handle_is_gifted_subscription_query_var( $query, $query_vars ) {
|
||||
if ( ! empty( $query_vars['is_gifted_subscription'] ) && true === $query_vars['is_gifted_subscription'] ) {
|
||||
$query['meta_query'][] = array(
|
||||
array(
|
||||
'key' => '_recipient_user',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the site requires shipping address data for non-virtual products. Default: true
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function require_shipping_address_for_virtual_products() {
|
||||
return apply_filters( 'wcsg_require_shipping_address_for_virtual_products', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of Gifted Subscriptions.
|
||||
*
|
||||
* @return int
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
public static function get_gifted_subscriptions_count() {
|
||||
if ( ! class_exists( \Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Subscriptions::class ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$telemetry = new \Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Subscriptions();
|
||||
return $telemetry->get_gifted_subscriptions_count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/queue admin scripts.
|
||||
*/
|
||||
public static function admin_scripts() {
|
||||
_deprecated_function( __METHOD__, '2.0.0', 'WCSG_Admin::enqueue_scripts()' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Install wcsg
|
||||
*/
|
||||
public static function wcsg_install() {
|
||||
_deprecated_function( __METHOD__, '2.0.0', 'WCS_Gifting::maybe_activate()' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules if they haven't been flushed since plugin activation
|
||||
*/
|
||||
public static function maybe_flush_rewrite_rules() {
|
||||
_deprecated_function( __METHOD__, '2.0.0', 'flush_rewrite_rules()' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default recent order template for gifted subscriptions
|
||||
*
|
||||
* @param string $located Path to template.
|
||||
* @param string $template_name Template name.
|
||||
* @param array $args Arguments.
|
||||
*/
|
||||
public static function get_recent_orders_template( $located, $template_name, $args ) {
|
||||
_deprecated_function( __FUNCTION__, '2.0.0', 'WCSG_Template_Loader::get_recent_orders_template()' );
|
||||
WCSG_Template_Loader::get_recent_orders_template( $located, $template_name, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of arguments used to create the recipient email html fields.
|
||||
*
|
||||
* @param string $email E-mail address.
|
||||
* @return array email_field_args A set of html attributes
|
||||
* @deprecated 2.1
|
||||
*/
|
||||
public static function get_recipient_email_field_args( $email ) {
|
||||
_deprecated_function( __METHOD__, '2.1', 'WCS_Gifting::get_add_recipient_template_args()' );
|
||||
$args = self::get_add_recipient_template_args( $email );
|
||||
return $args['email_field_args'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of arguments used to create the recipient checkbox html fields
|
||||
*
|
||||
* @param string $email The email of the gift recipient.
|
||||
* @return array checkbox_field_args A set of html attributes
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
* @deprecated 2.1
|
||||
*/
|
||||
public static function get_recipient_checkbox_field_args( $email ) {
|
||||
_deprecated_function( __METHOD__, '2.1', 'WCS_Gifting::get_add_recipient_template_args()' );
|
||||
$args = self::get_add_recipient_template_args( $email );
|
||||
return $args['checkbox_field_args'];
|
||||
}
|
||||
|
||||
public static function setup_blocks_integration() {
|
||||
if ( ! class_exists( 'Automattic\WooCommerce\Blocks\Package' ) || ! version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '4.4.0', '>' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! WCSG_Admin::is_gifting_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the compatible blocks for WooCommerce Subscriptions.
|
||||
* @since 7.8.2
|
||||
*/
|
||||
$compatible_blocks = apply_filters(
|
||||
'wcsg_compatible_blocks',
|
||||
[ 'cart', 'checkout', 'mini-cart' ]
|
||||
);
|
||||
|
||||
foreach ( $compatible_blocks as $block_name ) {
|
||||
add_action(
|
||||
"woocommerce_blocks_{$block_name}_block_registration",
|
||||
function ( $integration_registry ) {
|
||||
$integration_registry->register( new WCSG_Blocks_Integration() );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Handles System report functionality
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* System Status Class
|
||||
*/
|
||||
class WCSG_Admin_System_Status {
|
||||
|
||||
/**
|
||||
* Array of Gifting information for display on the System Status page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $gifting_data = array();
|
||||
|
||||
/**
|
||||
* Hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_system_status_report', array( __CLASS__, 'render_system_status_items' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Gifting system status report.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
public static function render_system_status_items() {
|
||||
self::set_gifting_information();
|
||||
self::set_theme_overrides();
|
||||
|
||||
$system_status_sections = array(
|
||||
array(
|
||||
'title' => __( 'Subscriptions Gifting', 'woocommerce-subscriptions' ),
|
||||
'tooltip' => __( 'This section shows any information about Subscriptions Gifting.', 'woocommerce-subscriptions' ),
|
||||
'data' => apply_filters( 'wcsg_system_status', self::$gifting_data ),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ( $system_status_sections as $section ) {
|
||||
$section_title = $section['title'];
|
||||
$section_tooltip = $section['tooltip'];
|
||||
$debug_data = $section['data'];
|
||||
|
||||
include plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/admin/status.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme overrides area for Subscriptions Gifting.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
private static function set_theme_overrides() {
|
||||
$theme_overrides = self::get_theme_overrides();
|
||||
|
||||
if ( ! empty( $theme_overrides['overrides'] ) ) {
|
||||
self::$gifting_data['wcsg_theme_overrides'] = array(
|
||||
'name' => _x( 'Subscriptions Gifting Template Theme Overrides', 'name for the system status page', 'woocommerce-subscriptions' ),
|
||||
'label' => _x( 'Subscriptions Gifting Template Theme Overrides', 'label for the system status page', 'woocommerce-subscriptions' ),
|
||||
'data' => $theme_overrides['overrides'],
|
||||
);
|
||||
|
||||
// Include a note on how to update if the templates are out of date.
|
||||
if ( ! empty( $theme_overrides['has_outdated_templates'] ) && true === $theme_overrides['has_outdated_templates'] ) {
|
||||
self::$gifting_data['wcsg_theme_overrides'] += array(
|
||||
'mark_icon' => 'warning',
|
||||
/* Translators: 1) an <a> tag pointing to a doc on how to fix outdated templates, 2) closing </a> tag. */
|
||||
'note' => sprintf( __( '%1$sLearn how to update%2$s', 'woocommerce-subscriptions' ), '<a href="https://docs.woocommerce.com/document/fix-outdated-templates-woocommerce/" target="_blank">', '</a>' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which of our files have been overridden by the theme and if the theme files are outdated.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
* @return array
|
||||
*/
|
||||
private static function get_theme_overrides() {
|
||||
$wcsg_template_dir = dirname( WC_Subscriptions::$plugin_file ) . '/templates/gifting/';
|
||||
$wc_template_path = trailingslashit( wc()->template_path() );
|
||||
$theme_root = trailingslashit( get_theme_root() );
|
||||
$overridden = array();
|
||||
$outdated = false;
|
||||
$templates = WC_Admin_Status::scan_template_files( $wcsg_template_dir );
|
||||
|
||||
foreach ( $templates as $template ) {
|
||||
$theme_file = false;
|
||||
$locations = array(
|
||||
get_stylesheet_directory() . "/{$template}",
|
||||
get_stylesheet_directory() . "/{$wc_template_path}{$template}",
|
||||
get_template_directory() . "/{$template}",
|
||||
get_template_directory() . "/{$wc_template_path}{$template}",
|
||||
);
|
||||
|
||||
foreach ( $locations as $location ) {
|
||||
if ( is_readable( $location ) ) {
|
||||
$theme_file = $location;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( ! empty( $theme_file ) ) {
|
||||
$core_version = WC_Admin_Status::get_file_version( $wcsg_template_dir . $template );
|
||||
$theme_version = WC_Admin_Status::get_file_version( $theme_file );
|
||||
$overridden_template_output = sprintf( '<code>%s</code>', esc_html( str_replace( $theme_root, '', $theme_file ) ) );
|
||||
if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) {
|
||||
$outdated = true;
|
||||
$overridden_template_output .= sprintf(
|
||||
/* translators: %1$s is the file version, %2$s is the core version */
|
||||
esc_html__( 'version %1$s is out of date. The core version is %2$s', 'woocommerce-subscriptions' ),
|
||||
'<strong style="color:red">' . esc_html( $theme_version ) . '</strong>',
|
||||
'<strong>' . esc_html( $core_version ) . '</strong>'
|
||||
);
|
||||
}
|
||||
$overridden['overrides'][] = $overridden_template_output;
|
||||
}
|
||||
}
|
||||
|
||||
$overridden['has_outdated_templates'] = $outdated;
|
||||
|
||||
return $overridden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of Gifted Subscriptions and adds it to the system status.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
private static function set_gifting_information() {
|
||||
$gifted_subscriptions_count = WCS_Gifting::get_gifted_subscriptions_count();
|
||||
|
||||
self::$gifting_data['wcsg_gifted_subscriptions_count'] = array(
|
||||
'name' => _x( 'Gifted Subscriptions Count', 'name for the system status page', 'woocommerce-subscriptions' ),
|
||||
'label' => _x( 'Gifted Subscriptions Count', 'label for the system status page', 'woocommerce-subscriptions' ),
|
||||
'data' => array( $gifted_subscriptions_count ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Gifting Admin Announcement Handler Class
|
||||
*
|
||||
* @package WooCommerce Subscriptions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class WCSG_Admin_Welcome_Announcement {
|
||||
|
||||
/**
|
||||
* Initialize the tour handler
|
||||
*/
|
||||
public static function init() {
|
||||
// Register scripts and styles
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
|
||||
// Add the tour HTML to the admin footer
|
||||
add_action( 'admin_footer', array( __CLASS__, 'output_tour' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue required scripts and styles
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
wp_localize_script(
|
||||
'wcs-admin',
|
||||
'wcsGiftingSettings',
|
||||
array(
|
||||
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
|
||||
'pluginsUrl' => admin_url( 'plugins.php' ),
|
||||
'subscriptionsUrl' => WC_Subscriptions_Admin::settings_tab_url() . '#woocommerce_subscriptions_gifting_enable_gifting',
|
||||
'isStandaloneGiftingEnabled' => is_plugin_active( 'woocommerce-subscriptions-gifting/woocommerce-subscriptions-gifting.php' ),
|
||||
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the tour HTML in the admin footer
|
||||
*/
|
||||
public static function output_tour() {
|
||||
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a div for the tour to be rendered into
|
||||
echo '<div id="wcs-gifting-welcome-announcement-root" class="woocommerce-tour-kit"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the welcome tour has been dismissed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_welcome_announcement_dismissed() {
|
||||
return '1' === get_option(
|
||||
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current screen is WooCommerce Admin or subscriptions listing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_woocommerce_admin_or_subscriptions_listing() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$action_param = isset( $_GET['action'] ) ? wc_clean( wp_unslash( $_GET['action'] ) ) : '';
|
||||
|
||||
$is_woocommerce_admin = 'woocommerce_page_wc-admin' === $screen->id;
|
||||
$is_subscriptions_listing = 'woocommerce_page_wc-orders--shop_subscription' === $screen->id && empty( $action_param );
|
||||
|
||||
return $is_woocommerce_admin || $is_subscriptions_listing;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,575 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin integration.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting/Admin
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for admin-side integration.
|
||||
*/
|
||||
class WCSG_Admin {
|
||||
|
||||
/**
|
||||
* Prefix used in all Gifting settings names.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $option_prefix = 'woocommerce_subscriptions_gifting';
|
||||
|
||||
/**
|
||||
* Setup hooks & filters, when the class is initialised.
|
||||
*/
|
||||
public static function init() {
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
|
||||
add_filter( 'woocommerce_subscription_list_table_column_content', __CLASS__ . '::display_recipient_name_in_subscription_title', 1, 3 );
|
||||
|
||||
add_filter( 'woocommerce_order_items_meta_get_formatted', __CLASS__ . '::remove_recipient_order_item_meta', 1, 1 );
|
||||
|
||||
add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::add_settings' );
|
||||
|
||||
if ( wcsg_is_wc_subscriptions_pre( '2.3.5' ) ) {
|
||||
add_filter( 'request', __CLASS__ . '::request_query', 11, 1 );
|
||||
} else {
|
||||
add_filter( 'wcs_admin_request_query_subscriptions_for_customer', array( __CLASS__, 'request_query_customer_filter' ), 10, 2 );
|
||||
}
|
||||
|
||||
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::display_edit_subscription_recipient_field', 10, 1 );
|
||||
|
||||
// Save recipient user after WC have saved all subscription order items (40).
|
||||
add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::save_subscription_recipient_meta', 50, 2 );
|
||||
|
||||
add_action( 'admin_notices', __CLASS__ . '::admin_installed_notice' );
|
||||
|
||||
// Filter for gifted subscriptions.
|
||||
add_action( 'restrict_manage_posts', array( __CLASS__, 'add_gifted_subscriptions_filter' ), 50 );
|
||||
add_action( 'woocommerce_order_list_table_restrict_manage_orders', array( __CLASS__, 'add_gifted_subscriptions_filter' ), 50 );
|
||||
|
||||
add_action( 'pre_get_posts', array( __CLASS__, 'maybe_filter_by_gifted_subscriptions' ) );
|
||||
add_action( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', array( __CLASS__, 'filter_subscription_list_table_by_gifted_subscriptions' ) );
|
||||
|
||||
// Add "Resend new recipient account email".
|
||||
add_filter( 'woocommerce_order_actions', array( __CLASS__, 'add_resend_new_recipient_account_email_action' ), 10, 1 );
|
||||
add_action( 'woocommerce_order_action_wcsg_resend_new_recipient_account_email', array( __CLASS__, 'resend_new_recipient_account_email' ), 10, 1 );
|
||||
|
||||
add_filter( 'woocommerce_hidden_order_itemmeta', array( __CLASS__, 'hide_wcsg_recipient_meta' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the wcsg_recipient meta from the order item meta in the edit order page.
|
||||
*
|
||||
* @param array $hidden_order_itemmeta The hidden order item meta.
|
||||
* @return array The hidden order item meta.
|
||||
*/
|
||||
public static function hide_wcsg_recipient_meta( $hidden_order_itemmeta ) {
|
||||
$hidden_order_itemmeta[] = '_wcsg_cart_key';
|
||||
return $hidden_order_itemmeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/queue admin scripts.
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
global $post;
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( 'shop_subscription' === $screen->id && WCS_Gifting::is_gifted_subscription( $post->ID ) ) {
|
||||
|
||||
wp_register_script( 'wcs_gifting_admin', plugins_url( '/assets/js/gifting/wcsg-admin.js', WC_Subscriptions::$plugin_file ), array( 'jquery', 'wc-admin-order-meta-boxes' ), WC_Subscriptions::$version, true );
|
||||
|
||||
wp_localize_script(
|
||||
'wcs_gifting_admin',
|
||||
'wcs_gifting',
|
||||
array(
|
||||
'revoke_download_permission_nonce' => wp_create_nonce( 'revoke_download_permission' ),
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
)
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'wcs_gifting_admin' );
|
||||
}
|
||||
|
||||
if ( true == get_transient( 'wcsg_show_activation_notice' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), WC_VERSION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the subscription title in the admin subscriptions table to include the recipient's name.
|
||||
*
|
||||
* @param string $column_content The column content HTML elements.
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @param string $column The column name being rendered.
|
||||
*/
|
||||
public static function display_recipient_name_in_subscription_title( $column_content, $subscription, $column ) {
|
||||
|
||||
if ( 'order_title' === $column && WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
|
||||
$recipient_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
|
||||
if ( ! $recipient_id ) {
|
||||
return $column_content;
|
||||
}
|
||||
|
||||
$recipient_user = get_userdata( $recipient_id );
|
||||
$recipient_name = '<a href="' . esc_url( get_edit_user_link( $recipient_id ) ) . '">';
|
||||
|
||||
if ( ! empty( $recipient_user->first_name ) || ! empty( $recipient_user->last_name ) ) {
|
||||
$recipient_name .= ucfirst( $recipient_user->first_name ) . ( ( ! empty( $recipient_user->last_name ) ) ? ' ' . ucfirst( $recipient_user->last_name ) : '' );
|
||||
} else {
|
||||
$recipient_name .= ucfirst( $recipient_user->display_name );
|
||||
}
|
||||
$recipient_name .= '</a>';
|
||||
|
||||
$purchaser_id = $subscription->get_user_id();
|
||||
$purchaser_user = get_userdata( $purchaser_id );
|
||||
$purchaser_name = '<a href="' . esc_url( get_edit_user_link( $purchaser_id ) ) . '">';
|
||||
|
||||
if ( ! empty( $purchaser_user->first_name ) || ! empty( $purchaser_user->last_name ) ) {
|
||||
$purchaser_name .= ucfirst( $purchaser_user->first_name ) . ( ( ! empty( $purchaser_user->last_name ) ) ? ' ' . ucfirst( $purchaser_user->last_name ) : '' );
|
||||
} else {
|
||||
$purchaser_name .= ucfirst( $purchaser_user->display_name );
|
||||
}
|
||||
$purchaser_name .= '</a>';
|
||||
|
||||
// translators: $1: is subscription order number,$2: is recipient user's name, $3: is the purchaser user's name.
|
||||
$column_content = sprintf( _x( '%1$s for %2$s purchased by %3$s', 'Subscription title on admin table. (e.g.: #211 for John Doe Purchased by: Jane Doe)', 'woocommerce-subscriptions' ), '<a href="' . esc_url( $subscription->get_edit_order_url() ) . '">#<strong>' . esc_attr( $subscription->get_order_number() ) . '</strong></a>', $recipient_name, $purchaser_name );
|
||||
|
||||
$column_content .= '</div>';
|
||||
}
|
||||
|
||||
return $column_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the recipient order item meta from the admin subscriptions table.
|
||||
*
|
||||
* @param array $formatted_meta formatted order item meta key, label and value.
|
||||
*/
|
||||
public static function remove_recipient_order_item_meta( $formatted_meta ) {
|
||||
|
||||
if ( is_admin() ) {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( isset( $screen->id ) && 'edit-shop_subscription' === $screen->id ) {
|
||||
foreach ( $formatted_meta as $meta_id => $meta ) {
|
||||
if ( 'wcsg_recipient' === $meta['key'] ) {
|
||||
unset( $formatted_meta[ $meta_id ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted_meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Gifting specific settings to standard Subscriptions settings
|
||||
*
|
||||
* @param array $settings Current set of settings.
|
||||
* @return array $settings New set of settings.
|
||||
*/
|
||||
public static function add_settings( $settings ) {
|
||||
$gifting_settings = array(
|
||||
array(
|
||||
'name' => __( 'Gifting', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'id' => self::$option_prefix,
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Enable gifting', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'Allow shoppers to gift a subscription', 'woocommerce-subscriptions' ),
|
||||
'id' => self::$option_prefix . '_enable_gifting',
|
||||
'default' => 'no',
|
||||
'type' => 'checkbox',
|
||||
'row_class' => 'enable-gifting',
|
||||
),
|
||||
array(
|
||||
'name' => '',
|
||||
'desc' => __( 'You can override this global setting on each product.', 'woocommerce-subscriptions' ),
|
||||
'id' => self::$option_prefix . '_default_option',
|
||||
'default' => 'disabled',
|
||||
'type' => 'radio',
|
||||
'desc_at_end' => true,
|
||||
'row_class' => 'gifting-radios',
|
||||
'options' => array(
|
||||
'enabled' => __( 'Enabled for all products', 'woocommerce-subscriptions' ),
|
||||
'disabled' => __( 'Disabled for all products', 'woocommerce-subscriptions' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Gifting Checkbox Text', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'This is what shoppers will see in the product page and cart.', 'woocommerce-subscriptions' ),
|
||||
'id' => self::$option_prefix . '_gifting_checkbox_text',
|
||||
'default' => __( 'This is a gift', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'row_class' => 'gifting-checkbox-text',
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => self::$option_prefix,
|
||||
),
|
||||
);
|
||||
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_renewal_options', $gifting_settings, 'multiple_settings', 'sectionend' ) ) {
|
||||
return array_merge( $settings, $gifting_settings );
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds meta query to also include subscriptions the user is the recipient of when filtering subscriptions by customer.
|
||||
* Compatibility method for Subscriptions < 2.3.5.
|
||||
*
|
||||
* @param array $vars Request vars.
|
||||
* @return array
|
||||
*/
|
||||
public static function request_query( $vars ) {
|
||||
global $typenow;
|
||||
|
||||
if ( 'shop_subscription' === $typenow ) {
|
||||
|
||||
// Add _recipient_user meta check when filtering by customer.
|
||||
$user_id = isset( $_GET['_customer_user'] ) ? intval( $_GET['_customer_user'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( $user_id ) {
|
||||
$vars['meta_query'][] = array(
|
||||
'key' => '_recipient_user',
|
||||
'value' => $user_id,
|
||||
'compare' => '=',
|
||||
);
|
||||
|
||||
$vars['meta_query']['relation'] = 'OR';
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds subscriptions the user is the recipient of when filtering subscriptions by customer on the backend.
|
||||
*
|
||||
* @param array $subscription_ids Current set of subscription IDs.
|
||||
* @param int $customer_user_id User ID.
|
||||
* @return array New set of subscription IDs.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
public static function request_query_customer_filter( $subscription_ids, $customer_user_id ) {
|
||||
$recipient_subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions( $customer_user_id );
|
||||
return array_merge( $subscription_ids, $recipient_subscriptions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a recipient user select field in the edit subscription data metabox.
|
||||
*
|
||||
* @param WP_Post $subscription Subscription's post object.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
|
||||
*/
|
||||
public static function display_edit_subscription_recipient_field( $subscription ) {
|
||||
|
||||
if ( ! wcs_is_subscription( $subscription ) ) {
|
||||
return;
|
||||
} ?>
|
||||
|
||||
<p class="form-field form-field-wide wc-customer-user">
|
||||
<label for="recipient_user"><?php esc_html_e( 'Recipient:', 'woocommerce-subscriptions' ); ?></label>
|
||||
<?php
|
||||
$user_string = '';
|
||||
$user_id = '';
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
$user_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
|
||||
if ( $user_id ) {
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
$user_string = esc_html( $user->display_name ) . ' (#' . absint( $user->ID ) . ' – ' . esc_html( $user->user_email );
|
||||
}
|
||||
}
|
||||
|
||||
if ( is_callable( array( 'WCS_Select2', 'render' ) ) ) {
|
||||
WCS_Select2::render(
|
||||
array(
|
||||
'class' => 'wc-customer-search',
|
||||
'name' => 'recipient_user',
|
||||
'id' => 'recipient_user',
|
||||
'placeholder' => esc_attr__( 'Search for a recipient…', 'woocommerce-subscriptions' ),
|
||||
'selected' => $user_string,
|
||||
'value' => $user_id,
|
||||
'allow_clear' => 'true',
|
||||
)
|
||||
);
|
||||
} else {
|
||||
?>
|
||||
<input type="hidden" class="wc-customer-search" id="recipient_user" name="recipient_user" data-placeholder="<?php esc_attr_e( 'Search for a recipient…', 'woocommerce-subscriptions' ); ?>" data-selected="<?php echo esc_attr( $user_string ); ?>" value="<?php echo esc_attr( $user_id ); ?>" data-allow_clear="true"/>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save subscription recipient user meta by updating or deleting _recipient_user post meta.
|
||||
* Also updates the recipient id stored in subscription line item meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param WP_Post $post Post object.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
|
||||
*/
|
||||
public static function save_subscription_recipient_meta( $post_id, $post ) {
|
||||
|
||||
if ( 'shop_subscription' !== WC_Data_Store::load( 'subscription' )->get_order_type( $post_id ) || empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
return;
|
||||
}
|
||||
|
||||
$recipient_user = empty( $_POST['recipient_user'] ) ? '' : absint( $_POST['recipient_user'] );
|
||||
$subscription = wcs_get_subscription( $post_id );
|
||||
$customer_user = $subscription->get_user_id();
|
||||
$is_gifted_subscription = WCS_Gifting::is_gifted_subscription( $subscription );
|
||||
|
||||
if ( $recipient_user == $customer_user ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
// Remove the recipient.
|
||||
$recipient_user = '';
|
||||
wcs_add_admin_notice( __( 'Error saving subscription recipient: customer and recipient cannot be the same. The recipient user has been removed.', 'woocommerce-subscriptions' ), 'error' );
|
||||
}
|
||||
|
||||
if ( ( $is_gifted_subscription && WCS_Gifting::get_recipient_user( $subscription ) == $recipient_user ) || ( ! $is_gifted_subscription && empty( $recipient_user ) ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
// Recipient user remains unchanged - do nothing.
|
||||
return;
|
||||
} elseif ( empty( $recipient_user ) ) {
|
||||
WCS_Gifting::delete_recipient_user( $subscription );
|
||||
|
||||
// Delete recipient meta from subscription order items.
|
||||
foreach ( $subscription->get_items() as $order_item_id => $order_item ) {
|
||||
wc_delete_order_item_meta( $order_item_id, 'wcsg_recipient' );
|
||||
}
|
||||
} else {
|
||||
WCS_Gifting::set_recipient_user( $subscription, $recipient_user );
|
||||
|
||||
// Update all subscription order items.
|
||||
foreach ( $subscription->get_items() as $order_item_id => $order_item ) {
|
||||
wc_update_order_item_meta( $order_item_id, 'wcsg_recipient', 'wcsg_recipient_id_' . $recipient_user );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a welcome message. Called when the Subscriptions extension is activated.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
|
||||
*/
|
||||
public static function admin_installed_notice() {
|
||||
|
||||
if ( true == get_transient( 'wcsg_show_activation_notice' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
wc_get_template( 'activation-notice.php', array( 'settings_tab_url' => self::settings_tab_url() ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
|
||||
delete_transient( 'wcsg_show_activation_notice' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A WooCommerce version aware function for getting the Subscriptions/Gifting admin settings tab URL.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
|
||||
* @return string
|
||||
*/
|
||||
public static function settings_tab_url() {
|
||||
return apply_filters( 'woocommerce_subscriptions_settings_tab_url', admin_url( 'admin.php?page=wc-settings&tab=subscriptions' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dropdown to the Subscriptions admin screen to allow filtering by gifted subscriptions.
|
||||
*
|
||||
* @param string $post_type Current post type.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
|
||||
*/
|
||||
public static function add_gifted_subscriptions_filter( $post_type = '' ) {
|
||||
$post_type = ! empty( $post_type ) ? $post_type : $GLOBALS['typenow'];
|
||||
|
||||
if ( 'shop_subscription' !== $post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gifted_subscriptions = WCS_Gifting::get_gifted_subscriptions(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'return' => 'ids',
|
||||
)
|
||||
);
|
||||
if ( empty( $gifted_subscriptions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$options = array(
|
||||
'' => __( 'All Subscriptions', 'woocommerce-subscriptions' ),
|
||||
'true' => __( 'Gifted Subscriptions', 'woocommerce-subscriptions' ),
|
||||
'false' => __( 'Non-gifted subscriptions', 'woocommerce-subscriptions' ),
|
||||
);
|
||||
$wcsg_is_gifted = isset( $_GET['wcsg_is_gifted'] ) ? $_GET['wcsg_is_gifted'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
|
||||
echo '<select class="wcsg_is_gifted_selector last" name="wcsg_is_gifted" id="wcsg_is_gifted">';
|
||||
foreach ( $options as $value => $text ) {
|
||||
echo '<option value="' . esc_attr( $value ) . '" ' . selected( $value, $wcsg_is_gifted, false ) . '>' . esc_html( $text ) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the main admin query to include only gifted or non-gifted subscriptions.
|
||||
*
|
||||
* @param WP_Query $query The WP_Query instance (passed by reference).
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
|
||||
*/
|
||||
public static function maybe_filter_by_gifted_subscriptions( $query ) {
|
||||
global $typenow;
|
||||
|
||||
if ( ! is_admin() || ! $query->is_main_query() || 'shop_subscription' !== $typenow || ! isset( $_GET['wcsg_is_gifted'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return;
|
||||
}
|
||||
|
||||
$wcsg_is_gifted = trim( $_GET['wcsg_is_gifted'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
|
||||
$compare = '';
|
||||
if ( 'true' === $wcsg_is_gifted ) {
|
||||
$compare = 'EXISTS';
|
||||
} elseif ( 'false' === $wcsg_is_gifted ) {
|
||||
$compare = 'NOT EXISTS';
|
||||
}
|
||||
|
||||
if ( ! $compare ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta_query = $query->get( 'meta_query' );
|
||||
if ( ! $meta_query ) {
|
||||
$meta_query = array();
|
||||
}
|
||||
|
||||
$meta_query[] = array(
|
||||
'key' => '_recipient_user',
|
||||
'compare' => $compare,
|
||||
);
|
||||
|
||||
$query->set( 'meta_query', $meta_query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the Subscriptions Admin Table to filter only gifted or non-gifted subscriptions.
|
||||
*
|
||||
* @param array $request_query The query args sent to wc_get_orders().
|
||||
*/
|
||||
public static function filter_subscription_list_table_by_gifted_subscriptions( $request_query ) {
|
||||
/**
|
||||
* Note this request isn't nonced as we're only filtering a list table and not modifying data.
|
||||
*/
|
||||
if ( ! isset( $_GET['wcsg_is_gifted'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return $request_query;
|
||||
}
|
||||
|
||||
$wcsg_is_gifted = trim( $_GET['wcsg_is_gifted'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
|
||||
if ( ! in_array( $wcsg_is_gifted, array( 'true', 'false' ), true ) ) {
|
||||
return $request_query;
|
||||
}
|
||||
|
||||
$request_query['meta_query'][] = array(
|
||||
'key' => '_recipient_user',
|
||||
'compare' => 'true' === $wcsg_is_gifted ? 'EXISTS' : 'NOT EXISTS',
|
||||
);
|
||||
|
||||
return $request_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds actions to the admin edit subscriptions page, if the subscription is a gifted one.
|
||||
*
|
||||
* @param array $actions Current admin actions.
|
||||
* @return array $actions The subscription actions with the "Renew Now" action added if it's permitted.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
public static function add_resend_new_recipient_account_email_action( $actions ) {
|
||||
global $theorder;
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $theorder ) ) {
|
||||
$actions['wcsg_resend_new_recipient_account_email'] = __( 'Resend "new recipient account" email', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends the "new recipient" e-mail.
|
||||
*
|
||||
* @param WC_Order $subscription Subscription object.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
|
||||
*/
|
||||
public static function resend_new_recipient_account_email( $subscription ) {
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription->get_id() ) ) {
|
||||
WCSG_Email::resend_new_recipient_user_email( $subscription );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enable gifting setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_gifting_enabled() {
|
||||
$global_enable_gifting = get_option( self::$option_prefix . '_enable_gifting', 'no' );
|
||||
|
||||
return apply_filters( 'wcsg_enable_gifting', 'yes' === $global_enable_gifting );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if gifting is enabled by default for all products.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_gifting_enabled_for_all_products() {
|
||||
$global_default_option = get_option( self::$option_prefix . '_default_option', 'disabled' );
|
||||
|
||||
return apply_filters( 'wcsg_is_enabled_for_all_products', 'enabled' === $global_default_option );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for the gifting option.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_gifting_option_text() {
|
||||
return self::is_gifting_enabled_for_all_products()
|
||||
? __( 'Follow global setting (enabled)', 'woocommerce-subscriptions' )
|
||||
: __( 'Follow global setting (disabled)', 'woocommerce-subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for the gifting option.
|
||||
*/
|
||||
public static function get_gifting_global_override_text() {
|
||||
?>
|
||||
<p class="_subscription_gifting_field_description form-field">
|
||||
<span class="description">
|
||||
<?php
|
||||
/* translators: %1$s opening anchor tag with url, %2$s closing anchor tag */
|
||||
$gifting_description = __( 'Overriding your %1$sstore\'s settings%2$s', 'woocommerce-subscriptions' );
|
||||
|
||||
echo wp_kses_post(
|
||||
sprintf(
|
||||
$gifting_description,
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=subscriptions#woocommerce_subscriptions_gifting_enable_gifting' ) . '">',
|
||||
'</a>'
|
||||
)
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Blocks integration.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for blocks integration.
|
||||
*/
|
||||
class WCSG_Blocks_Integration implements IntegrationInterface {
|
||||
public function get_name() {
|
||||
return 'wcsg_subscriptions';
|
||||
}
|
||||
|
||||
public function initialize() {
|
||||
$script_path = 'build/wcsg-blocks-integration.js';
|
||||
|
||||
$script_url = \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( $script_path );
|
||||
|
||||
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/wcsg-blocks-integration.asset.php' );
|
||||
$script_asset = file_exists( $script_asset_path )
|
||||
? require $script_asset_path
|
||||
: array(
|
||||
'dependencies' => array(),
|
||||
'version' => WCS_Blocks_Integration::get_file_version( $script_asset_path ),
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
'wcsg-blocks-integration',
|
||||
$script_url,
|
||||
$script_asset['dependencies'],
|
||||
$script_asset['version'],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of script handles to enqueue in the frontend context.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_script_handles() {
|
||||
return array( 'wcsg-blocks-integration' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of script handles to enqueue in the editor context.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_editor_script_handles() {
|
||||
return array( 'wcsg-blocks-integration' );
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of key, value pairs of data made available to the block on the client side.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_script_data() {
|
||||
$gifting_checkbox_text = apply_filters(
|
||||
'wcsg_enable_gifting_checkbox_label',
|
||||
get_option(
|
||||
WCSG_Admin::$option_prefix . '_gifting_checkbox_text',
|
||||
__(
|
||||
'This is a gift',
|
||||
'woocommerce-subscriptions'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'gifting_checkbox_text' => $gifting_checkbox_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,418 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart integration.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for cart integration.
|
||||
*/
|
||||
class WCSG_Cart {
|
||||
|
||||
/**
|
||||
* Setup hooks & filters, when the class is initialised.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_after_cart_item_name', __CLASS__ . '::print_gifting_option_cart', 10, 2 );
|
||||
|
||||
add_filter( 'woocommerce_widget_cart_item_quantity', __CLASS__ . '::add_gifting_option_minicart', 1, 3 );
|
||||
|
||||
add_filter( 'woocommerce_update_cart_action_cart_updated', __CLASS__ . '::cart_update', 1, 1 );
|
||||
|
||||
add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::prevent_products_in_gifted_renewal_orders', 10 );
|
||||
|
||||
add_filter( 'woocommerce_order_again_cart_item_data', __CLASS__ . '::add_recipient_to_resubscribe_initial_payment_item', 10, 3 );
|
||||
|
||||
add_filter( 'woocommerce_order_again_cart_item_data', __CLASS__ . '::remove_recipient_from_order_again_cart_item_meta', 10, 1 );
|
||||
|
||||
add_filter( 'woocommerce_checkout_create_order_line_item', __CLASS__ . '::add_recipient_to_order_line_item', 10, 4 );
|
||||
|
||||
if ( did_action( 'woocommerce_blocks_loaded' ) ) {
|
||||
self::register_blocks_update_callback();
|
||||
} else {
|
||||
add_action( 'woocommerce_blocks_loaded', __CLASS__ . '::register_blocks_update_callback' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the wcsg_cart_key meta to the order line item
|
||||
* So it's possible to track which subscription came from which parent order line item.
|
||||
*
|
||||
* @param WC_Order_Item_Product $item The order line item.
|
||||
* @param string $cart_item_key The cart item key.
|
||||
* @param array $values The cart item values.
|
||||
* @param WC_Order $order The order.
|
||||
*/
|
||||
public static function add_recipient_to_order_line_item( $item, $cart_item_key, $values, $order ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
if ( ! isset( $values['wcsg_gift_recipients_email'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$item->add_meta_data( '_wcsg_cart_key', $cart_item_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the blocks cart update callback.
|
||||
*/
|
||||
public static function register_blocks_update_callback() {
|
||||
woocommerce_store_api_register_update_callback(
|
||||
array(
|
||||
'namespace' => 'wcsg-cart',
|
||||
'callback' => __CLASS__ . '::handle_blocks_update_cart_item_recipient',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the blocks cart update callback.
|
||||
*
|
||||
* @param array $data The data from the blocks update callback.
|
||||
*/
|
||||
public static function handle_blocks_update_cart_item_recipient( $data ) {
|
||||
if ( ! isset( $data['recipient'] ) || ! isset( $data['itemKey'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$recipient = $data['recipient'];
|
||||
$key = $data['itemKey'];
|
||||
|
||||
if ( ! WCS_Gifting::validate_recipient_emails( array( $recipient ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( WC()->cart->cart_contents[ $key ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC()->cart->cart_contents[ $key ]['wcsg_gift_recipients_email'] = $recipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds gifting ui elements to subscription cart items.
|
||||
*
|
||||
* @param string $title The product title displayed in the cart table.
|
||||
* @param array $cart_item Details of an item in WC_Cart.
|
||||
* @param string $cart_item_key The key of the cart item being displayed in the cart table.
|
||||
*/
|
||||
public static function add_gifting_option_cart( $title, $cart_item, $cart_item_key ) {
|
||||
|
||||
$is_mini_cart = did_action( 'woocommerce_before_mini_cart' ) && ! did_action( 'woocommerce_after_mini_cart' );
|
||||
|
||||
if ( is_cart() && ! $is_mini_cart ) {
|
||||
$title .= self::maybe_display_gifting_information( $cart_item, $cart_item_key );
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds gifting ui elements to subscription items in the mini cart.
|
||||
*
|
||||
* @param int $quantity The quantity of the cart item.
|
||||
* @param array $cart_item Details of an item in WC_Cart.
|
||||
* @param string $cart_item_key Key of the cart item being displayed in the mini cart.
|
||||
*/
|
||||
public static function add_gifting_option_minicart( $quantity, $cart_item, $cart_item_key ) {
|
||||
$recipient_email = '';
|
||||
$html_string = '';
|
||||
|
||||
if ( self::contains_gifted_renewal() ) {
|
||||
$recipient_user_id = self::get_recipient_from_cart_item( wcs_cart_contains_renewal() );
|
||||
$recipient_user = get_userdata( $recipient_user_id );
|
||||
|
||||
if ( $recipient_user ) {
|
||||
$recipient_email = $recipient_user->user_email;
|
||||
}
|
||||
} elseif ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
|
||||
$recipient_email = $cart_item['wcsg_gift_recipients_email'];
|
||||
}
|
||||
|
||||
if ( '' !== $recipient_email ) {
|
||||
ob_start();
|
||||
wc_get_template( 'html-flat-gifting-recipient-details.php', array( 'email' => $recipient_email ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
|
||||
$html_string = ob_get_clean();
|
||||
}
|
||||
|
||||
return $quantity . $html_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cart items for changes made to recipient infomation on the cart page.
|
||||
*
|
||||
* @param bool $cart_updated whether the cart has been updated.
|
||||
*/
|
||||
public static function cart_update( $cart_updated ) {
|
||||
if ( ! empty( $_POST['recipient_email'] ) ) {
|
||||
if ( ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$recipients = $_POST['recipient_email']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
WCS_Gifting::validate_recipient_emails( $recipients );
|
||||
foreach ( WC()->cart->cart_contents as $key => $item ) {
|
||||
if ( isset( $_POST['recipient_email'][ $key ] ) ) {
|
||||
WCS_Gifting::update_cart_item_recipient( $item, $key, $_POST['recipient_email'][ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wc_add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
return $cart_updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent products being added to the cart if the cart contains a gifted subscription renewal.
|
||||
*
|
||||
* @param bool $passed Whether adding to cart is valid.
|
||||
*/
|
||||
public static function prevent_products_in_gifted_renewal_orders( $passed ) {
|
||||
if ( $passed ) {
|
||||
foreach ( WC()->cart->cart_contents as $key => $item ) {
|
||||
if ( isset( $item['subscription_renewal'] ) ) {
|
||||
$subscription = wcs_get_subscription( $item['subscription_renewal']['subscription_id'] );
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
$passed = false;
|
||||
wc_add_notice( __( 'You cannot add additional products to the cart. Please pay for the subscription renewal first.', 'woocommerce-subscriptions' ), 'error' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $passed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a cart item is able to be gifted.
|
||||
* Only subscriptions that are not a renewal or switch subscription are giftable.
|
||||
*
|
||||
* @param array $cart_item Cart item.
|
||||
* @return bool Whether the cart item is giftable.
|
||||
*/
|
||||
public static function is_giftable_item( $cart_item ) {
|
||||
return WCSG_Product::is_giftable( $cart_item['data'] ) && ! isset( $cart_item['subscription_renewal'] ) && ! isset( $cart_item['subscription_switch'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relevant html (static/flat, interactive or none at all) depending on
|
||||
* whether the cart item is a giftable cart item or is a gifted renewal item.
|
||||
*
|
||||
* @param array $cart_item The cart item.
|
||||
* @param string $cart_item_key The cart item key.
|
||||
* @param string $print_or_return Wether to print or return the HTML content. Optional. Default behaviour is to return the string. Pass 'print' to print the HTML content directly.
|
||||
* @return string Returns the HTML string if $print_or_return is set to 'return', otherwise prints the HTML and nothing is returned.
|
||||
*/
|
||||
public static function maybe_display_gifting_information( $cart_item, $cart_item_key, $print_or_return = 'return' ) {
|
||||
$output = '';
|
||||
|
||||
if ( self::is_giftable_item( $cart_item ) ) {
|
||||
$email = ( empty( $cart_item['wcsg_gift_recipients_email'] ) ) ? '' : $cart_item['wcsg_gift_recipients_email'];
|
||||
$output = WCS_Gifting::render_add_recipient_fields( $email, $cart_item_key, 'return' );
|
||||
} elseif ( self::contains_gifted_renewal() ) {
|
||||
$recipient_user_id = self::get_recipient_from_cart_item( wcs_cart_contains_renewal() );
|
||||
$recipient_user = get_userdata( $recipient_user_id );
|
||||
|
||||
if ( $recipient_user ) {
|
||||
$output = wc_get_template_html(
|
||||
'html-flat-gifting-recipient-details.php',
|
||||
array( 'email' => $recipient_user->user_email ),
|
||||
'',
|
||||
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'return' === $print_or_return ) {
|
||||
return $output;
|
||||
} else {
|
||||
echo wp_kses(
|
||||
$output,
|
||||
array(
|
||||
'fieldset' => array(),
|
||||
'input' => array(
|
||||
'type' => array(),
|
||||
'id' => array(),
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
'value' => array(),
|
||||
'checked' => array(),
|
||||
'disabled' => array(),
|
||||
'data-recipient' => array(),
|
||||
'name' => array(),
|
||||
'placeholder' => array(),
|
||||
),
|
||||
'label' => array(
|
||||
'for' => array(),
|
||||
),
|
||||
'div' => array(
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
),
|
||||
'p' => array(
|
||||
'class' => array(),
|
||||
'style' => array(),
|
||||
'id' => array(),
|
||||
),
|
||||
'svg' => array(
|
||||
'xmlns' => array(),
|
||||
'viewbox' => array(),
|
||||
'width' => array(),
|
||||
'height' => array(),
|
||||
'aria-hidden' => array(),
|
||||
'focusable' => array(),
|
||||
),
|
||||
'path' => array(
|
||||
'd' => array(),
|
||||
),
|
||||
'span' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* When setting up the cart for resubscribes or initial subscription payment carts, ensure the existing subscription recipient email is added to the cart item.
|
||||
*
|
||||
* @param array $cart_item_data Cart item data.
|
||||
* @param array $line_item Line item.
|
||||
* @param object $subscription Subscription object.
|
||||
* @return array Updated cart item data.
|
||||
*/
|
||||
public static function add_recipient_to_resubscribe_initial_payment_item( $cart_item_data, $line_item, $subscription ) {
|
||||
$recipient_user_id = 0;
|
||||
|
||||
if ( $subscription instanceof WC_Order && isset( $line_item['wcsg_recipient'] ) ) {
|
||||
$recipient_user_id = substr( $line_item['wcsg_recipient'], strlen( 'wcsg_recipient_id_' ) );
|
||||
|
||||
} elseif ( ! array_key_exists( 'subscription_renewal', $cart_item_data ) && WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
}
|
||||
|
||||
if ( ! empty( $recipient_user_id ) ) {
|
||||
$recipient_user = get_userdata( $recipient_user_id );
|
||||
|
||||
if ( $recipient_user ) {
|
||||
$cart_item_data['wcsg_gift_recipients_email'] = $recipient_user->user_email;
|
||||
}
|
||||
}
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if it contains a gifted subscription renewal.
|
||||
*
|
||||
* @return bool
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
|
||||
*/
|
||||
public static function contains_gifted_renewal() {
|
||||
$cart_contains_gifted_renewal = false;
|
||||
|
||||
$item = wcs_cart_contains_renewal();
|
||||
|
||||
if ( $item ) {
|
||||
$cart_contains_gifted_renewal = WCS_Gifting::is_gifted_subscription( $item['subscription_renewal']['subscription_id'] );
|
||||
}
|
||||
|
||||
return $cart_contains_gifted_renewal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if a gift recipient email is set.
|
||||
*
|
||||
* @return bool
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
|
||||
*/
|
||||
public static function contains_gift_recipient_email() {
|
||||
$has_recipient_email = false;
|
||||
|
||||
if ( ! empty( WC()->cart->cart_contents ) ) {
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
if ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
|
||||
$has_recipient_email = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $has_recipient_email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a recipient user's ID from a cart item.
|
||||
*
|
||||
* @param array $cart_item Cart item.
|
||||
* @return string the recipient id. If the cart item doesn't belong to a recipient an empty string is returned
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
|
||||
*/
|
||||
public static function get_recipient_from_cart_item( $cart_item ) {
|
||||
$recipient_email = '';
|
||||
$recipient_user_id = '';
|
||||
|
||||
if ( isset( $cart_item['subscription_renewal'] ) && WCS_Gifting::is_gifted_subscription( $cart_item['subscription_renewal']['subscription_id'] ) ) {
|
||||
$recipient_id = WCS_Gifting::get_recipient_user( wcs_get_subscription( $cart_item['subscription_renewal']['subscription_id'] ) );
|
||||
$recipient = get_user_by( 'id', $recipient_id );
|
||||
$recipient_email = $recipient->user_email;
|
||||
} elseif ( isset( $cart_item['wcsg_gift_recipients_email'] ) ) {
|
||||
$recipient_email = $cart_item['wcsg_gift_recipients_email'];
|
||||
}
|
||||
|
||||
if ( ! empty( $recipient_email ) ) {
|
||||
$recipient_user_id = email_exists( $recipient_email );
|
||||
}
|
||||
|
||||
return $recipient_user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove recipient line item meta from order again cart item meta. This meta is re-added to the line item after
|
||||
* checkout and so doesn't need to copied through the cart in this way.
|
||||
*
|
||||
* @param array $cart_item_data Cart item data.
|
||||
* @return array Updated cart item data.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
|
||||
*/
|
||||
public static function remove_recipient_from_order_again_cart_item_meta( $cart_item_data ) {
|
||||
|
||||
foreach ( array( 'subscription_renewal', 'subscription_resubscribe', 'subscription_initial_payment' ) as $subscription_order_again_key ) {
|
||||
if ( isset( $cart_item_data[ $subscription_order_again_key ]['custom_line_item_meta']['wcsg_recipient'] ) ) {
|
||||
unset( $cart_item_data[ $subscription_order_again_key ]['custom_line_item_meta']['wcsg_recipient'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe print gifting HTML elements.
|
||||
*
|
||||
* @param array $cart_item The cart item array data.
|
||||
* @param string $cart_item_key The cart item key.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
|
||||
*/
|
||||
public static function print_gifting_option_cart( $cart_item, $cart_item_key ) {
|
||||
self::maybe_display_gifting_information( $cart_item, $cart_item_key, 'print' );
|
||||
}
|
||||
|
||||
/** Deprecated **/
|
||||
|
||||
/**
|
||||
* Returns gifting ui html elements displaying the email of the recipient.
|
||||
*
|
||||
* @param string $cart_item_key The key of the cart item being displayed in the mini cart.
|
||||
* @param string $email The email of the gift recipient.
|
||||
* @deprecated 2.0.1
|
||||
*/
|
||||
public static function generate_static_gifting_html( $cart_item_key, $email ) {
|
||||
wcs_deprecated_function( __METHOD__, '2.0.1', "the 'html-flat-gifting-recipient-details.php' template. For example usage, see " . __METHOD__ );
|
||||
|
||||
ob_start();
|
||||
wc_get_template( 'html-flat-gifting-recipient-details.php', array( 'email' => $email ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Checkout integration.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for checkout integration.
|
||||
*/
|
||||
class WCSG_Checkout {
|
||||
|
||||
/**
|
||||
* Setup hooks & filters, when the class is initialised.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_checkout_cart_item_quantity', __CLASS__ . '::add_gifting_option_checkout', 1, 3 );
|
||||
|
||||
add_action( 'woocommerce_checkout_subscription_created', __CLASS__ . '::subscription_created', 1, 3 );
|
||||
|
||||
add_filter( 'woocommerce_subscriptions_recurring_cart_key', __CLASS__ . '::add_recipient_email_recurring_cart_key', 1, 2 );
|
||||
|
||||
add_action( 'woocommerce_checkout_process', __CLASS__ . '::update_cart_before_checkout' );
|
||||
|
||||
add_filter( 'woocommerce_ship_to_different_address_checked', __CLASS__ . '::maybe_ship_to_recipient', 100, 1 );
|
||||
|
||||
add_filter( 'woocommerce_checkout_get_value', __CLASS__ . '::maybe_get_recipient_shipping', 10, 2 );
|
||||
|
||||
add_action( 'woocommerce_checkout_update_order_review', __CLASS__ . '::store_recipients_in_session', 10, 1 );
|
||||
|
||||
add_action( 'woocommerce_before_checkout_shipping_form', __CLASS__ . '::maybe_display_recipient_shipping_notice', 10 );
|
||||
|
||||
add_filter( 'woocommerce_get_item_data', __CLASS__ . '::woocommerce_get_item_data', 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds gifting ui elements to the checkout page. Also updates recipient information
|
||||
* stored on the cart item from session data if it exists.
|
||||
*
|
||||
* @param int $quantity Quantity.
|
||||
* @param object $cart_item The Cart_Item for which we are adding ui elements.
|
||||
* @param string $cart_item_key Cart item key.
|
||||
* @return int The quantity of the cart item with ui elements appended on.
|
||||
*/
|
||||
public static function add_gifting_option_checkout( $quantity, $cart_item, $cart_item_key ) {
|
||||
return $quantity . WCSG_Cart::maybe_display_gifting_information( $cart_item, $cart_item_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the recipient email address to a subscription when it is purchased via checkout.
|
||||
*
|
||||
* @param WC_Subscription $subscription The subscription that has just been created.
|
||||
* @param WC_Order $order Order object.
|
||||
* @param WC_Cart $recurring_cart An array of subscription products that make up the subscription.
|
||||
*/
|
||||
public static function subscription_created( $subscription, $order, $recurring_cart ) {
|
||||
$cart_item = reset( $recurring_cart->cart_contents );
|
||||
if ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
|
||||
$subscription->update_meta_data( '_recipient_user_email_address', $cart_item['wcsg_gift_recipients_email'] );
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the recipient email to a recurring cart key to differentiate subscription products
|
||||
* gifted to different recipients.
|
||||
*
|
||||
* @param string $cart_key Cart key.
|
||||
* @param object $cart_item Cart item.
|
||||
* @return string The cart_key with a recipient's email appended
|
||||
*/
|
||||
public static function add_recipient_email_recurring_cart_key( $cart_key, $cart_item ) {
|
||||
if ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
|
||||
$cart_key .= '_' . $cart_item['wcsg_gift_recipients_email'];
|
||||
}
|
||||
return $cart_key;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cart items for changes made to recipient infomation on the checkout page.
|
||||
* This needs to occur right before WooCommerce processes the cart.
|
||||
* If an error occurs schedule a checkout reload so the user can see the emails causing the errors.
|
||||
*/
|
||||
public static function update_cart_before_checkout() {
|
||||
if ( ! empty( $_POST['recipient_email'] ) ) {
|
||||
if ( ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$recipients = $_POST['recipient_email']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
if ( ! WCS_Gifting::validate_recipient_emails( $recipients ) ) {
|
||||
WC()->session->set( 'reload_checkout', true );
|
||||
}
|
||||
foreach ( WC()->cart->cart_contents as $key => $item ) {
|
||||
if ( isset( $_POST['recipient_email'][ $key ] ) ) {
|
||||
WCS_Gifting::update_cart_item_recipient( $item, $key, $_POST['recipient_email'][ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wc_add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the cart contains a gifted subscription renewal or a gift recipient, tell the checkout to ship to a different address.
|
||||
*
|
||||
* @param bool $ship_to_different_address Whether the order will ship to a different address.
|
||||
* @return bool
|
||||
*/
|
||||
public static function maybe_ship_to_recipient( $ship_to_different_address ) {
|
||||
|
||||
if ( WCSG_Admin::is_gifting_enabled() && ! $ship_to_different_address && ( WCSG_Cart::contains_gifted_renewal() || WCSG_Cart::contains_gift_recipient_email() ) ) {
|
||||
$ship_to_different_address = true;
|
||||
}
|
||||
return $ship_to_different_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recipient's shipping address if the checkout is requesting
|
||||
* the shipping fields for a gifted subscription renewal.
|
||||
*
|
||||
* @param string $value Default checkout field value.
|
||||
* @param string $key The checkout form field name/key.
|
||||
*/
|
||||
public static function maybe_get_recipient_shipping( $value, $key ) {
|
||||
$shipping_fields = WC()->countries->get_address_fields( '', 'shipping_' );
|
||||
|
||||
if ( array_key_exists( $key, $shipping_fields ) && WCSG_Cart::contains_gifted_renewal() ) {
|
||||
$item = wcs_cart_contains_renewal();
|
||||
$subscription = wcs_get_subscription( $item['subscription_renewal']['subscription_id'] );
|
||||
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
$value = get_user_meta( $recipient_user_id, $key, true );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores recipient email data in the session to prevent losing changes made to recipient emails
|
||||
* during the checkout updating the order review fields.
|
||||
*
|
||||
* @param string $checkout_data Checkout _POST data in a query string format.
|
||||
*/
|
||||
public static function store_recipients_in_session( $checkout_data ) {
|
||||
|
||||
parse_str( $checkout_data, $checkout_data );
|
||||
|
||||
if ( isset( $checkout_data['recipient_email'] ) ) {
|
||||
// Store recipient emails on the cart items so they can be repopulated after checkout update.
|
||||
foreach ( WC()->cart->cart_contents as $key => $item ) {
|
||||
if ( isset( $checkout_data['recipient_email'][ $key ] ) ) {
|
||||
WCS_Gifting::update_cart_item_recipient( $item, $key, $checkout_data['recipient_email'][ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a notice to guide the shopper on how to fill the shipping address. The visibility of this notice is controlled by CSS depending on
|
||||
* the status of the gifting checkbox.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
|
||||
*/
|
||||
public static function maybe_display_recipient_shipping_notice() {
|
||||
if ( ! WCSG_Admin::is_gifting_enabled() || ! WCSG_Cart::contains_gift_recipient_email() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wc_print_notice( esc_html__( 'Please enter the gift recipient’s shipping address here, or we’ll collect it directly from them when they log in.', 'woocommerce-subscriptions' ), 'notice' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds meta data so it can be displayed in the Cart.
|
||||
*/
|
||||
public static function woocommerce_get_item_data( $other_data, $cart_item ) {
|
||||
$product = $cart_item['data'];
|
||||
|
||||
if ( ! WC_Subscriptions_Product::is_subscription( $product ) || ! WCSG_Product::is_giftable( $product ) ) {
|
||||
return $other_data;
|
||||
}
|
||||
|
||||
$other_data[] = array(
|
||||
'name' => 'item_key',
|
||||
'value' => $cart_item['key'],
|
||||
'hidden' => true,
|
||||
'__experimental_woocommerce_blocks_hidden' => false,
|
||||
);
|
||||
|
||||
$gift_recipient = $cart_item['wcsg_gift_recipients_email'] ?? '';
|
||||
|
||||
if ( $gift_recipient ) {
|
||||
$other_data[] = array(
|
||||
'name' => __( 'Gifting to', 'woocommerce-subscriptions' ),
|
||||
'value' => $gift_recipient,
|
||||
'hidden' => true,
|
||||
'__experimental_woocommerce_blocks_hidden' => false,
|
||||
);
|
||||
|
||||
$other_data[] = array(
|
||||
'name' => 'gifting_to_hidden',
|
||||
'value' => $gift_recipient,
|
||||
'hidden' => true,
|
||||
'__experimental_woocommerce_blocks_hidden' => false,
|
||||
);
|
||||
}
|
||||
|
||||
return $other_data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,622 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Downloadable files access policy for gifted subscriptions.
|
||||
*
|
||||
* @package WooCommerce Subscriptions Gifting
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly handles permissions and access for downloadable files associated to gifted subscriptions.
|
||||
*/
|
||||
class WCSG_Download_Handler {
|
||||
|
||||
/**
|
||||
* Cache of subscription download permissions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $subscription_download_permissions = array();
|
||||
|
||||
/**
|
||||
* Temporary cache of recipient download permissions stored before a subscription is saved.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $recipient_download_permissions = array();
|
||||
|
||||
/**
|
||||
* Setup hooks & filters, when the class is initialised.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::register_download_settings', 11, 1 );
|
||||
add_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
|
||||
add_filter( 'woocommerce_get_item_downloads', __CLASS__ . '::get_item_download_links', 15, 3 );
|
||||
|
||||
// Download Permission Meta Box Functions.
|
||||
add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::download_permissions_meta_box_save', 10, 1 );
|
||||
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::get_download_permissions_before_meta_box', 10, 1 );
|
||||
add_filter( 'woocommerce_admin_download_permissions_title', __CLASS__ . '::add_user_to_download_permission_title', 10, 3 );
|
||||
|
||||
// Grant access via download meta box - hooked on prior to WC_AJAX::grant_access_to_download().
|
||||
add_action( 'wp_ajax_woocommerce_grant_access_to_download', __CLASS__ . '::ajax_grant_download_permission', 9 );
|
||||
|
||||
// Revoke access via download meta box - hooked to a custom Ajax handler in place of WC_AJAX::revoke_access_to_download().
|
||||
add_action( 'wp_ajax_wcsg_revoke_access_to_download', __CLASS__ . '::ajax_revoke_download_permission' );
|
||||
|
||||
// Handle subscriptions created on the admin. Needs to be hooked on prior WCS_Download_Handler::grant_permissions_on_admin_created_subscription().
|
||||
add_action( 'woocommerce_admin_created_subscription', array( __CLASS__, 'grant_permissions_on_admin_created_subscription' ), 1 );
|
||||
|
||||
// Handle recipient download permissions when the subscription's customer or recipient is changed.
|
||||
add_action( 'woocommerce_before_subscription_object_save', array( __CLASS__, 'maybe_store_recipient_permissions_before_save' ) );
|
||||
add_action( 'woocommerce_order_object_updated_props', array( __CLASS__, 'maybe_restore_recipient_permissions_after_save' ), 10, 2 );
|
||||
add_action( 'woocommerce_subscriptions_gifting_recipient_changed', array( __CLASS__, 'maybe_grant_permissions_to_new_recipient' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct user's download links for a downloadable order item.
|
||||
* If the request is from within an email, the links belonging to the email recipient are returned otherwise
|
||||
* if the request is from the view subscription page use the current user id,
|
||||
* otherwise the links for order's customer user are returned.
|
||||
*
|
||||
* @param array $files Downloadable files for the order item.
|
||||
* @param array $item Order line item.
|
||||
* @param object $order Order object.
|
||||
* @return array $files Files.
|
||||
*/
|
||||
public static function get_item_download_links( $files, $item, $order ) {
|
||||
$recipient_user_id = WCS_Gifting::get_recipient_user( $order );
|
||||
|
||||
if ( $recipient_user_id ) {
|
||||
$user_id = ( wcs_is_subscription( $order ) && wcs_is_view_subscription_page() ) ? get_current_user_id() : $order->get_user_id();
|
||||
$mailer = WC()->mailer();
|
||||
|
||||
foreach ( $mailer->emails as $email ) {
|
||||
if ( isset( $email->wcsg_sending_recipient_email ) ) {
|
||||
$user_id = $recipient_user_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$files = self::get_user_downloads_for_order_item( $order, $user_id, $item );
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants download permissions to the recipient rather than the purchaser by default. However if the
|
||||
* purchaser can download setting is selected, permissions are granted to both recipient and purchaser.
|
||||
*
|
||||
* @param array $data download permission data inserted into the wp_woocommerce_downloadable_product_permissions table.
|
||||
* @return array $data
|
||||
*/
|
||||
public static function grant_recipient_download_permissions( $data ) {
|
||||
|
||||
$subscription = wcs_get_subscription( $data['order_id'] );
|
||||
|
||||
if ( $subscription && WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
|
||||
$can_purchaser_download = ( 'yes' === get_option( WCSG_Admin::$option_prefix . '_downloadable_products', 'no' ) ) ? true : false;
|
||||
|
||||
if ( $can_purchaser_download ) {
|
||||
remove_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
|
||||
|
||||
wc_downloadable_file_permission( $data['download_id'], $data['product_id'], $subscription );
|
||||
|
||||
add_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
|
||||
}
|
||||
|
||||
$recipient_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
$recipient = get_user_by( 'id', $recipient_id );
|
||||
$data['user_id'] = $recipient_id;
|
||||
$data['user_email'] = $recipient->user_email;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert Gifting download specific settings into Subscriptions settings
|
||||
*
|
||||
* @param array $settings Subscription's current set of settings.
|
||||
* @return array $settings new settings with appended wcsg specific settings.
|
||||
*/
|
||||
public static function register_download_settings( $settings ) {
|
||||
|
||||
$insert_index = array_search( // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => WCSG_Admin::$option_prefix,
|
||||
),
|
||||
$settings
|
||||
);
|
||||
|
||||
array_splice(
|
||||
$settings,
|
||||
$insert_index,
|
||||
0,
|
||||
array(
|
||||
array(
|
||||
'name' => __( 'Downloadable Products', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'Allow both purchaser and recipient to download subscription products.', 'woocommerce-subscriptions' ),
|
||||
'id' => WCSG_Admin::$option_prefix . '_downloadable_products',
|
||||
'default' => 'no',
|
||||
'type' => 'checkbox',
|
||||
'row_class' => 'gifting-downloadable-products',
|
||||
'desc_tip' => __( 'If you want both the recipient and purchaser of a subscription to have access to downloadable products.', 'woocommerce-subscriptions' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before displaying the meta box, save an unmodified set of the download permissions so they can be used later
|
||||
* when displaying user information and outputting download permission hidden fields (which needs to be done just
|
||||
* once per permission).
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*/
|
||||
public static function get_download_permissions_before_meta_box( $subscription ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
|
||||
self::$subscription_download_permissions = self::get_subscription_download_permissions( wcsg_get_objects_id( $subscription ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the download permission title to also include information about the user the permission belongs to.
|
||||
* This is to make it clear to store managers which user's permissions are being edited.
|
||||
*
|
||||
* We also sneak in hidden fields for the user and permission ID to make sure that we can revoke or modify
|
||||
* permissions for a specific user, because WC doesn't use permission IDs and instead uses download IDs, which
|
||||
* are a hash that do not take into account user ID and duplicate permissions for the same product on the same
|
||||
* order for different users.
|
||||
*
|
||||
* @param string $download_title The download permission title displayed in order download permission meta boxes.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $order_id Order ID.
|
||||
*/
|
||||
public static function add_user_to_download_permission_title( $download_title, $product_id, $order_id ) {
|
||||
|
||||
$subscription = wcs_get_subscription( $order_id );
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
|
||||
foreach ( self::$subscription_download_permissions as $index => $download ) {
|
||||
if ( ! isset( $download->displayed ) ) {
|
||||
?>
|
||||
<input type="hidden" class="wcsg_download_permission_id" name="wcsg_download_permission_ids[<?php echo esc_attr( $index ); ?>]" value="<?php echo absint( $download->permission_id ); ?>" />
|
||||
<input type="hidden" class="wcsg_download_permission_id" name="wcsg_download_user_ids[<?php echo esc_attr( $index ); ?>]" value="<?php echo absint( $download->user_id ); ?>" />
|
||||
<?php
|
||||
|
||||
$user_role = ( WCS_Gifting::get_recipient_user( $subscription ) == $download->user_id ) ? __( 'Recipient', 'woocommerce-subscriptions' ) : __( 'Purchaser', 'woocommerce-subscriptions' ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$user = get_userdata( $download->user_id );
|
||||
$user_name = ucfirst( $user->first_name ) . ( ( ! empty( $user->last_name ) ) ? ' ' . ucfirst( $user->last_name ) : '' );
|
||||
|
||||
$download_title = $user_role . ' (' . ( empty( $user_name ) ? ucfirst( $user->display_name ) : $user_name ) . ') — ' . $download_title;
|
||||
$download->displayed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $download_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save download permission meta box data.
|
||||
*
|
||||
* We need to unhook WC_Meta_Box_Order_Downloads::save() to prevent the WC save function from being called because
|
||||
* it does not differentiate between duplicate permissions for the same product on the same order even when the
|
||||
* permissions are for different users (and with different permission IDs). This means it would modify all
|
||||
* permissions on that order for that product and set them all to be for the same user, instead of keeping
|
||||
* them for the different users.
|
||||
*
|
||||
* @param int $subscription_id Subscription ID.
|
||||
*/
|
||||
public static function download_permissions_meta_box_save( $subscription_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Post WC 3.0 WC_Meta_Box_Order_Downloads::save() no longer overrides the user ID associated with the download permissions so the contents of this function aren't necessary.
|
||||
if ( ! wcsg_is_woocommerce_pre( '3.0' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_POST['wcsg_download_permission_ids'] ) && isset( $_POST['woocommerce_meta_nonce'] ) && wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
|
||||
remove_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30 );
|
||||
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$permission_ids = $_POST['wcsg_download_permission_ids'];
|
||||
$user_ids = $_POST['wcsg_download_user_ids'];
|
||||
$download_ids = $_POST['download_id'];
|
||||
$product_ids = $_POST['product_id'];
|
||||
$downloads_remaining = $_POST['downloads_remaining'];
|
||||
$access_expires = $_POST['access_expires'];
|
||||
// phpcs:enable
|
||||
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
foreach ( $download_ids as $index => $download_id ) {
|
||||
|
||||
$expiry = ( array_key_exists( $index, $access_expires ) && '' != $access_expires[ $index ] ) ? date_i18n( 'Y-m-d', strtotime( $access_expires[ $index ] ) ) : null; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
|
||||
$data = array(
|
||||
'downloads_remaining' => wc_clean( $downloads_remaining[ $index ] ),
|
||||
'access_expires' => $expiry,
|
||||
);
|
||||
|
||||
$format = array( '%s', '%s' );
|
||||
|
||||
// if we're updating the purchaser's permissions, update the download user id and email, in case it has changed.
|
||||
if ( WCS_Gifting::get_recipient_user( $subscription ) != $user_ids[ $index ] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$data['user_id'] = absint( $_POST['customer_user'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
|
||||
$format[] = '%d';
|
||||
|
||||
$data['user_email'] = wc_clean( wp_unslash( $_POST['_billing_email'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
|
||||
$data,
|
||||
array(
|
||||
'order_id' => $subscription_id,
|
||||
'product_id' => absint( $product_ids[ $index ] ),
|
||||
'download_id' => wc_clean( $download_ids[ $index ] ),
|
||||
'permission_id' => $permission_ids[ $index ],
|
||||
),
|
||||
$format,
|
||||
array( '%d', '%d', '%s', '%d' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all download permissions for a subscription
|
||||
*
|
||||
* @param int $subscription_id Subscription ID.
|
||||
* @param string $order_by Column to use inside the ORDER BY clause.
|
||||
*/
|
||||
private static function get_subscription_download_permissions( $subscription_id, $order_by = 'product_id' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Only allow ordering by permissions_id and product_id (because we can't sanitise $order_by with $wpdb->prepare(), we need it as a column not a string).
|
||||
if ( 'permission_id' !== $order_by ) {
|
||||
$order_by = 'product_id';
|
||||
}
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d ORDER BY %s",
|
||||
$subscription_id,
|
||||
$order_by
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants download permissions from the edit subscription meta box grant access button.
|
||||
* Outputs meta box table rows for each permission granted.
|
||||
*/
|
||||
public static function ajax_grant_download_permission() {
|
||||
|
||||
check_ajax_referer( 'grant-access', 'security' );
|
||||
|
||||
if ( ! current_user_can( 'edit_shop_orders' ) ) {
|
||||
die( -1 );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->hide_errors();
|
||||
|
||||
$order_id = isset( $_POST['order_id'] ) ? intval( $_POST['order_id'] ) : 0;
|
||||
$product_ids = isset( $_POST['product_ids'] ) ? wp_unslash( $_POST['product_ids'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$loop = isset( $_POST['loop'] ) ? intval( $_POST['loop'] ) : 0;
|
||||
$file_counter = 0;
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $order_id ) ) {
|
||||
|
||||
/** @var WC_Subscription $subscription */
|
||||
$subscription = wcs_get_subscription( $order_id );
|
||||
$download_permissions = self::get_subscription_download_permissions( $order_id, 'permission_id' );
|
||||
$file_names = array();
|
||||
$billing_email = is_callable( array( $subscription, 'get_billing_email' ) ) ? $subscription->get_billing_email() : $subscription->billing_email;
|
||||
|
||||
if ( ! $billing_email ) {
|
||||
die();
|
||||
}
|
||||
|
||||
if ( ! is_array( $product_ids ) ) {
|
||||
$product_ids = array( $product_ids );
|
||||
}
|
||||
|
||||
foreach ( $product_ids as $product_id ) {
|
||||
/** @var WC_Product $product */
|
||||
$product = wc_get_product( $product_id );
|
||||
$files = is_callable( array( $product, 'get_downloads' ) ) ? $product->get_downloads() : $product->get_files();
|
||||
|
||||
if ( $files ) {
|
||||
foreach ( $files as $download_id => $file ) {
|
||||
|
||||
++$file_counter;
|
||||
|
||||
if ( isset( $file['name'] ) ) {
|
||||
$file_names[ $download_id ] = $file['name'];
|
||||
} else {
|
||||
// Translators: placeholder is the number of files.
|
||||
$file_names[ $download_id ] = sprintf( __( 'File %d', 'woocommerce-subscriptions' ), $file_counter );
|
||||
}
|
||||
|
||||
wc_downloadable_file_permission( $download_id, $product_id, $subscription );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 < count( $file_names ) ) {
|
||||
$updated_download_permissions = self::get_subscription_download_permissions( $order_id, 'permission_id' );
|
||||
$new_download_permissions = array_diff( array_keys( $updated_download_permissions ), array_keys( $download_permissions ) );
|
||||
|
||||
foreach ( $new_download_permissions as $new_download_permission_index ) {
|
||||
|
||||
++$loop;
|
||||
|
||||
$download = $updated_download_permissions[ $new_download_permission_index ];
|
||||
$file_count = $file_names[ $download->download_id ];
|
||||
|
||||
self::$subscription_download_permissions[ $loop ] = $download;
|
||||
|
||||
if ( class_exists( 'WC_Customer_Download' ) ) {
|
||||
// Post WC 3.0 the template expects a WC_Customer_Download object rather than stdClass objects.
|
||||
$download = new WC_Customer_Download( $download );
|
||||
}
|
||||
include plugin_dir_path( WC_PLUGIN_FILE ) . 'includes/admin/meta-boxes/views/html-order-download-permission.php';
|
||||
}
|
||||
}
|
||||
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce revokes download permissions based only on the product an order ID, that means when
|
||||
* revoking downloads on a gift subscription with permissions for both the purchaser and recipient,
|
||||
* it will revoke both sets of permissions instead of only the permission against which the store
|
||||
* manager clicked the "Revoke Access" button.
|
||||
*
|
||||
* To workaround this, we add the permission ID as a hidden fields against each download permission
|
||||
* with @see self::add_user_to_download_permission_title(). We then trigger a custom Ajax request
|
||||
* that passes the permission ID to the server to make sure we only revoke only that permission.
|
||||
*
|
||||
* We also need to remove WC's handler, which is the WC_Ajax:;revoke_access_to_download() method attached
|
||||
* to the 'woocommerce_revoke_access_to_download' Ajax action. To do this, we have out wcsg-admin.js file
|
||||
* enqueued after WooCommerce's 'wc-admin-order-meta-boxes' script and then in our JavaScript call
|
||||
* $( '.order_download_permissions' ).off() to remove WooCommerce's Ajax method.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
|
||||
*/
|
||||
public static function ajax_revoke_download_permission() {
|
||||
global $wpdb;
|
||||
|
||||
check_admin_referer( 'revoke_download_permission', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'edit_shop_orders' ) ) {
|
||||
die( -1 );
|
||||
}
|
||||
|
||||
$subscription_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription_id ) ) {
|
||||
|
||||
$permission_id = isset( $_POST['download_permission_id'] ) ? intval( $_POST['download_permission_id'] ) : 0;
|
||||
|
||||
if ( ! empty( $permission_id ) ) {
|
||||
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d AND permission_id = %d", $subscription_id, $permission_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user's download permissions for an order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param int $user_id User ID.
|
||||
* @param array $item Order item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_downloads_for_order_item( $order, $user_id, $item ) {
|
||||
global $wpdb;
|
||||
|
||||
$product_id = wcs_get_canonical_product_id( $item );
|
||||
|
||||
$downloads = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT *
|
||||
FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
|
||||
WHERE user_id = %d
|
||||
AND order_id = %d
|
||||
AND product_id = %d",
|
||||
$user_id,
|
||||
wcsg_get_objects_id( $order ),
|
||||
$product_id
|
||||
)
|
||||
);
|
||||
|
||||
$files = array();
|
||||
/** @var WC_Product $product */
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
foreach ( $downloads as $download ) {
|
||||
|
||||
if ( $product->has_file( $download->download_id ) ) {
|
||||
if ( wcsg_is_woocommerce_pre( '3.0' ) ) {
|
||||
$files[ $download->download_id ] = $product->get_file( $download->download_id );
|
||||
} else {
|
||||
$customer_download = new WC_Customer_Download( $download );
|
||||
/** @var WC_Product_Download $file */
|
||||
$file = $product->get_file( $download->download_id );
|
||||
|
||||
$files[ $download->download_id ] = $file->get_data();
|
||||
$files[ $download->download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining();
|
||||
$files[ $download->download_id ]['access_expires'] = $customer_download->get_access_expires();
|
||||
}
|
||||
|
||||
$files[ $download->download_id ]['download_url'] = add_query_arg(
|
||||
array(
|
||||
'download_file' => $product_id,
|
||||
'order' => $download->order_key,
|
||||
'email' => $download->user_email,
|
||||
'key' => $download->download_id,
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the user's download permissions for an order by checking
|
||||
* for downloads stored on the subscriptions in the order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_downloads_for_order( $order, $user_id ) {
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => array( 'any' ) ) );
|
||||
$order_downloads = array();
|
||||
|
||||
foreach ( $subscriptions as $subscription ) {
|
||||
foreach ( $subscription->get_items() as $subscription_item ) {
|
||||
$order_downloads = array_merge( $order_downloads, self::get_user_downloads_for_order_item( $subscription, $user_id, $subscription_item ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $order_downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure download permissions on newly created subscriptions (admin-side) are granted after the recipient has
|
||||
* been set.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
public static function grant_permissions_on_admin_created_subscription( $subscription ) {
|
||||
// Prevent WC Subscriptions from granting permissions to the subscription before Gifting has had a chance
|
||||
// to save the recipient information.
|
||||
remove_action( current_action(), array( 'WCS_Download_Handler', 'grant_download_permissions' ) );
|
||||
|
||||
// Grant permissions after recipient information has been saved (which happens with priority 50).
|
||||
add_action( 'woocommerce_process_shop_order_meta', 'wc_downloadable_product_permissions', 60 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores recipient download permissions before a subscription is saved, just in case they are needed later.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
public static function maybe_store_recipient_permissions_before_save( $subscription ) {
|
||||
global $wpdb;
|
||||
|
||||
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
if ( ! $recipient_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( self::$recipient_download_permissions[ $subscription->get_id() ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data_store = WC_Data_Store::load( 'customer-download' );
|
||||
self::$recipient_download_permissions[ $subscription->get_id() ] = $data_store->get_downloads(
|
||||
array(
|
||||
'order_id' => $subscription->get_id(),
|
||||
'user_id' => $recipient_user_id,
|
||||
'return' => 'ids',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores recipient download permissions from cached values.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
private static function restore_recipient_permissions( $subscription ) {
|
||||
$subscription = wcs_get_subscription( $subscription );
|
||||
if ( ! $subscription || empty( self::$recipient_download_permissions[ $subscription->get_id() ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
|
||||
$recipient = $recipient_user_id ? get_user_by( 'id', $recipient_user_id ) : false;
|
||||
|
||||
if ( ! $recipient ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( self::$recipient_download_permissions[ $subscription->get_id() ] as $permission_id ) {
|
||||
$download = new WC_Customer_Download( $permission_id );
|
||||
$download->set_user_id( $recipient_user_id );
|
||||
$download->set_user_email( $recipient->user_email );
|
||||
$download->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores previous recipient permissions when the subscription's customer changes.
|
||||
* This is required because WC resets all download permissions to the new customer (effectively disabling recipient access) when such a change is made.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $updated_props Properties that changed.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
public static function maybe_restore_recipient_permissions_after_save( $order, $updated_props ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! wcs_is_subscription( $order ) || ! array_intersect( array( 'customer_id', 'billing_email' ), $updated_props ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::restore_recipient_permissions( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants new recipients the downloads permissions the previous recipient had.
|
||||
*
|
||||
* @param WC_Subscription $subscription Subscription object.
|
||||
* @param int $new_recipient_id New recipient user ID.
|
||||
* @param int $old_recipient_id Old recipient user ID.
|
||||
*
|
||||
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
|
||||
*/
|
||||
public static function maybe_grant_permissions_to_new_recipient( $subscription, $new_recipient_id, $old_recipient_id ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $old_recipient_id || ! $new_recipient_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::restore_recipient_permissions( $subscription );
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue