This commit is contained in:
Prospress Inc
2016-07-08 15:02:27 +02:00
committed by I
parent 183dce9069
commit 4733fa00c6
208 changed files with 57323 additions and 0 deletions

169
assets/css/about.css Normal file
View File

@@ -0,0 +1,169 @@
/*------------------------------------------------------------------------------
Subscriptions 2.0 About Page CSS
------------------------------------------------------------------------------*/
.wcs-badge:before {
font-family: WooCommerce !important;
content: "\e03d";
color: #fff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 80px;
font-weight: normal;
width: 165px;
height: 165px;
line-height: 165px;
text-align: center;
position: absolute;
top: 0;
left: 0;
margin: 0;
vertical-align: middle;
}
.wcs-badge {
position: relative;;
background: #9c5d90;
text-rendering: optimizeLegibility;
padding-top: 150px;
height: 52px;
width: 165px;
font-weight: 600;
font-size: 14px;
text-align: center;
color: #ddc8d9;
margin: 5px 0 0 0;
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,.2);
box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
.about-wrap .feature-section {
padding-bottom: 2em;
}
.about-wrap .wcs-badge {
position: absolute;
top: 0;
right: 0;
}
.about-wrap .wcs-feature {
overflow: visible !important;
*zoom:1;
}
.about-wrap .wcs-feature:before,
.about-wrap .wcs-feature:after {
content: " ";
display: table;
}
.about-wrap .wcs-feature:after {
clear: both;
}
.about-wrap .two-col .feature-right {
float: right;
}
.about-wrap .two-col .feature-copy,
.about-wrap .two-col .feature-image {
margin-top: 2em;
}
.about-wrap div.icon {
width: 0 !important;
padding: 0;
margin: 0;
}
.woocommerce-message {
position: relative;
border-left-color: #cc99c2!important;
overflow: hidden;
}
.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary,
.woocommerce-message a.button-primary:focus, p.woocommerce-actions a.button-primary:focus,
.woocommerce-message a.button-primary:active, p.woocommerce-actions a.button-primary:active {
background: #cc99c2;
border-color: #b366a4;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
color: #fff;
text-decoration: none;
}
.woocommerce-message a.button-primary:hover,p.woocommerce-actions a.button-primary:hover {
background: #bb77ae;
border-color: #aa559a;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
}
.woocommerce-message a.button-primary:active,p.woocommerce-actions a.button-primary:active {
background: #aa559a;
border-color: #aa559a;
}
.woocommerce-message a.docs,.woocommerce-message a.skip,p.woocommerce-actions a.docs,p.woocommerce-actions a.skip {
opacity: .7;
}
.woocommerce-message .twitter-share-button,p.woocommerce-actions .twitter-share-button {
vertical-align: middle;
margin-left: 3px;
}
p.woocommerce-actions {
margin-bottom: 2em;
}
.woocommerce-about-text {
margin-bottom: 1em!important;
}
.about-wrap .feature-section.three-col .col {
width: 29.95%;
margin-right: 4.999999999%;
float: left;
}
.about-wrap .feature-section.two-col .col {
float: left;
}
.about-wrap .feature-section .col.last-feature {
margin-right: 0;
}
.about-wrap .feature-section .col.feature-right {
margin-right: 0;
float: right !important;
}
@media only screen and (max-width: 1200px) {
.about-wrap .two-col .feature-copy {
margin-top: 0;
}
}
@media only screen and (max-width: 781px) {
.about-wrap .two-col .feature-copy,
.about-wrap .feature-section {
padding-bottom: 1em;
}
.about-wrap .two-col .feature-image,
.about-wrap .changelog {
border-bottom: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.about-wrap .feature-section.three-col .col {
width: 100%;
margin: 40px 0 0;
padding: 0 0 40px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
}
@media only screen and (max-width: 500px) {
.about-wrap .wcs-badge:before {
width: 100%;
}
.about-wrap .wcs-badge {
position: relative;
margin-bottom: 1.5em;
width: 100%;
}
.about-wrap .three-col .col {
width: 100% !important;
float: none !important;
}
}

496
assets/css/admin.css Normal file
View File

@@ -0,0 +1,496 @@
/* Plugin Activation */
.woocommerce-subscriptions-activated p a.button-primary {
display: inline-block;
}
.woocommerce-subscriptions-activated a.button-primary:hover {
background: #bb77ae;
border-color: #aa559a;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
box-shadow: inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);
}
.woocommerce-subscriptions-activated a.button-primary:active,
.woocommerce-subscriptions-activated a.button-primary:active {
background: #aa559a;
border-color: #aa559a;
}
/* Subscriptions Admin Page */
.post-type-shop_subscription .widefat .column-status,
.post-type-shop_subscription .widefat .column-order_id,
.post-type-shop_subscription .widefat .column-order_title {
width: 142px;
text-align: left;
}
.widefat .type-shop_subscription td.column-status {
padding: 6px 7px;
}
.widefat .type-shop_subscription .column-status mark {
display:block;
text-align: center;
white-space: nowrap;
padding: 0 2px;
background: #999;
border: 1px solid #999;
-webkit-box-shadow:inset 0 0 2px 1px rgba(255,255,255,0.5);
-webkit-border-radius:4px;
-moz-border-radius:4px;
-o-border-radius:4px;
border-radius:4px;
margin: 0;
font-size:9px;
text-transform: uppercase;
color:#fff;
font-weight: bold;
text-shadow:0 1px 0 rgba(0,0,0,0.3);
}
.widefat .type-shop_subscription .column-status mark.active {
background-color: #339933;
border-color: #339933;
}
.widefat .type-shop_subscription .column-status mark.expired {
background-color: #A0658B;
border-color: #A0658B;
}
.widefat .type-shop_subscription .column-status mark.on-hold {
background-color: #E66F00;
border-color: #E66F00;
}
.widefat .type-shop_subscription .column-status mark.failed {
background-color: red;
border-color: red;
}
.widefat .type-shop_subscription .column-status mark.cancelled {
background-color: #ccc;
border-color: #ccc;
}
.widefat .type-shop_subscription .column-status mark.processing {
background-color: #2184c2;
border-color: #2184c2;
}
/* Product List table */
table.wp-list-table span.product-type.variable-subscription {
background-position: -48px 0;
}
/* Orders List Table */
a.close-subscriptions-search {
font-size: 1em;
padding: 0em 0.45em;
border-radius: 1.4em;
background: #dd0000;
color: #ffffff;
margin: -0.3em 0.6em 0 0;
}
.dismiss-subscriptions-search {
position: relative;
}
.dismiss-subscriptions-search a {
padding-bottom: 9px;
}
/* Edit Product/Subscriptions Page */
.woocommerce_options_panel .subscription_pricing {
float: left;
clear: both;
height: 100%;
width: 100%;
}
.woocommerce_options_panel .subscription_pricing .description {
font-style: normal;
}
.subscription_pricing .form-field,
.subscription_pricing .form-field *,
.variable_subscription_pricing .form-field,
.variable_subscription_pricing .form-field *,
.variable_subscription_trial .form-field,
.variable_subscription_trial .form-field * {
float:left;
}
p._subscription_price_field,
p._subscription_period_field,
p._subscription_length_field {
display: inline-block;
}
.woocommerce_options_panel input[type=text].wc_input_subscription_price,
.woocommerce_options_panel input[type=text].wc_input_subscription_intial_price,
.woocommerce_options_panel input[type=text].wc_input_subscription_trial_length,
.woocommerce_options_panel input[type=text].wc_input_subscription_payment_sync {
width: 5.5em;
}
p._subscription_period_field label,
p._subscription_length_field label,
p._subscription_period_interval_field label,
p._subscription_trial_period_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_interval_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field label,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field label {
display: none;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_price_field {
padding-right: 0 !important;
}
p._subscription_price_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field input,
p._subscription_sign_up_fee_field input,
p._subscription_trial_length_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field input,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field input {
width: 5em;
}
.woocommerce_options_panel #sale-price-period {
margin-right: 1em;
}
.wc_input_subscription_length {
min-width: 90px;
}
.wc-metaboxes-wrapper .wc-metabox table td .wc_input_subscription_length {
width: 90px;
}
._subscription_trial_period {
min-width: 7.1em;
}
.wc-metaboxes-wrapper .wc-metabox table td ._subscription_trial_period {
width: 7.1em;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_period_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_length_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_period_interval_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_trial_period_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_payment_sync_date_month_field {
padding: 5px 0 5px 5px !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field {
width: 70px !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_sign_up_fee_field {
width: 100% !important;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_price_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_sign_up_fee_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_length_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_payment_sync_field {
padding: 0 0 0 0 !important;
margin: 0;
}
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_field select,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field select {
width: auto !important;
}
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field select {
width: 88px !important;
}
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field img.help_tip {
margin-top: 4px;
}
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_field span,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field span,
.wc-metaboxes-wrapper .wc-metabox table td p._subscription_sign_up_fee_field span {
padding: 5px 0 0 2px;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_length_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_period_interval_field,
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field {
padding: 0 0 0 5px !important;
margin: 0;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field select {
margin-left: 5px;
}
#woocommerce-product-data .woocommerce_options_panel p._subscription_trial_length_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_sign_up_fee_field,
#woocommerce-product-data .woocommerce_options_panel p._subscription_payment_sync_date_day_field {
clear: left;
padding-right: 0px !important;
}
.woocommerce_options_panel p._subscription_trial_length_field,
.woocommerce_options_panel p._subscription_trial_period_field,
.woocommerce_options_panel p._subscription_sign_up_fee_field {
margin-top: 0;
}
.woocommerce_options_panel p._subscription_trial_period_field img,
.woocommerce_options_panel p._subscription_sign_up_fee_field img,
.woocommerce_options_panel p._subscription_payment_sync_date_field img,
.woocommerce_options_panel p._subscription_payment_sync_date_month_field img {
padding-top: 0.7em;
}
.subscription_sync_week_month select.wc_input_subscription_payment_sync {
min-width: 180px;
}
.wc-metaboxes-wrapper .wc-metabox table tr.variable_subscription_sync td select.wc_input_subscription_payment_sync {
width: auto !important;
}
._subscription_payment_sync_field img.help_tip {
padding-top: 8px;
}
.variable_subscription_sync p._subscription_payment_sync_field {
padding-left: 0 !important;
}
/* Variation Pricing Fields in WooCommerce 2.3 */
.variable_subscription_pricing_2_3 .wcs_hidden_label {
display: none;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_price {
max-width: 24%;
clear: left;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_period_interval {
max-width: 41%;
float: right;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_period {
max-width: 32%;
float: right;
}
.variable_subscription_pricing_2_3.variable_subscription_trial p.form-row {
margin-bottom: 0;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_trial_period {
max-width: 47%;
float: right;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_trial_length {
max-width: 48%;
}
.variable_subscription_pricing_2_3 .variable_subscription_trial_sign_up {
clear: both;
}
.variable_subscription_pricing_2_3 p._subscription_length_field label {
display: inline-block;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_payment_sync_day {
max-width: 13%;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_payment_sync_month {
max-width: 86%;
float: right;
}
@media screen and (max-width: 1190px) {
.variable_subscription_pricing_2_3 p._subscription_price_field,
.variable_subscription_pricing_2_3 p._subscription_length_field {
width: 100%;
}
.variable_subscription_pricing_2_3 p._subscription_price_field {
width: 100%;
margin-bottom: 0;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_payment_sync_day {
max-width: 20%;
}
.variable_subscription_pricing_2_3 .wc_input_subscription_payment_sync_month {
max-width: 78%;
float: right;
}
}
/* Users Administration Screen */
.woocommerce_active_subscriber .active-subscriber:before {
content: "\f147";
display: inline-block;
-webkit-font-smoothing: antialiased;
font: normal 24px/1 'dashicons';
vertical-align: top;
}
/* Add/Edit Subscription Screen */
#woocommerce-subscription-data .handlediv,
#woocommerce-subscription-data h3.hndle {
display: none;
}
#woocommerce-subscription-data .inside {
padding: 0;
margin: 0;
}
#woocommerce-subscription-schedule strong {
display: inline-block;
}
#woocommerce-subscription-schedule .date-fields {
margin: 1em 0;
}
#woocommerce-subscription-schedule p._billing_period_field,
#woocommerce-subscription-schedule p._billing_interval_field {
display: inline-block;
padding: 0 0 0 1px;
margin: 0.5em 0 0;
}
#woocommerce-subscription-schedule p._billing_interval_field {
padding-left: 0;
}
#woocommerce-subscription-schedule p._billing_interval_field {
width: 66%;
}
#woocommerce-subscription-schedule p._billing_period_field {
width: 30%;
}
#woocommerce-subscription-schedule p._billing_interval_field label {
display: inline-block;
font-weight: bold;
}
#woocommerce-subscription-schedule p._billing_period_field label {
display: none;
}
#woocommerce-subscription-schedule p._billing_interval_field select,
#woocommerce-subscription-schedule p._billing_period_field select {
display: inline-block;
}
#woocommerce-subscription-schedule p._billing_interval_field select {
width: 58%;
margin-left: 5px;
}
#woocommerce-subscription-schedule p._billing_period_field select {
width: 100%;
}
#woocommerce-subscription-schedule .date-fields label,
#woocommerce-subscription-schedule .wcs-edit-date-action {
display: inline-block;
font-weight: bold;
}
#woocommerce-subscription-schedule .wcs-date-input input[type="text"]:first-of-type {
width: 8em;
}
#woocommerce-subscription-schedule img.ui-datepicker-trigger {
float: left;
margin: 0.5em 0.1em;
}
#woocommerce-subscription-schedule .wcs-date-input,
#woocommerce-subscription-schedule .billing-schedule-edit {
width: 100%;
}
#woocommerce-subscription-schedule .wcs-edit-date-action {
width: 1.5em;
float: right;
}
#woocommerce-subscription-schedule .wcs-edit-date,
#woocommerce-subscription-schedule .wcs-delete-date {
text-indent: -9999px;
position: relative;
height: 1em;
width: 1em;
display: inline-block;
margin: 0 .5em 0 0;
}
#woocommerce-subscription-schedule .wcs-edit-date:before,
#woocommerce-subscription-schedule .wcs-delete-date:before {
font-family: WooCommerce;
speak: none;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
margin: 0;
text-indent: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
color: #999;
}
#woocommerce-subscription-schedule .wcs-edit-date:before {
content: "\e603";
}
#woocommerce-subscription-schedule .wcs-edit-date:hover:before {
color: #555555;
}
#woocommerce-subscription-schedule .wcs-delete-date:before {
content: "\e013";
}
#woocommerce-subscription-schedule .wcs-delete-date:hover:before {
color: #AA0000;
}
/* Related Orders Metabox on Edit Subscription/Order Admin Screen */
#subscription_renewal_orders .inside {
margin: 0;
padding: 0;
}
.woocommerce_subscriptions_related_orders {
margin: 0;
overflow: auto;
}
.woocommerce_subscriptions_related_orders table {
width: 100%;
background: #fff;
border-collapse: collapse;
}
.woocommerce_subscriptions_related_orders table thead th {
background: #f8f8f8;
padding: 8px;
font-size: 11px;
text-align: left;
color: #555;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.woocommerce_subscriptions_related_orders table thead th:last-child {
padding-right: 12px;
}
.woocommerce_subscriptions_related_orders table thead th:first-child {
padding-left: 12px;
}
.woocommerce_subscriptions_related_orders table thead th:last-of-type,
.woocommerce_subscriptions_related_orders table td:last-of-type {
text-align: right;
}
.woocommerce_subscriptions_related_orders table tbody th,
.woocommerce_subscriptions_related_orders table td {
padding: 8px;
text-align: left;
line-height: 26px;
vertical-align: top;
border-bottom: 1px dotted #ececec;
}
.woocommerce_subscriptions_related_orders table tbody th:last-child,
.woocommerce_subscriptions_related_orders table td:last-child {
padding-right: 12px;
}
.woocommerce_subscriptions_related_orders table tbody th:first-child,
.woocommerce_subscriptions_related_orders table td:first-child {
padding-left: 12px;
}
.woocommerce_subscriptions_related_orders table tbody tr:last-child td {
border-bottom: none;
}
/* Hide irrelevant sections on Edit Subscription screen */
body.post-type-shop_subscription .order_actions #actions optgroup[label='Resend order emails'],
body.post-type-shop_subscription .add-items .description.tips,
body.post-type-shop_subscription .add-items .button.refund-items {
display: none;
}
@media screen and (max-width: 782px) {
#woocommerce-subscription-schedule .wcs-date-input input[type="text"]:first-of-type {
width: 45%;
}
#woocommerce-subscription-schedule .wcs-date-input input[type="text"]:not(:first-of-type) {
width: 19%;
}
}

21
assets/css/checkout.css Normal file
View File

@@ -0,0 +1,21 @@
/* Cart/Checkout Pages */
.shipping.recurring-total ul {
list-style: none outside;
margin: 0;
padding: 0;
}
.shipping.recurring-total ul li {
margin: 0;
padding: .25em 0 .25em 22px;
text-indent: -22px;
list-style: none outside;
}
.shipping.recurring-total ul li input {
margin: 3px 0.5ex;
}
.shipping.recurring-total ul li label {
display: inline;
}
.shipping.recurring-total ul .amount {
font-weight: 700;
}

View File

@@ -0,0 +1,6 @@
.subscription_details .button {
margin-bottom: 2px;
width: 100%;
max-width: 200px;
text-align: center;
}

View File

@@ -0,0 +1,50 @@
body {
margin-top: 100px;
padding-bottom: 2em;
}
.help_tip .tooltip {
display: none;
}
.help_tip:hover .tooltip {
display: block;
}
.tooltip {
position: absolute;
width: 480px;
height: 270px;
line-height: 1.5em;
padding: 10px;
font-size: 14px;
text-align: left;
color: rgb(18, 18, 18);
background: rgb(249, 249, 249);
border: 4px solid rgb(249, 249, 249);
border-radius: 5px;
text-shadow: none;
box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 4px 1px;
margin-top: -332px;
margin-left: 100px;
}
.tooltip:after {
content: "";
position: absolute;
width: 0;
height: 0;
border-width: 14px;
border-style: solid;
border-color: #F9F9F9 transparent transparent transparent;
top: 292px;
left: 366px;
}
#update-messages,
#update-complete,
#update-error {
display: none;
}
.log-notice {
color: #a0a0a0;
}
p.log-notice {
font-size: 0.8em;
color: #a0a0a0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,57 @@
jQuery(document).ready(function($){
if(arePointersEnabled()){
setTimeout(showSubscriptionPointers, 800); // give TinyMCE a chance to finish loading
}
$('select#product-type').change(function(){
if(arePointersEnabled()){
$('#product-type').pointer('close');
}
});
$('#_subscription_price, #_subscription_period, #_subscription_length').change(function(){
if(arePointersEnabled()){
$('.options_group.subscription_pricing').pointer('close');
$('#product-type').pointer('close');
}
});
function arePointersEnabled(){
if($.getParameterByName('subscription_pointers')=='true'){
return true;
} else {
return false;
}
}
function showSubscriptionPointers(){
$('#product-type').pointer({
content: WCSPointers.typePointerContent,
position: {
edge: 'left',
align: 'center'
},
close: function() {
if ($('select#product-type').val()==WCSubscriptions.productType){
$('.options_group.subscription_pricing:not(".subscription_sync")').pointer({
content: WCSPointers.pricePointerContent,
position: 'bottom',
close: function() {
dismissSubscriptionPointer();
}
}).pointer('open');
}
dismissSubscriptionPointer();
}
}).pointer('open');
}
function dismissSubscriptionPointer(){
$.post( ajaxurl, {
pointer: 'wcs_pointer',
action: 'dismiss-wp-pointer'
});
}
});

490
assets/js/admin/admin.js Normal file
View File

@@ -0,0 +1,490 @@
jQuery(document).ready(function($){
$.extend({
getParameterByName: function(name) {
name = name.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]');
var regexS = '[\\?&]' + name + '=([^&#]*)';
var regex = new RegExp(regexS);
var results = regex.exec(window.location.search);
if(results == null) {
return '';
} else {
return decodeURIComponent(results[1].replace(/\+/g, ' '));
}
},
showHideSubscriptionMeta: function(){
if ($('select#product-type').val()==WCSubscriptions.productType) {
$('.show_if_simple').show();
$('.grouping_options').hide();
$('.options_group.pricing ._regular_price_field').hide();
$('#sale-price-period').show();
$('.hide_if_subscription').hide();
$( 'input#_manage_stock' ).change();
if('day' == $('#_subscription_period').val()) {
$('.subscription_sync').hide();
}
} else {
$('.options_group.pricing ._regular_price_field').show();
$('#sale-price-period').hide();
}
},
showHideVariableSubscriptionMeta: function(){
if ($('select#product-type').val()=='variable-subscription') {
$( 'input#_downloadable' ).prop( 'checked', false );
$( 'input#_virtual' ).removeAttr( 'checked' );
$('.show_if_variable').show();
$('.hide_if_variable').hide();
$('.show_if_variable-subscription').show();
$('.hide_if_variable-subscription').hide();
$( 'input#_manage_stock' ).change();
// Make the sale price row full width
$('.sale_price_dates_fields').prev('.form-row').addClass('form-row-full').removeClass('form-row-last');
} else {
if ($('select#product-type').val()=='variable') {
$('.show_if_variable-subscription').hide();
$('.show_if_variable').show();
$('.hide_if_variable').hide();
}
// Restore the sale price row width to half
$('.sale_price_dates_fields').prev('.form-row').removeClass('form-row-full').addClass('form-row-last');
}
},
setSubscriptionLengths: function(){
$('[name^="_subscription_length"], [name^="variable_subscription_length"]').each(function(){
var $lengthElement = $(this),
selectedLength = $lengthElement.val(),
hasSelectedLength = false,
matches = $lengthElement.attr('name').match(/\[(.*?)\]/),
periodSelector,
interval;
if (matches) { // Variation
periodSelector = '[name="variable_subscription_period['+matches[1]+']"]';
billingInterval = parseInt($('[name="variable_subscription_period_interval['+matches[1]+']"]').val());
} else {
periodSelector = '#_subscription_period';
billingInterval = parseInt($('#_subscription_period_interval').val());
}
$lengthElement.empty();
$.each(WCSubscriptions.subscriptionLengths[ $(periodSelector).val() ], function(length,description) {
if(parseInt(length) == 0 || 0 == (parseInt(length) % billingInterval)) {
$lengthElement.append($('<option></option>').attr('value',length).text(description));
}
});
$lengthElement.children('option').each(function(){
if (this.value == selectedLength) {
hasSelectedLength = true;
return false;
}
});
if(hasSelectedLength){
$lengthElement.val(selectedLength);
} else {
$lengthElement.val(0);
}
});
},
setTrialPeriods: function(){
$('[name^="_subscription_trial_length"], [name^="variable_subscription_trial_length"]').each(function(){
var $trialLengthElement = $(this),
trialLength = $trialLengthElement.val(),
matches = $trialLengthElement.attr('name').match(/\[(.*?)\]/),
periodStrings;
if (matches) { // Variation
$trialPeriodElement = $('[name="variable_subscription_trial_period['+matches[1]+']"]');
} else {
$trialPeriodElement = $('#_subscription_trial_period');
}
selectedTrialPeriod = $trialPeriodElement.val();
$trialPeriodElement.empty();
if( parseInt(trialLength) == 1 ) {
periodStrings = WCSubscriptions.trialPeriodSingular;
} else {
periodStrings = WCSubscriptions.trialPeriodPlurals;
}
$.each(periodStrings, function(key,description) {
$trialPeriodElement.append($('<option></option>').attr('value',key).text(description));
});
$trialPeriodElement.val(selectedTrialPeriod);
});
},
setSalePeriod: function(){
$('#sale-price-period').fadeOut(80,function(){
$('#sale-price-period').text($('#_subscription_period_interval option:selected').text()+' '+$('#_subscription_period option:selected').text());
$('#sale-price-period').fadeIn(180);
});
},
setSyncOptions: function(periodField) {
if ( typeof periodField != 'undefined' ) {
if ($('select#product-type').val()=='variable-subscription') {
var $container = periodField.closest('.woocommerce_variable_attributes').find('.variable_subscription_sync');
} else {
$container = periodField.closest('#general_product_data').find('.subscription_sync')
}
var $syncWeekMonthContainer = $container.find('.subscription_sync_week_month'),
$syncWeekMonthSelect = $syncWeekMonthContainer.find('select'),
$syncAnnualContainer = $container.find('.subscription_sync_annual'),
$varSubField = $container.find('[name^="variable_subscription_payment_sync_date"]'),
billingPeriod;
if ($varSubField.length > 0) { // Variation
var matches = $varSubField.attr('name').match(/\[(.*?)\]/);
$subscriptionPeriodElement = $('[name="variable_subscription_period['+matches[1]+']"]');
} else {
$subscriptionPeriodElement = $('#_subscription_period');
}
billingPeriod = $subscriptionPeriodElement.val();
if('day'==billingPeriod) {
$syncWeekMonthSelect.val(0);
$syncAnnualContainer.find('input[type="number"]').val(0);
} else {
if('year'==billingPeriod) {
// Make sure the year sync fields are reset
$syncAnnualContainer.find('input[type="number"]').val(0);
// And the week/month field has no option selected
$syncWeekMonthSelect.val(0);
} else {
// Make sure the year sync value is 0
$syncAnnualContainer.find('input[type="number"]').val(0);
// And the week/month field has the appropriate options
$syncWeekMonthSelect.empty();
$.each(WCSubscriptions.syncOptions[billingPeriod], function(key,description) {
$syncWeekMonthSelect.append($('<option></option>').attr('value',key).text(description));
});
}
}
}
},
showHideSyncOptions: function(){
if($('#_subscription_payment_sync_date').length > 0 || $('.wc_input_subscription_payment_sync').length > 0){
$('.subscription_sync, .variable_subscription_sync').each(function(){ // loop through all sync field groups
var $syncWeekMonthContainer = $(this).find('.subscription_sync_week_month'),
$syncWeekMonthSelect = $syncWeekMonthContainer.find('select'),
$syncAnnualContainer = $(this).find('.subscription_sync_annual'),
$varSubField = $(this).find('[name^="variable_subscription_payment_sync_date"]'),
$slideSwitch = false, // stop the general sync field group sliding down if editing a variable subscription
billingPeriod;
if ($varSubField.length > 0) { // Variation
var matches = $varSubField.attr('name').match(/\[(.*?)\]/);
$subscriptionPeriodElement = $('[name="variable_subscription_period['+matches[1]+']"]');
if ($('select#product-type').val()=='variable-subscription') {
$slideSwitch = true;
}
} else {
$subscriptionPeriodElement = $('#_subscription_period');
if ($('select#product-type').val()==WCSubscriptions.productType) {
$slideSwitch = true;
}
}
billingPeriod = $subscriptionPeriodElement.val();
if('day'==billingPeriod) {
$(this).slideUp(400);
} else {
if ( $slideSwitch ) {
$(this).slideDown(400);
if('year'==billingPeriod) {
// Make sure the year sync fields are visible
$syncAnnualContainer.slideDown(400);
// And the week/month field is hidden
$syncWeekMonthContainer.slideUp(400);
} else {
// Make sure the year sync fields are hidden
$syncAnnualContainer.slideUp(400);
// And the week/month field is visible
$syncWeekMonthContainer.slideDown(400);
}
}
}
});
}
},
moveSubscriptionVariationFields: function(){
$('#variable_product_options .variable_subscription_pricing').not('wcs_moved').each(function(){
var $regularPriceRow = $(this).siblings('.variable_pricing'),
$trialSignUpRow = $(this).siblings('.variable_subscription_trial_sign_up'),
$saleDatesRow;
$saleDatesRow = $(this).siblings('.variable_pricing');
// Add the subscription price fields above the standard price fields
$(this).insertBefore($regularPriceRow);
$trialSignUpRow.insertBefore($(this));
// Replace the regular price field with the trial period field
$regularPriceRow.children(':first').addClass('hide_if_variable-subscription');
$(this).addClass('wcs_moved');
});
},
getVariationBulkEditValue: function(variation_action){
var value;
switch( variation_action ) {
case 'variable_subscription_period':
case 'variable_subscription_trial_period':
value = prompt( WCSubscriptions.bulkEditPeriodMessage );
break;
case 'variable_subscription_period_interval':
value = prompt( WCSubscriptions.bulkEditIntervalhMessage );
break;
case 'variable_subscription_trial_length':
case 'variable_subscription_length':
value = prompt( WCSubscriptions.bulkEditLengthMessage );
break;
case 'variable_subscription_sign_up_fee':
value = prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value );
value = accounting.unformat( value, woocommerce_admin.mon_decimal_point );
break;
}
return value;
},
});
$('.options_group.pricing ._sale_price_field .description').prepend('<span id="sale-price-period" style="display: none;"></span>');
// Move the subscription pricing section to the same location as the normal pricing section
$('.options_group.subscription_pricing').not('.variable_subscription_pricing .options_group.subscription_pricing').insertBefore($('.options_group.pricing:first'));
$('.show_if_subscription.clear').insertAfter($('.options_group.subscription_pricing'));
// Move the subscription variation pricing section to a better location in the DOM on load
if($('#variable_product_options .variable_subscription_pricing').length > 0) {
$.moveSubscriptionVariationFields();
}
// When a variation is added
$('#woocommerce-product-data').on('woocommerce_variations_added woocommerce_variations_loaded',function(){
$.moveSubscriptionVariationFields();
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
$.setSubscriptionLengths();
});
if($('.options_group.pricing').length > 0) {
$.setSalePeriod();
$.showHideSubscriptionMeta();
$.showHideVariableSubscriptionMeta();
$.setSubscriptionLengths();
$.setTrialPeriods();
$.showHideSyncOptions();
}
// Update subscription ranges when subscription period or interval is changed
$('#woocommerce-product-data').on('change','[name^="_subscription_period"], [name^="_subscription_period_interval"], [name^="variable_subscription_period"], [name^="variable_subscription_period_interval"]',function(){
$.setSubscriptionLengths();
$.showHideSyncOptions();
$.setSyncOptions( $(this) );
$.setSalePeriod();
});
$('#woocommerce-product-data').on('propertychange keyup input paste change','[name^="_subscription_trial_length"], [name^="variable_subscription_trial_length"]',function(){
$.setTrialPeriods();
});
$('body').bind('woocommerce-product-type-change',function(){
$.showHideSubscriptionMeta();
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
});
$('input#_downloadable, input#_virtual').change(function(){
$.showHideSubscriptionMeta();
$.showHideVariableSubscriptionMeta();
});
// Make sure the "Used for variations" checkbox is visible when adding attributes to a variable subscription
$('body').on('woocommerce_added_attribute', function(){
$.showHideVariableSubscriptionMeta();
});
if($.getParameterByName('select_subscription')=='true'){
$('select#product-type option[value="'+WCSubscriptions.productType+'"]').attr('selected', 'selected');
$('select#product-type').select().change();
}
// Before saving a subscription product, validate the trial period
$('#post').submit(function(e){
if ( WCSubscriptions.subscriptionLengths !== undefined ){
var trialLength = $('#_subscription_trial_length').val(),
selectedTrialPeriod = $('#_subscription_trial_period').val();
if ( parseInt(trialLength) >= WCSubscriptions.subscriptionLengths[selectedTrialPeriod].length ) {
alert(WCSubscriptions.trialTooLongMessages[selectedTrialPeriod]);
$('#ajax-loading').hide();
$('#publish').removeClass('button-primary-disabled');
e.preventDefault();
}
}
});
// Notify store manager that deleting an order via the Orders screen also deletes subscriptions associated with the orders
$('#posts-filter').submit(function(){
if($('[name="post_type"]').val()=='shop_order'&&($('[name="action"]').val()=='trash'||$('[name="action2"]').val()=='trash')){
var containsSubscription = false;
$('[name="post[]"]:checked').each(function(){
if(true===$('.contains_subscription',$('#post-'+$(this).val())).data('contains_subscription')){
containsSubscription = true;
}
return (false === containsSubscription);
});
if(containsSubscription){
return confirm(WCSubscriptions.bulkTrashWarning);
}
}
});
$('.order_actions .submitdelete').click(function(){
if($('[name="contains_subscription"]').val()=='true'){
return confirm(WCSubscriptions.bulkTrashWarning);
}
});
$(window).load(function(){
if($('[name="contains_subscription"]').length > 0 && $('[name="contains_subscription"]').val()=='true'){
// Show the Recurring Order Totals meta box in WC 2.2
$('#woocommerce-order-totals').show();
} else {
$('#woocommerce-order-totals').hide();
}
});
// Editing a variable product
$('#variable_product_options').on('change','[name^="variable_regular_price"]',function(){
var matches = $(this).attr('name').match(/\[(.*?)\]/);
if (matches) {
var loopIndex = matches[1];
$('[name="variable_subscription_price['+loopIndex+']"]').val($(this).val());
}
});
// Editing a variable product
$('#variable_product_options').on('change','[name^="variable_subscription_price"]',function(){
var matches = $(this).attr('name').match(/\[(.*?)\]/);
if (matches) {
var loopIndex = matches[1];
$('[name="variable_regular_price['+loopIndex+']"]').val($(this).val());
}
});
// Update hidden regular price when subscription price is update on simple products
$('#general_product_data').on('change', '[name^="_subscription_price"]', function() {
$('[name="_regular_price"]').val($(this).val());
});
// Notify store manager that deleting an user via the Users screen also removed them from any subscriptions.
$('.users-php .submitdelete').on('click',function(){
return confirm(WCSubscriptions.deleteUserWarning);
});
// WC 2.4+ variation bulk edit handling
$('select.variation_actions').on('variable_subscription_sign_up_fee_ajax_data variable_subscription_period_interval_ajax_data variable_subscription_period_ajax_data variable_subscription_trial_period_ajax_data variable_subscription_trial_length_ajax_data variable_subscription_length_ajax_data', function(event, data) {
value = $.getVariationBulkEditValue(event.type.replace(/_ajax_data/g,''));
if ( value != null ) {
data.value = value;
}
return data;
});
// We're on the Subscriptions settings page
if($('#woocommerce_subscriptions_allow_switching').length > 0 ){
var allowSwitching = $('#woocommerce_subscriptions_allow_switching').val(),
$switchSettingsRows = $('#woocommerce_subscriptions_allow_switching').parents('tr').siblings('tr'),
$syncProratationRow = $('#woocommerce_subscriptions_prorate_synced_payments').parents('tr'),
$suspensionExtensionRow = $('#woocommerce_subscriptions_recoup_suspension').parents('tr');
if('no'==allowSwitching){
$switchSettingsRows.hide();
}
$('#woocommerce_subscriptions_allow_switching').on('change',function(){
if('no'==$(this).val()){
$switchSettingsRows.children('td, th').animate({paddingTop:0, paddingBottom:0}).wrapInner('<div />').children().slideUp(function(){
$(this).closest('tr').hide();
$(this).replaceWith($(this).html());
});
} else if('no'==allowSwitching) { // switching was previously disable, so settings will be hidden
$switchSettingsRows.fadeIn();
$switchSettingsRows.children('td, th').css({paddingTop:0, paddingBottom:0}).animate({paddingTop:'15px', paddingBottom:'15px'}).wrapInner('<div style="display: none;"/>').children().slideDown(function(){
$switchSettingsRows.children('td, th').removeAttr('style');
$(this).replaceWith($(this).html());
});
}
allowSwitching = $(this).val();
});
// Show/hide suspension extension setting
if ($('#woocommerce_subscriptions_max_customer_suspensions').val() > 0) {
$suspensionExtensionRow.show();
} else {
$suspensionExtensionRow.hide();
}
$('#woocommerce_subscriptions_max_customer_suspensions').on('change', function(){
if ($(this).val() > 0) {
$suspensionExtensionRow.show();
} else {
$suspensionExtensionRow.hide();
}
});
// Show/hide sync proration setting
if ($('#woocommerce_subscriptions_sync_payments').is(':checked')) {
$syncProratationRow.show();
} else {
$syncProratationRow.hide();
}
$('#woocommerce_subscriptions_sync_payments').on('change', function(){
if ($(this).is(':checked')) {
$syncProratationRow.show();
} else {
$syncProratationRow.hide();
}
});
}
// Don't display the variation notice for variable subscription products
$( 'body' ).on( 'woocommerce-display-product-type-alert', function(e, select_val) {
if (select_val=='variable-subscription') {
return false;
}
});
$('.wcs_payment_method_selector').on('change', function() {
var payment_method = $(this).val();
$('.wcs_payment_method_meta_fields').hide();
$('#wcs_' + payment_method + '_fields').show();
});
});

1313
assets/js/admin/jstz.js Normal file

File diff suppressed because it is too large Load Diff

2
assets/js/admin/jstz.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,183 @@
jQuery(document).ready(function($){
var timezone = jstz.determine();
// Display the timezone for date changes
$( '#wcs-timezone' ).text( timezone.name() );
// Display times in client's timezone (based on UTC)
$( '.woocommerce-subscriptions.date-picker' ).each(function(){
var $date_input = $(this),
date_type = $date_input.attr( 'id' ),
$hour_input = $( '#'+date_type+'_hour' ),
$minute_input = $( '#'+date_type+'_minute' ),
time = $('#'+date_type+'_timestamp_utc').val(),
date = moment.unix(time);
if ( time > 0 ) {
date.local();
$date_input.val( date.year() + '-' + ( zeroise( date.months() + 1 ) ) + '-' + ( date.format( 'DD' ) ) );
$hour_input.val( date.format( 'HH' ) );
$minute_input.val( date.format( 'mm' ) );
}
});
// Make sure date pickers are in the future
$( '.woocommerce-subscriptions.date-picker:not(#start)' ).datepicker( 'option','minDate',moment().add(1,'hours').toDate());
// Validate date when hour/minute inputs change
$( '[name$="_hour"], [name$="_minute"]' ).on( 'change', function() {
$( '#' + $(this).attr( 'name' ).replace( '_hour', '' ).replace( '_minute', '' ) ).change();
});
// Validate entire date
$( '.woocommerce-subscriptions.date-picker' ).on( 'change',function(){
// The date was deleted, clear hour/minute inputs values and set the UTC timestamp to 0
if( '' == $(this).val() ) {
$( '#' + $(this).attr( 'id' ) + '_hour' ).val('');
$( '#' + $(this).attr( 'id' ) + '_minute' ).val('');
$( '#' + $(this).attr( 'id' ) + '_timestamp_utc' ).val(0);
return;
}
var time_now = moment(),
one_hour_from_now = moment().add(1,'hours' ),
$date_input = $(this),
date_type = $date_input.attr( 'id' ),
date_pieces = $date_input.val().split( '-' ),
$hour_input = $( '#'+date_type+'_hour' ),
$minute_input = $( '#'+date_type+'_minute' ),
chosen_hour = (0 == $hour_input.val().length) ? one_hour_from_now.format( 'HH' ) : $hour_input.val(),
chosen_minute = (0 == $minute_input.val().length) ? one_hour_from_now.format( 'mm' ) : $minute_input.val(),
chosen_date = moment({
years: date_pieces[0],
months: (date_pieces[1] - 1),
date: (date_pieces[2]),
hours: chosen_hour,
minutes: chosen_minute,
seconds: one_hour_from_now.format( 'ss' )
});
// Make sure start date is before now
if ( 'start' == date_type ) {
if ( false === chosen_date.isBefore( time_now ) ) {
alert( wcs_admin_meta_boxes.i18n_start_date_notice );
$date_input.val( time_now.year() + '-' + ( zeroise( time_now.months() + 1 ) ) + '-' + ( time_now.format( 'DD' ) ) );
$hour_input.val( time_now.format( 'HH' ) );
$minute_input.val( time_now.format( 'mm' ) );
}
}
// Make sure trial end and next payment are after start date
else if ( ( 'trial_end' == date_type || 'next_payment' == date_type ) && '' != $( '#start_timestamp_utc' ).val() ) {
var change_date = false,
start = moment.unix( $('#start_timestamp_utc').val() );
// Make sure trial end is after start date
if ( 'trial_end' == date_type && chosen_date.isBefore( start, 'minute' ) ) {
if ( 'trial_end' == date_type ) {
alert( wcs_admin_meta_boxes.i18n_trial_end_start_notice );
} else if ( 'next_payment' == date_type ) {
alert( wcs_admin_meta_boxes.i18n_next_payment_start_notice );
}
// Change the date
$date_input.val( start.year() + '-' + ( zeroise( start.months() + 1 ) ) + '-' + ( start.format( 'DD' ) ) );
$hour_input.val( start.format( 'HH' ) );
$minute_input.val( start.format( 'mm' ) );
}
}
// Make sure next payment is after trial end
if ( 'next_payment' == date_type && '' != $( '#trial_end_timestamp_utc' ).val() ) {
var trial_end = moment.unix( $('#trial_end_timestamp_utc').val() );
if ( chosen_date.isBefore( trial_end, 'minute' ) ) {
alert( wcs_admin_meta_boxes.i18n_next_payment_trial_notice );
$date_input.val( trial_end.year() + '-' + ( zeroise( trial_end.months() + 1 ) ) + '-' + ( trial_end.format( 'DD' ) ) );
$hour_input.val( trial_end.format( 'HH' ) );
$minute_input.val( trial_end.format( 'mm' ) );
}
}
// Make sure trial end is before next payment and expiration is after next payment date
else if ( ( 'trial_end' == date_type || 'end' == date_type ) && '' != $( '#next_payment' ).val() ) {
var change_date = false,
next_payment = moment.unix( $('#next_payment_timestamp_utc').val() );
// Make sure trial end is before or equal to next payment
if ( 'trial_end' == date_type && next_payment.isBefore( chosen_date, 'minute' ) ) {
alert( wcs_admin_meta_boxes.i18n_trial_end_next_notice );
change_date = true;
}
// Make sure end date is after next payment date
else if ( 'end' == date_type && chosen_date.isBefore( next_payment, 'minute' ) ) {
alert( wcs_admin_meta_boxes.i18n_end_date_notice );
change_date = true;
}
if ( true === change_date ) {
$date_input.val( next_payment.year() + '-' + ( zeroise( next_payment.months() + 1 ) ) + '-' + ( next_payment.format( 'DD' ) ) );
$hour_input.val( next_payment.format( 'HH' ) );
$minute_input.val( next_payment.format( 'mm' ) );
}
}
// Make sure the date is more than an hour in the future
if ( 'trial_end' != date_type && 'start' != date_type && chosen_date.unix() < one_hour_from_now.unix() ) {
alert( wcs_admin_meta_boxes.i18n_past_date_notice );
// Set date to current day
$date_input.val( one_hour_from_now.year() + '-' + ( zeroise( one_hour_from_now.months() + 1 ) ) + '-' + ( one_hour_from_now.format( 'DD' ) ) );
// Set time if current time is in the past
if ( chosen_date.hours() < one_hour_from_now.hours() || ( chosen_date.hours() == one_hour_from_now.hours() && chosen_date.minutes() < one_hour_from_now.minutes() ) ) {
$hour_input.val( one_hour_from_now.format( 'HH' ) );
$minute_input.val( one_hour_from_now.format( 'mm' ) );
}
}
if( 0 == $hour_input.val().length ){
$hour_input.val(one_hour_from_now.format( 'HH' ));
}
if( 0 == $minute_input.val().length ){
$minute_input.val(one_hour_from_now.format( 'mm' ));
}
// Update the UTC timestamp sent to the server
date_pieces = $date_input.val().split( '-' );
$('#'+date_type+'_timestamp_utc').val(moment({
years: date_pieces[0],
months: (date_pieces[1] - 1),
date: (date_pieces[2]),
hours: $hour_input.val(),
minutes: $minute_input.val(),
seconds: one_hour_from_now.format( 'ss' )
}).utc().unix());
$( 'body' ).trigger( 'wcs-updated-date',date_type);
});
function zeroise( val ) {
return (val > 9 ) ? val : '0' + val;
}
$('body.post-type-shop_subscription #post').submit(function(){
if('wcs_process_renewal' == $( "body.post-type-shop_subscription select[name='wc_order_action']" ).val()) {
return confirm(wcs_admin_meta_boxes.process_renewal_action_warning);
}
});
$('body.post-type-shop_subscription #post').submit(function(){
if ( typeof wcs_admin_meta_boxes.change_payment_method_warning != 'undefined' && wcs_admin_meta_boxes.payment_method != $('#_payment_method').val() ) {
return confirm(wcs_admin_meta_boxes.change_payment_method_warning);
}
});
});

2936
assets/js/admin/moment.js Normal file

File diff suppressed because it is too large Load Diff

7
assets/js/admin/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

206
assets/js/wcs-upgrade.js Normal file
View File

@@ -0,0 +1,206 @@
jQuery(document).ready(function($){
var upgrade_start_time = null,
total_subscriptions = wcs_update_script_data.subscription_count;
$('#update-messages').slideUp();
$('#upgrade-step-3').slideUp();
$('form#subscriptions-upgrade').on('submit',function(e){
$('#update-welcome').slideUp(600);
$('#update-messages').slideDown(600);
if('true'==wcs_update_script_data.really_old_version){
wcs_ajax_update_really_old_version();
} else if('true'==wcs_update_script_data.upgrade_to_1_5){
wcs_ajax_update_products();
wcs_ajax_update_hooks();
} else if('true'==wcs_update_script_data.upgrade_to_2_0){
wcs_ajax_update_subscriptions();
} else if('true'==wcs_update_script_data.repair_2_0){
wcs_ajax_repair_subscriptions();
} else {
wcs_ajax_update_complete();
}
e.preventDefault();
});
function wcs_ajax_update_really_old_version(){
$.ajax({
url: wcs_update_script_data.ajax_url,
type: 'POST',
data: {
action: 'wcs_upgrade',
upgrade_step: 'really_old_version',
nonce: wcs_update_script_data.upgrade_nonce
},
success: function(results) {
$('#update-messages ol').append($('<li />').text(results.message));
wcs_ajax_update_products();
wcs_ajax_update_hooks();
},
error: function(results,status,errorThrown){
wcs_ajax_update_error();
}
});
}
function wcs_ajax_update_products(){
$.ajax({
url: wcs_update_script_data.ajax_url,
type: 'POST',
data: {
action: 'wcs_upgrade',
upgrade_step: 'products',
nonce: wcs_update_script_data.upgrade_nonce
},
success: function(results) {
$('#update-messages ol').append($('<li />').text(results.message));
},
error: function(results,status,errorThrown){
wcs_ajax_update_error();
}
});
}
function wcs_ajax_update_hooks() {
var start_time = new Date();
$.ajax({
url: wcs_update_script_data.ajax_url,
type: 'POST',
data: {
action: 'wcs_upgrade',
upgrade_step: 'hooks',
nonce: wcs_update_script_data.upgrade_nonce
},
success: function(results) {
if(results.message){
var end_time = new Date(),
execution_time = Math.ceil( ( end_time.getTime() - start_time.getTime() ) / 1000 );
$('#update-messages ol').append($('<li />').text(results.message.replace('{execution_time}',execution_time)));
}
if( undefined == typeof(results.upgraded_count) || parseInt(results.upgraded_count) <= ( wcs_update_script_data.hooks_per_request - 1 ) ){
wcs_ajax_update_subscriptions();
} else {
wcs_ajax_update_hooks();
}
},
error: function(results,status,errorThrown){
wcs_ajax_update_error();
}
});
}
function wcs_ajax_update_subscriptions() {
var start_time = new Date();
if ( null === upgrade_start_time ) {
upgrade_start_time = start_time;
}
$.ajax({
url: wcs_update_script_data.ajax_url,
type: 'POST',
data: {
action: 'wcs_upgrade',
upgrade_step: 'subscriptions',
nonce: wcs_update_script_data.upgrade_nonce
},
success: function(results) {
if('success'==results.status){
var end_time = new Date(),
execution_time = Math.ceil( ( end_time.getTime() - start_time.getTime() ) / 1000 );
$('#update-messages ol').append($('<li />').text(results.message.replace('{execution_time}',execution_time)));
wcs_update_script_data.subscription_count -= results.upgraded_count;
if( "undefined" === typeof(results.upgraded_count) || parseInt(wcs_update_script_data.subscription_count) <= 0 ) {
wcs_ajax_update_complete();
} else {
wcs_ajax_update_estimated_time(results.time_message);
wcs_ajax_update_subscriptions();
}
} else {
wcs_ajax_update_error(results.message);
}
},
error: function(results,status,errorThrown){
$('<br/><span>Error: ' + results.status + ' ' + errorThrown + '</span>').appendTo('#update-error p');
wcs_ajax_update_error( $('#update-error p').html() );
}
});
}
function wcs_ajax_repair_subscriptions() {
var start_time = new Date();
if ( null === upgrade_start_time ) {
upgrade_start_time = start_time;
}
$.ajax({
url: wcs_update_script_data.ajax_url,
type: 'POST',
data: {
action: 'wcs_upgrade',
upgrade_step: 'subscription_dates_repair',
nonce: wcs_update_script_data.upgrade_nonce
},
success: function(results) {
if('success'==results.status){
var end_time = new Date(),
execution_time = Math.ceil( ( end_time.getTime() - start_time.getTime() ) / 1000 );
$('#update-messages ol').append($('<li />').text(results.message.replace('{execution_time}',execution_time)));
wcs_update_script_data.subscription_count -= results.repaired_count;
wcs_update_script_data.subscription_count -= results.unrepaired_count;
if( parseInt(wcs_update_script_data.subscription_count) <= 0 ) {
wcs_ajax_update_complete();
} else {
wcs_ajax_update_estimated_time(results.time_message);
wcs_ajax_repair_subscriptions();
}
} else {
wcs_ajax_update_error(results.message);
}
},
error: function(results,status,errorThrown){
$('<br/><span>Error: ' + results.status + ' ' + errorThrown + '</span>').appendTo('#update-error p');
wcs_ajax_update_error( $('#update-error p').html() );
}
});
}
function wcs_ajax_update_complete() {
$('#update-ajax-loader, #estimated_time').slideUp(function(){
$('#update-complete').slideDown();
});
}
function wcs_ajax_update_error(message) {
message = message || '';
if ( message.length > 0 ){
$('#update-error p').html(message);
}
$('#update-ajax-loader, #estimated_time').slideUp(function(){
$('#update-error').slideDown();
});
}
function wcs_ajax_update_estimated_time(message) {
var total_updated = total_subscriptions - wcs_update_script_data.subscription_count,
now = new Date(),
execution_time,
time_per_update,
time_left,
time_left_minutes,
time_left_seconds;
execution_time = Math.ceil( ( now.getTime() - upgrade_start_time.getTime() ) / 1000 );
time_per_update = execution_time / total_updated;
time_left = Math.floor( wcs_update_script_data.subscription_count * time_per_update );
time_left_minutes = Math.floor( time_left / 60 );
time_left_seconds = time_left % 60;
$('#estimated_time').html(message.replace( '{time_left}', time_left_minutes + ":" + zeropad(time_left_seconds) ));
}
function zeropad(number) {
var pad_char = 0,
pad = new Array(3).join(pad_char);
return (pad + number).slice(-pad.length);
}
});

1038
changelog.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
<?php
/**
* Abstract Subscription Cache Manager Class
*
* Implements methods to deal with the soft caching layer
*
* @class WCS_Cache_Manager
* @version 2.0
* @package WooCommerce Subscriptions/Classes
* @category Class
* @author Gabor Javorszky
*/
abstract class WCS_Cache_Manager {
final public static function get_instance() {
/**
* Modeled after WP_Session_Tokens
*/
$manager = apply_filters( 'wcs_cache_manager_class', 'WCS_Cache_Manager_TLC' );
return new $manager;
}
/**
* WCS_Cache_Manager constructor.
*
* Loads the logger if it's not overwritten.
*/
abstract function __construct();
/**
* Initialises some form of logger
*/
abstract public function load_logger();
/**
* This method should implement adding to the log file
* @return mixed
*/
abstract public function log( $message );
/**
* Caches and returns data. Implementation can vary by classes.
*
* @return mixed
*/
abstract public function cache_and_get( $key, $callback, $params = array(), $expires = 0 );
/**
* Deletes a cached version of data.
*
* @return mixed
*/
abstract public function delete_cached( $key );
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Deprecate actions and filters that use a dynamic hook by appending a variable, like a payment gateway's name.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
abstract class WCS_Dynamic_Hook_Deprecator extends WCS_Hook_Deprecator {
/* The prefixes of hooks that have been deprecated, 'new_hook' => 'old_hook_prefix' */
protected $deprecated_hook_prefixes = array();
/**
* Bootstraps the class and hooks required actions & filters.
*
* We need to use the special 'all' hook here because we don't actually know the full hook names
* in advance, just their prefix. We can't simply hook in to 'plugins_loaded' and check the
* $wp_filter global for our hooks either, because sometime, hooks are dynamically hooked based
* on other hooks. Sigh.
*
* @since 2.0
*/
public function __construct() {
add_filter( 'all', array( &$this, 'check_for_deprecated_hooks' ) );
}
/**
* Check if the current hook contains the prefix of any dynamic hook that has been deprecated.
*
* @since 2.0
*/
public function check_for_deprecated_hooks() {
$current_filter = current_filter();
foreach ( $this->deprecated_hook_prefixes as $new_hook_prefix => $old_hook_prefixes ) {
if ( is_array( $old_hook_prefixes ) ) {
foreach ( $old_hook_prefixes as $old_hook_prefix ) {
$this->check_for_deprecated_hook( $current_filter, $new_hook_prefix, $old_hook_prefix );
}
} else {
$this->check_for_deprecated_hook( $current_filter, $new_hook_prefix, $old_hook_prefixes );
}
}
}
/**
* Check if a given hook contains the prefix and if it does, attach the @see $this->maybe_handle_deprecated_hook() method
* as a callback to it.
*
* @since 2.0
*/
protected function check_for_deprecated_hook( $current_hook, $new_hook_prefix, $old_hook_prefix ) {
if ( false !== strpos( $current_hook, $new_hook_prefix ) ) {
// Get the dynamic suffix on the hook, usually a payment gateway name, like 'stripe' or 'authorize_net_cim'
$hook_suffix = str_replace( $new_hook_prefix, '', $current_hook );
$old_hook = $old_hook_prefix . $hook_suffix;
// register the entire new and old hook
$this->deprecated_hooks[ $current_hook ][] = $old_hook;
// and attach our handler now that we know the hooks
add_filter( $current_hook, array( &$this, 'maybe_handle_deprecated_hook' ), -1000, 8 );
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Provide shared utilities for deprecating actions and filters.
*
* Because Subscriptions v2.0 changed the way subscription data is stored and accessed, it needed
* to deprecate a number of hooks which passed callbacks deprecated data structions, like the old
* subscription array instead of a WC_Subscription object.
*
* This is the base class for handling those deprecated hooks.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
abstract class WCS_Hook_Deprecator {
/* The hooks that have been deprecated, 'new_hook' => 'old_hook' */
protected $deprecated_hooks = array();
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
foreach ( $this->deprecated_hooks as $new_hook => $old_hook ) {
add_filter( $new_hook, array( &$this, 'maybe_handle_deprecated_hook' ), -1000, 8 );
}
}
/**
* Check if an old hook still has callbacks attached to it, and if so, display a notice and trigger the old hook.
*
* @since 2.0
*/
public function maybe_handle_deprecated_hook() {
$new_hook = current_filter();
$old_hooks = ( isset( $this->deprecated_hooks[ $new_hook ] ) ) ? $this->deprecated_hooks[ $new_hook ] : '';
$new_callback_args = func_get_args();
$return_value = $new_callback_args[0];
if ( ! empty( $old_hooks ) ) {
if ( is_array( $old_hooks ) ) {
foreach ( $old_hooks as $old_hook ) {
$return_value = $this->handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value );
}
} else {
$return_value = $this->handle_deprecated_hook( $new_hook, $old_hooks, $new_callback_args, $return_value );
}
}
return $return_value;
}
/**
* Check if an old hook still has callbacks attached to it, and if so, display a notice and trigger the old hook.
*
* @since 2.0
*/
protected function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) {
if ( has_filter( $old_hook ) ) {
$this->display_notice( $old_hook, $new_hook );
$return_value = $this->trigger_hook( $old_hook, $new_callback_args );
}
return $return_value;
}
/**
* Display a deprecated notice for old hooks.
*
* @since 2.0
*/
protected static function display_notice( $old_hook, $new_hook ) {
_deprecated_function( sprintf( 'The "%s" hook uses out of date data structures so', esc_html( $old_hook ) ), '2.0 of WooCommerce Subscriptions', esc_html( $new_hook ) );
}
/**
* Trigger the old hook with the original callback parameters
*
* @since 2.0
*/
abstract protected function trigger_hook( $old_hook, $new_callback_args );
/**
* Get the order for a subscription to pass to callbacks.
*
* Because a subscription can exist without an order in Subscriptions 2.0, the order might actually
* fallback to being the subscription rather than the order used to purchase the subscription.
*
* @since 2.0
*/
protected static function get_order( $subscription ) {
$order = isset( $subscription->order->id ) ? $subscription->order : $subscription;
return $order;
}
/**
* Get the order ID for a subscription to pass to callbacks.
*
* Because a subscription can exist without an order in Subscriptions 2.0, the order might actually
* fallback to being the subscription rather than the order used to purchase the subscription.
*
* @since 2.0
*/
protected static function get_order_id( $subscription ) {
return isset( $subscription->order->id ) ? $subscription->order->id : $subscription->id;
}
/**
* Get the first product ID for a subscription to pass to callbacks.
*
* @since 2.0
*/
protected static function get_product_id( $subscription ) {
$order_items = $subscription->get_items();
$product_id = ( empty( $order_items ) ) ? 0 : WC_Subscriptions_Order::get_items_product_id( reset( $order_items ) );
return $product_id;
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Base class for creating a scheduler
*
* Schedulers are responsible for triggering subscription events/action, like when a payment is due
* or subscription expires.
*
* @class WCS_Scheduler
* @version 2.0.0
* @package WooCommerce Subscriptions/Abstracts
* @category Abstract Class
* @author Prospress
*/
abstract class WCS_Scheduler {
/** @protected array The types of dates which this class should schedule */
protected $date_types_to_schedule;
public function __construct() {
add_action( 'init', array( $this, 'set_date_types_to_schedule' ) );
add_action( 'woocommerce_subscription_date_updated', array( &$this, 'update_date' ), 10, 3 );
add_action( 'woocommerce_subscription_date_deleted', array( &$this, 'delete_date' ), 10, 2 );
add_action( 'woocommerce_subscription_status_updated', array( &$this, 'update_status' ), 10, 3 );
}
public function set_date_types_to_schedule() {
$this->date_types_to_schedule = apply_filters( 'woocommerce_subscriptions_date_types_to_schedule', array_keys( wcs_get_subscription_date_types() ) );
}
/**
* When a subscription's date is updated, maybe schedule an event
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
*/
abstract public function update_date( $subscription, $date_type, $datetime );
/**
* When a subscription's date is deleted, clear it from the scheduler
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
*/
abstract public function delete_date( $subscription, $date_type );
/**
* When a subscription's status is updated, maybe schedule an event
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
*/
abstract public function update_status( $subscription, $new_status, $old_status );
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
<?php
/**
* WooCommerce Subscriptions Admin Meta Boxes
*
* Sets up the write panels used by the subscription custom order/post type
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WC_Admin_Meta_Boxes
*/
class WCS_Admin_Meta_Boxes {
/**
* Constructor
*/
public function __construct() {
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 25 );
add_action( 'add_meta_boxes', array( $this, 'remove_meta_boxes' ), 35 );
// We need to remove core WC save methods for meta boxes we don't use
add_action( 'woocommerce_process_shop_order_meta', array( $this, 'remove_meta_box_save' ), -1, 2 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles_scripts' ), 20 );
// We need to hook to the 'shop_order' rather than 'shop_subscription' because we declared that the 'shop_susbcription' order type supports 'order-meta-boxes'
add_action( 'woocommerce_process_shop_order_meta', 'WCS_Meta_Box_Schedule::save', 10, 2 );
add_action( 'woocommerce_process_shop_order_meta', 'WCS_Meta_Box_Subscription_Data::save', 10, 2 );
add_filter( 'woocommerce_order_actions', __CLASS__ . '::add_subscription_actions', 10, 1 );
add_action( 'woocommerce_order_action_wcs_process_renewal', __CLASS__ . '::process_renewal_action_request', 10, 1 );
add_action( 'woocommerce_order_action_wcs_create_pending_renewal', __CLASS__ . '::create_pending_renewal_action_request', 10, 1 );
add_filter( 'woocommerce_resend_order_emails_available', __CLASS__ . '::remove_order_email_actions', 0, 1 );
}
/**
* Add WC Meta boxes
*/
public function add_meta_boxes() {
global $current_screen, $post_ID;
add_meta_box( 'woocommerce-subscription-data', _x( 'Subscription Data', 'meta box title', 'woocommerce-subscriptions' ), 'WCS_Meta_Box_Subscription_Data::output', 'shop_subscription', 'normal', 'high' );
add_meta_box( 'woocommerce-subscription-schedule', _x( 'Billing Schedule', 'meta box title', 'woocommerce-subscriptions' ), 'WCS_Meta_Box_Schedule::output', 'shop_subscription', 'side', 'default' );
remove_meta_box( 'woocommerce-order-data', 'shop_subscription', 'normal' );
add_meta_box( 'subscription_renewal_orders', __( 'Related Orders', 'woocommerce-subscriptions' ), 'WCS_Meta_Box_Related_Orders::output', 'shop_subscription', 'normal', 'low' );
// Only display the meta box if an order relates to a subscription
if ( 'shop_order' === get_post_type( $post_ID ) && wcs_order_contains_subscription( $post_ID, 'any' ) ) {
add_meta_box( 'subscription_renewal_orders', __( 'Related Orders', 'woocommerce-subscriptions' ), 'WCS_Meta_Box_Related_Orders::output', 'shop_order', 'normal', 'low' );
}
}
/**
* Removes the core Order Data meta box as we add our own Subscription Data meta box
*/
public function remove_meta_boxes() {
remove_meta_box( 'woocommerce-order-data', 'shop_subscription', 'normal' );
}
/**
* Don't save save some order related meta boxes
*/
public function remove_meta_box_save( $post_id, $post ) {
if ( 'shop_subscription' == $post->post_type ) {
remove_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40, 2 );
}
}
/**
* Print admin styles/scripts
*/
public function enqueue_styles_scripts() {
global $post;
// Get admin screen id
$screen = get_current_screen();
if ( 'shop_subscription' == $screen->id ) {
wp_register_script( 'jstz', plugin_dir_url( WC_Subscriptions::$plugin_file ) . '/assets/js/admin/jstz.min.js' );
wp_register_script( 'momentjs', plugin_dir_url( WC_Subscriptions::$plugin_file ) . '/assets/js/admin/moment.min.js' );
wp_enqueue_script( 'wcs-admin-meta-boxes-subscription', plugin_dir_url( WC_Subscriptions::$plugin_file ) . '/assets/js/admin/meta-boxes-subscription.js', array( 'wc-admin-meta-boxes', 'jstz', 'momentjs' ), WC_VERSION );
wp_localize_script( 'wcs-admin-meta-boxes-subscription', 'wcs_admin_meta_boxes', apply_filters( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', array(
'i18n_start_date_notice' => __( 'Please enter a start date in the past.', 'woocommerce-subscriptions' ),
'i18n_past_date_notice' => __( 'Please enter a date at least one hour into the future.', 'woocommerce-subscriptions' ),
'i18n_next_payment_start_notice' => __( 'Please enter a date after the trial end.', 'woocommerce-subscriptions' ),
'i18n_next_payment_trial_notice' => __( 'Please enter a date after the start date.', 'woocommerce-subscriptions' ),
'i18n_trial_end_start_notice' => __( 'Please enter a date after the start date.', 'woocommerce-subscriptions' ),
'i18n_trial_end_next_notice' => __( 'Please enter a date before the next payment.', 'woocommerce-subscriptions' ),
'i18n_end_date_notice' => __( 'Please enter a date after the next payment.', 'woocommerce-subscriptions' ),
'process_renewal_action_warning' => __( "Are you sure you want to process a renewal?\n\nThis will charge the customer and email them the renewal order (if emails are enabled).", 'woocommerce-subscriptions' ),
'payment_method' => wcs_get_subscription( $post )->payment_method,
'search_customers_nonce' => wp_create_nonce( 'search-customers' ),
) ) );
}
}
/**
* Adds actions to the admin edit subscriptions page, if the subscription hasn't ended and the payment method supports them.
*
* @param array $actions An array of available actions
* @return array An array of updated actions
* @since 2.0
*/
public static function add_subscription_actions( $actions ) {
global $theorder;
if ( wcs_is_subscription( $theorder ) && ! $theorder->has_status( wcs_get_subscription_ended_statuses() ) ) {
if ( $theorder->payment_method_supports( 'subscription_date_changes' ) && $theorder->has_status( 'active' ) ) {
$actions['wcs_process_renewal'] = esc_html__( 'Process renewal', 'woocommerce-subscriptions' );
}
$actions['wcs_create_pending_renewal'] = esc_html__( 'Create pending renewal order', 'woocommerce-subscriptions' );
}
return $actions;
}
/**
* Handles the action request to process a renewal order.
*
* @param array $subscription
* @since 2.0
*/
public static function process_renewal_action_request( $subscription ) {
do_action( 'woocommerce_scheduled_subscription_payment', $subscription->id );
$subscription->add_order_note( __( 'Process renewal order action requested by admin.', 'woocommerce-subscriptions' ), false, true );
}
/**
* Handles the action request to create a pending renewal order.
*
* @param array $subscription
* @since 2.0
*/
public static function create_pending_renewal_action_request( $subscription ) {
$subscription->update_status( 'on-hold' );
$renewal_order = wcs_create_renewal_order( $subscription );
if ( ! $subscription->is_manual() ) {
$renewal_order->set_payment_method( $subscription->payment_gateway );
}
$subscription->add_order_note( __( 'Create pending renewal order requested by admin action.', 'woocommerce-subscriptions' ), false, true );
}
/**
* Removes order related emails from the available actions.
*
* @param array $available_emails
* @since 2.0
*/
public static function remove_order_email_actions( $email_actions ) {
global $theorder;
if ( wcs_is_subscription( $theorder ) ) {
$email_actions = array();
}
return $email_actions;
}
}
new WCS_Admin_Meta_Boxes();

View File

@@ -0,0 +1,886 @@
<?php
/**
* Post Types Admin
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
} // Exit if accessed directly
if ( class_exists( 'WCS_Admin_Post_Types' ) ) {
return new WCS_Admin_Post_Types();
}
/**
* WC_Admin_Post_Types Class
*
* Handles the edit posts views and some functionality on the edit post screen for WC post types.
*/
class WCS_Admin_Post_Types {
/**
* Constructor
*/
public function __construct() {
// Subscription list table columns and their content
add_filter( 'manage_edit-shop_subscription_columns', array( $this, 'shop_subscription_columns' ) );
add_filter( 'manage_edit-shop_subscription_sortable_columns', array( $this, 'shop_subscription_sortable_columns' ) );
add_action( 'manage_shop_subscription_posts_custom_column', array( $this, 'render_shop_subscription_columns' ), 2 );
// Bulk actions
add_filter( 'bulk_actions-edit-shop_subscription', array( $this, 'remove_bulk_actions' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_bulk_actions_script' ) );
add_action( 'load-edit.php', array( $this, 'parse_bulk_actions' ) );
add_action( 'admin_notices', array( $this, 'bulk_admin_notices' ) );
// Subscription order/filter
add_filter( 'request', array( $this, 'request_query' ) );
// Subscription Search
add_filter( 'get_search_query', array( $this, 'shop_subscription_search_label' ) );
add_filter( 'query_vars', array( $this, 'add_custom_query_var' ) );
add_action( 'parse_query', array( $this, 'shop_subscription_search_custom_fields' ) );
add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
add_action( 'restrict_manage_posts', array( $this, 'restrict_by_product' ) );
add_action( 'restrict_manage_posts', array( $this, 'restrict_by_payment_method' ) );
}
/**
* Modifies the actual SQL that is needed to order by last payment date on subscriptions. Data is pulled from related
* but independent posts, so subqueries are needed. That's something we can't get by filtering the request. This is hooked
* in @see WCS_Admin_Post_Types::request_query function.
*
* @param array $pieces all the pieces of the resulting SQL once WordPress has finished parsing it
* @param WP_Query $query the query object that forms the basis of the SQL
* @return array modified pieces of the SQL query
*/
public function posts_clauses( $pieces, $query ) {
global $wpdb;
if ( ! is_admin() || ! isset( $query->query['post_type'] ) || 'shop_subscription' !== $query->query['post_type'] ) {
return $pieces;
}
// we need to name ID again due to name conflict if we don't
$pieces['fields'] .= ", {$wpdb->posts}.ID AS original_id, {$wpdb->posts}.post_parent AS original_parent, CASE (SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_subscription_renewal' AND meta_value = original_id)
WHEN 0 THEN CASE (SELECT COUNT(*) FROM {$wpdb->posts} WHERE ID = original_parent)
WHEN 0 THEN 0
ELSE (SELECT post_date_gmt FROM {$wpdb->posts} WHERE ID = original_parent)
END
ELSE (SELECT p.post_date_gmt FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_subscription_renewal' AND meta_value = original_id ORDER BY p.post_date_gmt DESC LIMIT 1)
END
AS last_payment";
$order = strtoupper( $query->query['order'] );
$pieces['orderby'] = "CAST(last_payment AS DATETIME) {$order}";
return $pieces;
}
/**
* Displays the dropdown for the product filter
* @return string the html dropdown element
*/
public function restrict_by_product() {
global $typenow;
if ( 'shop_subscription' !== $typenow ) {
return;
}
$product_id = '';
$product_string = '';
if ( ! empty( $_GET['_wcs_product'] ) ) {
$product_id = absint( $_GET['_wcs_product'] );
$product_string = wc_get_product( $product_id )->get_formatted_name();
}
?>
<input type="hidden" class="wc-product-search" name="_wcs_product" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce-subscriptions' ); ?>" data-action="woocommerce_json_search_products_and_variations" data-selected="<?php echo esc_attr( strip_tags( $product_string ) ); ?>" value="<?php echo esc_attr( $product_id ); ?>" data-allow_clear="true" />
<?php
}
/**
* Remove "edit" from the bulk actions.
*
* @param array $actions
* @return array
*/
public function remove_bulk_actions( $actions ) {
if ( isset( $actions['edit'] ) ) {
unset( $actions['edit'] );
}
return $actions;
}
/**
* Add extra options to the bulk actions dropdown
*
* It's only on the All Shop Subscriptions screen.
* Introducing new filter: woocommerce_subscription_bulk_actions. This has to be done through jQuery as the
* 'bulk_actions' filter that WordPress has can only be used to remove bulk actions, not to add them.
*
* This is a filterable array where the key is the action (will become query arg), and the value is a translatable
* string. The same array is used to
*
*/
public function print_bulk_actions_script() {
$post_status = ( isset( $_GET['post_status'] ) ) ? $_GET['post_status'] : '';
if ( 'shop_subscription' !== get_post_type() || in_array( $post_status, array( 'cancelled', 'trash', 'wc-expired' ) ) ) {
return;
}
// Make it filterable in case extensions want to change this
$bulk_actions = apply_filters( 'woocommerce_subscription_bulk_actions', array(
'active' => _x( 'Activate', 'an action on a subscription', 'woocommerce-subscriptions' ),
'on-hold' => _x( 'Put on-hold', 'an action on a subscription', 'woocommerce-subscriptions' ),
'cancelled' => _x( 'Cancel', 'an action on a subscription', 'woocommerce-subscriptions' ),
) );
// No need to display certain bulk actions if we know all the subscriptions on the page have that status already
switch ( $post_status ) {
case 'wc-active' :
unset( $bulk_actions['active'] );
break;
case 'wc-on-hold' :
unset( $bulk_actions['on-hold'] );
break;
}
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
<?php
foreach ( $bulk_actions as $action => $title ) {
?>
$('<option>')
.val('<?php echo esc_attr( $action ); ?>')
.text('<?php echo esc_html( $title ); ?>')
.appendTo("select[name='action'], select[name='action2']" );
<?php
}
?>
});
</script>
<?php
}
/**
* Deals with bulk actions. The style is similar to what WooCommerce is doing. Extensions will have to define their
* own logic by copying the concept behind this method.
*/
public function parse_bulk_actions() {
// We only want to deal with shop_subscriptions. In case any other CPTs have an 'active' action
if ( ! isset( $_REQUEST['post_type'] ) || 'shop_subscription' !== $_REQUEST['post_type'] || ! isset( $_REQUEST['post'] ) ) {
return;
}
$action = '';
if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
$action = $_REQUEST['action'];
} else if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] ) {
$action = $_REQUEST['action2'];
}
switch ( $action ) {
case 'active':
case 'on-hold':
case 'cancelled' :
$new_status = $action;
break;
default:
return;
}
$report_action = 'marked_' . $new_status;
$changed = 0;
$subscription_ids = array_map( 'absint', (array) $_REQUEST['post'] );
$sendback_args = array(
'post_type' => 'shop_subscription',
$report_action => true,
'ids' => join( ',', $subscription_ids ),
'error_count' => 0,
);
foreach ( $subscription_ids as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
$order_note = _x( 'Subscription status changed by bulk edit:', 'Used in order note. Reason why status changed.', 'woocommerce-subscriptions' );
try {
if ( 'cancelled' == $action ) {
$subscription->cancel_order( $order_note );
} else {
$subscription->update_status( $new_status, $order_note, true );
}
// Fire the action hooks
switch ( $action ) {
case 'active' :
case 'on-hold' :
case 'cancelled' :
case 'trash' :
do_action( 'woocommerce_admin_changed_subscription_to_' . $action, $subscription_id );
break;
}
$changed++;
} catch ( Exception $e ) {
$sendback_args['error'] = urlencode( $e->getMessage() );
$sendback_args['error_count']++;
}
}
$sendback_args['changed'] = $changed;
$sendback = add_query_arg( $sendback_args, wp_get_referer() ? wp_get_referer() : '' );
wp_safe_redirect( esc_url_raw( $sendback ) );
exit();
}
/**
* Show confirmation message that subscription status was changed
*/
public function bulk_admin_notices() {
global $post_type, $pagenow;
// Bail out if not on shop order list page
if ( 'edit.php' !== $pagenow || 'shop_subscription' !== $post_type ) {
return;
}
$subscription_statuses = wcs_get_subscription_statuses();
// Check if any status changes happened
foreach ( $subscription_statuses as $slug => $name ) {
if ( isset( $_REQUEST[ 'marked_' . str_replace( 'wc-', '', $slug ) ] ) ) {
$number = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0;
// translators: placeholder is the number of subscriptions updated
$message = sprintf( _n( '%s subscription status changed.', '%s subscription statuses changed.', $number, 'woocommerce-subscriptions' ), number_format_i18n( $number ) );
echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
if ( ! empty( $_REQUEST['error_count'] ) ) {
$error_msg = isset( $_REQUEST['error'] ) ? stripslashes( $_REQUEST['error'] ) : '';
$error_count = isset( $_REQUEST['error_count'] ) ? absint( $_REQUEST['error_count'] ) : 0;
// translators: 1$: is the number of subscriptions not updated, 2$: is the error message
$message = sprintf( _n( '%1$s subscription could not be updated: %2$s', '%1$s subscriptions could not be updated: %2$s', $error_count, 'woocommerce-subscriptions' ), number_format_i18n( $error_count ), $error_msg );
echo '<div class="error"><p>' . esc_html( $message ) . '</p></div>';
}
break;
}
}
}
/**
* Define custom columns for subscription
*
* Column names that have a corresponding `WC_Order` column use the `order_` prefix here
* to take advantage of core WooCommerce assets, like JS/CSS.
*
* @param array $existing_columns
* @return array
*/
public function shop_subscription_columns( $existing_columns ) {
$columns = array(
'cb' => '<input type="checkbox" />',
'status' => __( 'Status', 'woocommerce-subscriptions' ),
'order_title' => __( 'Subscription', 'woocommerce-subscriptions' ),
'order_items' => __( 'Items', 'woocommerce-subscriptions' ),
'recurring_total' => __( 'Total', 'woocommerce-subscriptions' ),
'start_date' => __( 'Start Date', 'woocommerce-subscriptions' ),
'trial_end_date' => __( 'Trial End', 'woocommerce-subscriptions' ),
'next_payment_date' => __( 'Next Payment', 'woocommerce-subscriptions' ),
'last_payment_date' => __( 'Last Payment', 'woocommerce-subscriptions' ),
'end_date' => __( 'End Date', 'woocommerce-subscriptions' ),
'orders' => _x( 'Orders', 'number of orders linked to a subscription', 'woocommerce-subscriptions' ),
);
return $columns;
}
/**
* Output custom columns for subscriptions
* @param string $column
*/
public function render_shop_subscription_columns( $column ) {
global $post, $the_subscription;
if ( empty( $the_subscription ) || $the_subscription->id != $post->ID ) {
$the_subscription = wcs_get_subscription( $post->ID );
}
$column_content = '';
switch ( $column ) {
case 'status' :
// The status label
$column_content = sprintf( '<mark class="%s tips" data-tip="%s">%s</mark>', sanitize_title( $the_subscription->get_status() ), wcs_get_subscription_status_name( $the_subscription->get_status() ), wcs_get_subscription_status_name( $the_subscription->get_status() ) );
// Inline actions
$wp_list_table = _get_list_table( 'WP_Posts_List_Table' );
$post_type_object = get_post_type_object( $post->post_type );
$actions = array();
$action_url = add_query_arg(
array(
'post' => $the_subscription->id,
'_wpnonce' => wp_create_nonce( 'bulk-posts' ),
)
);
if ( isset( $_REQUEST['status'] ) ) {
$action_url = add_query_arg( array( 'status' => $_REQUEST['status'] ), $action_url );
}
$all_statuses = array(
'active' => __( 'Reactivate', 'woocommerce-subscriptions' ),
'on-hold' => __( 'Suspend', 'woocommerce-subscriptions' ),
'cancelled' => _x( 'Cancel', 'an action on a subscription', 'woocommerce-subscriptions' ),
'trash' => __( 'Trash', 'woocommerce-subscriptions' ),
'deleted' => __( 'Delete Permanently', 'woocommerce-subscriptions' ),
);
foreach ( $all_statuses as $status => $label ) {
if ( $the_subscription->can_be_updated_to( $status ) ) {
if ( in_array( $status, array( 'trash', 'deleted' ) ) ) {
if ( current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) {
if ( 'trash' == $post->post_status ) {
$actions['untrash'] = '<a title="' . esc_attr( __( 'Restore this item from the Trash', 'woocommerce-subscriptions' ) ) . '" href="' . wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&amp;action=untrash', $post->ID ) ), 'untrash-post_' . $post->ID ) . '">' . __( 'Restore', 'woocommerce-subscriptions' ) . '</a>';
} elseif ( EMPTY_TRASH_DAYS ) {
$actions['trash'] = '<a class="submitdelete" title="' . esc_attr( __( 'Move this item to the Trash', 'woocommerce-subscriptions' ) ) . '" href="' . get_delete_post_link( $post->ID ) . '">' . __( 'Trash', 'woocommerce-subscriptions' ) . '</a>';
}
if ( 'trash' == $post->post_status || ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = '<a class="submitdelete" title="' . esc_attr( __( 'Delete this item permanently', 'woocommerce-subscriptions' ) ) . '" href="' . get_delete_post_link( $post->ID, '', true ) . '">' . __( 'Delete Permanently', 'woocommerce-subscriptions' ) . '</a>';
}
}
} else {
if ( 'pending-cancel' === $the_subscription->get_status() ) {
$label = __( 'Cancel Now', 'woocommerce-subscriptions' );
}
$actions[ $status ] = sprintf( '<a href="%s">%s</a>', add_query_arg( 'action', $status, $action_url ), $label );
}
}
}
if ( 'pending' === $the_subscription->get_status() ) {
unset( $actions['active'] );
unset( $actions['trash'] );
} elseif ( ! in_array( $the_subscription->get_status(), array( 'cancelled', 'pending-cancel', 'expired', 'switched', 'suspended' ) ) ) {
unset( $actions['trash'] );
}
$actions = apply_filters( 'woocommerce_subscription_list_table_actions', $actions, $the_subscription );
$column_content .= $wp_list_table->row_actions( $actions );
$column_content = apply_filters( 'woocommerce_subscription_list_table_column_status_content', $column_content, $the_subscription, $actions );
break;
case 'order_title' :
$customer_tip = '';
if ( $address = $the_subscription->get_formatted_billing_address() ) {
$customer_tip .= _x( 'Billing:', 'meaning billing address', 'woocommerce-subscriptions' ) . ' ' . esc_html( $address );
}
if ( $the_subscription->billing_email ) {
// translators: placeholder is customer's billing email
$customer_tip .= '<br/><br/>' . sprintf( __( 'Email: %s', 'woocommerce-subscriptions' ), esc_attr( $the_subscription->billing_email ) );
}
if ( $the_subscription->billing_phone ) {
// translators: placeholder is customer's billing phone number
$customer_tip .= '<br/><br/>' . sprintf( __( 'Tel: %s', 'woocommerce-subscriptions' ), esc_html( $the_subscription->billing_phone ) );
}
if ( ! empty( $customer_tip ) ) {
echo '<div class="tips" data-tip="' . esc_attr( $customer_tip ) . '">';
}
// This is to stop PHP from complaining
$username = '';
if ( $the_subscription->get_user_id() && ( false !== ( $user_info = get_userdata( $the_subscription->get_user_id() ) ) ) ) {
$username = '<a href="user-edit.php?user_id=' . absint( $user_info->ID ) . '">';
if ( $the_subscription->billing_first_name || $the_subscription->billing_last_name ) {
$username .= esc_html( ucfirst( $the_subscription->billing_first_name ) . ' ' . ucfirst( $the_subscription->billing_last_name ) );
} elseif ( $user_info->first_name || $user_info->last_name ) {
$username .= esc_html( ucfirst( $user_info->first_name ) . ' ' . ucfirst( $user_info->last_name ) );
} else {
$username .= esc_html( ucfirst( $user_info->display_name ) );
}
$username .= '</a>';
} elseif ( $the_subscription->billing_first_name || $the_subscription->billing_last_name ) {
$username = trim( $the_subscription->billing_first_name . ' ' . $the_subscription->billing_last_name );
}
// translators: $1: is opening link, $2: is subscription order number, $3: is closing link tag, $4: is user's name
$column_content = sprintf( _x( '%1$s#%2$s%3$s for %4$s', 'Subscription title on admin table. (e.g.: #211 for John Doe)', 'woocommerce-subscriptions' ), '<a href="' . esc_url( admin_url( 'post.php?post=' . absint( $post->ID ) . '&action=edit' ) ) . '">', '<strong>' . esc_attr( $the_subscription->get_order_number() ) . '</strong>', '</a>', $username );
$column_content .= '</div>';
break;
case 'order_items' :
// Display either the item name or item count with a collapsed list of items
$subscription_items = $the_subscription->get_items();
switch ( count( $subscription_items ) ) {
case 0 :
$column_content .= '&ndash;';
break;
case 1 :
foreach ( $the_subscription->get_items() as $item ) {
$_product = apply_filters( 'woocommerce_order_item_product', $the_subscription->get_product_from_item( $item ), $item );
$item_meta = wcs_get_order_item_meta( $item, $_product );
$item_meta_html = $item_meta->display( true, true );
$item_quantity = absint( $item['qty'] );
$item_name = '';
if ( wc_product_sku_enabled() && $_product && $_product->get_sku() ) {
$item_name .= $_product->get_sku() . ' - ';
}
$item_name .= $item['name'];
$item_name = apply_filters( 'woocommerce_order_item_name', $item_name, $item );
$item_name = esc_html( $item_name );
if ( $item_quantity > 1 ) {
$item_name = sprintf( '%s &times; %s', absint( $item_quantity ), $item_name );
}
if ( $_product ) {
$item_name = sprintf( '<a href="%s">%s</a>', get_edit_post_link( $_product->id ), $item_name );
}
ob_start();
?>
<div class="order-item">
<?php echo wp_kses( $item_name, array( 'a' => array( 'href' => array() ) ) ); ?>
<?php if ( $item_meta_html ) : ?>
<a class="tips" href="#" data-tip="<?php echo esc_attr( $item_meta_html ); ?>">[?]</a>
<?php endif; ?>
</div>
<?php
$column_content .= ob_get_clean();
}
break;
default :
$column_content .= '<a href="#" class="show_order_items">' . esc_html( apply_filters( 'woocommerce_admin_order_item_count', sprintf( _n( '%d item', '%d items', $the_subscription->get_item_count(), 'woocommerce-subscriptions' ), $the_subscription->get_item_count() ), $the_subscription ) ) . '</a>';
$column_content .= '<table class="order_items" cellspacing="0">';
foreach ( $the_subscription->get_items() as $item ) {
$_product = apply_filters( 'woocommerce_order_item_product', $the_subscription->get_product_from_item( $item ), $item );
$item_meta = wcs_get_order_item_meta( $item, $_product );
$item_meta_html = $item_meta->display( true, true );
ob_start();
?>
<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_admin_order_item_class', '', $item ) ); ?>">
<td class="qty"><?php echo absint( $item['qty'] ); ?></td>
<td class="name">
<?php
if ( wc_product_sku_enabled() && $_product && $_product->get_sku() ) {
echo esc_html( $_product->get_sku() ) . ' - ';
}
echo esc_html( apply_filters( 'woocommerce_order_item_name', $item['name'], $item ) );
if ( $item_meta_html ) { ?>
<a class="tips" href="#" data-tip="<?php echo esc_attr( $item_meta_html ); ?>">[?]</a>
<?php } ?>
</td>
</tr>
<?php
$column_content .= ob_get_clean();
}
$column_content .= '</table>';
break;
}
break;
case 'recurring_total' :
$column_content .= esc_html( strip_tags( $the_subscription->get_formatted_order_total() ) );
// translators: placeholder is the display name of a payment gateway a subscription was paid by
$column_content .= '<small class="meta">' . esc_html( sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $the_subscription->get_payment_method_to_display() ) ) . '</small>';
break;
case 'start_date':
case 'trial_end_date':
case 'next_payment_date':
case 'last_payment_date':
case 'end_date':
if ( 0 == $the_subscription->get_time( $column, 'gmt' ) ) {
$column_content .= '-';
} else {
$column_content .= sprintf( '<time class="%s" title="%s">%s</time>', esc_attr( $column ), esc_attr( date( __( 'Y/m/d g:i:s A', 'woocommerce-subscriptions' ) , $the_subscription->get_time( $column, 'site' ) ) ), esc_html( $the_subscription->get_date_to_display( $column ) ) );
if ( 'next_payment_date' == $column && $the_subscription->payment_method_supports( 'gateway_scheduled_payments' ) && ! $the_subscription->is_manual() && $the_subscription->has_status( 'active' ) ) {
$column_content .= '<div class="woocommerce-help-tip" data-tip="' . esc_attr__( 'This date should be treated as an estimate only. The payment gateway for this subscription controls when payments are processed.', 'woocommerce-subscriptions' ) . '"></div>';
}
}
$column_content = $column_content;
break;
case 'orders' :
$column_content .= $this->get_related_orders_link( $the_subscription );
break;
}
echo wp_kses( apply_filters( 'woocommerce_subscription_list_table_column_content', $column_content, $the_subscription, $column ), array( 'a' => array( 'class' => array(), 'href' => array(), 'data-tip' => array(), 'title' => array() ), 'time' => array( 'class' => array(), 'title' => array() ), 'mark' => array( 'class' => array(), 'data-tip' => array() ), 'small' => array( 'class' => array() ), 'table' => array( 'class' => array(), 'cellspacing' => array(), 'cellpadding' => array() ), 'tr' => array( 'class' => array() ), 'td' => array( 'class' => array() ), 'div' => array( 'class' => array(), 'data-tip' => array() ), 'br' => array(), 'strong' => array(), 'span' => array( 'class' => array() ), 'p' => array( 'class' => array() ) ) );
}
/**
* Make columns sortable
*
* @param array $columns
* @return array
*/
public function shop_subscription_sortable_columns( $columns ) {
$sortable_columns = array(
'status' => 'post_status',
'order_title' => 'ID',
'recurring_total' => 'order_total',
'start_date' => 'date',
'trial_end_date' => 'trial_end_date',
'next_payment_date' => 'next_payment_date',
'last_payment_date' => 'last_payment_date',
'end_date' => 'end_date',
);
return wp_parse_args( $sortable_columns, $columns );
}
/**
* Search custom fields as well as content.
*
* @access public
* @param WP_Query $wp
* @return void
*/
public function shop_subscription_search_custom_fields( $wp ) {
global $pagenow, $wpdb;
if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_subscription' !== $wp->query_vars['post_type'] ) {
return;
}
$search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_shop_subscription_search_fields', array(
'_order_key',
'_billing_company',
'_billing_address_1',
'_billing_address_2',
'_billing_city',
'_billing_postcode',
'_billing_country',
'_billing_state',
'_billing_email',
'_billing_phone',
'_shipping_address_1',
'_shipping_address_2',
'_shipping_city',
'_shipping_postcode',
'_shipping_country',
'_shipping_state',
) ) );
$search_order_id = str_replace( 'Order #', '', $_GET['s'] );
if ( ! is_numeric( $search_order_id ) ) {
$search_order_id = 0;
}
// Search orders
$post_ids = array_unique( array_merge(
$wpdb->get_col(
$wpdb->prepare( "
SELECT p1.post_id
FROM {$wpdb->postmeta} p1
INNER JOIN {$wpdb->postmeta} p2 ON p1.post_id = p2.post_id
WHERE
( p1.meta_key = '_billing_first_name' AND p2.meta_key = '_billing_last_name' AND CONCAT(p1.meta_value, ' ', p2.meta_value) LIKE '%%%s%%' )
OR
( p1.meta_key = '_shipping_first_name' AND p2.meta_key = '_shipping_last_name' AND CONCAT(p1.meta_value, ' ', p2.meta_value) LIKE '%%%s%%' )
OR
( p1.meta_key IN ('" . implode( "','", esc_sql( $search_fields ) ) . "') AND p1.meta_value LIKE '%%%s%%' )
",
esc_attr( $_GET['s'] ), esc_attr( $_GET['s'] ), esc_attr( $_GET['s'] )
)
),
$wpdb->get_col(
$wpdb->prepare( "
SELECT order_id
FROM {$wpdb->prefix}woocommerce_order_items as order_items
WHERE order_item_name LIKE '%%%s%%'
",
esc_attr( $_GET['s'] )
)
),
$wpdb->get_col(
$wpdb->prepare( "
SELECT p1.ID
FROM {$wpdb->posts} p1
INNER JOIN {$wpdb->postmeta} p2 ON p1.ID = p2.post_id
INNER JOIN {$wpdb->users} u ON p2.meta_value = u.ID
WHERE u.user_email LIKE '%%%s%%'
AND p2.meta_key = '_customer_user'
AND p1.post_type = 'shop_subscription'
",
esc_attr( $_GET['s'] )
)
),
array( $search_order_id )
) );
// Remove s - we don't want to search order name
unset( $wp->query_vars['s'] );
// so we know we're doing this
$wp->query_vars['shop_subscription_search'] = true;
// Search by found posts
$wp->query_vars['post__in'] = $post_ids;
}
/**
* Change the label when searching orders.
*
* @access public
* @param mixed $query
* @return string
*/
public function shop_subscription_search_label( $query ) {
global $pagenow, $typenow;
if ( 'edit.php' !== $pagenow ) {
return $query;
}
if ( 'shop_subscription' !== $typenow ) {
return $query;
}
if ( ! get_query_var( 'shop_subscription_search' ) ) {
return $query;
}
return wp_unslash( $_GET['s'] );
}
/**
* Query vars for custom searches.
*
* @access public
* @param mixed $public_query_vars
* @return array
*/
public function add_custom_query_var( $public_query_vars ) {
$public_query_vars[] = 'sku';
$public_query_vars[] = 'shop_subscription_search';
return $public_query_vars;
}
/**
* Filters and sorting handler
*
* @param array $vars
* @return array
*/
public function request_query( $vars ) {
global $typenow;
if ( 'shop_subscription' === $typenow ) {
// Filter the orders by the posted customer.
if ( isset( $_GET['_customer_user'] ) && $_GET['_customer_user'] > 0 ) {
$vars['meta_query'][] = array(
'key' => '_customer_user',
'value' => (int) $_GET['_customer_user'],
'compare' => '=',
);
}
if ( isset( $_GET['_wcs_product'] ) && $_GET['_wcs_product'] > 0 ) {
$subscription_ids = wcs_get_subscriptions_for_product( $_GET['_wcs_product'] );
if ( ! empty( $subscription_ids ) ) {
$vars['post__in'] = $subscription_ids;
} else {
// no subscriptions contain this product, but we need to pass post__in an ID that no post will have because WP returns all posts when post__in is an empty array: https://core.trac.wordpress.org/ticket/28099
$vars['post__in'] = array( 0 );
}
}
if ( ! empty( $_GET['_payment_method'] ) ) {
$payment_gateway_filter = ( 'none' == $_GET['_payment_method'] ) ? '' : $_GET['_payment_method'];
$query_vars = array(
'post_type' => 'shop_subscription',
'posts_per_page' => -1,
'post_status' => 'any',
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_payment_method',
'value' => $payment_gateway_filter,
),
),
);
// If there are already set post restrictions (post__in) apply them to this query
if ( isset( $vars['post__in'] ) ) {
$query_vars['post__in'] = $vars['post__in'];
}
$subscription_ids = get_posts( $query_vars );
if ( ! empty( $subscription_ids ) ) {
$vars['post__in'] = $subscription_ids;
} else {
$vars['post__in'] = array( 0 );
}
}
// Sorting
if ( isset( $vars['orderby'] ) ) {
switch ( $vars['orderby'] ) {
case 'order_total' :
$vars = array_merge( $vars, array(
'meta_key' => '_order_total',
'orderby' => 'meta_value_num',
) );
break;
case 'last_payment_date' :
add_filter( 'posts_clauses', array( $this, 'posts_clauses' ), 10, 2 );
break;
case 'trial_end_date' :
case 'next_payment_date' :
case 'end_date' :
$vars = array_merge( $vars, array(
'meta_key' => sprintf( '_schedule_%s', str_replace( '_date', '', $vars['orderby'] ) ),
'meta_type' => 'DATETIME',
'orderby' => 'meta_value',
) );
break;
}
}
// Status
if ( ! isset( $vars['post_status'] ) ) {
$vars['post_status'] = array_keys( wcs_get_subscription_statuses() );
}
}
return $vars;
}
/**
* Change messages when a post type is updated.
*
* @param array $messages
* @return array
*/
public function post_updated_messages( $messages ) {
global $post, $post_ID;
$messages['shop_subscription'] = array(
0 => '', // Unused. Messages start at index 1.
1 => __( 'Subscription updated.', 'woocommerce-subscriptions' ),
2 => __( 'Custom field updated.', 'woocommerce-subscriptions' ),
3 => __( 'Custom field deleted.', 'woocommerce-subscriptions' ),
4 => __( 'Subscription updated.', 'woocommerce-subscriptions' ),
// translators: placeholder is previous post title
5 => isset( $_GET['revision'] ) ? sprintf( _x( 'Subscription restored to revision from %s', 'used in post updated messages', 'woocommerce-subscriptions' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
6 => __( 'Subscription updated.', 'woocommerce-subscriptions' ),
7 => __( 'Subscription saved.', 'woocommerce-subscriptions' ),
8 => __( 'Subscription submitted.', 'woocommerce-subscriptions' ),
// translators: php date string
9 => sprintf( __( 'Subscription scheduled for: %1$s.', 'woocommerce-subscriptions' ), '<strong>' . date_i18n( _x( 'M j, Y @ G:i', 'used in "Subscription scheduled for <date>"', 'woocommerce-subscriptions' ), strtotime( $post->post_date ) ) . '</strong>' ),
10 => __( 'Subscription draft updated.', 'woocommerce-subscriptions' ),
);
return $messages;
}
/**
* Returns a clickable link that takes you to a collection of orders relating to the subscription.
*
* @uses self::get_related_orders()
* @since 2.0
* @return string the link string
*/
public function get_related_orders_link( $the_subscription ) {
$order_id = isset( $the_subscription->order->id ) ? $the_subscription->order->id : 0;
return sprintf(
'<a href="%s">%s</a>',
admin_url( 'edit.php?post_status=all&post_type=shop_order&_subscription_related_orders=' . absint( $the_subscription->id ) ),
count( $the_subscription->get_related_orders() )
);
}
/**
* Displays the dropdown for the payment method filter.
*
* @since 2.0
*/
public static function restrict_by_payment_method() {
global $typenow;
if ( 'shop_subscription' !== $typenow ) {
return;
}
$selected_gateway_id = ( ! empty( $_GET['_payment_method'] ) ) ? $_GET['_payment_method'] : ''; ?>
<select class="wcs_payment_method_selector" name="_payment_method" id="_payment_method" class="first">
<option value=""><?php esc_html_e( 'Any Payment Method', 'woocommerce-subscriptions' ) ?></option>
<option value="none" <?php echo esc_attr( 'none' == $selected_gateway_id ? 'selected' : '' ) . '>' . esc_html__( 'None', 'woocommerce-subscriptions' ) ?></option>
<?php
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id == $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
}?>
</select> <?php
}
}
new WCS_Admin_Post_Types();

View File

@@ -0,0 +1,120 @@
<?php
/**
* Related Orders Meta Box
*
* Display the related orders table on the Edit Order and Edit Subscription screens.
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Meta Boxes
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WCS_Meta_Box_Related_Orders Class
*/
class WCS_Meta_Box_Related_Orders {
/**
* Output the metabox
*/
public static function output( $post ) {
if ( wcs_is_subscription( $post->ID ) ) {
$subscription = wcs_get_subscription( $post->ID );
$order = ( false == $subscription->order ) ? $subscription : $subscription->order;
} else {
$order = wc_get_order( $post->ID );
}
add_action( 'woocommerce_subscriptions_related_orders_meta_box_rows', __CLASS__ . '::output_rows', 10 );
include_once( 'views/html-related-orders-table.php' );
do_action( 'woocommerce_subscriptions_related_orders_meta_box', $order, $post );
}
/**
* Displays the renewal orders in the Related Orders meta box.
*
* @param object $post A WordPress post
* @since 2.0
*/
public static function output_rows( $post ) {
$subscriptions = array();
$orders = array();
// On the subscription page, just show related orders
if ( wcs_is_subscription( $post->ID ) ) {
$subscriptions[] = wcs_get_subscription( $post->ID );
} elseif ( wcs_order_contains_subscription( $post->ID, array( 'parent', 'renewal' ) ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $post->ID, array( 'order_type' => array( 'parent', 'renewal' ) ) );
}
// First, display all the subscriptions
foreach ( $subscriptions as $subscription ) {
$subscription->relationship = __( 'Subscription', 'woocommerce-subscriptions' );
$orders[] = $subscription;
}
//Resubscribed
$initial_subscriptions = array();
if ( wcs_is_subscription( $post->ID ) ) {
$initial_subscriptions = wcs_get_subscriptions_for_resubscribe_order( $post->ID );
$resubscribed_subscriptions = get_posts( array(
'meta_key' => '_subscription_resubscribe',
'meta_value' => $post->ID,
'post_type' => 'shop_subscription',
'post_status' => 'any',
'posts_per_page' => -1,
) );
foreach ( $resubscribed_subscriptions as $subscription ) {
$subscription = wcs_get_subscription( $subscription );
$subscription->relationship = _x( 'Resubscribed Subscription', 'relation to order', 'woocommerce-subscriptions' );
$orders[] = $subscription;
}
} else if ( wcs_order_contains_subscription( $post->ID, array( 'resubscribe' ) ) ) {
$initial_subscriptions = wcs_get_subscriptions_for_order( $post->ID, array( 'order_type' => array( 'resubscribe' ) ) );
}
foreach ( $initial_subscriptions as $subscription ) {
$subscription->relationship = _x( 'Initial Subscription', 'relation to order', 'woocommerce-subscriptions' );
$orders[] = $subscription;
}
// Now, if we're on a single subscription or renewal order's page, display the parent orders
if ( 1 == count( $subscriptions ) ) {
foreach ( $subscriptions as $subscription ) {
if ( false !== $subscription->order ) {
$subscription->order->relationship = _x( 'Parent Order', 'relation to order', 'woocommerce-subscriptions' );
$orders[] = $subscription->order;
}
}
}
// Finally, display the renewal orders
foreach ( $subscriptions as $subscription ) {
foreach ( $subscription->get_related_orders( 'all', 'renewal' ) as $order ) {
$order->relationship = _x( 'Renewal Order', 'relation to order', 'woocommerce-subscriptions' );
$orders[] = $order;
}
}
foreach ( $orders as $order ) {
if ( $order->id == $post->ID ) {
continue;
}
include( 'views/html-related-orders-row.php' );
}
}
}

View File

@@ -0,0 +1,281 @@
<?php
/**
* Order Data
*
* Functions for displaying the order data meta box.
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Meta Boxes
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WCS_Meta_Box_Subscription_Data Class
*/
class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
/**
* Output the metabox
*/
public static function output( $post ) {
global $the_subscription;
if ( ! is_object( $the_subscription ) || $the_subscription->id !== $post->ID ) {
$the_subscription = wc_get_order( $post->ID );
}
$subscription = $the_subscription;
self::init_address_fields();
wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );
?>
<style type="text/css">
#post-body-content, #titlediv, #major-publishing-actions, #minor-publishing-actions, #visibility, #submitdiv { display:none }
</style>
<div class="panel-wrap woocommerce">
<input name="post_title" type="hidden" value="<?php echo empty( $post->post_title ) ? esc_attr( get_post_type_object( $subscription->post->post_type )->labels->singular_name ) : esc_attr( $post->post_title ); ?>" />
<input name="post_status" type="hidden" value="<?php echo esc_attr( 'wc-' . $subscription->get_status() ); ?>" />
<div id="order_data" class="panel">
<h2><?php
// translators: placeholder is the ID of the subscription
printf( esc_html_x( 'Subscription #%s details', 'edit subscription header', 'woocommerce-subscriptions' ), esc_html( $subscription->get_order_number() ) ); ?></h2>
<div class="order_data_column_container">
<div class="order_data_column">
<p class="form-field form-field-wide wc-customer-user">
<label for="customer_user"><?php esc_html_e( 'Customer:', 'woocommerce-subscriptions' ) ?> <?php
if ( ! empty( $subscription->customer_user ) ) {
$args = array(
'post_status' => 'all',
'post_type' => 'shop_subscription',
'_customer_user' => absint( $subscription->customer_user ),
);
printf( '<a href="%s">%s &rarr;</a>',
esc_url( add_query_arg( $args, admin_url( 'edit.php' ) ) ),
esc_html__( 'View other subscriptions', 'woocommerce-subscriptions' )
);
}
?></label>
<?php
$user_string = '';
$user_id = '';
if ( ! empty( $subscription->customer_user ) && ( false !== get_userdata( $subscription->customer_user ) ) ) {
$user_id = absint( $subscription->customer_user );
$user = get_user_by( 'id', $user_id );
$user_string = esc_html( $user->display_name ) . ' (#' . absint( $user->ID ) . ' &ndash; ' . esc_html( $user->user_email );
}
?>
<input type="hidden" class="wc-customer-search" id="customer_user" name="customer_user" data-placeholder="<?php esc_attr_e( 'Search for a customer&hellip;', 'woocommerce-subscriptions' ); ?>" data-selected="<?php echo esc_attr( $user_string ); ?>" value="<?php echo esc_attr( $user_id ); ?>" />
</p>
<p class="form-field form-field-wide">
<label for="order_status"><?php esc_html_e( 'Subscription Status:', 'woocommerce-subscriptions' ); ?></label>
<select id="order_status" name="order_status">
<?php
$statuses = wcs_get_subscription_statuses();
foreach ( $statuses as $status => $status_name ) {
if ( ! $subscription->can_be_updated_to( $status ) && ! $subscription->has_status( str_replace( 'wc-', '', $status ) ) ) {
continue;
}
echo '<option value="' . esc_attr( $status ) . '" ' . selected( $status, 'wc-' . $subscription->get_status(), false ) . '>' . esc_html( $status_name ) . '</option>';
}
?>
</select>
</p>
<?php do_action( 'woocommerce_admin_order_data_after_order_details', $subscription ); ?>
</div>
<div class="order_data_column">
<h4><?php esc_html_e( 'Billing Details', 'woocommerce-subscriptions' ); ?> <a class="edit_address" href="#"><a href="#" class="tips load_customer_billing" data-tip="Load billing address" style="display:none;">Load billing address</a></a></h4>
<?php
// Display values
echo '<div class="address">';
if ( $subscription->get_formatted_billing_address() ) {
echo '<p><strong>' . esc_html__( 'Address', 'woocommerce-subscriptions' ) . ':</strong>' . wp_kses( $subscription->get_formatted_billing_address(), array( 'br' => array() ) ) . '</p>';
} else {
echo '<p class="none_set"><strong>' . esc_html__( 'Address', 'woocommerce-subscriptions' ) . ':</strong> ' . esc_html__( 'No billing address set.', 'woocommerce-subscriptions' ) . '</p>';
}
foreach ( self::$billing_fields as $key => $field ) {
if ( isset( $field['show'] ) && false === $field['show'] ) {
continue;
}
$field_name = 'billing_' . $key;
if ( $subscription->$field_name ) {
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $subscription->$field_name ) ) ) . '</p>';
}
}
echo '<p' . ( ! empty( $subscription->payment_method ) ? ' class="' . esc_attr( $subscription->payment_method ) . '"' : '' ) . '><strong>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':</strong>' . wp_kses_post( nl2br( $subscription->get_payment_method_to_display() ) );
// Display help tip
if ( ! empty( $subscription->payment_method ) && ! $subscription->is_manual() ) {
echo '<img class="help_tip" data-tip="Gateway ID: [' . esc_attr( $subscription->payment_gateway->id ) . ']" src="' . esc_url( WC()->plugin_url() ) . '/assets/images/help.png" height="16" width="16" />';
}
echo '</p>';
echo '</div>';
// Display form
echo '<div class="edit_address">';
foreach ( self::$billing_fields as $key => $field ) {
if ( ! isset( $field['type'] ) ) {
$field['type'] = 'text';
}
switch ( $field['type'] ) {
case 'select' :
// allow for setting a default value programaticaly, and draw the selectbox
woocommerce_wp_select( array( 'id' => '_billing_' . $key, 'label' => $field['label'], 'options' => $field['options'], 'value' => isset( $field['value'] ) ? $field['value'] : null ) );
break;
default :
// allow for setting a default value programaticaly, and draw the textbox
woocommerce_wp_text_input( array( 'id' => '_billing_' . $key, 'label' => $field['label'], 'value' => isset( $field['value'] ) ? $field['value'] : null ) );
break;
}
}
WCS_Change_Payment_Method_Admin::display_fields( $subscription );
echo '</div>';
do_action( 'woocommerce_admin_order_data_after_billing_address', $subscription );
?>
</div>
<div class="order_data_column">
<h4><?php esc_html_e( 'Shipping Details', 'woocommerce-subscriptions' ); ?>
<a class="edit_address" href="#">
<a href="#" class="tips billing-same-as-shipping" data-tip="Copy from billing" style="display:none;">Copy from billing</a>
<a href="#" class="tips load_customer_shipping" data-tip="Load shipping address" style="display:none;">Load shipping address</a>
</a>
</h4>
<?php
// Display values
echo '<div class="address">';
if ( $subscription->get_formatted_shipping_address() ) {
echo '<p><strong>' . esc_html__( 'Address', 'woocommerce-subscriptions' ) . ':</strong>' . wp_kses( $subscription->get_formatted_shipping_address(), array( 'br' => array() ) ) . '</p>';
} else {
echo '<p class="none_set"><strong>' . esc_html__( 'Address', 'woocommerce-subscriptions' ) . ':</strong> ' . esc_html__( 'No shipping address set.', 'woocommerce-subscriptions' ) . '</p>';
}
if ( self::$shipping_fields ) {
foreach ( self::$shipping_fields as $key => $field ) {
if ( isset( $field['show'] ) && false === $field['show'] ) {
continue;
}
$field_name = 'shipping_' . $key;
if ( ! empty( $subscription->$field_name ) ) {
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $subscription->$field_name ) ) ) . '</p>';
}
}
}
if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) && $post->post_excerpt ) {
echo '<p><strong>' . esc_html__( 'Customer Note:', 'woocommerce-subscriptions' ) . '</strong> ' . wp_kses_post( nl2br( $post->post_excerpt ) ) . '</p>';
}
echo '</div>';
// Display form
echo '<div class="edit_address">';
if ( self::$shipping_fields ) {
foreach ( self::$shipping_fields as $key => $field ) {
if ( ! isset( $field['type'] ) ) {
$field['type'] = 'text';
}
switch ( $field['type'] ) {
case 'select' :
woocommerce_wp_select( array( 'id' => '_shipping_' . $key, 'label' => $field['label'], 'options' => $field['options'] ) );
break;
default :
woocommerce_wp_text_input( array( 'id' => '_shipping_' . $key, 'label' => $field['label'] ) );
break;
}
}
}
if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) {
?>
<p class="form-field form-field-wide"><label for="excerpt"><?php esc_html_e( 'Customer Note:', 'woocommerce-subscriptions' ) ?></label>
<textarea rows="1" cols="40" name="excerpt" tabindex="6" id="excerpt" placeholder="<?php esc_attr_e( 'Customer\'s notes about the order', 'woocommerce-subscriptions' ); ?>"><?php echo wp_kses_post( $post->post_excerpt ); ?></textarea></p>
<?php
}
echo '</div>';
do_action( 'woocommerce_admin_order_data_after_shipping_address', $subscription );
?>
</div>
</div>
<div class="clear"></div>
</div>
</div>
<?php
}
/**
* Save meta box data
*/
public static function save( $post_id, $post ) {
global $wpdb;
if ( 'shop_subscription' != $post->post_type || empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) {
return;
}
self::init_address_fields();
// Update meta
update_post_meta( $post_id, '_customer_user', absint( $_POST['customer_user'] ) );
if ( self::$billing_fields ) {
foreach ( self::$billing_fields as $key => $field ) {
update_post_meta( $post_id, '_billing_' . $key, wc_clean( $_POST[ '_billing_' . $key ] ) );
}
}
if ( self::$shipping_fields ) {
foreach ( self::$shipping_fields as $key => $field ) {
update_post_meta( $post_id, '_shipping_' . $key, wc_clean( $_POST[ '_shipping_' . $key ] ) );
}
}
$subscription = wcs_get_subscription( $post_id );
try {
WCS_Change_Payment_Method_Admin::save_meta( $subscription );
if ( 'cancelled' == $_POST['order_status'] ) {
$subscription->cancel_order();
} else {
$subscription->update_status( $_POST['order_status'], '', true );
}
} catch ( Exception $e ) {
// translators: placeholder is error message from the payment gateway or subscriptions when updating the status
wcs_add_admin_notice( sprintf( __( 'Error updating subscription: %s', 'woocommerce-subscriptions' ), $e->getMessage() ), 'error' );
}
do_action( 'woocommerce_process_shop_subscription_meta', $post_id, $post );
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Subscription Billing Schedule
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Meta Boxes
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WCS_Meta_Box_Schedule
*/
class WCS_Meta_Box_Schedule {
/**
* Output the metabox
*/
public static function output( $post ) {
global $post, $the_subscription;
if ( empty( $the_subscription ) ) {
$the_subscription = wcs_get_subscription( $post->ID );
}
include( 'views/html-subscription-schedule.php' );
}
/**
* Save meta box data
*/
public static function save( $post_id, $post ) {
if ( 'shop_subscription' == $post->post_type && ! empty( $_POST['woocommerce_meta_nonce'] ) && wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) {
if ( isset( $_POST['_billing_interval'] ) ) {
update_post_meta( $post_id, '_billing_interval', $_POST['_billing_interval'] );
}
if ( ! empty( $_POST['_billing_period'] ) ) {
update_post_meta( $post_id, '_billing_period', $_POST['_billing_period'] );
}
$subscription = wcs_get_subscription( $post_id );
$dates = array();
foreach ( wcs_get_subscription_date_types() as $date_key => $date_label ) {
if ( 'last_payment' == $date_key ) {
continue;
}
$utc_timestamp_key = $date_key . '_timestamp_utc';
// A subscription needs a start date, even if it wasn't set
if ( isset( $_POST[ $utc_timestamp_key ] ) ) {
$datetime = $_POST[ $utc_timestamp_key ];
} elseif ( 'start' === $date_key ) {
$datetime = current_time( 'timestamp', true );
} else { // No date to set
continue;
}
$dates[ $date_key ] = date( 'Y-m-d H:i:s', $datetime );
}
try {
$subscription->update_dates( $dates, 'gmt' );
wp_cache_delete( $post_id, 'posts' );
} catch ( Exception $e ) {
wcs_add_admin_notice( $e->getMessage(), 'error' );
}
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Display a row in the related orders table for a subscription or order
*
* @var array $order A WC_Order or WC_Subscription order object to display
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<tr>
<td>
<a href="<?php echo esc_url( get_edit_post_link( $order->id ) ); ?>">
<?php echo sprintf( esc_html_x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), esc_html( $order->get_order_number() ) ); ?>
</a>
</td>
<td>
<?php echo esc_html( $order->relationship ); ?>
</td>
<td>
<?php
$timestamp_gmt = strtotime( $order->post->post_date_gmt );
if ( $timestamp_gmt > 0 ) {
// translators: php date format
$t_time = get_the_time( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $order->post );
$time_diff = $timestamp_gmt - current_time( 'timestamp', true );
if ( $time_diff > 0 && $time_diff < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
$date_to_display = sprintf( __( 'In %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} elseif ( $time_diff < 0 && absint( $time_diff ) < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
$date_to_display = sprintf( __( '%s ago', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} else {
$timestamp_site = strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp_gmt ) ) );
$date_to_display = date_i18n( wc_date_format(), $timestamp_site ) . ' ' . date_i18n( wc_time_format(), $timestamp_site );
}
} else {
$t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' );
} ?>
<abbr title="<?php echo esc_attr( $t_time ); ?>">
<?php echo esc_html( apply_filters( 'post_date_column_time', $date_to_display, $order->post ) ); ?>
</abbr>
</td>
<td>
<?php echo esc_html( ucwords( $order->get_status() ) ); ?>
</td>
<td>
<span class="amount"><?php echo wp_kses( $order->get_formatted_order_total(), array( 'small' => array(), 'span' => array( 'class' => array() ), 'del' => array(), 'ins' => array() ) ); ?></span>
</td>
</tr>

View File

@@ -0,0 +1,28 @@
<?php
/**
* Display the related orders for a subscription or order
*
* @var object $post The primitive post object that is being displayed (as an order or subscription)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<div class="woocommerce_subscriptions_related_orders">
<table>
<thead>
<tr>
<th><?php esc_html_e( 'Order Number', 'woocommerce-subscriptions' ); ?></th>
<th><?php esc_html_e( 'Relationship', 'woocommerce-subscriptions' ); ?></th>
<th><?php esc_html_e( 'Date', 'woocommerce-subscriptions' ); ?></th>
<th><?php esc_html_e( 'Status', 'woocommerce-subscriptions' ); ?></th>
<th><?php echo esc_html_x( 'Total', 'table heading', 'woocommerce-subscriptions' ); ?></th>
</tr>
</thead>
<tbody>
<?php do_action( 'woocommerce_subscriptions_related_orders_meta_box_rows', $post ); ?>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,61 @@
<?php
/**
* Display the billing schedule for a subscription
*
* @var object $the_subscription The WC_Subscription object to display the billing schedule for
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<div class="wc-metaboxes-wrapper">
<div id="billing-schedule">
<?php if ( $the_subscription->can_date_be_updated( 'next_payment' ) ) : ?>
<div class="billing-schedule-edit wcs-date-input"><?php
// Subscription Period Interval
echo woocommerce_wp_select( array(
'id' => '_billing_interval',
'class' => 'billing_interval',
'label' => __( 'Recurring:', 'woocommerce-subscriptions' ),
'value' => empty( $the_subscription->billing_interval ) ? 1 : $the_subscription->billing_interval,
'options' => wcs_get_subscription_period_interval_strings(),
)
);
// Billing Period
echo woocommerce_wp_select( array(
'id' => '_billing_period',
'class' => 'billing_period',
'label' => __( 'Billing Period', 'woocommerce-subscriptions' ),
'value' => empty( $the_subscription->billing_period ) ? 'month' : $the_subscription->billing_period,
'options' => wcs_get_subscription_period_strings(),
)
);
?>
<input type="hidden" name="wcs-lengths" id="wcs-lengths" data-subscription_lengths="<?php echo esc_attr( wcs_json_encode( wcs_get_subscription_ranges() ) ); ?>">
</div>
<?php else : ?>
<strong><?php esc_html_e( 'Recurring:', 'woocommerce-subscriptions' ); ?></strong>
<?php printf( '%s %s', esc_html( wcs_get_subscription_period_interval_strings( $the_subscription->billing_interval ) ), esc_html( wcs_get_subscription_period_strings( 1, $the_subscription->billing_period ) ) ); ?>
<?php endif; ?>
</div>
<?php foreach ( wcs_get_subscription_date_types() as $date_key => $date_label ) : ?>
<?php if ( 'last_payment' === $date_key ) : ?>
<?php continue; ?>
<?php endif;?>
<div id="subscription-<?php echo esc_attr( $date_key ); ?>-date" class="date-fields">
<strong><?php echo esc_html( $date_label ); ?>:</strong>
<input type="hidden" name="<?php echo esc_attr( $date_key ); ?>_timestamp_utc" id="<?php echo esc_attr( $date_key ); ?>_timestamp_utc" value="<?php echo esc_attr( $the_subscription->get_time( $date_key, 'gmt' ) ); ?>"/>
<?php if ( $the_subscription->can_date_be_updated( $date_key ) ) : ?>
<?php echo wp_kses( wcs_date_input( $the_subscription->get_time( $date_key, 'site' ), array( 'name_attr' => $date_key ) ), array( 'input' => array( 'type' => array(), 'class' => array(), 'placeholder' => array(), 'name' => array(), 'id' => array(), 'maxlength' => array(), 'size' => array(), 'value' => array(), 'patten' => array() ), 'div' => array( 'class' => array() ), 'span' => array(), 'br' => array() ) ); ?>
<?php else : ?>
<?php echo esc_html( $the_subscription->get_date_to_display( $date_key ) ); ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
<p><?php esc_html_e( 'Timezone:', 'woocommerce-subscriptions' ); ?> <span id="wcs-timezone"><?php esc_html_e( 'Error: unable to find timezone of your browser.', 'woocommerce-subscriptions' ); ?></span></p>
</div>

View File

@@ -0,0 +1,71 @@
<?php
/**
* WooCommerce Subscriptions Admin Functions
*
* @author Prospress
* @category Core
* @package WooCommerce Subscriptions/Functions
* @version 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Store a message to display via @see wcs_display_admin_notices().
*
* @param string The message to display
* @since 2.0
*/
function wcs_add_admin_notice( $message, $notice_type = 'success' ) {
$notices = get_transient( '_wcs_admin_notices' );
if ( false === $notices ) {
$notices = array();
}
$notices[ $notice_type ][] = $message;
set_transient( '_wcs_admin_notices', $notices, 60 * 60 );
}
/**
* Display any notices added with @see wcs_add_admin_notice()
*
* This method is also hooked to 'admin_notices' to display notices there.
*
* @since 2.0
*/
function wcs_display_admin_notices( $clear = true ) {
$notices = get_transient( '_wcs_admin_notices' );
if ( false !== $notices && ! empty( $notices ) ) {
if ( ! empty( $notices['success'] ) ) {
array_walk( $notices['success'], 'esc_html' );
echo '<div id="moderated" class="updated"><p>' . wp_kses_post( implode( "</p>\n<p>", $notices['success'] ) ) . '</p></div>';
}
if ( ! empty( $notices['error'] ) ) {
array_walk( $notices['error'], 'esc_html' );
echo '<div id="moderated" class="error"><p>' . wp_kses_post( implode( "</p>\n<p>", $notices['error'] ) ) . '</p></div>';
}
}
if ( false !== $clear ) {
wcs_clear_admin_notices();
}
}
add_action( 'admin_notices', 'wcs_display_admin_notices' );
/**
* Delete any admin notices we stored for display later.
*
* @since 2.0
*/
function wcs_clear_admin_notices() {
delete_transient( '_wcs_admin_notices' );
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* WooCommerce Subscriptions API Customers Class
*
* Handles requests to the /customers/subscriptions endpoint
*
* @author Prospress
* @category API
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class: WC_Subscription_API_Customers
* extends @see WC_API_Customer to provide functionality to subscriptions
*
* @since 2.0
*/
class WC_API_Subscriptions_Customers extends WC_API_Customers {
public function __construct( WC_API_Server $server ) {
parent::__construct( $server );
// remove the add customer data because WC_API_Customers already did that
remove_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10 );
// remove the modify user query because WC_API_Customers already did that
remove_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
}
/**
* Register the routes for this class
*
* GET /customers/<id>/subscriptions
*
* @since 2.0
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
$routes = parent::register_routes( $routes );
# GET /customers/<id>/subscriptions
$routes[ $this->base . '/(?P<id>\d+)/subscriptions' ] = array(
array( array( $this, 'get_customer_subscriptions' ), WC_API_SERVER::READABLE ),
);
return $routes;
}
/**
* WCS API function to get all the subscriptions tied to a particular customer.
*
* @since 2.0
* @param $id int
* @param $fields array
*/
public function get_customer_subscriptions( $id, $fields = null ) {
global $wpdb;
// check the customer id given is a valid customer in the store. We're able to leech off WC-API for this.
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) ) {
return $id;
}
$subscription_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID, post_date_gmt
FROM {$wpdb->posts} AS posts
LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
WHERE meta.meta_key = '_customer_user'
AND meta.meta_value = '%d'
AND posts.post_type = 'shop_subscription'
AND posts.post_status IN ( '" . implode( "','", array_keys( wcs_get_subscription_statuses() ) ) . "' )
GROUP BY posts.ID
ORDER BY posts.post_date_gmt DESC
", $id ) );
$subscriptions = array();
foreach ( $subscription_ids as $subscription_id ) {
$subscriptions[] = WC()->api->WC_API_Subscriptions->get_subscription( $subscription_id, $fields );
}
return array( 'customer_subscriptions' => apply_filters( 'wc_subscriptions_api_customer_subscriptions', $subscriptions, $id, $fields, $subscription_ids, $this->server ) );
}
}

View File

@@ -0,0 +1,702 @@
<?php
/**
* WooCommerce Subscriptions API Subscriptions Class
*
* Handles requests to the /subscriptions endpoint
*
* @author Prospress
* @category API
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_API_Subscriptions extends WC_API_Orders {
/* @var string $base the route base */
protected $base = '/subscriptions';
/**
* Register the routes for this class
*
* GET|POST /subscriptions
* GET /subscriptions/count
* GET|PUT|DELETE /subscriptions/<subscription_id>
* GET /subscriptions/<subscription_id>/notes
* GET /subscriptions/<subscription_id>/notes/<id>
* GET /subscriptions/<subscription_id>/orders
*
* @since 2.0
* @param array $routes
* @return array $routes
*/
public function register_routes( $routes ) {
$this->post_type = 'shop_subscription';
# GET /subscriptions
$routes[ $this->base ] = array(
array( array( $this, 'get_subscriptions' ), WC_API_Server::READABLE ),
array( array( $this, 'create_subscription' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /subscriptions/count
$routes[ $this->base . '/count' ] = array(
array( array( $this, 'get_subscription_count' ), WC_API_Server::READABLE ),
);
# GET /subscriptions/statuses
$routes[ $this->base . '/statuses' ] = array(
array( array( $this, 'get_statuses' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /subscriptions/<subscription_id>
$routes[ $this->base . '/(?P<subscription_id>\d+)' ] = array(
array( array( $this, 'get_subscription' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_subscription' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_subscription' ), WC_API_Server::DELETABLE ),
);
# GET /subscriptions/<subscription_id>/notes
$routes[ $this->base . '/(?P<subscription_id>\d+)/notes' ] = array(
array( array( $this, 'get_subscription_notes' ), WC_API_Server::READABLE ),
array( array( $this, 'create_subscription_note' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /subscriptions/<subscription_id>/notes/<id>
$routes[ $this->base . '/(?P<subscription_id>\d+)/notes/(?P<id>\d+)' ] = array(
array( array( $this, 'get_subscription_note' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_subscription_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_subscription_note' ), WC_API_SERVER::DELETABLE ),
);
# GET /subscriptions/<subscription_id>/orders
$routes[ $this->base . '/(?P<subscription_id>\d+)/orders' ] = array(
array( array( $this, 'get_all_subscription_orders' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Ensures the statuses are in the correct format and are valid subscription statues.
*
* @since 2.0
* @param $status string | array
*/
protected function format_statuses( $status = null ) {
$statuses = 'any';
if ( ! empty( $status ) ) {
// get list of statuses and check each on is in the correct format and is valid
$statuses = explode( ',', $status );
// attach the wc- prefix to those statuses that have not specified it
foreach ( $statuses as &$status ) {
if ( 'wc-' != substr( $status, 0, 3 ) ) {
$status = 'wc-' . $status;
if ( ! array_key_exists( $status, wcs_get_subscription_statuses() ) ) {
return new WP_Error( 'wcs_api_invalid_subscription_status', __( 'Invalid subscription status given.', 'woocommerce-subscriptions' ) );
}
}
}
}
return $statuses;
}
/**
* Gets all subscriptions
*
* @since 2.0
* @param null $fields
* @param array $filter
* @param null $status
* @param null $page
* @return array
*/
public function get_subscriptions( $fields = null, $filter = array(), $status = null, $page = 1 ) {
// check user permissions
if ( ! current_user_can( 'read_private_shop_orders' ) ) {
return new WP_Error( 'wcs_api_user_cannot_read_susbcription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) );
}
$status = $this->format_statuses( $status );
if ( is_wp_error( $status ) ) {
return $status;
}
$filter['page'] = $page;
$base_args = array(
'post_status' => $status,
'post_type' => 'shop_subscription',
'fields' => 'ids',
);
$subscriptions = array();
$query_args = array_merge( $base_args, $filter );
$query = $this->query_orders( $query_args );
foreach ( $query->posts as $subscription_id ) {
if ( ! $this->is_readable( $subscription_id ) ) {
continue;
}
$subscriptions[] = current( $this->get_subscription( $subscription_id, $fields, $filter ) );
}
$this->server->add_pagination_headers( $query );
return array( 'subscriptions' => apply_filters( 'wcs_api_get_subscriptions_response', $subscriptions, $fields, $filter, $status, $page, $this->server ) );
}
/**
* Creating Subscription.
*
* @since 2.0
* @param array data raw order data
* @return array
*/
public function create_subscription( $data ) {
$data = isset( $data['subscription'] ) ? $data['subscription'] : array();
try {
if ( ! current_user_can( 'publish_shop_orders' ) ) {
throw new WC_API_Exception( 'wcs_api_user_cannot_create_subscription', __( 'You do not have permission to create subscriptions', 'woocommerce-subscriptions' ), 401 );
}
$data['order'] = $data;
remove_filter( 'woocommerce_api_order_response', array( WC()->api->WC_API_Customers, 'add_customer_data' ), 10 );
$subscription = $this->create_order( $data );
add_filter( 'woocommerce_api_order_response', array( WC()->api->WC_API_Customers, 'add_customer_data' ), 10, 2 );
unset( $data['order'] );
if ( is_wp_error( $subscription ) ) {
$data = $subscription->get_error_data();
throw new WC_API_Exception( $subscription->get_error_code(), $subscription->get_error_message(), $data['status'] );
}
$subscription = wcs_get_subscription( $subscription['order']['id'] );
unset( $data['billing_period'] );
unset( $data['billing_interval'] );
$this->update_schedule( $subscription, $data );
// allow order total to be manually set, especially for those cases where there's no line items added to the subscription
if ( isset( $data['order_total'] ) ) {
update_post_meta( $subscription->id, '_order_total', wc_format_decimal( $data['order_total'], get_option( 'woocommerce_price_num_decimals' ) ) );
}
if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {
$this->update_payment_method( $subscription, $data['payment_details'], false );
}
do_action( 'wcs_api_subscription_created', $subscription->id, $this );
return array( 'creating_subscription' => $this->get_subscription( $subscription->id ) );
} catch ( WC_API_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
} catch ( Exception $e ) {
$response = array( 'error', array( 'wcs_api_error_create_subscription' => array( 'message' => $e->getMessage(), 'status' => $e->getCode() ) ) );
// show the subscription in response if it was still created but errored.
if ( ! empty( $subscription ) && ! is_wp_error( $subscription ) ) {
$response['creating_subscription'] = $this->get_subscription( $subscription->id );
}
return $response;
}
}
/**
* Edit Subscription
*
* @since 2.0
* @return array
*/
public function edit_subscription( $subscription_id, $data, $fields = null ) {
$data = apply_filters( 'wcs_api_edit_subscription_data', isset( $data['subscription'] ) ? $data['subscription'] : array(), $subscription_id, $fields );
try {
$subscription_id = $this->validate_request( $subscription_id, $this->post_type, 'edit' );
if ( is_wp_error( $subscription_id ) ) {
throw new WC_API_Exception( 'wcs_api_cannot_edit_subscription', __( 'The requested subscription cannot be edited.', 'woocommerce-subscriptions' ), 400 );
}
$subscription = wcs_get_subscription( $subscription_id );
if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {
if ( empty( $data['payment_details']['method_id'] ) || 'manual' == $data['payment_details']['method_id'] ) {
$subscription->set_payment_method( '' );
} else {
$this->update_payment_method( $subscription, $data['payment_details'], true );
}
}
if ( ! empty( $data['order_id'] ) ) {
wp_update_post( array( 'ID' => $subscription_id, 'post_parent' => $data['order_id'] ) );
}
// set $data['order'] = $data['subscription'] so that edit_order can read in the request
$data['order'] = $data;
// edit subscription by calling WC_API_Orders::edit_order()
$edited = $this->edit_order( $subscription_id, $data, $fields );
// remove part of the array that isn't being used
unset( $data['order'] );
if ( is_wp_error( $edited ) ) {
$data = $edited->get_error_data();
// translators: placeholder is error message
throw new WC_API_Exception( 'wcs_api_cannot_edit_subscription', sprintf( _x( 'Edit subscription failed with error: %s', 'API error message when editing the order failed', 'woocommerce-subscriptions' ), $edited->get_error_message() ), $data['status'] );
}
$this->update_schedule( $subscription, $data );
do_action( 'wcs_api_subscription_updated', $subscription_id, $data, $this );
return $this->get_subscription( $subscription_id );
} catch ( WC_API_Excpetion $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
} catch ( Exception $e ) {
return new WP_Error( 'wcs_api_cannot_edit_subscription', $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Setup the new payment information to call WC_Subscription::set_payment_method()
*
* @param $subscription WC_Subscription
* @param $payment_details array payment data from api request
* @since 2.0
*/
public function update_payment_method( $subscription, $payment_details, $updating ) {
global $wpdb;
$payment_gateways = WC()->payment_gateways->get_available_payment_gateways();
$payment_method = ( ! empty( $payment_details['method_id'] ) ) ? $payment_details['method_id'] : 'manual';
$payment_gateway = ( isset( $payment_gateways[ $payment_details['method_id'] ] ) ) ? $payment_gateways[ $payment_details['method_id'] ] : '';
try {
$wpdb->query( 'START TRANSACTION' );
if ( $updating && ! array_key_exists( $payment_method, WCS_Change_Payment_Method_Admin::get_valid_payment_methods( $subscription ) ) ) {
throw new Exception( 'wcs_api_edit_subscription_error', __( 'Gateway does not support admin changing the payment method on a Subscription.', 'woocommerce-subscriptions' ) );
}
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
if ( ! empty( $payment_gateway ) && isset( $payment_method_meta[ $payment_gateway->id ] ) ) {
$payment_method_meta = $payment_method_meta[ $payment_gateway->id ];
if ( ! empty( $payment_method_meta ) ) {
foreach ( $payment_method_meta as $meta_table => &$meta ) {
if ( ! is_array( $meta ) ) {
continue;
}
foreach ( $meta as $meta_key => &$meta_data ) {
if ( isset( $payment_details[ $meta_table ][ $meta_key ] ) ) {
$meta_data['value'] = $payment_details[ $meta_table ][ $meta_key ];
}
}
}
}
}
if ( empty( $subscription->payment_gateway ) ) {
$subscription->payment_gateway = $payment_gateway;
}
$subscription->set_payment_method( $payment_gateway, $payment_method_meta );
$wpdb->query( 'COMMIT' );
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
// translators: 1$: gateway id, 2$: error message
throw new Exception( sprintf( __( 'Subscription payment method could not be set to %1$s and has been set to manual with error message: %2$s', 'woocommerce-subscriptions' ), ( ! empty( $payment_gateway->id ) ) ? $payment_gateway->id : 'manual', $e->getMessage() ) );
}
}
/**
* Override WC_API_Order::create_base_order() to create a subscription
* instead of a WC_Order when calling WC_API_Order::create_order().
*
* @since 2.0
* @param $array
* @return WC_Subscription
*/
protected function create_base_order( $args, $data ) {
$args['order_id'] = ( ! empty( $data['order_id'] ) ) ? $data['order_id'] : '';
$args['billing_interval'] = ( ! empty( $data['billing_interval'] ) ) ? $data['billing_interval'] : '';
$args['billing_period'] = ( ! empty( $data['billing_period'] ) ) ? $data['billing_period'] : '';
return wcs_create_subscription( $args );
}
/**
* Update all subscription specific meta (i.e. Billing interval/period and date fields )
*
* @since 2.0
* @param $data array
* @param $subscription WC_Subscription
*/
protected function update_schedule( $subscription, $data ) {
if ( isset( $data['billing_interval'] ) ) {
$interval = absint( $data['billing_interval'] );
if ( 0 == $interval ) {
throw new WC_API_Exception( 'wcs_api_invalid_subscription_meta', __( 'Invalid subscription billing interval given. Must be an integer greater than 0.', 'woocommerce-subscriptions' ), 400 );
}
update_post_meta( $subscription->id, '_billing_interval', $interval );
}
if ( ! empty( $data['billing_period'] ) ) {
$period = strtolower( $data['billing_period'] );
if ( ! in_array( $period, array_keys( wcs_get_subscription_period_strings() ) ) ) {
throw new WC_API_Exception( 'wcs_api_invalid_subscription_meta', __( 'Invalid subscription billing period given.', 'woocommerce-subscriptions' ), 400 );
}
update_post_meta( $subscription->id, '_billing_period', $period );
}
$dates_to_update = array();
foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) {
if ( isset( $data[ $date_type . '_date' ] ) ) {
$dates_to_update[ $date_type ] = $data[ $date_type . '_date' ];
}
}
if ( ! empty( $dates_to_update ) ) {
$subscription->update_dates( $dates_to_update );
}
}
/**
* Delete subscription
*
* @since 2.0
*/
public function delete_subscription( $subscription_id, $force = false ) {
$subscription_id = $this->validate_request( $subscription_id, $this->post_type, 'delete' );
if ( is_wp_error( $subscription_id ) ) {
return $subscription_id;
}
wc_delete_shop_order_transients( $subscription_id );
do_action( 'woocommerce_api_delete_subscription', $subscription_id, $this );
return $this->delete( $subscription_id, 'subscription', ( 'true' === $force ) );
}
/**
* Retrieves the subscription by the given id.
*
* Called by: /subscriptions/<subscription_id>
*
* @since 2.0
* @param int $subscription_id
* @param array $fields
* @param array $filter
* @return array
*/
public function get_subscription( $subscription_id, $fields = null, $filter = array() ) {
$subscription_id = $this->validate_request( $subscription_id, $this->post_type, 'read' );
if ( is_wp_error( $subscription_id ) ) {
return $subscription_id;
}
$subscription = wcs_get_subscription( $subscription_id );
$order_data = $this->get_order( $subscription_id );
$subscription_data = $order_data['order'];
// Not all order meta relates to a subscription (a subscription doesn't "complete")
if ( isset( $subscription_data['completed_at'] ) ) {
unset( $subscription_data['completed_at'] );
}
$subscription_data['billing_schedule'] = array(
'period' => $subscription->billing_period,
'interval' => $subscription->billing_interval,
'start_at' => $this->get_formatted_datetime( $subscription, 'start' ),
'trial_end_at' => $this->get_formatted_datetime( $subscription, 'trial_end' ),
'next_payment_at' => $this->get_formatted_datetime( $subscription, 'next_payment' ),
'end_at' => $this->get_formatted_datetime( $subscription, 'end' ),
);
if ( ! empty( $subscription->order ) ) {
$subscription_data['parent_order_id'] = $subscription->order->id;
} else {
$subscription_data['parent_order_id'] = array();
}
return array( 'subscription' => apply_filters( 'wcs_api_get_subscription_response', $subscription_data, $fields, $filter, $this->server ) );
}
/**
* Returns a list of all the available subscription statuses.
*
* @see wcs_get_subscription_statuses() in wcs-functions.php
* @since 2.0
* @return array
*
*/
public function get_statuses() {
return array( 'subscription_statuses' => wcs_get_subscription_statuses() );
}
/**
* Get the total number of subscriptions
*
* Called by: /subscriptions/count
* @since 2.0
* @param $status string
* @param $filter array
* @return int | WP_Error
*/
public function get_subscription_count( $status = null, $filter = array() ) {
return $this->get_orders_count( $status, $filter );
}
/**
* Returns all the notes tied to the subscription
*
* Called by: subscription/<subscription_id>/notes
* @since 2.0
* @param $subscription_id
* @param $fields
* @return WP_Error|array
*/
public function get_subscription_notes( $subscription_id, $fields = null ) {
$notes = $this->get_order_notes( $subscription_id, $fields );
if ( is_wp_error( $notes ) ) {
return $notes;
}
return array( 'subscription_notes' => apply_filters( 'wcs_api_subscription_notes_response', $notes['order_notes'], $subscription_id, $fields ) );
}
/**
* Get information about a subscription note.
*
* @since 2.0
* @param int $subscription_id
* @param int $id
* @param array $fields
*
* @return array Subscription note
*/
public function get_subscription_note( $subscription_id, $id, $fields = null ) {
$note = $this->get_order_note( $subscription_id, $id, $fields );
if ( is_wp_error( $note ) ) {
return $note;
}
return array( 'subscription_note' => apply_filters( 'wcs_api_subscription_note_response', $note['order_note'], $subscription_id, $id, $fields ) );
}
/**
* Get information about a subscription note.
*
* @param int $subscription_id
* @param int $id
* @param array $fields
*
* @return WP_Error|array Subscription note
*/
public function create_subscription_note( $subscription_id, $data ) {
$note = $this->create_order_note( $subscription_id, $data );
if ( is_wp_error( $note ) ) {
return $note;
}
do_action( 'wcs_api_created_subscription_note', $subscription_id, $note['order_note'], $this );
return array( 'subscription_note' => $note['order_note'] );
}
/**
* Verify and edit subscription note.
*
* @since 2.0
* @param int $subscription_id
* @param int $id
*
* @return WP_Error|array Subscription note edited
*/
public function edit_subscription_note( $subscription_id, $id, $data ) {
$note = $this->edit_order_note( $subscription_id, $id, $data );
if ( is_wp_error( $note ) ) {
return $note;
}
do_action( 'wcs_api_edit_subscription_note', $subscription_id, $id, $note['order_note'], $this );
return array( 'subscription_note' => $note['order_note'] );
}
/**
* Verify and delete subscription note.
*
* @since 2.0
* @param int $subscription_id
* @param int $id
* @return WP_Error|array deleted subscription note status
*/
public function delete_subscription_note( $subscription_id, $id ) {
$deleted_note = $this->delete_order_note( $subscription_id, $id );
if ( is_wp_error( $deleted_note ) ) {
return $deleted_note;
}
do_action( 'wcs_api_subscription_note_status', $subscription_id, $id, $this );
return array( 'message' => _x( 'Permanently deleted subscription note', 'API response confirming order note deleted from a subscription', 'woocommerce-subscriptions' ) );
}
/**
* Get information about the initial order and renewal orders of a subscription.
*
* Called by: /subscriptions/<subscription_id>/orders
* @since 2.0
* @param $subscription_id
* @param $fields
*/
public function get_all_subscription_orders( $subscription_id, $filters = null ) {
$subscription_id = $this->validate_request( $subscription_id, $this->post_type, 'read' );
if ( is_wp_error( $subscription_id ) ) {
return $subscription_id;
}
$subscription = wcs_get_subscription( $subscription_id );
$subscription_orders = $subscription->get_related_orders();
$formatted_orders = array();
if ( ! empty( $subscription_orders ) ) {
// set post_type back to shop order so that get_orders doesn't try return a subscription.
$this->post_type = 'shop_order';
foreach ( $subscription_orders as $order_id ) {
$formatted_orders[] = $this->get_order( $order_id );
}
$this->post_type = 'shop_subscription';
}
return array( 'subscription_orders' => apply_filters( 'wcs_api_subscription_orders_response', $formatted_orders, $subscription_id, $filters, $this->server ) );
}
/**
* Get a certain date for a subscription, if it exists, formatted for return
*
* @since 2.0
* @param $subscription
* @param $date_type
*/
protected function get_formatted_datetime( $subscription, $date_type ) {
$timestamp = $subscription->get_time( $date_type );
if ( $timestamp > 0 ) {
$formatted_datetime = $this->server->format_datetime( $timestamp );
} else {
$formatted_datetime = '';
}
return $formatted_datetime;
}
/**
* Helper method to get order post objects
*
* We need to override WC_API_Orders::query_orders() because it uses wc_get_order_statuses()
* for the query, but subscriptions use the values returned by wcs_get_subscription_statuses().
*
* @since 2.0
* @param array $args request arguments for filtering query
* @return WP_Query
*/
protected function query_orders( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => $this->post_type,
'post_status' => array_keys( wcs_get_subscription_statuses() ),
);
// add status argument
if ( ! empty( $args['status'] ) ) {
$statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
$statuses = explode( ',', $statuses );
$query_args['post_status'] = $statuses;
unset( $args['status'] );
}
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
}

View File

@@ -0,0 +1,179 @@
<?php
/**
* Subscription Product Variation Class
*
* The subscription product variation class extends the WC_Product_Variation product class
* to create subscription product variations.
*
* @class WC_Product_Subscription
* @package WooCommerce Subscriptions
* @category Class
* @since 1.3
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Subscription_Variation extends WC_Product_Variation {
var $product_type;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product, $args = array() ) {
parent::__construct( $product, $args = array() );
$this->parent_product_type = $this->product_type;
$this->product_type = 'subscription_variation';
$this->subscription_variation_level_meta_data = array(
'subscription_price' => 0,
'subscription_period' => '',
'subscription_period_interval' => 'day',
'subscription_length' => 0,
'subscription_trial_length' => 0,
'subscription_trial_period' => 'day',
'subscription_sign_up_fee' => 0,
'subscription_payment_sync_date' => 0,
);
$this->variation_level_meta_data = array_merge( $this->variation_level_meta_data, $this->subscription_variation_level_meta_data );
}
/**
* Get variation price HTML. Prices are not inherited from parents.
*
* @return string containing the formatted price
*/
public function get_price_html( $price = '' ) {
$price = parent::get_price_html( $price );
if ( ! empty( $price ) ) {
$price = WC_Subscriptions_Product::get_price_string( $this, array( 'price' => $price ) );
}
return $price;
}
/**
* Get the add to cart button text
*
* @access public
* @return string
*/
public function add_to_cart_text() {
if ( $this->is_purchasable() && $this->is_in_stock() ) {
$text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) );
} else {
$text = parent::add_to_cart_text(); // translated "Read More"
}
return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
}
/**
* Get the add to cart button text for the single page
*
* @access public
* @return string
*/
public function single_add_to_cart_text() {
return apply_filters( 'woocommerce_product_single_add_to_cart_text', self::add_to_cart_text(), $this );
}
/**
* Returns the sign up fee (including tax) by filtering the products price used in
* @see WC_Product::get_price_including_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_including_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_including_tax = parent::get_price_including_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_including_tax;
}
/**
* Returns the sign up fee (excluding tax) by filtering the products price used in
* @see WC_Product::get_price_excluding_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_excluding_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_excluding_tax = parent::get_price_excluding_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_excluding_tax;
}
/**
* Return the sign-up fee for this product
*
* @return string
*/
public function get_sign_up_fee() {
return WC_Subscriptions_Product::get_sign_up_fee( $this );
}
/**
* Checks if the variable product this variation belongs to is purchasable.
*
* @access public
* @return bool
*/
function is_purchasable() {
$purchasable = $this->parent->is_purchasable();
// if we have a limited subscription product, make sure the customer doesn't already have another variation for the same variable product in their cart, but only if we're not on the order received or PayPal return pages (we can't use is_order_received_page() to check that becuase get_cart_from_session() is called before the query vars are setup)
if ( 'no' != $this->parent->limit_subscriptions && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
foreach ( WC()->cart->cart_contents as $cart_item ) { // can't use WC()->cart->get_cart() because it will trigger an infinite loop when this is called within WC_Cart::get_cart_from_session()
if ( $this->id == $cart_item['data']->id && $this->variation_id != $cart_item['data']->variation_id ) {
$purchasable = false;
break;
}
}
}
return apply_filters( 'woocommerce_subscription_variation_is_purchasable', $purchasable, $this );
}
/**
* Checks the product type to see if it is either this product's type or the parent's
* product type.
*
* @access public
* @param mixed $type Array or string of types
* @return bool
*/
public function is_type( $type ) {
if ( $this->product_type == $type || ( is_array( $type ) && in_array( $this->product_type, $type ) ) ) {
return true;
} elseif ( $this->parent_product_type == $type || ( is_array( $type ) && in_array( $this->parent_product_type, $type ) ) ) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* Subscription Product Class
*
* The subscription product class is an extension of the simple product class.
*
* @class WC_Product_Subscription
* @package WooCommerce Subscriptions
* @subpackage WC_Product_Subscription
* @category Class
* @since 1.3
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Subscription extends WC_Product_Simple {
var $subscription_price;
var $subscription_period;
var $subscription_period_interval;
var $subscription_length;
var $subscription_trial_length;
var $subscription_trial_period;
var $subscription_sign_up_fee;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product ) {
parent::__construct( $product );
$this->product_type = 'subscription';
// Load all meta fields
$this->product_custom_fields = get_post_meta( $this->id );
// Convert selected subscription meta fields for easy access
if ( ! empty( $this->product_custom_fields['_subscription_price'][0] ) ) {
$this->subscription_price = $this->product_custom_fields['_subscription_price'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_sign_up_fee'][0] ) ) {
$this->subscription_sign_up_fee = $this->product_custom_fields['_subscription_sign_up_fee'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period'][0] ) ) {
$this->subscription_period = $this->product_custom_fields['_subscription_period'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period_interval'][0] ) ) {
$this->subscription_period_interval = $this->product_custom_fields['_subscription_period_interval'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_length'][0] ) ) {
$this->subscription_length = $this->product_custom_fields['_subscription_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_length'][0] ) ) {
$this->subscription_trial_length = $this->product_custom_fields['_subscription_trial_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_period'][0] ) ) {
$this->subscription_trial_period = $this->product_custom_fields['_subscription_trial_period'][0];
}
$this->subscription_payment_sync_date = ( ! isset( $this->product_custom_fields['_subscription_payment_sync_date'][0] ) ) ? 0 : maybe_unserialize( $this->product_custom_fields['_subscription_payment_sync_date'][0] );
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
$this->limit_subscriptions = 'no';
} elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
$this->limit_subscriptions = 'any';
} else {
$this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
}
}
/**
* Get subscription's price HTML.
*
* @return string containing the formatted price
*/
public function get_price_html( $price = '' ) {
$price = parent::get_price_html( $price );
if ( ! empty( $price ) ) {
$price = WC_Subscriptions_Product::get_price_string( $this, array( 'price' => $price ) );
}
return $price;
}
/**
* Get the add to cart button text
*
* @access public
* @return string
*/
public function add_to_cart_text() {
if ( $this->is_purchasable() && $this->is_in_stock() ) {
$text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) );
} else {
$text = parent::add_to_cart_text(); // translated "Read More"
}
return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
}
/**
* Get the add to cart button text for the single page
*
* @access public
* @return string
*/
public function single_add_to_cart_text() {
return apply_filters( 'woocommerce_product_single_add_to_cart_text', self::add_to_cart_text(), $this );
}
/**
* Returns the sign up fee (including tax) by filtering the products price used in
* @see WC_Product::get_price_including_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_including_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_including_tax = parent::get_price_including_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_including_tax;
}
/**
* Returns the sign up fee (excluding tax) by filtering the products price used in
* @see WC_Product::get_price_excluding_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_excluding_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_excluding_tax = parent::get_price_excluding_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_excluding_tax;
}
/**
* Return the sign-up fee for this product
*
* @return string
*/
public function get_sign_up_fee() {
return WC_Subscriptions_Product::get_sign_up_fee( $this );
}
/**
* Checks if the store manager has requested the current product be limited to one purchase
* per customer, and if so, checks whether the customer already has an active subscription to
* the product.
*
* @access public
* @return bool
*/
function is_purchasable() {
$purchasable = parent::is_purchasable();
if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
$purchasable = false;
}
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}
}

View File

@@ -0,0 +1,585 @@
<?php
/**
* Variable Subscription Product Class
*
* This class extends the WC Variable product class to create variable products with recurring payments.
*
* @class WC_Product_Variable_Subscription
* @package WooCommerce Subscriptions
* @category Class
* @since 1.3
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Variable_Subscription extends WC_Product_Variable {
var $subscription_price;
var $subscription_period;
var $max_variation_period;
var $subscription_period_interval;
var $max_variation_period_interval;
var $product_type;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product ) {
parent::__construct( $product );
$this->parent_product_type = $this->product_type;
$this->product_type = 'variable-subscription';
// Load all meta fields
$this->product_custom_fields = get_post_meta( $this->id );
// Convert selected subscription meta fields for easy access
if ( ! empty( $this->product_custom_fields['_subscription_price'][0] ) ) {
$this->subscription_price = $this->product_custom_fields['_subscription_price'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_sign_up_fee'][0] ) ) {
$this->subscription_sign_up_fee = $this->product_custom_fields['_subscription_sign_up_fee'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period'][0] ) ) {
$this->subscription_period = $this->product_custom_fields['_subscription_period'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period_interval'][0] ) ) {
$this->subscription_period_interval = $this->product_custom_fields['_subscription_period_interval'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_length'][0] ) ) {
$this->subscription_length = $this->product_custom_fields['_subscription_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_length'][0] ) ) {
$this->subscription_trial_length = $this->product_custom_fields['_subscription_trial_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_period'][0] ) ) {
$this->subscription_trial_period = $this->product_custom_fields['_subscription_trial_period'][0];
}
$this->subscription_payment_sync_date = 0;
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
if ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) {
$this->limit_subscriptions = 'no';
} elseif ( 'yes' == $this->product_custom_fields['_subscription_limit'][0] ) { // backward compatibility
$this->limit_subscriptions = 'any';
} else {
$this->limit_subscriptions = $this->product_custom_fields['_subscription_limit'][0];
}
add_filter( 'woocommerce_add_to_cart_handler', array( &$this, 'add_to_cart_handler' ), 10, 2 );
}
/**
* Get the add to cart button text for the single page
*
* @access public
* @return string
*/
public function single_add_to_cart_text() {
if ( $this->is_purchasable() && $this->is_in_stock() ) {
$text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign Up Now', 'woocommerce-subscriptions' ) );
} else {
$text = parent::add_to_cart_text(); // translated "Read More"
}
return apply_filters( 'woocommerce_product_single_add_to_cart_text', $text, $this );
}
/**
* Sync variable product prices with the childs lowest/highest prices.
*
* @access public
* @return void
*/
public function variable_product_sync( $product_id = '' ) {
parent::variable_product_sync();
$children = get_posts( array(
'post_parent' => $this->id,
'posts_per_page' => -1,
'post_type' => 'product_variation',
'fields' => 'ids',
'post_status' => 'publish',
) );
$lowest_initial_amount = $highest_initial_amount = $lowest_price = $highest_price = '';
$shortest_initial_period = $longest_initial_period = $shortest_trial_period = $longest_trial_period = $shortest_trial_length = $longest_trial_length = '';
$longest_initial_interval = $shortest_initial_interval = $variable_subscription_period = $variable_subscription_period_interval = '';
$lowest_regular_price = $highest_regular_price = $lowest_sale_price = $highest_sale_price = $max_subscription_period = $max_subscription_period_interval = '';
$variable_subscription_sign_up_fee = $variable_subscription_trial_period = $variable_subscription_trial_length = $variable_subscription_length = $variable_subscription_sign_up_fee = $variable_subscription_trial_period = $variable_subscription_trial_length = $variable_subscription_length = '';
$min_variation_id = $max_variation_id = null;
if ( $children ) {
foreach ( $children as $child ) {
$is_max = $is_min = false;
// WC has already determined the correct price which accounts for sale price
$child_price = get_post_meta( $child, '_price', true );
$child_billing_period = get_post_meta( $child, '_subscription_period', true );
$child_billing_interval = get_post_meta( $child, '_subscription_period_interval', true );
$child_sign_up_fee = get_post_meta( $child, '_subscription_sign_up_fee', true );
$child_free_trial_length = get_post_meta( $child, '_subscription_trial_length', true );
$child_free_trial_period = get_post_meta( $child, '_subscription_trial_period', true );
if ( '' === $child_price && '' === $child_sign_up_fee ) {
continue;
}
$child_price = ( '' === $child_price ) ? 0 : $child_price;
$child_sign_up_fee = ( '' === $child_sign_up_fee ) ? 0 : $child_sign_up_fee;
$has_free_trial = ( '' !== $child_free_trial_length && $child_free_trial_length > 0 ) ? true : false;
// Determine some recurring price flags
$is_lowest_price = ( $child_price < $lowest_price || '' === $lowest_price ) ? true : false;
$is_longest_period = ( WC_Subscriptions::get_longest_period( $variable_subscription_period, $child_billing_period ) === $child_billing_period ) ? true : false;
$is_longest_interval = ( $child_billing_interval >= $variable_subscription_period_interval || '' === $variable_subscription_period_interval ) ? true : false;
// Find the amount the subscriber will have to pay up-front
if ( $has_free_trial ) {
$initial_amount = $child_sign_up_fee;
$initial_period = $child_free_trial_period;
$initial_interval = $child_free_trial_length;
} else {
$initial_amount = $child_price + $child_sign_up_fee;
$initial_period = $child_billing_period;
$initial_interval = $child_billing_interval;
}
// We have a free trial & no sign-up fee, so need to choose the longest free trial (and maybe the shortest)
if ( $has_free_trial && 0 == $child_sign_up_fee ) {
// First variation
if ( '' === $longest_trial_period ) {
$is_min = true;
// If two variations have the same free trial, choose the variation with the lowest recurring price for the longest period
} elseif ( $variable_subscription_trial_period === $child_free_trial_period && $child_free_trial_length === $variable_subscription_trial_length ) {
// If the variation has the lowest recurring price, it's the cheapest
if ( $is_lowest_price ) {
$is_min = true;
// When current variation's free trial is the same as the lowest, it's the cheaper if it has a longer billing schedule
} elseif ( $child_price === $lowest_price ) {
if ( $is_longest_period && $is_longest_interval ) {
$is_min = true;
// Longest with a new billing period
} elseif ( $is_longest_period && $child_billing_period !== $variable_subscription_trial_period ) {
$is_min = true;
}
}
// Otherwise the cheapest variation is the one with the longer trial
} elseif ( $variable_subscription_trial_period === $child_free_trial_period ) {
$is_min = ( $child_free_trial_length > $variable_subscription_trial_length ) ? true : false;
// Otherwise just a longer trial period (that isn't equal to the longest period)
} elseif ( WC_Subscriptions::get_longest_period( $longest_trial_period, $child_free_trial_period ) === $child_free_trial_period ) {
$is_min = true;
}
if ( $is_min ) {
$longest_trial_period = $child_free_trial_period;
$longest_trial_length = $child_free_trial_length;
}
// If the current cheapest variation is also free then the shortest trial period is the most expensive
if ( 0 == $lowest_price || '' === $lowest_price ) {
if ( '' === $shortest_trial_period ) {
$is_max = true;
// Need to check trial length
} elseif ( $shortest_trial_period === $child_free_trial_period ) {
$is_max = ( $child_free_trial_length < $shortest_trial_length ) ? true : false;
// Need to find shortest period
} elseif ( WC_Subscriptions::get_shortest_period( $shortest_trial_period, $child_free_trial_period ) === $child_free_trial_period ) {
$is_max = true;
}
if ( $is_max ) {
$shortest_trial_period = $child_free_trial_period;
$shortest_trial_length = $child_free_trial_length;
}
}
} else {
$longest_initial_period = WC_Subscriptions::get_longest_period( $longest_initial_period, $initial_period );
$shortest_initial_period = WC_Subscriptions::get_shortest_period( $shortest_initial_period, $initial_period );
$is_lowest_initial_amount = ( $initial_amount < $lowest_initial_amount || '' === $lowest_initial_amount ) ? true : false;
$is_longest_initial_period = ( $initial_period === $longest_initial_period ) ? true : false;
$is_longest_initial_interval = ( $initial_interval >= $longest_initial_interval || '' === $longest_initial_interval ) ? true : false;
$is_highest_initial = ( $initial_amount > $highest_initial_amount || '' === $highest_initial_amount ) ? true : false;
$is_shortest_period = ( $initial_period === $shortest_initial_period || '' === $shortest_initial_period ) ? true : false;
$is_shortest_interval = ( $initial_interval < $shortest_initial_interval || '' === $shortest_initial_interval ) ? true : false;
// If we're not dealing with the lowest initial access amount, then ignore this variation
if ( ! $is_lowest_initial_amount && $initial_amount !== $lowest_initial_amount ) {
continue;
}
// If the variation has the lowest price, it's the cheapest
if ( $is_lowest_initial_amount ) {
$is_min = true;
// When current variation's price is the same as the lowest, it's the cheapest only if it has a longer billing schedule
} elseif ( $initial_amount === $lowest_initial_amount ) {
// We need to check the recurring schedule when the sign-up fee & free trial periods are equal
if ( $has_free_trial && $initial_period == $longest_initial_period && $initial_interval == $longest_initial_interval ) {
// If the variation has the lowest recurring price, it's the cheapest
if ( $is_lowest_price ) {
$is_min = true;
// When current variation's price is the same as the lowest, it's the cheapest only if it has a longer billing schedule
} elseif ( $child_price === $lowest_price ) {
if ( $is_longest_period && $is_longest_interval ) {
$is_min = true;
// Longest with a new billing period
} elseif ( $is_longest_period && $child_billing_period !== $variable_subscription_period ) {
$is_min = true;
}
}
// Longest initial term is the cheapest
} elseif ( $is_longest_initial_period && $is_longest_initial_interval ) {
$is_min = true;
// Longest with a new billing period
} elseif ( $is_longest_initial_period && $initial_period !== $variable_subscription_period ) {
$is_min = true;
}
}
// If we have the highest price for the shortest period, we might have the maximum variation
if ( $is_highest_initial && $is_shortest_period && $is_shortest_interval ) {
$is_max = true;
// But only if its for the shortest billing period
} elseif ( $child_price === $highest_price ) {
if ( $is_shortest_period && $is_shortest_interval ) {
$is_max = true;
} elseif ( $is_shortest_period ) {
$is_max = true;
}
}
}
// If it's the min subscription terms
if ( $is_min ) {
$min_variation_id = $child;
$lowest_price = $child_price;
$lowest_regular_price = get_post_meta( $child, '_regular_price', true );
$lowest_sale_price = get_post_meta( $child, '_sale_price', true );
$lowest_regular_price = ( '' === $lowest_regular_price ) ? 0 : $lowest_regular_price;
$lowest_sale_price = ( '' === $lowest_sale_price ) ? 0 : $lowest_sale_price;
$lowest_initial_amount = $initial_amount;
$longest_initial_period = $initial_period;
$longest_initial_interval = $initial_interval;
$variable_subscription_sign_up_fee = $child_sign_up_fee;
$variable_subscription_period = $child_billing_period;
$variable_subscription_period_interval = $child_billing_interval;
$variable_subscription_trial_length = $child_free_trial_length;
$variable_subscription_trial_period = $child_free_trial_period;
$variable_subscription_length = get_post_meta( $child, '_subscription_length', true );
}
if ( $is_max ) {
$max_variation_id = $child;
$highest_price = $child_price;
$highest_regular_price = get_post_meta( $child, '_regular_price', true );
$highest_sale_price = get_post_meta( $child, '_sale_price', true );
$highest_initial_amount = $initial_amount;
$highest_regular_price = ( '' === $highest_regular_price ) ? 0 : $highest_regular_price;
$highest_sale_price = ( '' === $highest_sale_price ) ? 0 : $highest_sale_price;
$max_subscription_period = $child_billing_period;
$max_subscription_period_interval = $child_billing_interval;
}
}
update_post_meta( $this->id, '_min_price_variation_id', $min_variation_id );
update_post_meta( $this->id, '_max_price_variation_id', $max_variation_id );
update_post_meta( $this->id, '_price', $lowest_price );
update_post_meta( $this->id, '_min_variation_price', $lowest_price );
update_post_meta( $this->id, '_max_variation_price', $highest_price );
update_post_meta( $this->id, '_min_variation_regular_price', $lowest_regular_price );
update_post_meta( $this->id, '_max_variation_regular_price', $highest_regular_price );
update_post_meta( $this->id, '_min_variation_sale_price', $lowest_sale_price );
update_post_meta( $this->id, '_max_variation_sale_price', $highest_sale_price );
update_post_meta( $this->id, '_min_variation_period', $variable_subscription_period );
update_post_meta( $this->id, '_max_variation_period', $variable_subscription_period_interval );
update_post_meta( $this->id, '_min_variation_period_interval', $max_subscription_period );
update_post_meta( $this->id, '_max_variation_period_interval', $max_subscription_period_interval );
update_post_meta( $this->id, '_subscription_price', $lowest_price );
update_post_meta( $this->id, '_subscription_sign_up_fee', $variable_subscription_sign_up_fee );
update_post_meta( $this->id, '_subscription_period', $variable_subscription_period );
update_post_meta( $this->id, '_subscription_period_interval', $variable_subscription_period_interval );
update_post_meta( $this->id, '_subscription_trial_period', $variable_subscription_trial_period );
update_post_meta( $this->id, '_subscription_trial_length', $variable_subscription_trial_length );
update_post_meta( $this->id, '_subscription_length', $variable_subscription_length );
$this->subscription_price = $lowest_price;
$this->subscription_sign_up_fee = $variable_subscription_sign_up_fee;
$this->subscription_period = $variable_subscription_period;
$this->subscription_period_interval = $variable_subscription_period_interval;
$this->subscription_trial_period = $variable_subscription_trial_period;
$this->subscription_trial_length = $variable_subscription_trial_length;
$this->subscription_length = $variable_subscription_length;
if ( function_exists( 'wc_delete_product_transients' ) ) {
wc_delete_product_transients( $this->id );
} else {
WC()->clear_product_transients( $this->id );
}
} else { // No variations yet
$this->subscription_price = '';
$this->subscription_sign_up_fee = '';
$this->subscription_period = 'day';
$this->subscription_period_interval = 1;
$this->subscription_trial_period = 'day';
$this->subscription_trial_length = 1;
$this->subscription_length = 0;
}
}
/**
* Returns the price in html format.
*
* @access public
* @param string $price (default: '')
* @return string
*/
public function get_price_html( $price = '' ) {
$price = parent::get_price_html( $price );
if ( ! isset( $this->subscription_period ) || ! isset( $this->subscription_period_interval ) || ! isset( $this->max_variation_period ) || ! isset( $this->max_variation_period_interval ) ) {
$this->variable_product_sync();
}
// Only create the subscription price string when a price has been set
if ( $this->subscription_price !== '' || $this->subscription_sign_up_fee !== '' ) {
$price = '';
if ( $this->is_on_sale() && isset( $this->min_variation_price ) && $this->min_variation_regular_price !== $this->get_price() ) {
if ( ! $this->min_variation_price || $this->min_variation_price !== $this->max_variation_price ) {
$price .= $this->get_price_html_from_text();
}
$variation_id = get_post_meta( $this->id, '_min_price_variation_id', true );
$variation = $this->get_child( $variation_id );
$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
$sale_price = 'incl' == $tax_display_mode ? $variation->get_price_including_tax( 1, $variation->get_sale_price() ) : $variation->get_price_excluding_tax( 1, $variation->get_sale_price() );
$regular_price = 'incl' == $tax_display_mode ? $variation->get_price_including_tax( 1, $variation->get_regular_price() ) : $variation->get_price_excluding_tax( 1, $variation->get_regular_price() );
$price .= $this->get_price_html_from_to( $regular_price, $sale_price );
} else {
if ( $this->min_variation_price !== $this->max_variation_price ) {
$price .= $this->get_price_html_from_text();
}
$price .= wc_price( $this->get_variation_price( 'min', true ) );
}
// Make sure the price contains "From:" when billing schedule differs between variations
if ( false === strpos( $price, $this->get_price_html_from_text() ) ) {
if ( $this->subscription_period !== $this->max_variation_period ) {
$price = $this->get_price_html_from_text() . $price;
} elseif ( $this->subscription_period_interval !== $this->max_variation_period_interval ) {
$price = $this->get_price_html_from_text() . $price;
}
}
$price .= $this->get_price_suffix();
$price = WC_Subscriptions_Product::get_price_string( $this, array( 'price' => $price ) );
}
return apply_filters( 'woocommerce_variable_subscription_price_html', $price, $this );
}
/**
* Returns the sign up fee (including tax) by filtering the products price used in
* @see WC_Product::get_price_including_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_including_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_including_tax = parent::get_price_including_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_including_tax;
}
/**
* Returns the sign up fee (excluding tax) by filtering the products price used in
* @see WC_Product::get_price_excluding_tax( $qty )
*
* @return string
*/
public function get_sign_up_fee_excluding_tax( $qty = 1 ) {
add_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
$sign_up_fee_excluding_tax = parent::get_price_excluding_tax( $qty );
remove_filter( 'woocommerce_get_price', array( &$this, 'get_sign_up_fee' ), 100, 0 );
return $sign_up_fee_excluding_tax;
}
/**
* Return the sign-up fee for this product
*
* @return string
*/
public function get_sign_up_fee() {
return WC_Subscriptions_Product::get_sign_up_fee( $this );
}
/**
* get_child function.
*
* @access public
* @param mixed $child_id
* @return object WC_Product_Subscription or WC_Product_Subscription_Variation
*/
public function get_child( $child_id ) {
return wc_get_product( $child_id, array(
'product_type' => 'Subscription_Variation',
'parent_id' => $this->id,
'parent' => $this,
) );
}
/**
*
* @param string $product_type A string representation of a product type
*/
public function add_to_cart_handler( $handler, $product ) {
if ( 'variable-subscription' === $handler ) {
$handler = 'variable';
}
return $handler;
}
/**
* Checks if the store manager has requested the current product be limited to one purchase
* per customer, and if so, checks whether the customer already has an active subscription to
* the product.
*
* @access public
* @return bool
*/
function is_purchasable() {
$purchasable = parent::is_purchasable();
if ( true === $purchasable && false === WC_Subscriptions_Product::is_purchasable( $purchasable, $this ) ) {
$purchasable = false;
}
return apply_filters( 'woocommerce_subscription_is_purchasable', $purchasable, $this );
}
/**
* Checks the product type to see if it is either this product's type or the parent's
* product type.
*
* @access public
* @param mixed $type Array or string of types
* @return bool
*/
public function is_type( $type ) {
if ( $this->product_type == $type || ( is_array( $type ) && in_array( $this->product_type, $type ) ) ) {
return true;
} elseif ( $this->parent_product_type == $type || ( is_array( $type ) && in_array( $this->parent_product_type, $type ) ) ) {
return true;
} else {
return false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
<?php
/**
* Subscriptions Address Class
*
* Hooks into WooCommerce to handle editing addresses for subscriptions (by editing the original order for the subscription)
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Addresses
* @category Class
* @author Brent Shepherd
* @since 1.3
*/
class WC_Subscriptions_Addresses {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.3
*/
public static function init() {
add_filter( 'wcs_view_subscription_actions', __CLASS__ . '::add_edit_address_subscription_action', 10, 2 );
add_action( 'woocommerce_after_edit_address_form_billing', __CLASS__ . '::maybe_add_edit_address_checkbox', 10 );
add_action( 'woocommerce_after_edit_address_form_shipping', __CLASS__ . '::maybe_add_edit_address_checkbox', 10 );
add_action( 'woocommerce_customer_save_address', __CLASS__ . '::maybe_update_subscription_addresses', 10, 2 );
add_filter( 'woocommerce_address_to_edit', __CLASS__ . '::maybe_populate_subscription_addresses', 10 );
}
/**
* Add a "Change Shipping Address" button to the "My Subscriptions" table for those subscriptions
* which require shipping.
*
* @param array $all_actions The $subscription_id => $actions array with all actions that will be displayed for a subscription on the "My Subscriptions" table
* @param array $subscriptions All of a given users subscriptions that will be displayed on the "My Subscriptions" table
* @since 1.3
*/
public static function add_edit_address_subscription_action( $actions, $subscription ) {
if ( $subscription->needs_shipping_address() && $subscription->has_status( array( 'active', 'on-hold' ) ) ) {
$actions['change_address'] = array(
'url' => add_query_arg( array( 'subscription' => $subscription->id ), wc_get_endpoint_url( 'edit-address', 'shipping' ) ),
'name' => __( 'Change Address', 'woocommerce-subscriptions' ),
);
}
return $actions;
}
/**
* Outputs the necessary markup on the "My Account" > "Edit Address" page for editing a single subscription's
* address or to check if the customer wants to update the addresses for all of their subscriptions.
*
* If editing their default shipping address, this function adds a checkbox to the to allow subscribers to
* also update the address on their active subscriptions. If editing a single subscription's address, the
* subscription key is added as a hidden field.
*
* @since 1.3
*/
public static function maybe_add_edit_address_checkbox() {
global $wp;
if ( wcs_user_has_subscription() ) {
if ( isset( $_GET['subscription'] ) ) {
echo '<p>' . esc_html__( 'Both the shipping address used for the subscription and your default shipping address for future purchases will be updated.', 'woocommerce-subscriptions' ) . '</p>';
echo '<input type="hidden" name="update_subscription_address" value="' . absint( $_GET['subscription'] ) . '" id="update_subscription_address" />';
} elseif ( ( ( isset( $wp->query_vars['edit-address'] ) && ! empty( $wp->query_vars['edit-address'] ) ) || isset( $_GET['address'] ) ) ) {
if ( isset( $wp->query_vars['edit-address'] ) ) {
$address_type = esc_attr( $wp->query_vars['edit-address'] ) . ' ';
} else {
$address_type = ( ! isset( $_GET['address'] ) ) ? esc_attr( $_GET['address'] ) . ' ' : '';
}
// translators: $1: address type (Shipping Address / Billing Address), $2: opening <strong> tag, $3: closing </strong> tag
$label = sprintf( __( 'Update the %1$s used for %2$sall%3$s of my active subscriptions', 'woocommerce-subscriptions' ), wcs_get_address_type_to_display( $address_type ), '<strong>', '</strong>' );
woocommerce_form_field( 'update_all_subscriptions_addresses', array(
'type' => 'checkbox',
'class' => array( 'form-row-wide' ),
'label' => $label,
)
);
}
wp_nonce_field( 'wcs_edit_address', '_wcsnonce' );
}
}
/**
* When a subscriber's billing or shipping address is successfully updated, check if the subscriber
* has also requested to update the addresses on existing subscriptions and if so, go ahead and update
* the addresses on the initial order for each subscription.
*
* @param int $user_id The ID of a user who own's the subscription (and address)
* @since 1.3
*/
public static function maybe_update_subscription_addresses( $user_id, $address_type ) {
if ( ! wcs_user_has_subscription( $user_id ) || wc_notice_count( 'error' ) > 0 || empty( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_edit_address' ) ) {
return;
}
$address_type = ( 'billing' == $address_type || 'shipping' == $address_type ) ? $address_type : '';
$address_fields = WC()->countries->get_address_fields( esc_attr( $_POST[ $address_type . '_country' ] ), $address_type . '_' );
$address = array();
foreach ( $address_fields as $key => $field ) {
if ( isset( $_POST[ $key ] ) ) {
$address[ str_replace( $address_type . '_', '', $key ) ] = wc_clean( $_POST[ $key ] );
}
}
if ( isset( $_POST['update_all_subscriptions_addresses'] ) ) {
$users_subscriptions = wcs_get_users_subscriptions( $user_id );
foreach ( $users_subscriptions as $subscription ) {
if ( $subscription->has_status( array( 'active', 'on-hold' ) ) ) {
$subscription->set_address( $address, $address_type );
}
}
} elseif ( isset( $_POST['update_subscription_address'] ) ) {
$subscription = wcs_get_subscription( intval( $_POST['update_subscription_address'] ) );
// Update the address only if the user actually owns the subscription
if ( ! empty( $subscription ) ) {
$subscription->set_address( $address, $address_type );
}
wp_safe_redirect( $subscription->get_view_order_url() );
exit();
}
}
/**
* Prepopulate the address fields on a subscription item
*
* @param array $address A WooCommerce address array
* @since 1.5
*/
public static function maybe_populate_subscription_addresses( $address ) {
if ( isset( $_GET['subscription'] ) ) {
$subscription = wcs_get_subscription( absint( $_GET['subscription'] ) );
foreach ( array_keys( $address ) as $key ) {
$address[ $key ]['value'] = $subscription->$key;
}
}
return $address;
}
/**
* Update the address fields on an order
*
* @param array $subscription A WooCommerce Subscription array
* @param array $address_fields Locale aware address fields of the form returned by WC_Countries->get_address_fields() for a given country
* @since 1.3
*/
public static function maybe_update_order_address( $subscription, $address_fields ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Order::set_address() or WC_Subscription::set_address()' );
}
}
WC_Subscriptions_Addresses::init();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,592 @@
<?php
/**
* Make it possible for customers to change the payment gateway used for an existing subscription.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Change_Payment_Gateway
* @category Class
* @author Brent Shepherd
* @since 1.4
*/
class WC_Subscriptions_Change_Payment_Gateway {
public static $is_request_to_change_payment = false;
private static $woocommerce_messages = array();
private static $woocommerce_errors = array();
private static $original_order_dates = array();
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.4
*/
public static function init() {
// Maybe allow for a recurring payment method to be changed
add_action( 'plugins_loaded', __CLASS__ . '::set_change_payment_method_flag' );
// Keep a record of any messages or errors that should be displayed
add_action( 'before_woocommerce_pay', __CLASS__ . '::store_pay_shortcode_mesages', 100 );
// Hijack the default pay shortcode
add_action( 'after_woocommerce_pay', __CLASS__ . '::maybe_replace_pay_shortcode', 100 );
// Maybe allow for a recurring payment method to be changed
add_filter( 'wcs_view_subscription_actions', __CLASS__ . '::change_payment_method_button', 10, 2 );
// Maybe allow for a recurring payment method to be changed
add_action( 'wp_loaded', __CLASS__ . '::change_payment_method_via_pay_shortcode', 20 );
// Filter the available payment gateways to only show those which support acting as the new payment method
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' );
// If we're changing the payment method, we want to make sure a number of totals return $0 (to prevent payments being processed now)
add_filter( 'woocommerce_subscriptions_total_initial_payment', __CLASS__ . '::maybe_zero_total', 11, 2 );
add_filter( 'woocommerce_subscriptions_sign_up_fee', __CLASS__ . '::maybe_zero_total', 11, 2 );
add_filter( 'woocommerce_order_amount_total', __CLASS__ . '::maybe_zero_total', 11, 2 );
// Redirect to My Account page after changing payment method
add_filter( 'woocommerce_get_return_url', __CLASS__ . '::get_return_url', 11 );
// Update the recurring payment method when a customer has completed the payment for a renewal payment which previously failed
add_action( 'woocommerce_subscriptions_paid_for_failed_renewal_order', __CLASS__ . '::change_failing_payment_method', 10, 2 );
// Add a 'new-payment-method' handler to the WC_Subscription::can_be_updated_to() function
add_filter( 'woocommerce_can_subscription_be_updated_to_new-payment-method', __CLASS__ . '::can_subscription_be_updated_to_new_payment_method', 10, 2 );
// Change the "Pay for Order" page title to "Change Payment Method"
add_filter( 'the_title', __CLASS__ . '::change_payment_method_page_title', 100 );
// Maybe filter subscriptions_needs_payment to return false when processing change-payment-gateway requests
add_filter( 'woocommerce_subscription_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 1 );
}
/**
* Set a flag to indicate that the current request is for changing payment. Better than requiring other extensions
* to check the $_GET global as it allows for the flag to be overridden.
*
* @since 1.4
*/
public static function set_change_payment_method_flag() {
if ( isset( $_GET['change_payment_method'] ) ) {
self::$is_request_to_change_payment = true;
}
}
/**
* Store any messages or errors added by other plugins, particularly important for those occasions when the new payment
* method caused and error or failure.
*
* @since 1.4
*/
public static function store_pay_shortcode_mesages() {
if ( wc_notice_count( 'notice' ) > 0 ) {
self::$woocommerce_messages = wc_get_notices( 'success' );
self::$woocommerce_messages += wc_get_notices( 'notice' );
}
if ( wc_notice_count( 'error' ) > 0 ) {
self::$woocommerce_errors = wc_get_notices( 'error' );
}
}
/**
* If requesting a payment method change, replace the woocommerce_pay_shortcode() with a change payment form.
*
* @since 1.4
*/
public static function maybe_replace_pay_shortcode() {
global $wp;
$valid_request = false;
// if the request to pay for the order belongs to a subscription but there's no GET params for changing payment method, show receipt page.
if ( ! self::$is_request_to_change_payment && isset( $wp->query_vars['order-pay'] ) && wcs_is_subscription( absint( $wp->query_vars['order-pay'] ) ) ) {
$valid_request = true;
ob_clean();
do_action( 'before_woocommerce_pay' );
$subscription_key = isset( $_GET['key'] ) ? wc_clean( $_GET['key'] ) : '';
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
if ( $subscription->id == absint( $wp->query_vars['order-pay'] ) && $subscription->order_key == $subscription_key ) {
?>
<div class="woocommerce">
<ul class="order_details">
<li class="order">
<?php
// translators: placeholder is the subscription order number wrapped in <strong> tags
echo wp_kses( sprintf( esc_html__( 'Subscription Number: %s', 'woocommerce-subscriptions' ), '<strong>' . esc_html( $subscription->get_order_number() ) . '</strong>' ), array( 'strong' => true ) );
?>
</li>
<li class="date">
<?php
// translators: placeholder is the subscription's next payment date (either human readable or normal date) wrapped in <strong> tags
echo wp_kses( sprintf( esc_html__( 'Next Payment Date: %s', 'woocommerce-subscriptions' ), '<strong>' . esc_html( $subscription->get_date_to_display( 'next_payment' ) ) . '</strong>' ), array( 'strong' => true ) );
?>
</li>
<li class="total">
<?php
// translators: placeholder is the formatted total to be paid for the subscription wrapped in <strong> tags
echo wp_kses_post( sprintf( esc_html__( 'Total: %s', 'woocommerce-subscriptions' ), '<strong>' . $subscription->get_formatted_order_total() . '</strong>' ) );
?>
</li>
<?php if ( $subscription->payment_method_title ) : ?>
<li class="method">
<?php
// translators: placeholder is the display name of the payment method
echo wp_kses( sprintf( esc_html__( 'Payment Method: %s', 'woocommerce-subscriptions' ), '<strong>' . esc_html( $subscription->get_payment_method_to_display() ) . '</strong>' ), array( 'strong' => true ) );
?>
</li>
<?php endif; ?>
</ul>
<?php do_action( 'woocommerce_receipt_' . $subscription->payment_method, $subscription->id ); ?>
<div class="clear"></div>
<?php
} else {
wc_add_notice( __( 'Sorry, this subscription change payment method request is invalid and cannot be processed.', 'woocommerce-subscriptions' ), 'error' );
}
wc_print_notices();
} elseif ( ! self::$is_request_to_change_payment ) {
return;
} else {
ob_clean();
do_action( 'before_woocommerce_pay' );
echo '<div class="woocommerce">';
if ( ! empty( self::$woocommerce_errors ) ) {
foreach ( self::$woocommerce_errors as $error ) {
WC_Subscriptions::add_notice( $error, 'error' );
}
}
if ( ! empty( self::$woocommerce_messages ) ) {
foreach ( self::$woocommerce_messages as $message ) {
WC_Subscriptions::add_notice( $message, 'success' );
}
}
$subscription = wcs_get_subscription( absint( $_GET['change_payment_method'] ) );
if ( wp_verify_nonce( $_GET['_wpnonce'], __FILE__ ) === false ) {
WC_Subscriptions::add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( empty( $subscription ) ) {
WC_Subscriptions::add_notice( __( 'Invalid Subscription.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! current_user_can( 'edit_shop_subscription_payment_method', $subscription->id ) ) {
WC_Subscriptions::add_notice( __( 'That doesn\'t appear to be one of your subscriptions.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! $subscription->can_be_updated_to( 'new-payment-method' ) ) {
WC_Subscriptions::add_notice( __( 'The payment method can not be changed for that subscription.', 'woocommerce-subscriptions' ), 'error' );
} else {
if ( $subscription->get_time( 'next_payment' ) > 0 ) {
// translators: placeholder is next payment's date
$next_payment_string = sprintf( __( ' Next payment is due %s.', 'woocommerce-subscriptions' ), $subscription->get_date_to_display( 'next_payment' ) );
} else {
$next_payment_string = '';
}
// translators: placeholder is either empty or "Next payment is due..."
WC_Subscriptions::add_notice( sprintf( __( 'Choose a new payment method.%s', 'woocommerce-subscriptions' ), $next_payment_string ), 'notice' );
WC_Subscriptions::print_notices();
if ( $subscription->order_key == $_GET['key'] ) {
// Set customer location to order location
if ( $subscription->billing_country ) {
WC()->customer->set_country( $subscription->billing_country );
}
if ( $subscription->billing_state ) {
WC()->customer->set_state( $subscription->billing_state );
}
if ( $subscription->billing_postcode ) {
WC()->customer->set_postcode( $subscription->billing_postcode );
}
wc_get_template( 'checkout/form-change-payment-method.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
$valid_request = true;
} else {
WC_Subscriptions::add_notice( __( 'Invalid order.', 'woocommerce-subscriptions' ), 'error' );
}
}
}
if ( false === $valid_request ) {
WC_Subscriptions::print_notices();
}
}
/**
* Add a "Change Payment Method" button to the "My Subscriptions" table.
*
* @param array $all_actions The $subscription_key => $actions array with all actions that will be displayed for a subscription on the "My Subscriptions" table
* @param array $subscriptions All of a given users subscriptions that will be displayed on the "My Subscriptions" table
* @since 1.4
*/
public static function change_payment_method_button( $actions, $subscription ) {
if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) {
$actions['change_payment_method'] = array(
'url' => wp_nonce_url( add_query_arg( array( 'change_payment_method' => $subscription->id ), $subscription->get_checkout_payment_url() ), __FILE__ ),
'name' => _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ),
);
}
return $actions;
}
/**
* Process the change payment form.
*
* Based on the @see woocommerce_pay_action() function.
*
* @access public
* @return void
* @since 1.4
*/
public static function change_payment_method_via_pay_shortcode() {
if ( isset( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) {
$subscription = wcs_get_subscription( absint( $_POST['woocommerce_change_payment'] ) );
do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
ob_start();
if ( $subscription->order_key == $_GET['key'] ) {
// Set customer location to order location
if ( $subscription->billing_country ) {
WC()->customer->set_country( $subscription->billing_country );
}
if ( $subscription->billing_state ) {
WC()->customer->set_state( $subscription->billing_state );
}
if ( $subscription->billing_postcode ) {
WC()->customer->set_postcode( $subscription->billing_postcode );
}
if ( $subscription->billing_city ) {
WC()->customer->set_city( $subscription->billing_city );
}
// Update payment method
$new_payment_method = wc_clean( $_POST['payment_method'] );
// Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
self::update_payment_method( $subscription, $new_payment_method );
}
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
// Validate
$available_gateways[ $new_payment_method ]->validate_fields();
// Process payment for the new method (with a $0 order total)
if ( wc_notice_count( 'error' ) == 0 ) {
$result = $available_gateways[ $new_payment_method ]->process_payment( $subscription->id );
if ( 'success' == $result['result'] && wc_get_page_permalink( 'myaccount' ) == $result['redirect'] ) {
$result['redirect'] = $subscription->get_view_order_url();
}
$result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription );
// Redirect to success/confirmation/payment page
if ( 'success' == $result['result'] ) {
WC_Subscriptions::add_notice( __( 'Payment method updated.', 'woocommerce-subscriptions' ), 'success' );
wp_redirect( $result['redirect'] );
exit;
}
}
}
}
}
/**
* Update the recurring payment method on a subscription order.
*
* @param array $available_gateways The payment gateways which are currently being allowed.
* @since 1.4
*/
public static function update_payment_method( $subscription, $new_payment_method ) {
$old_payment_method = $subscription->payment_method;
$old_payment_method_title = $subscription->payment_method_title;
$available_gateways = WC()->payment_gateways->get_available_payment_gateways(); // Also inits all payment gateways to make sure that hooks are attached correctly
do_action( 'woocommerce_subscriptions_pre_update_payment_method', $subscription, $new_payment_method, $old_payment_method );
// Make sure the subscription is cancelled with the current gateway
WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, 'cancelled' );
// Update meta
update_post_meta( $subscription->id, '_old_payment_method', $old_payment_method );
update_post_meta( $subscription->id, '_payment_method', $new_payment_method );
if ( isset( $available_gateways[ $new_payment_method ] ) ) {
$new_payment_method_title = $available_gateways[ $new_payment_method ]->get_title();
} else {
$new_payment_method_title = '';
}
update_post_meta( $subscription->id, '_old_payment_method_title', $old_payment_method_title );
update_post_meta( $subscription->id, '_payment_method_title', $new_payment_method_title );
if ( empty( $old_payment_method_title ) ) {
$old_payment_method_title = $old_payment_method;
}
if ( empty( $new_payment_method_title ) ) {
$new_payment_method_title = $new_payment_method;
}
// Log change on order
$subscription->add_order_note( sprintf( _x( 'Payment method changed from "%1$s" to "%2$s" by the subscriber from their account page.', '%1$s: old payment title, %2$s: new payment title', 'woocommerce-subscriptions' ), $old_payment_method_title, $new_payment_method_title ) );
do_action( 'woocommerce_subscription_payment_method_updated', $subscription, $new_payment_method, $old_payment_method );
do_action( 'woocommerce_subscription_payment_method_updated_to_' . $new_payment_method, $subscription, $old_payment_method );
do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method );
}
/**
* Only display gateways which support changing payment method when paying for a failed renewal order or
* when requesting to change the payment method.
*
* @param array $available_gateways The payment gateways which are currently being allowed.
* @since 1.4
*/
public static function get_available_payment_gateways( $available_gateways ) {
if ( isset( $_GET['change_payment_method'] ) || wcs_cart_contains_failed_renewal_order_payment() ) {
foreach ( $available_gateways as $gateway_id => $gateway ) {
if ( true !== $gateway->supports( 'subscription_payment_method_change_customer' ) ) {
unset( $available_gateways[ $gateway_id ] );
}
}
}
return $available_gateways;
}
/**
* Make sure certain totals are set to 0 when the request is to change the payment method without charging anything.
*
* @since 1.4
*/
public static function maybe_zero_total( $total, $subscription ) {
global $wp;
if ( ! empty( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) && isset( $_POST['woocommerce_change_payment'] ) && $subscription->order_key == $_GET['key'] && $subscription->id == absint( $_POST['woocommerce_change_payment'] ) ) {
$total = 0;
} elseif ( ! self::$is_request_to_change_payment && isset( $wp->query_vars['order-pay'] ) && wcs_is_subscription( absint( $wp->query_vars['order-pay'] ) ) ) {
// if the request to pay for the order belongs to a subscription but there's no GET params for changing payment method, the receipt page is being used to collect credit card details so we still need to $0 the total
$total = 0;
}
return $total;
}
/**
* Redirect back to the "My Account" page instead of the "Thank You" page after changing the payment method.
*
* @since 1.4
*/
public static function get_return_url( $return_url ) {
if ( ! empty( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) && isset( $_POST['woocommerce_change_payment'] ) ) {
$return_url = get_permalink( wc_get_page_id( 'myaccount' ) );
}
return $return_url;
}
/**
* Update the recurring payment method for a subscription after a customer has paid for a failed renewal order
* (which usually failed because of an issue with the existing payment, like an expired card or token).
*
* Also trigger a hook for payment gateways to update any meta on the original order for a subscription.
*
* @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
* @param WC_Order $original_order The original order in which the subscription was purchased.
* @since 1.4
*/
public static function change_failing_payment_method( $renewal_order, $subscription ) {
if ( ! $subscription->is_manual() ) {
if ( ! empty( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) && isset( $_POST['payment_method'] ) ) {
$new_payment_method = wc_clean( $_POST['payment_method'] );
} else {
$new_payment_method = $renewal_order->payment_method;
}
self::update_payment_method( $subscription, $new_payment_method );
do_action( 'woocommerce_subscription_failing_payment_method_updated', $subscription, $renewal_order );
do_action( 'woocommerce_subscription_failing_payment_method_updated_' . $new_payment_method, $subscription, $renewal_order );
}
}
/**
* Add a 'new-payment-method' handler to the @see WC_Subscription::can_be_updated_to() function
* to determine whether the recurring payment method on a subscription can be changed.
*
* For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments
* and use a payment gateway which allows the subscription to be cancelled.
*
* @param bool $subscription_can_be_changed Flag of whether the subscription can be changed to
* @param string $new_status_or_meta The status or meta data you want to change th subscription to. Can be 'active', 'on-hold', 'cancelled', 'expired', 'trash', 'deleted', 'failed', 'new-payment-date' or some other value attached to the 'woocommerce_can_subscription_be_changed_to' filter.
* @param object $args Set of values used in @see WC_Subscriptions_Manager::can_subscription_be_changed_to() for determining if a subscription can be changes, include:
* 'subscription_key' string A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* 'subscription' array Subscription of the form returned by @see WC_Subscriptions_Manager::get_subscription()
* 'user_id' int The ID of the subscriber.
* 'order' WC_Order The order which recorded the successful payment (to make up for the failed automatic payment).
* 'payment_gateway' WC_Payment_Gateway The subscription's recurring payment gateway
* 'order_uses_manual_payments' bool A boolean flag indicating whether the subscription requires manual renewal payment.
* @since 1.4
*/
public static function can_subscription_be_updated_to_new_payment_method( $subscription_can_be_changed, $subscription ) {
if ( WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) && $subscription->get_time( 'next_payment' ) > 0 && ! $subscription->is_manual() && $subscription->payment_method_supports( 'subscription_cancellation' ) && $subscription->has_status( 'active' ) ) {
$subscription_can_be_changed = true;
} else {
$subscription_can_be_changed = false;
}
return $subscription_can_be_changed;
}
/**
* Replace a page title with the endpoint title
*
* @param string $title
* @return string
* @since 2.0
*/
public static function change_payment_method_page_title( $title ) {
if ( is_main_query() && in_the_loop() && is_page() && is_checkout_pay_page() && self::$is_request_to_change_payment ) {
$title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' );
}
return $title;
}
/**
* When processing a change_payment_method request on a subscription that has a failed or pending renewal,
* we don't want the `$order->needs_payment()` check inside WC_Shortcode_Checkout::order_pay() to pass.
* This is causing `$gateway->payment_fields()` to be called multiple times.
*
* @param bool $needs_payment
* @param WC_Subscription $subscription
* @return bool
* @since 2.0.7
*/
public static function maybe_override_needs_payment( $needs_payment ) {
if ( $needs_payment && self::$is_request_to_change_payment ) {
$needs_payment = false;
}
return $needs_payment;
}
/** Deprecated Functions **/
/**
* Update the recurring payment method on a subscription order.
*
* @param array $available_gateways The payment gateways which are currently being allowed.
* @since 1.4
* @deprecated 2.0
*/
public static function update_recurring_payment_method( $subscription_key, $order, $new_payment_method ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::update_payment_method()' );
self::update_payment_method( wcs_get_subscription_from_key( $subscription_key ), $new_payment_method );
}
/**
* Keep a record of an order's dates if we're marking it as completed during a request to change the payment method.
*
* Deprecated as we now operate on a WC_Subscription object instead of the parent order, so we don't need to hack around date changes.
*
* @since 1.4
* @deprecated 2.0
*/
public static function store_original_order_dates( $new_order_status, $subscription_id ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Restore an order's dates if we marked it as completed during a request to change the payment method.
*
* Deprecated as we now operate on a WC_Subscription object instead of the parent order, so we don't need to hack around date changes.
*
* @since 1.4
* @deprecated 2.0
*/
public static function restore_original_order_dates( $order_id ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Add a 'new-payment-method' handler to the @see WC_Subscription::can_be_updated_to() function
* to determine whether the recurring payment method on a subscription can be changed.
*
* For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments
* and use a payment gateway which allows the subscription to be cancelled.
*
* @param bool $subscription_can_be_changed Flag of whether the subscription can be changed to
* @param string $new_status_or_meta The status or meta data you want to change th subscription to. Can be 'active', 'on-hold', 'cancelled', 'expired', 'trash', 'deleted', 'failed', 'new-payment-date' or some other value attached to the 'woocommerce_can_subscription_be_changed_to' filter.
* @param object $args Set of values used in @see WC_Subscriptions_Manager::can_subscription_be_changed_to() for determining if a subscription can be changes, include:
* 'subscription_key' string A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* 'subscription' array Subscription of the form returned by @see WC_Subscriptions_Manager::get_subscription()
* 'user_id' int The ID of the subscriber.
* 'order' WC_Order The order which recorded the successful payment (to make up for the failed automatic payment).
* 'payment_gateway' WC_Payment_Gateway The subscription's recurring payment gateway
* 'order_uses_manual_payments' bool A boolean flag indicating whether the subscription requires manual renewal payment.
* @since 1.4
*/
public static function can_subscription_be_changed_to( $subscription_can_be_changed, $new_status_or_meta, $args ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::can_subscription_be_updated_to_new_payment_method()' );
if ( 'new-payment-method' === $new_status_or_meta ) {
$subscription_can_be_changed = wcs_get_subscription_from_key( $args->subscription_key )->can_be_updated_to( 'new-payment-method' );
}
return $subscription_can_be_changed;
}
}
WC_Subscriptions_Change_Payment_Gateway::init();

View File

@@ -0,0 +1,437 @@
<?php
/**
* Subscriptions Checkout
*
* Extends the WooCommerce checkout class to add subscription meta on checkout.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Checkout
* @category Class
* @author Brent Shepherd
*/
class WC_Subscriptions_Checkout {
private static $signup_option_changed = false;
private static $guest_checkout_option_changed = false;
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.0
*/
public static function init() {
// We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees
add_action( 'woocommerce_checkout_order_processed', __CLASS__ . '::process_checkout', 100, 2 );
// Make sure users can register on checkout (before any other hooks before checkout)
add_action( 'woocommerce_before_checkout_form', __CLASS__ . '::make_checkout_registration_possible', -1 );
// Display account fields as required
add_action( 'woocommerce_checkout_fields', __CLASS__ . '::make_checkout_account_fields_required', 10 );
// Restore the settings after switching them for the checkout form
add_action( 'woocommerce_after_checkout_form', __CLASS__ . '::restore_checkout_registration_settings', 100 );
// Make sure guest checkout is not enabled in option param passed to WC JS
add_filter( 'woocommerce_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 );
add_filter( 'wc_checkout_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 );
// Force registration during checkout process
add_action( 'woocommerce_before_checkout_process', __CLASS__ . '::force_registration_during_checkout', 10 );
}
/**
* Create subscriptions purchased on checkout.
*
* @param int $order_id The post_id of a shop_order post/WC_Order object
* @param array $posted_data The data posted on checkout
* @since 2.0
*/
public static function process_checkout( $order_id, $posted_data ) {
if ( ! WC_Subscriptions_Cart::cart_contains_subscription() ) {
return;
}
$order = new WC_Order( $order_id );
$subscriptions = array();
// First clear out any subscriptions created for a failed payment to give us a clean slate for creating new subscriptions
$subscriptions = wcs_get_subscriptions_for_order( $order->id, array( 'order_type' => 'parent' ) );
if ( ! empty( $subscriptions ) ) {
remove_action( 'before_delete_post', 'WC_Subscriptions_Manager::maybe_cancel_subscription' );
foreach ( $subscriptions as $subscription ) {
wp_delete_post( $subscription->id );
}
add_action( 'before_delete_post', 'WC_Subscriptions_Manager::maybe_cancel_subscription' );
}
WC_Subscriptions_Cart::set_global_recurring_shipping_packages();
// Create new subscriptions for each group of subscription products in the cart (that is not a renewal)
foreach ( WC()->cart->recurring_carts as $recurring_cart ) {
$subscription = self::create_subscription( $order, $recurring_cart ); // Exceptions are caught by WooCommerce
if ( is_wp_error( $subscription ) ) {
throw new Exception( $subscription->get_error_message() );
}
do_action( 'woocommerce_checkout_subscription_created', $subscription, $order, $recurring_cart );
}
do_action( 'subscriptions_created_for_order', $order ); // Backward compatibility
}
/**
* Create a new subscription from a cart item on checkout.
*
* The function doesn't validate whether the cart item is a subscription product, meaning it can be used for any cart item,
* but the item will need a `subscription_period` and `subscription_period_interval` value set on it, at a minimum.
*
* @param WC_Order $order
* @param WC_Cart $cart
* @since 2.0
*/
public static function create_subscription( $order, $cart ) {
global $wpdb;
try {
// Start transaction if available
$wpdb->query( 'START TRANSACTION' );
// Set the recurring line totals on the subscription
$variation_id = wcs_cart_pluck( $cart, 'variation_id' );
$product_id = empty( $variation_id ) ? wcs_cart_pluck( $cart, 'product_id' ) : $variation_id;
// We need to use the $order->order_date value because the post_date_gmt isn't always set
$order_date_gmt = get_gmt_from_date( $order->order_date );
$subscription = wcs_create_subscription( array(
'start_date' => $cart->start_date,
'order_id' => $order->id,
'customer_id' => $order->get_user_id(),
'billing_period' => wcs_cart_pluck( $cart, 'subscription_period' ),
'billing_interval' => wcs_cart_pluck( $cart, 'subscription_period_interval' ),
'customer_note' => $order->customer_note,
) );
if ( is_wp_error( $subscription ) ) {
throw new Exception( $subscription->get_error_message() );
}
// Set the subscription's billing and shipping address
$subscription = wcs_copy_order_address( $order, $subscription );
$subscription->update_dates( array(
'trial_end' => $cart->trial_end_date,
'next_payment' => $cart->next_payment_date,
'end' => $cart->end_date,
) );
// Store trial period for PayPal
if ( wcs_cart_pluck( $cart, 'subscription_trial_length' ) > 0 ) {
update_post_meta( $subscription->id, '_trial_period', wcs_cart_pluck( $cart, 'subscription_trial_period' ) );
}
// Set the payment method on the subscription
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( $cart->needs_payment() && isset( $available_gateways[ $order->payment_method ] ) ) {
$subscription->set_payment_method( $available_gateways[ $order->payment_method ] );
}
if ( ! $cart->needs_payment() || 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
$subscription->update_manual( 'true' );
} elseif ( ! isset( $available_gateways[ $order->payment_method ] ) || ! $available_gateways[ $order->payment_method ]->supports( 'subscriptions' ) ) {
$subscription->update_manual( 'true' );
}
wcs_copy_order_meta( $order, $subscription, 'subscription' );
// Store the line items
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
$item_id = self::add_cart_item( $subscription, $cart_item, $cart_item_key );
}
// Store fees (although no fees recur by default, extensions may add them)
foreach ( $cart->get_fees() as $fee_key => $fee ) {
$item_id = $subscription->add_fee( $fee );
if ( ! $item_id ) {
// translators: placeholder is an internal error number
throw new Exception( sprintf( __( 'Error %d: Unable to create subscription. Please try again.', 'woocommerce-subscriptions' ), 403 ) );
}
// Allow plugins to add order item meta to fees
do_action( 'woocommerce_add_order_fee_meta', $order->id, $item_id, $fee, $fee_key );
}
self::add_shipping( $subscription, $cart );
// Store tax rows
foreach ( array_keys( $cart->taxes + $cart->shipping_taxes ) as $tax_rate_id ) {
if ( $tax_rate_id && ! $subscription->add_tax( $tax_rate_id, $cart->get_tax_amount( $tax_rate_id ), $cart->get_shipping_tax_amount( $tax_rate_id ) ) && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
// translators: placeholder is an internal error number
throw new Exception( sprintf( __( 'Error %d: Unable to add tax to subscription. Please try again.', 'woocommerce-subscriptions' ), 405 ) );
}
}
// Store coupons
foreach ( $cart->get_coupons() as $code => $coupon ) {
if ( ! $subscription->add_coupon( $code, $cart->get_coupon_discount_amount( $code ), $cart->get_coupon_discount_tax_amount( $code ) ) ) {
// translators: placeholder is an internal error number
throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce-subscriptions' ), 406 ) );
}
}
// Set the recurring totals on the subscription
$subscription->set_total( $cart->shipping_total, 'shipping' );
$subscription->set_total( $cart->get_cart_discount_total(), 'cart_discount' );
$subscription->set_total( $cart->get_cart_discount_tax_total(), 'cart_discount_tax' );
$subscription->set_total( $cart->tax_total, 'tax' );
$subscription->set_total( $cart->shipping_tax_total, 'shipping_tax' );
$subscription->set_total( $cart->total );
// If we got here, the subscription was created without problems
$wpdb->query( 'COMMIT' );
} catch ( Exception $e ) {
// There was an error adding the subscription
$wpdb->query( 'ROLLBACK' );
return new WP_Error( 'checkout-error', $e->getMessage() );
}
return $subscription;
}
/**
* Stores shipping info on the subscription
*
* @param WC_Subscription $subscription instance of a subscriptions object
* @param WC_Cart $cart A cart with recurring items in it
*/
public static function add_shipping( $subscription, $cart ) {
// We need to make sure we only get recurring shipping packages
WC_Subscriptions_Cart::set_calculation_type( 'recurring_total' );
foreach ( $cart->get_shipping_packages() as $package_index => $base_package ) {
$package = WC()->shipping->calculate_shipping_for_package( $base_package );
$recurring_shipping_package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $cart->recurring_cart_key, $package_index );
$shipping_method_id = isset( WC()->checkout()->shipping_methods[ $package_index ] ) ? WC()->checkout()->shipping_methods[ $package_index ] : '';
if ( isset( WC()->checkout()->shipping_methods[ $recurring_shipping_package_key ] ) ) {
$shipping_method_id = WC()->checkout()->shipping_methods[ $recurring_shipping_package_key ];
$package_key = $recurring_shipping_package_key;
} else {
$package_key = $package_index;
}
if ( isset( $package['rates'][ $shipping_method_id ] ) ) {
$item_id = $subscription->add_shipping( $package['rates'][ $shipping_method_id ] );
if ( ! $item_id ) {
throw new Exception( __( 'Error: Unable to create subscription. Please try again.', 'woocommerce-subscriptions' ) );
}
// Allows plugins to add order item meta to shipping
do_action( 'woocommerce_add_shipping_order_item', $subscription->id, $item_id, $package_key );
do_action( 'woocommerce_subscriptions_add_recurring_shipping_order_item', $subscription->id, $item_id, $package_key );
}
}
WC_Subscriptions_Cart::set_calculation_type( 'none' );
}
/**
* Add a cart item to a subscription.
*
* @since 2.0
*/
public static function add_cart_item( $subscription, $cart_item, $cart_item_key ) {
$item_id = $subscription->add_product(
$cart_item['data'],
$cart_item['quantity'],
array(
'variation' => $cart_item['variation'],
'totals' => array(
'subtotal' => $cart_item['line_subtotal'],
'subtotal_tax' => $cart_item['line_subtotal_tax'],
'total' => $cart_item['line_total'],
'tax' => $cart_item['line_tax'],
'tax_data' => $cart_item['line_tax_data'],
),
)
);
if ( ! $item_id ) {
// translators: placeholder is an internal error number
throw new Exception( sprintf( __( 'Error %d: Unable to create subscription. Please try again.', 'woocommerce-subscriptions' ), 402 ) );
}
$cart_item_product_id = ( 0 != $cart_item['variation_id'] ) ? $cart_item['variation_id'] : $cart_item['product_id'];
if ( WC_Subscriptions_Product::get_trial_length( wcs_get_canonical_product_id( $cart_item ) ) > 0 ) {
wc_add_order_item_meta( $item_id, '_has_trial', 'true' );
}
// Allow plugins to add order item meta
do_action( 'woocommerce_add_order_item_meta', $item_id, $cart_item, $cart_item_key );
do_action( 'woocommerce_add_subscription_item_meta', $item_id, $cart_item, $cart_item_key );
return $item_id;
}
/**
* When a new order is inserted, add subscriptions related order meta.
*
* @since 1.0
*/
public static function add_order_meta( $order_id, $posted ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Add each subscription product's details to an order so that the state of the subscription persists even when a product is changed
*
* @since 1.2.5
*/
public static function add_order_item_meta( $item_id, $values ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* If shopping cart contains subscriptions, make sure a user can register on the checkout page
*
* @since 1.0
*/
public static function make_checkout_registration_possible( $checkout = '' ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && ! is_user_logged_in() ) {
// Make sure users can sign up
if ( false === $checkout->enable_signup ) {
$checkout->enable_signup = true;
self::$signup_option_changed = true;
}
// Make sure users are required to register an account
if ( true === $checkout->enable_guest_checkout ) {
$checkout->enable_guest_checkout = false;
self::$guest_checkout_option_changed = true;
if ( ! is_user_logged_in() ) {
$checkout->must_create_account = true;
}
}
}
}
/**
* Make sure account fields display the required "*" when they are required.
*
* @since 1.3.5
*/
public static function make_checkout_account_fields_required( $checkout_fields ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && ! is_user_logged_in() ) {
$account_fields = array(
'account_username',
'account_password',
'account_password-2',
);
foreach ( $account_fields as $account_field ) {
if ( isset( $checkout_fields['account'][ $account_field ] ) ) {
$checkout_fields['account'][ $account_field ]['required'] = true;
}
}
}
return $checkout_fields;
}
/**
* After displaying the checkout form, restore the store's original registration settings.
*
* @since 1.1
*/
public static function restore_checkout_registration_settings( $checkout = '' ) {
if ( self::$signup_option_changed ) {
$checkout->enable_signup = false;
}
if ( self::$guest_checkout_option_changed ) {
$checkout->enable_guest_checkout = true;
if ( ! is_user_logged_in() ) { // Also changed must_create_account
$checkout->must_create_account = false;
}
}
}
/**
* Also make sure the guest checkout option value passed to the woocommerce.js forces registration.
* Otherwise the registration form is hidden by woocommerce.js.
*
* @since 1.1
*/
public static function filter_woocommerce_script_paramaters( $woocommerce_params ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && ! is_user_logged_in() && isset( $woocommerce_params['option_guest_checkout'] ) && 'yes' == $woocommerce_params['option_guest_checkout'] ) {
$woocommerce_params['option_guest_checkout'] = 'no';
}
return $woocommerce_params;
}
/**
* During the checkout process, force registration when the cart contains a subscription.
*
* @since 1.1
*/
public static function force_registration_during_checkout( $woocommerce_params ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && ! is_user_logged_in() ) {
$_POST['createaccount'] = 1;
}
}
/**
* When creating an order at checkout, if the checkout is to renew a subscription from a failed
* payment, hijack the order creation to make a renewal order - not a plain WooCommerce order.
*
* @since 1.3
* @deprecated 2.0
*/
public static function filter_woocommerce_create_order( $order_id, $checkout_object ) {
_deprecated_function( __METHOD__, '2.0' );
return $order_id;
}
/**
* Customise which actions are shown against a subscriptions order on the My Account page.
*
* @since 1.3
*/
public static function filter_woocommerce_my_account_my_orders_actions( $actions, $order ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::filter_my_account_my_orders_actions()' );
return $actions;
}
}
WC_Subscriptions_Checkout::init();

View File

@@ -0,0 +1,701 @@
<?php
/**
* Subscriptions Coupon Class
*
* Mirrors a few functions in the WC_Cart class to handle subscription-specific discounts
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Coupon
* @category Class
* @author Max Rice
* @since 1.2
*/
class WC_Subscriptions_Coupon {
/** @var string error message for invalid subscription coupons */
public static $coupon_error;
/**
* Stores the coupons not applied to a given calculation (so they can be applied later)
*
* @since 1.3.5
*/
private static $removed_coupons = array();
/**
* Set up the class, including it's hooks & filters, when the file is loaded.
*
* @since 1.2
**/
public static function init() {
// Add custom coupon types
add_filter( 'woocommerce_coupon_discount_types', __CLASS__ . '::add_discount_types' );
// Handle discounts
add_filter( 'woocommerce_coupon_get_discount_amount', __CLASS__ . '::get_discount_amount', 10, 5 );
// Validate subscription coupons
add_filter( 'woocommerce_coupon_is_valid', __CLASS__ . '::validate_subscription_coupon', 10, 2 );
// Remove coupons which don't apply to certain cart calculations
add_action( 'woocommerce_before_calculate_totals', __CLASS__ . '::remove_coupons', 10 );
// Add our recurring product coupon types to the list of coupon types that apply to individual products
add_filter( 'woocommerce_product_coupon_types', __CLASS__ . '::filter_product_coupon_types', 10, 1 );
}
/**
* Add discount types
*
* @since 1.2
*/
public static function add_discount_types( $discount_types ) {
return array_merge(
$discount_types,
array(
'sign_up_fee' => __( 'Sign Up Fee Discount', 'woocommerce-subscriptions' ),
'sign_up_fee_percent' => __( 'Sign Up Fee % Discount', 'woocommerce-subscriptions' ),
'recurring_fee' => __( 'Recurring Product Discount', 'woocommerce-subscriptions' ),
'recurring_percent' => __( 'Recurring Product % Discount', 'woocommerce-subscriptions' ),
)
);
}
/**
* Get the discount amount for Subscriptions coupon types
*
* @since 2.0.10
*/
public static function get_discount_amount( $discount, $discounting_amount, $cart_item, $single, $coupon ) {
// Only deal with subscriptions coupon types
if ( ! in_array( $coupon->type, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
return $discount;
}
$product_id = ( $cart_item['data']->is_type( array( 'subscription_variation' ) ) ) ? $cart_item['data']->variation_id : $cart_item['data']->id;
// If not a subscription product return the default discount
if ( ! wcs_cart_contains_renewal() && ! WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) {
return $discount;
}
// But if cart contains a renewal, we need to handle both subscription products and manually added non-susbscription products that could be part of a subscription
if ( wcs_cart_contains_renewal() && ! self::is_subsbcription_renewal_line_item( $product_id, $cart_item ) ) {
return $discount;
}
// Set our starting discount amount to 0
$discount_amount = 0;
// Item quantity
$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
// Get calculation type
$calculation_type = WC_Subscriptions_Cart::get_calculation_type();
// Set the defaults for our logic checks to false
$apply_recurring_coupon = $apply_recurring_percent_coupon = $apply_initial_coupon = $apply_initial_percent_coupon = $apply_renewal_cart_coupon = false;
// Check if we're applying any recurring discounts to recurring total calculations
if ( 'recurring_total' == $calculation_type ) {
$apply_recurring_coupon = ( 'recurring_fee' == $coupon->type ) ? true : false;
$apply_recurring_percent_coupon = ( 'recurring_percent' == $coupon->type ) ? true : false;
}
// Check if we're applying any initial discounts
if ( 'none' == $calculation_type ) {
// If all items have a free trial we don't need to apply recurring coupons to the initial total
if ( ! WC_Subscriptions_Cart::all_cart_items_have_free_trial() ) {
if ( 'recurring_fee' == $coupon->type ) {
$apply_initial_coupon = true;
}
if ( 'recurring_percent' == $coupon->type ) {
$apply_initial_percent_coupon = true;
}
}
// Apply sign-up discounts
if ( ! empty( $cart_item['data']->subscription_sign_up_fee ) ) {
if ( 'sign_up_fee' == $coupon->type ) {
$apply_initial_coupon = true;
}
if ( 'sign_up_fee_percent' == $coupon->type ) {
$apply_initial_percent_coupon = true;
}
// Only Sign up fee coupons apply to sign up fees, adjust the discounting_amount accordingly
if ( in_array( $coupon->type, array( 'sign_up_fee', 'sign_up_fee_percent' ) ) ) {
$discounting_amount = $cart_item['data']->subscription_sign_up_fee;
} else {
$discounting_amount -= $cart_item['data']->subscription_sign_up_fee;
}
}
// Apply renewal discounts
if ( 'renewal_fee' == $coupon->type ) {
$apply_recurring_coupon = true;
}
if ( 'renewal_percent' == $coupon->type ) {
$apply_recurring_percent_coupon = true;
}
if ( 'renewal_cart' == $coupon->type ) {
$apply_renewal_cart_coupon = true;
}
}
// Calculate our discount
if ( $apply_recurring_coupon || $apply_initial_coupon ) {
// Recurring coupons only apply when there is no free trial (carts can have a mix of free trial and non free trial items)
if ( $apply_initial_coupon && 'recurring_fee' == $coupon->type && ! empty( $cart_item['data']->subscription_trial_length ) ) {
$discounting_amount = 0;
}
$discount_amount = min( $coupon->coupon_amount, $discounting_amount );
$discount_amount = $single ? $discount_amount : $discount_amount * $cart_item_qty;
} elseif ( $apply_recurring_percent_coupon ) {
$discount_amount = ( $discounting_amount / 100 ) * $coupon->amount;
} elseif ( $apply_initial_percent_coupon ) {
// Recurring coupons only apply when there is no free trial (carts can have a mix of free trial and non free trial items)
if ( 'recurring_percent' == $coupon->type && ! empty( $cart_item['data']->subscription_trial_length ) ) {
$discounting_amount = 0;
}
$discount_amount = ( $discounting_amount / 100 ) * $coupon->amount;
} elseif ( $apply_renewal_cart_coupon ) {
/**
* See WC Core fixed_cart coupons - we need to divide the discount between rows based on their price in proportion to the subtotal.
* This is so rows with different tax rates get a fair discount, and so rows with no price (free) don't get discounted.
*
* BUT... we also need the subtotal to exclude non renewal products, so user the renewal subtotal
*/
$discount_percent = ( $discounting_amount * $cart_item['quantity'] ) / self::get_renewal_subtotal( $coupon->code );
$discount_amount = ( $coupon->amount * $discount_percent ) / $cart_item_qty;
}
// Round - consistent with WC approach
$discount_amount = round( $discount_amount, WC_ROUNDING_PRECISION );
return $discount_amount;
}
/**
* Determine if the cart contains a discount code of a given coupon type.
*
* Used internally for checking if a WooCommerce discount coupon ('core') has been applied, or for if a specific
* subscription coupon type, like 'recurring_fee' or 'sign_up_fee', has been applied.
*
* @param string $coupon_type Any available coupon type or a special keyword referring to a class of coupons. Can be:
* - 'any' to check for any type of discount
* - 'core' for any core WooCommerce coupon
* - 'recurring_fee' for the recurring amount subscription coupon
* - 'sign_up_fee' for the sign-up fee subscription coupon
*
* @since 1.3.5
*/
public static function cart_contains_discount( $coupon_type = 'any' ) {
$contains_discount = false;
$core_coupons = array( 'fixed_product', 'percent_product', 'fixed_cart', 'percent' );
if ( WC()->cart->applied_coupons ) {
foreach ( WC()->cart->applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
if ( 'any' == $coupon_type || $coupon_type == $coupon->type || ( 'core' == $coupon_type && in_array( $coupon->type, $core_coupons ) ) ) {
$contains_discount = true;
break;
}
}
}
return $contains_discount;
}
/**
* Check if a subscription coupon is valid before applying
*
* @since 1.2
*/
public static function validate_subscription_coupon( $valid, $coupon ) {
if ( ! apply_filters( 'woocommerce_subscriptions_validate_coupon_type', true, $coupon, $valid ) ) {
return $valid;
}
self::$coupon_error = '';
// ignore non-subscription coupons
if ( ! in_array( $coupon->type, array( 'recurring_fee', 'sign_up_fee', 'recurring_percent', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
// but make sure there is actually something for the coupon to be applied to (i.e. not a free trial)
if ( ( wcs_cart_contains_renewal() || WC_Subscriptions_Cart::cart_contains_subscription() ) && 0 == WC()->cart->subtotal ) {
self::$coupon_error = __( 'Sorry, this coupon is only valid for an initial payment and the cart does not require an initial payment.', 'woocommerce-subscriptions' );
}
} else {
// prevent subscription coupons from being applied to renewal payments
if ( wcs_cart_contains_renewal() && ! in_array( $coupon->type, array( 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
self::$coupon_error = __( 'Sorry, this coupon is only valid for new subscriptions.', 'woocommerce-subscriptions' );
}
// prevent subscription coupons from being applied to non-subscription products
if ( ! wcs_cart_contains_renewal() && ! WC_Subscriptions_Cart::cart_contains_subscription() ) {
self::$coupon_error = __( 'Sorry, this coupon is only valid for subscription products.', 'woocommerce-subscriptions' );
}
// prevent subscription renewal coupons from being applied to non renewal payments
if ( ! wcs_cart_contains_renewal() && in_array( $coupon->type, array( 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
// translators: 1$: coupon code that is being removed
self::$coupon_error = sprintf( __( 'Sorry, the "%1$s" coupon is only valid for renewals.', 'woocommerce-subscriptions' ), $coupon->code );
}
// prevent sign up fee coupons from being applied to subscriptions without a sign up fee
if ( 0 == WC_Subscriptions_Cart::get_cart_subscription_sign_up_fee() && in_array( $coupon->type, array( 'sign_up_fee', 'sign_up_fee_percent' ) ) ) {
self::$coupon_error = __( 'Sorry, this coupon is only valid for subscription products with a sign-up fee.', 'woocommerce-subscriptions' );
}
}
if ( ! empty( self::$coupon_error ) ) {
$valid = false;
add_filter( 'woocommerce_coupon_error', __CLASS__ . '::add_coupon_error', 10 );
}
return $valid;
}
/**
* Returns a subscription coupon-specific error if validation failed
*
* @since 1.2
*/
public static function add_coupon_error( $error ) {
if ( self::$coupon_error ) {
return self::$coupon_error;
} else {
return $error;
}
}
/**
* Checks a given product / coupon combination to determine if the subscription should be discounted
*
* @since 1.2
*/
private static function is_subscription_discountable( $cart_item, $coupon ) {
$product_cats = wp_get_post_terms( $cart_item['product_id'], 'product_cat', array( 'fields' => 'ids' ) );
$this_item_is_discounted = false;
// Specific products get the discount
if ( sizeof( $coupon->product_ids ) > 0 ) {
if ( in_array( wcs_get_canonical_product_id( $cart_item ), $coupon->product_ids ) || in_array( $cart_item['data']->get_parent(), $coupon->product_ids ) ) {
$this_item_is_discounted = true;
}
// Category discounts
} elseif ( sizeof( $coupon->product_categories ) > 0 ) {
if ( sizeof( array_intersect( $product_cats, $coupon->product_categories ) ) > 0 ) {
$this_item_is_discounted = true;
}
} else {
// No product ids - all items discounted
$this_item_is_discounted = true;
}
// Specific product ID's excluded from the discount
if ( sizeof( $coupon->exclude_product_ids ) > 0 ) {
if ( in_array( wcs_get_canonical_product_id( $cart_item ), $coupon->exclude_product_ids ) || in_array( $cart_item['data']->get_parent(), $coupon->exclude_product_ids ) ) {
$this_item_is_discounted = false;
}
}
// Specific categories excluded from the discount
if ( sizeof( $coupon->exclude_product_categories ) > 0 ) {
if ( sizeof( array_intersect( $product_cats, $coupon->exclude_product_categories ) ) > 0 ) {
$this_item_is_discounted = false;
}
}
// Apply filter
return apply_filters( 'woocommerce_item_is_discounted', $this_item_is_discounted, $cart_item, $before_tax = false );
}
/**
* Sets which coupons should be applied for this calculation.
*
* This function is hooked to "woocommerce_before_calculate_totals" so that WC will calculate a subscription
* product's total based on the total of it's price per period and sign up fee (if any).
*
* @since 1.3.5
*/
public static function remove_coupons( $cart ) {
$calculation_type = WC_Subscriptions_Cart::get_calculation_type();
// Only hook when totals are being calculated completely (on cart & checkout pages)
if ( 'none' == $calculation_type || ! WC_Subscriptions_Cart::cart_contains_subscription() || ( ! is_checkout() && ! is_cart() && ! defined( 'WOOCOMMERCE_CHECKOUT' ) && ! defined( 'WOOCOMMERCE_CART' ) ) ) {
return;
}
$applied_coupons = $cart->get_applied_coupons();
// If we're calculating a sign-up fee or recurring fee only amount, remove irrelevant coupons
if ( ! empty( $applied_coupons ) ) {
// Keep track of which coupons, if any, need to be reapplied immediately
$coupons_to_reapply = array();
foreach ( $applied_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
if ( in_array( $coupon->type, array( 'recurring_fee', 'recurring_percent' ) ) ) { // always apply coupons to their specific calculation case
if ( 'recurring_total' == $calculation_type ) {
$coupons_to_reapply[] = $coupon_code;
} elseif ( 'none' == $calculation_type && ! WC_Subscriptions_Cart::all_cart_items_have_free_trial() ) { // sometimes apply recurring coupons to initial total
$coupons_to_reapply[] = $coupon_code;
} else {
self::$removed_coupons[] = $coupon_code;
}
} elseif ( ( 'none' == $calculation_type ) && ! in_array( $coupon->type, array( 'recurring_fee', 'recurring_percent' ) ) ) { // apply all coupons to the first payment
$coupons_to_reapply[] = $coupon_code;
} else {
self::$removed_coupons[] = $coupon_code;
}
}
// Now remove all coupons (WC only provides a function to remove all coupons)
$cart->remove_coupons();
// And re-apply those which relate to this calculation
$cart->applied_coupons = $coupons_to_reapply;
if ( isset( $cart->coupons ) ) { // WC 2.3+
$cart->coupons = $cart->get_coupons();
}
}
}
/**
* Add our recurring product coupon types to the list of coupon types that apply to individual products.
* Used to control which validation rules will apply.
*
* @param array $product_coupon_types
* @return array $product_coupon_types
*/
public static function filter_product_coupon_types( $product_coupon_types ) {
if ( is_array( $product_coupon_types ) ) {
$product_coupon_types = array_merge( $product_coupon_types, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) );
}
return $product_coupon_types;
}
/**
* Get subtotals for a renewal subscription so that our pseudo renewal_cart discounts can be applied correctly even if other items have been added to the cart
*
* @param string $code coupon code
* @return array subtotal
* @since 2.0.10
*/
private static function get_renewal_subtotal( $code ) {
$renewal_coupons = WC()->session->get( 'wcs_renewal_coupons' );
if ( empty( $renewal_coupons ) ) {
return false;
}
$subtotal = 0;
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
if ( $coupon->code == $code ) {
if ( $subscription = wcs_get_subscription( $subscription_id ) ) {
$subtotal = $subscription->get_subtotal();
}
break;
}
}
}
return $subtotal;
}
/**
* Check if a product is a renewal order line item (rather than a "susbscription") - to pick up non-subsbcription products added a subscription manually
*
* @param int $product_id
* @param array $cart_item
* @param WC_Cart $cart The WooCommerce cart object.
* @return boolean whether a product is a renewal order line item
* @since 2.0.10
*/
private static function is_subsbcription_renewal_line_item( $product_id, $cart_item ) {
$is_subscription_line_item = false;
if ( is_object( $product_id ) ) {
$product = $product_id;
$product_id = $product->id;
} elseif ( is_numeric( $product_id ) ) {
$product = wc_get_product( $product_id );
}
if ( ! empty( $cart_item['subscription_renewal'] ) ) {
if ( $subscription = wcs_get_subscription( $cart_item['subscription_renewal']['subscription_id'] ) ) {
foreach ( $subscription->get_items() as $item ) {
$item_product_id = ( $item['variation_id'] ) ? $item['variation_id'] : $item['product_id'];
if ( ! empty( $item_product_id ) && $item_product_id == $product_id ) {
$is_subscription_line_item = true;
}
}
}
}
return apply_filters( 'woocommerce_is_subscription_renewal_line_item', $is_subscription_line_item, $product_id, $cart_item );
}
/* Deprecated */
/**
* Apply sign up fee or recurring fee discount
*
* @since 1.2
*/
public static function apply_subscription_discount( $original_price, $cart_item, $cart ) {
_deprecated_function( __METHOD__, '2.0.10', 'Have moved to filtering on "woocommerce_coupon_get_discount_amount" to return discount amount. See: '. __CLASS__ .'::get_discount_amount()' );
$product_id = ( $cart_item['data']->is_type( array( 'subscription_variation' ) ) ) ? $cart_item['data']->variation_id : $cart_item['data']->id;
if ( ! WC_Subscriptions_Product::is_subscription( $product_id ) ) {
return $original_price;
}
$price = $calculation_price = $original_price;
$calculation_type = WC_Subscriptions_Cart::get_calculation_type();
if ( ! empty( $cart->applied_coupons ) ) {
foreach ( $cart->applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
// Pre 2.5 is_valid_for_product() does not use wc_get_product_coupon_types()
if ( WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
$is_valid_for_product = true;
} else {
$is_valid_for_product = $coupon->is_valid_for_product( wc_get_product( $product_id ), $cart_item );
}
if ( $coupon->apply_before_tax() && $coupon->is_valid() && $is_valid_for_product ) {
$apply_recurring_coupon = $apply_recurring_percent_coupon = $apply_initial_coupon = $apply_initial_percent_coupon = false;
// Apply recurring fee discounts to recurring total calculations
if ( 'recurring_total' == $calculation_type ) {
$apply_recurring_coupon = ( 'recurring_fee' == $coupon->type ) ? true : false;
$apply_recurring_percent_coupon = ( 'recurring_percent' == $coupon->type ) ? true : false;
}
if ( 'none' == $calculation_type ) {
// If all items have a free trial we don't need to apply recurring coupons to the initial total
if ( ! WC_Subscriptions_Cart::all_cart_items_have_free_trial() ) {
if ( 'recurring_fee' == $coupon->type ) {
$apply_initial_coupon = true;
}
if ( 'recurring_percent' == $coupon->type ) {
$apply_initial_percent_coupon = true;
}
}
// Apply sign-up discounts to initial total
if ( ! empty( $cart_item['data']->subscription_sign_up_fee ) ) {
if ( 'sign_up_fee' == $coupon->type ) {
$apply_initial_coupon = true;
}
if ( 'sign_up_fee_percent' == $coupon->type ) {
$apply_initial_percent_coupon = true;
}
$calculation_price = $cart_item['data']->subscription_sign_up_fee;
}
}
if ( $apply_recurring_coupon || $apply_initial_coupon ) {
$discount_amount = ( $calculation_price < $coupon->amount ) ? $calculation_price : $coupon->amount;
// Recurring coupons only apply when there is no free trial (carts can have a mix of free trial and non free trial items)
if ( $apply_initial_coupon && 'recurring_fee' == $coupon->type && ! empty( $cart_item['data']->subscription_trial_length ) ) {
$discount_amount = 0;
}
$cart->discount_cart = $cart->discount_cart + ( $discount_amount * $cart_item['quantity'] );
$cart = self::increase_coupon_discount_amount( $cart, $coupon->code, $discount_amount * $cart_item['quantity'] );
$price = $price - $discount_amount;
} elseif ( $apply_recurring_percent_coupon ) {
$discount_amount = round( ( $calculation_price / 100 ) * $coupon->amount, WC()->cart->dp );
$cart->discount_cart = $cart->discount_cart + ( $discount_amount * $cart_item['quantity'] );
$cart = self::increase_coupon_discount_amount( $cart, $coupon->code, $discount_amount * $cart_item['quantity'] );
$price = $price - $discount_amount;
} elseif ( $apply_initial_percent_coupon ) {
// Recurring coupons only apply when there is no free trial (carts can have a mix of free trial and non free trial items)
if ( 'recurring_percent' == $coupon->type && empty( $cart_item['data']->subscription_trial_length ) ) {
$amount_to_discount = $cart_item['data']->subscription_price;
} else {
$amount_to_discount = 0;
}
// Sign up fee coupons only apply to sign up fees
if ( 'sign_up_fee_percent' == $coupon->type ) {
$amount_to_discount = $cart_item['data']->subscription_sign_up_fee;
}
$discount_amount = round( ( $amount_to_discount / 100 ) * $coupon->amount, WC()->cart->dp );
$cart->discount_cart = $cart->discount_cart + $discount_amount * $cart_item['quantity'];
$cart = self::increase_coupon_discount_amount( $cart, $coupon->code, $discount_amount * $cart_item['quantity'] );
$price = $price - $discount_amount;
}
}
}
if ( $price < 0 ) {
$price = 0;
}
}
return $price;
}
/**
* Store how much discount each coupon grants.
*
* @since 2.0
* @param WC_Cart $cart The WooCommerce cart object.
* @param mixed $code
* @param mixed $amount
* @return WC_Cart $cart
*/
public static function increase_coupon_discount_amount( $cart, $code, $amount ) {
_deprecated_function( __METHOD__, '2.0.10' );
if ( empty( $cart->coupon_discount_amounts[ $code ] ) ) {
$cart->coupon_discount_amounts[ $code ] = 0;
}
$cart->coupon_discount_amounts[ $code ] += $amount;
return $cart;
}
/**
* Determines if cart contains a recurring fee discount code
*
* Does not check if the code is valid, etc
*
* @since 1.2
*/
public static function cart_contains_recurring_discount() {
_deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "recurring_fee" )' );
return self::cart_contains_discount( 'recurring_fee' );
}
/**
* Determines if cart contains a sign up fee discount code
*
* Does not check if the code is valid, etc
*
* @since 1.2
*/
public static function cart_contains_sign_up_discount() {
_deprecated_function( __METHOD__, '1.3.5', __CLASS__ .'::cart_contains_discount( "sign_up_fee" )' );
return self::cart_contains_discount( 'sign_up_fee' );
}
/**
* Restores discount coupons which had been removed for special subscription calculations.
*
* @since 1.3.5
*/
public static function restore_coupons( $cart ) {
_deprecated_function( __METHOD__, '2.0' );
if ( ! empty( self::$removed_coupons ) ) {
// Can't use $cart->add_dicount here as it calls calculate_totals()
$cart->applied_coupons = array_merge( $cart->applied_coupons, self::$removed_coupons );
if ( isset( $cart->coupons ) ) { // WC 2.3+
$cart->coupons = $cart->get_coupons();
}
self::$removed_coupons = array();
}
}
/**
* Apply sign up fee or recurring fee discount before tax is calculated
*
* @since 1.2
*/
public static function apply_subscription_discount_before_tax( $original_price, $cart_item, $cart ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ .'::apply_subscription_discount( $original_price, $cart_item, $cart )' );
return self::apply_subscription_discount( $original_price, $cart_item, $cart );
}
/**
* Apply sign up fee or recurring fee discount after tax is calculated
*
* @since 1.2
* @version 1.3.6
*/
public static function apply_subscription_discount_after_tax( $coupon, $cart_item, $price ) {
_deprecated_function( __METHOD__, '2.0', 'WooCommerce 2.3 removed after tax discounts. Use ' . __CLASS__ .'::apply_subscription_discount( $original_price, $cart_item, $cart )' );
}
}
WC_Subscriptions_Coupon::init();

View File

@@ -0,0 +1,244 @@
<?php
/**
* Subscriptions Email Class
*
* Modifies the base WooCommerce email class and extends it to send subscription emails.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Email
* @category Class
* @author Brent Shepherd
*/
class WC_Subscriptions_Email {
private static $woocommerce_email;
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.0
*/
public static function init() {
add_action( 'woocommerce_email_classes', __CLASS__ . '::add_emails', 10, 1 );
add_action( 'woocommerce_init', __CLASS__ . '::hook_transactional_emails' );
add_filter( 'woocommerce_resend_order_emails_available', __CLASS__ . '::renewal_order_emails_available', -1 ); // run before other plugins so we don't remove their emails
}
/**
* Add Subscriptions' email classes.
*
* @since 1.4
*/
public static function add_emails( $email_classes ) {
require_once( 'emails/class-wcs-email-new-renewal-order.php' );
require_once( 'emails/class-wcs-email-new-switch-order.php' );
require_once( 'emails/class-wcs-email-customer-processing-renewal-order.php' );
require_once( 'emails/class-wcs-email-customer-completed-renewal-order.php' );
require_once( 'emails/class-wcs-email-customer-completed-switch-order.php' );
require_once( 'emails/class-wcs-email-customer-renewal-invoice.php' );
require_once( 'emails/class-wcs-email-cancelled-subscription.php' );
$email_classes['WCS_Email_New_Renewal_Order'] = new WCS_Email_New_Renewal_Order();
$email_classes['WCS_Email_New_Switch_Order'] = new WCS_Email_New_Switch_Order();
$email_classes['WCS_Email_Processing_Renewal_Order'] = new WCS_Email_Processing_Renewal_Order();
$email_classes['WCS_Email_Completed_Renewal_Order'] = new WCS_Email_Completed_Renewal_Order();
$email_classes['WCS_Email_Completed_Switch_Order'] = new WCS_Email_Completed_Switch_Order();
$email_classes['WCS_Email_Customer_Renewal_Invoice'] = new WCS_Email_Customer_Renewal_Invoice();
$email_classes['WCS_Email_Cancelled_Subscription'] = new WCS_Email_Cancelled_Subscription();
return $email_classes;
}
/**
* Hooks up all of Subscription's transaction emails after the WooCommerce object is constructed.
*
* @since 1.4
*/
public static function hook_transactional_emails() {
// Don't send subscription
if ( WC_Subscriptions::is_duplicate_site() && ! defined( 'WCS_FORCE_EMAIL' ) ) {
return;
}
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::send_cancelled_email', 10, 2 );
$order_email_actions = array(
'woocommerce_order_status_pending_to_processing',
'woocommerce_order_status_pending_to_completed',
'woocommerce_order_status_pending_to_on-hold',
'woocommerce_order_status_failed_to_processing',
'woocommerce_order_status_failed_to_completed',
'woocommerce_order_status_failed_to_on-hold',
'woocommerce_order_status_completed',
'woocommerce_generated_manual_renewal_order',
'woocommerce_order_status_failed',
);
foreach ( $order_email_actions as $action ) {
add_action( $action, __CLASS__ . '::maybe_remove_woocommerce_email', 9 );
add_action( $action, __CLASS__ . '::send_renewal_order_email', 10 );
add_action( $action, __CLASS__ . '::send_switch_order_email', 10 );
add_action( $action, __CLASS__ . '::maybe_reattach_woocommerce_email', 11 );
}
}
/**
* Init the mailer and call for the cancelled email notification hook.
*
* @param $subscription WC Subscription
* @since 2.0
*/
public static function send_cancelled_email( $subscription ) {
WC()->mailer();
if ( $subscription->has_status( array( 'pending-cancel', 'cancelled' ) ) && 'true' !== get_post_meta( $subscription->id, '_cancelled_email_sent', true ) ) {
do_action( 'cancelled_subscription_notification', $subscription );
}
}
/**
* Init the mailer and call the notifications for the renewal orders.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
*/
public static function send_renewal_order_email( $order_id ) {
WC()->mailer();
if ( wcs_order_contains_renewal( $order_id ) ) {
do_action( current_filter() . '_renewal_notification', $order_id );
}
}
/**
* If the order is a renewal order, don't send core emails.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
*/
public static function maybe_remove_woocommerce_email( $order_id ) {
if ( wcs_order_contains_renewal( $order_id ) || wcs_order_contains_switch( $order_id ) ) {
remove_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
}
}
/**
* If the order is a renewal order, don't send core emails.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
*/
public static function maybe_reattach_woocommerce_email( $order_id ) {
if ( wcs_order_contains_renewal( $order_id ) || wcs_order_contains_switch( $order_id ) ) {
add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
}
}
/**
* If viewing a renewal order on the the Edit Order screen, set the available email actions for the order to use
* renewal order emails, not core WooCommerce order emails.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
*/
public static function renewal_order_emails_available( $available_emails ) {
global $theorder;
if ( wcs_order_contains_renewal( $theorder->id ) ) {
$available_emails = array(
'new_renewal_order',
'customer_processing_renewal_order',
'customer_completed_renewal_order',
'customer_renewal_invoice',
);
}
return $available_emails;
}
/**
* Init the mailer and call the notifications for subscription switch orders.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
*/
public static function send_switch_order_email( $order_id ) {
WC()->mailer();
if ( wcs_order_contains_switch( $order_id ) ) {
do_action( current_filter() . '_switch_notification', $order_id );
}
}
/**
* Generate an order items table using a WC compatible version of the function.
*
* @param object $order
* @param array $args {
* @type bool 'show_download_links'
* @type bool 'show_sku'
* @type bool 'show_purchase_note'
* @type array 'image_size'
* @type bool 'plain_text'
* }
* @return string email order items table html
*/
public static function email_order_items_table( $order, $args = array() ) {
$items_table = '';
if ( is_numeric( $order ) ) {
$order = wc_get_order( $order );
}
if ( is_a( $order, 'WC_Abstract_Order' ) ) {
if ( WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
$items_table = call_user_func_array( array( $order, 'email_order_items_table' ), $args );
} else {
// 2.5 doesn't support both the show_download_links or show_purchase_note parameters but uses $order->is_download_permitted and $order->is_paid instead
$show_download_links_callback = ( isset( $args['show_download_links'] ) && $args['show_download_links'] ) ? '__return_true' : '__return_false';
$show_purchase_note_callback = ( isset( $args['show_purchase_note'] ) && $args['show_purchase_note'] ) ? '__return_true' : '__return_false';
unset( $args['show_download_links'] );
unset( $args['show_purchase_note'] );
add_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
add_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
$items_table = $order->email_order_items_table( $args );
remove_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
remove_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
}
}
return $items_table;
}
/**
* Init the mailer and call the notifications for the current filter.
*
* @param int $user_id The ID of the user who the subscription belongs to
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @return void
* @deprecated 2.0
*/
public static function send_subscription_email( $user_id, $subscription_key ) {
_deprecated_function( __FUNCTION__, '2.0' );
}
}
WC_Subscriptions_Email::init();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,739 @@
<?php
/**
* Subscriptions Renewal Order Class
*
* Provides an API for creating and handling renewal orders.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Order
* @category Class
* @author Brent Shepherd
* @since 1.2
*/
class WC_Subscriptions_Renewal_Order {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.0
*/
public static function init() {
// Trigger special hook when payment is completed on renewal orders
add_action( 'woocommerce_payment_complete', __CLASS__ . '::trigger_renewal_payment_complete', 10 );
// When a renewal order's status changes, check if a corresponding subscription's status should be changed by marking it as paid (we can't use the 'woocommerce_payment_complete' here because it's not triggered by all payment gateways)
add_filter( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_record_subscription_payment', 10, 3 );
add_filter( 'wcs_renewal_order_created', __CLASS__ . '::add_order_note', 10, 2 );
// Prevent customers from cancelling renewal orders. Needs to be hooked before WC_Form_Handler::cancel_order() (20)
add_filter( 'wp_loaded', __CLASS__ . '::prevent_cancelling_renewal_orders', 19, 3 );
// Don't copy switch order item meta to renewal order items
add_filter( 'wcs_new_order_items', __CLASS__ . '::remove_switch_item_meta_keys', 10, 1 );
}
/* Helper functions */
/**
* Trigger a special hook for payments on a completed renewal order.
*
* @since 1.5.4
*/
public static function trigger_renewal_payment_complete( $order_id ) {
if ( wcs_order_contains_renewal( $order_id ) ) {
do_action( 'woocommerce_renewal_order_payment_complete', $order_id );
}
}
/**
* Check if a given renewal order was created to replace a failed renewal order.
*
* @since 1.5.12
* @param int ID of the renewal order you want to check against
* @return mixed If the renewal order did replace a failed order, the ID of the fail order, else false
*/
public static function get_failed_order_replaced_by( $renewal_order_id ) {
global $wpdb;
$failed_order_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_failed_order_replaced_by' AND meta_value = %s", $renewal_order_id ) );
return ( null === $failed_order_id ) ? false : $failed_order_id;
}
/**
* Whenever a renewal order's status is changed, check if a corresponding subscription's status should be changed
*
* This function is hooked to 'woocommerce_order_status_changed', rather than 'woocommerce_payment_complete', to ensure
* subscriptions are updated even if payment is processed by a manual payment gateways (which would never trigger the
* 'woocommerce_payment_complete' hook) or by some other means that circumvents that hook.
*
* @since 2.0
*/
public static function maybe_record_subscription_payment( $order_id, $orders_old_status, $orders_new_status ) {
if ( ! wcs_order_contains_renewal( $order_id ) ) {
return;
}
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$was_activated = false;
$order_completed = in_array( $orders_new_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) );
$order_needed_payment = in_array( $orders_old_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
if ( $order_completed && $order_needed_payment ) {
$update_post_data = array(
'ID' => $order_id,
'post_date' => current_time( 'mysql', 0 ),
'post_date_gmt' => current_time( 'mysql', 1 ),
);
wp_update_post( $update_post_data );
}
foreach ( $subscriptions as $subscription ) {
// Do we need to activate a subscription?
if ( $order_completed && ! $subscription->has_status( wcs_get_subscription_ended_statuses() ) && ! $subscription->has_status( 'active' ) ) {
if ( $order_needed_payment ) {
$subscription->payment_complete();
$was_activated = true;
}
if ( 'failed' === $orders_old_status ) {
do_action( 'woocommerce_subscriptions_paid_for_failed_renewal_order', wc_get_order( $order_id ), $subscription );
}
} elseif ( 'failed' == $orders_new_status ) {
$subscription->payment_failed();
}
}
if ( $was_activated ) {
do_action( 'subscriptions_activated_for_order', $order_id );
}
}
/**
* Add order note to subscription to record the renewal order
*
* @param WC_Order|int $renewal_order
* @param WC_Subscription|int $subscription
* @since 2.0
*/
public static function add_order_note( $renewal_order, $subscription ) {
if ( ! is_object( $subscription ) ) {
$subscription = wcs_get_subscription( $subscription );
}
if ( ! is_object( $renewal_order ) ) {
$renewal_order = wc_get_order( $renewal_order );
}
if ( is_a( $renewal_order, 'WC_Order' ) && wcs_is_subscription( $subscription ) ) {
$order_number = sprintf( _x( '#%s', 'hash before order number', 'woocommerce-subscriptions' ), $renewal_order->get_order_number() );
// translators: placeholder is order ID
$subscription->add_order_note( sprintf( __( 'Order %s created to record renewal.', 'woocommerce-subscriptions' ), sprintf( '<a href="%s">%s</a> ', esc_url( wcs_get_edit_post_link( $renewal_order->id ) ), $order_number ) ) );
}
return $renewal_order;
}
/**
* Do not allow customers to cancel renewal orders.
*
* @since 2.0
*/
public static function prevent_cancelling_renewal_orders() {
if ( isset( $_GET['cancel_order'] ) && isset( $_GET['order'] ) && isset( $_GET['order_id'] ) ) {
$order_id = absint( $_GET['order_id'] );
$order = wc_get_order( $order_id );
$redirect = $_GET['redirect'];
if ( wcs_order_contains_renewal( $order ) ) {
remove_action( 'wp_loaded', 'WC_Form_Handler::cancel_order', 20 );
wc_add_notice( __( 'Subscription renewal orders cannot be cancelled.', 'woocommerce-subscriptions' ), 'notice' );
if ( $redirect ) {
wp_safe_redirect( $redirect );
exit;
}
}
}
}
/**
* Removes switch line item meta data so it isn't copied to renewal order line items
*
* @since 2.0.16
* @param array $order_items
* @return array $order_items
*/
public static function remove_switch_item_meta_keys( $order_items ) {
$switched_order_item_keys = array(
'_switched_subscription_sign_up_fee_prorated' => '',
'_switched_subscription_price_prorated' => '',
'_switched_subscription_item_id' => '',
);
foreach ( $order_items as $order_item_id => $item ) {
$order_items[ $order_item_id ]['item_meta'] = array_diff_key( $item['item_meta'], $switched_order_item_keys );
}
return $order_items;
}
/* Deprecated functions */
/**
* Hooks to the renewal order created action to determine if the order should be emailed to the customer.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 1.4
*/
public static function maybe_send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_email_renewal_order' ) ) {
self::send_customer_renewal_order_email( $order );
}
}
/**
* Processing Order
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 1.4
*/
public static function send_customer_renewal_order_email( $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( ! is_object( $order ) ) {
$order = new WC_Order( $order );
}
$mailer = WC()->mailer();
$mails = $mailer->get_emails();
$mails['WCS_Email_Customer_Renewal_Invoice']->trigger( $order->id );
}
/**
* Change the email subject of the new order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_new_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
// translators: 1$: blog name, 2$: order number
$subject = apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', sprintf( _x( '[%1$s] New Subscription Renewal Order (%2$s)', 'used in new renewal order email, deprecated', 'woocommerce-subscriptions' ), $blogname, $order->get_order_number() ), $order );
}
return $subject;
}
/**
* Change the email subject of the processing order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_customer_procesing_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$subject = apply_filters(
'woocommerce_subscriptions_email_subject_customer_procesing_renewal_order',
// translators: placeholder is blog name
sprintf( _x( '[%s] Subscription Renewal Order', 'used as email subject for renewal order notification email to customer', 'woocommerce-subscriptions' ), $blogname ),
$order
);
}
return $subject;
}
/**
* Change the email subject of the completed order email to specify the order is a subscription renewal order
*
* @param string $subject The default WooCommerce email subject
* @param WC_Order $order The WC_Order object which the email relates to
* @since 1.2
* @deprecated 1.4
*/
public static function email_subject_customer_completed_renewal_order( $subject, $order ) {
_deprecated_function( __METHOD__, '1.4' );
if ( wcs_order_contains_renewal( $order ) ) {
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$subject = apply_filters(
'woocommerce_subscriptions_email_subject_customer_completed_renewal_order',
// translators: placeholder is blog name
sprintf( _x( '[%s] Subscription Renewal Order', 'used as email subject for renewal order notification email to customer', 'woocommerce-subscriptions' ), $blogname ),
$order
);
}
return $subject;
}
/**
* Generate an order to record an automatic subscription payment.
*
* This function is hooked to the 'process_subscription_payment' which is fired when a payment gateway calls
* the @see WC_Subscriptions_Manager::process_subscription_payment() function. Because manual payments will
* also call this function, the function only generates a renewal order if the @see WC_Order::payment_complete()
* will be called for the renewal order.
*
* @param int $user_id The id of the user who purchased the subscription
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
* @deprecated 2.0
*/
public static function generate_paid_renewal_order( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_create_renewal_order( WC_Subscription $subscription )' );
$subscription = wcs_get_subscription_from_key( $subscription_key );
$renewal_order = wcs_create_renewal_order( $subscription );
$renewal_order->payment_complete();
return $renewal_order->id;
}
/**
* Generate an order to record a subscription payment failure.
*
* This function is hooked to the 'processed_subscription_payment_failure' hook called when a payment
* gateway calls the @see WC_Subscriptions_Manager::process_subscription_payment_failure()
*
* @param int $user_id The id of the user who purchased the subscription
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
* @deprecated 2.0
*/
public static function generate_failed_payment_renewal_order( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_create_renewal_order( WC_Subscription $subscription )' );
$renewal_order = wcs_create_renewal_order( wcs_get_subscription_from_key( $subscription_key ) );
$renewal_order->update_status( 'failed' );
return $renewal_order->id;
}
/**
* Generate an order to record a subscription payment.
*
* This function is hooked to the scheduled subscription payment hook to create a pending
* order for each scheduled subscription payment.
*
* When a payment gateway calls the @see WC_Subscriptions_Manager::process_subscription_payment()
* @see WC_Order::payment_complete() will be called for the renewal order.
*
* @param int $user_id The id of the user who purchased the subscription
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
*/
public static function maybe_generate_manual_renewal_order( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::maybe_create_manual_renewal_order( WC_Subscription $subscription )' );
self::maybe_create_manual_renewal_order( wcs_get_subscription_from_key( $subscription_key ) )->id;
}
/**
* Get the ID of the parent order for a subscription renewal order.
*
* Deprecated because a subscription's details are now stored in a WC_Subscription object, not the
* parent order.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 2.0
*/
public static function get_parent_order_id( $renewal_order ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_get_subscriptions_for_renewal_order()' );
$parent_order = self::get_parent_order( $renewal_order );
return ( null === $parent_order ) ? null : $parent_order->id;
}
/**
* Get the parent order for a subscription renewal order.
*
* Deprecated because a subscription's details are now stored in a WC_Subscription object, not the
* parent order.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 2.0 self::get_parent_subscription() is the better function to use now as a renewal order
*/
public static function get_parent_order( $renewal_order ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_get_subscriptions_for_renewal_order()' );
if ( ! is_object( $renewal_order ) ) {
$renewal_order = new WC_Order( $renewal_order );
}
$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
$subscription = array_pop( $subscriptions );
if ( false === $subscription->order ) { // There is no original order
$parent_order = null;
} else {
$parent_order = $subscription->order;
}
return apply_filters( 'woocommerce_subscriptions_parent_order', $parent_order, $renewal_order );
}
/**
* Returns the number of renewals for a given parent order
*
* @param int $order_id The ID of a WC_Order object.
* @since 1.2
* @deprecated 2.0
*/
public static function get_renewal_order_count( $order_id ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::get_related_orders()' );
$subscriptions_for_order = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'parent' ) );
if ( ! empty( $subscriptions_for_order ) ) {
$subscription = array_pop( $subscriptions_for_order );
$all_orders = $subscription->get_related_orders();
$renewal_order_count = count( $all_orders );
// Don't include the initial order (if any)
if ( false !== $subscription->order ) {
$renewal_order_count -= 1;
}
} else {
$renewal_order_count = 0;
}
return apply_filters( 'woocommerce_subscriptions_renewal_order_count', $renewal_order_count, $order_id );
}
/**
* Returns a URL including required parameters for an authenticated user to renew a subscription
*
* Deprecated because the use of a $subscription_key is deprecated.
*
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
* @deprecated 2.0
*/
public static function get_users_renewal_link( $subscription_key, $role = 'parent' ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_get_users_resubscribe_link( $subscription )' );
return wcs_get_users_resubscribe_link( wcs_get_subscription_from_key( $subscription_key ) );
}
/**
* Returns a URL including required parameters for an authenticated user to renew a subscription by product ID.
*
* Deprecated because the use of a $subscription_key is deprecated.
*
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
* @deprecated 2.0
*/
public static function get_users_renewal_link_for_product( $product_id ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_get_users_resubscribe_link_for_product( $subscription )' );
return wcs_get_users_resubscribe_link_for_product( $product_id );
}
/**
* Check if a given subscription can be renewed.
*
* Deprecated because the use of a $subscription_key is deprecated.
*
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @param int $user_id The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance.
* @since 1.2
* @deprecated 2.0
*/
public static function can_subscription_be_renewed( $subscription_key, $user_id = '' ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_can_user_resubscribe_to( $subscription, $user_id )' );
return wcs_can_user_resubscribe_to( wcs_get_subscription_from_key( $subscription_key ), $user_id );
}
/**
* Checks if the current request is by a user to renew their subscription, and if it is
* set up a subscription renewal via the cart for the product/variation that is being renewed.
*
* @since 1.2
* @deprecated 2.0
*/
public static function maybe_create_renewal_order_for_user() {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::maybe_setup_resubscribe_via_cart()' );
}
/**
* When restoring the cart from the session, if the cart item contains addons, but is also
* a subscription renewal, do not adjust the price because the original order's price will
* be used, and this includes the addons amounts.
*
* @since 1.5.5
* @deprecated 2.0
*/
public static function product_addons_adjust_price( $adjust_price, $cart_item ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::product_addons_adjust_price()' );
}
/**
* Created a new order for renewing a subscription product based on the details of a previous order.
*
* @param WC_Order|int $order The WC_Order object or ID of the order for which the a new order should be created.
* @param string $product_id The ID of the subscription product in the order which needs to be added to the new order.
* @param array $args (optional) An array of name => value flags:
* 'new_order_role' string A flag to indicate whether the new order should become the master order for the subscription. Accepts either 'parent' or 'child'. Defaults to 'parent' - replace the existing order.
* 'checkout_renewal' bool Indicates if invoked from an interactive cart/checkout session and certain order items are not set, like taxes, shipping as they need to be set in teh calling function, like @see WC_Subscriptions_Checkout::filter_woocommerce_create_order(). Default false.
* 'failed_order_id' int For checkout_renewal true, indicates order id being replaced
* @since 1.2
* @deprecated 2.0
*/
public static function generate_renewal_order( $original_order, $product_id, $args = array() ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_create_renewal_order() or wcs_create_resubscribe_order()' );
if ( ! wcs_order_contains_subscription( $original_order, 'parent' ) ) {
return false;
}
$args = wp_parse_args( $args, array(
'new_order_role' => 'parent',
'checkout_renewal' => false,
)
);
$subscriptions = wcs_get_subscriptions_for_order( $original_order, array( 'order_type' => 'parent' ) );
$subscription = array_shift( $subscriptions );
if ( 'parent' == $args['new_order_role'] ) {
$new_order = wcs_create_resubscribe_order( $subscription );
} else {
$new_order = wcs_create_renewal_order( $subscription );
}
return $new_order->id;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 1.5
* @deprecated 2.0
*/
public static function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::is_purchasable()' );
return $is_purchasable;
}
/**
* Check if a given order is a subscription renewal order and optionally, if it is a renewal order of a certain role.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
* @param array $args (optional) An array of name => value flags:
* 'order_role' string (optional) A specific role to check the order against. Either 'parent' or 'child'.
* 'via_checkout' Indicates whether to check if the renewal order was via the cart/checkout process.
* @since 1.2
*/
public static function is_renewal( $order, $args = array() ) {
$args = wp_parse_args( $args, array(
'order_role' => '',
'via_checkout' => false,
)
);
$is_resubscribe_order = wcs_order_contains_resubscribe( $order );
$is_renewal_order = wcs_order_contains_renewal( $order );
if ( empty( $args['new_order_role'] ) ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_order_contains_resubscribe( $order ) and wcs_order_contains_renewal( $order )' );
return ( $is_resubscribe_order || $is_renewal_order );
} elseif ( 'parent' == $args['new_order_role'] ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_order_contains_resubscribe( $order )' );
return $is_resubscribe_order;
} else {
_deprecated_function( __METHOD__, '2.0', 'wcs_order_contains_renewal( $order )' );
return $is_renewal_order;
}
}
/**
* Returns the renewal orders for a given parent order
*
* @param int $order_id The ID of a WC_Order object.
* @param string $output (optional) How you'd like the result. Can be 'ID' for IDs only or 'WC_Order' for order objects.
* @since 1.2
* @deprecated 2.0
*/
public static function get_renewal_orders( $order_id, $output = 'ID' ) {
_deprecated_function( __METHOD__, '2.0', 'WC_Subscription::get_related_orders()' );
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'parent' ) );
$subscription = array_shift( $subscriptions );
if ( 'WC_Order' == $output ) {
$renewal_orders = $subscription->get_related_orders( 'all', 'renewal' );
} else {
$renewal_orders = $subscription->get_related_orders( 'ids', 'renewal' );
}
return apply_filters( 'woocommerce_subscriptions_renewal_orders', $renewal_orders, $order_id );
}
/**
* Flag payment of manual renewal orders.
*
* This is particularly important to ensure renewals of limited subscriptions can be completed.
*
* @since 1.5.5
* @deprecated 2.0
*/
public static function get_checkout_payment_url( $pay_url, $order ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::get_checkout_payment_url() or WCS_Cart_Resubscribe::get_checkout_payment_url()' );
return $pay_url;
}
/**
* Process a renewal payment when a customer has completed the payment for a renewal payment which previously failed.
*
* @since 1.3
* @deprecated 2.0
*/
public static function maybe_process_failed_renewal_order_payment( $order_id ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::maybe_change_subscription_status( $order_id, $orders_old_status, $orders_new_status )' );
}
/**
* If the payment for a renewal order has previously failed and is then paid, then the
* @see WC_Subscriptions_Manager::process_subscription_payments_on_order() function would
* never be called. This function makes sure it is called.
*
* @param WC_Order|int $order A WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 2.0
*/
public static function process_failed_renewal_order_payment( $order_id ) {
_deprecated_function( __METHOD__, '2.0' );
if ( wcs_order_contains_renewal( $order_id ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$subscription = array_pop( $subscriptions );
if ( $subscription->is_manual() ) {
add_action( 'woocommerce_payment_complete', __CLASS__ . '::process_subscription_payment_on_child_order', 10, 1 );
}
}
}
/**
* Records manual payment of a renewal order against a subscription.
*
* @param WC_Order|int $order A WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 2.0
*/
public static function maybe_record_renewal_order_payment( $order_id ) {
_deprecated_function( __METHOD__, '2.0' );
if ( wcs_order_contains_renewal( $order_id ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$subscription = array_pop( $subscriptions );
if ( $subscription->is_manual() ) {
self::process_subscription_payment_on_child_order( $order_id );
}
}
}
/**
* Records manual payment of a renewal order against a subscription.
*
* @param WC_Order|int $order A WC_Order object or ID of a WC_Order order.
* @since 1.2
* @deprecated 2.0
*/
public static function maybe_record_renewal_order_payment_failure( $order_id ) {
_deprecated_function( __METHOD__, '2.0' );
if ( wcs_order_contains_renewal( $order_id ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$subscription = array_pop( $subscriptions );
if ( $subscription->is_manual() ) {
self::process_subscription_payment_on_child_order( $order_id, 'failed' );
}
}
}
/**
* If the payment for a renewal order has previously failed and is then paid, we need to make sure the
* subscription payment function is called.
*
* @param int $user_id The id of the user who purchased the subscription
* @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* @since 1.2
* @deprecated 2.0
*/
public static function process_subscription_payment_on_child_order( $order_id, $payment_status = 'completed' ) {
_deprecated_function( __METHOD__, '2.0' );
if ( wcs_order_contains_renewal( $order_id ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
foreach ( $subscriptions as $subscription ) {
if ( 'failed' == $payment_status ) {
$subscription->payment_failed();
} else {
$subscription->payment_complete();
$subscription->update_status( 'active' );
}
}
}
}
/**
* Adds a renewal orders section to the Related Orders meta box displayed on subscription orders.
*
* @deprecated 2.0
* @since 1.2
*/
public static function renewal_orders_meta_box_section( $order, $post ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Trigger a hook when a subscription suspended due to a failed renewal payment is reactivated
*
* @since 1.3
*/
public static function trigger_processed_failed_renewal_order_payment_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::maybe_record_subscription_payment( $order_id, $orders_old_status, $orders_new_status )' );
}
}
WC_Subscriptions_Renewal_Order::init();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
<?php
/**
* Scheduler for subscription events that uses the Action Scheduler
*
* @class WCS_Action_Scheduler
* @version 2.0.0
* @package WooCommerce Subscriptions/Classes
* @category Class
* @author Prospress
*/
class WCS_Action_Scheduler extends WCS_Scheduler {
/*@protected Array of $action_hook => $date_type values */
protected $action_hooks = array(
'woocommerce_scheduled_subscription_trial_end' => 'trial_end',
'woocommerce_scheduled_subscription_payment' => 'next_payment',
'woocommerce_scheduled_subscription_expiration' => 'end',
);
/**
* Maybe set a schedule action if the new date is in the future
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
*/
public function update_date( $subscription, $date_type, $datetime ) {
if ( in_array( $date_type, $this->date_types_to_schedule ) ) {
$action_hook = $this->get_scheduled_action_hook( $subscription, $date_type );
if ( ! empty( $action_hook ) ) {
$action_args = array( 'subscription_id' => $subscription->id );
$timestamp = strtotime( $datetime );
$next_scheduled = wc_next_scheduled_action( $action_hook, $action_args );
if ( $next_scheduled !== $timestamp ) {
// Maybe clear the existing schedule for this hook
if ( false !== $next_scheduled ) {
wc_unschedule_action( $action_hook, $action_args );
}
// Only reschedule if it's in the future
if ( $timestamp > current_time( 'timestamp', true ) && 'active' == $subscription->get_status() ) {
wc_schedule_single_action( $datetime, $action_hook, $action_args );
}
}
}
}
}
/**
* Delete a date from the action scheduler queue
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
*/
public function delete_date( $subscription, $date_type ) {
$this->update_date( $subscription, $date_type, 0 );
}
/**
* When a subscription's status is updated, maybe schedule an event
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'end', 'end_of_prepaid_term' or a custom date type
* @param string $datetime A MySQL formated date/time string in the GMT/UTC timezone.
*/
public function update_status( $subscription, $new_status, $old_status ) {
$action_args = array( 'subscription_id' => $subscription->id );
switch ( $new_status ) {
case 'active' :
foreach ( $this->action_hooks as $action_hook => $date_type ) {
$next_scheduled = wc_next_scheduled_action( $action_hook, $action_args );
$event_time = $subscription->get_time( $date_type );
// Maybe clear the existing schedule for this hook
if ( false !== $next_scheduled && $next_scheduled != $event_time ) {
wc_unschedule_action( $action_hook, $action_args );
}
if ( 0 != $event_time && $event_time > current_time( 'timestamp', true ) && $next_scheduled != $event_time ) {
wc_schedule_single_action( $event_time, $action_hook, $action_args );
}
}
break;
case 'pending-cancel' :
$end_time = $subscription->get_time( 'end' ); // This will have been set to the correct date already
// Now that we have the current times, clear the scheduled hooks
foreach ( $this->action_hooks as $action_hook => $date_type ) {
wc_unschedule_action( $action_hook, $action_args );
}
$next_scheduled = wc_next_scheduled_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
if ( false !== $next_scheduled && $next_scheduled != $end_time ) {
wc_unschedule_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
}
// The end date was set in WC_Subscriptions::update_dates() to the appropriate value, so we can schedule our action for that time
if ( $end_time > current_time( 'timestamp', true ) && $next_scheduled != $end_time ) {
wc_schedule_single_action( $end_time, 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
}
break;
case 'on-hold' :
case 'cancelled' :
case 'switched' :
case 'expired' :
case 'trash' :
foreach ( $this->action_hooks as $action_hook => $date_type ) {
wc_unschedule_action( $action_hook, $action_args );
}
wc_unschedule_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args );
break;
}
}
/**
* Get the hook to use in the action scheduler for the date type
*
* @param string $date_type Can be 'start', 'trial_end', 'next_payment', 'last_payment', 'expiration', 'end_of_prepaid_term' or a custom date type
* @param object $subscription An instance of WC_Subscription to get the hook for
*/
protected function get_scheduled_action_hook( $subscription, $date_type ) {
$hook = '';
switch ( $date_type ) {
case 'next_payment' :
$hook = 'woocommerce_scheduled_subscription_payment';
break;
case 'trial_end' :
$hook = 'woocommerce_scheduled_subscription_trial_end';
break;
case 'end' :
// End dates may need either an expiration or end of prepaid term hook, depending on the status
if ( $subscription->has_status( 'cancelled' ) ) {
$hook = 'woocommerce_scheduled_subscription_end_of_prepaid_term';
} elseif ( $subscription->has_status( 'active' ) ) {
$hook = 'woocommerce_scheduled_subscription_expiration';
}
break;
}
return apply_filters( 'woocommerce_subscriptions_scheduled_action_hook', $hook, $date_type );
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* WooCommerce Subscriptions API
*
* Handles WC-API endpoint requests related to Subscriptions
*
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WCS_API {
public static function init() {
add_filter( 'woocommerce_api_classes', __CLASS__ . '::includes' );
}
/**
* Include the required files for the REST API and add register the subscription
* API class in the WC_API_Server.
*
* @since 2.0
* @param Array $wc_api_classes WC_API::registered_resources list of api_classes
* @return array
*/
public static function includes( $wc_api_classes ) {
// include the subscription api classes
require_once( 'api/class-wc-api-subscriptions.php' );
require_once( 'api/class-wc-api-subscriptions-customers.php' );
array_push( $wc_api_classes, 'WC_API_Subscriptions' );
array_push( $wc_api_classes, 'WC_API_Subscriptions_Customers' );
return $wc_api_classes;
}
}
WCS_API::init();

View File

@@ -0,0 +1,53 @@
<?php
/**
* WooCommerce Auth
*
* Handles wc-auth endpoint requests
*
* @author Prospress
* @category API
* @since 2.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WCS_Auth {
/**
* Setup class
*
* @since 2.0.0
*/
public function __construct() {
add_filter( 'woocommerce_api_permissions_in_scope', array( $this, 'get_permissions_in_scope' ), 10, 2 );
}
/**
* Return a list of permissions a scope allows
*
* @param array $permissions
* @param string $scope
* @since 2.0.0
* @return array
*/
public function get_permissions_in_scope( $permissions, $scope ) {
switch ( $scope ) {
case 'read' :
$permissions[] = __( 'View subscriptions', 'woocommerce-subscriptions' );
break;
case 'write' :
$permissions[] = __( 'Create subscriptions', 'woocommerce-subscriptions' );
break;
case 'read_write' :
$permissions[] = __( 'View and manage subscriptions', 'woocommerce-subscriptions' );
break;
}
return $permissions;
}
}
return new WCS_Auth();

View File

@@ -0,0 +1,182 @@
<?php
/**
* Subscription Cache Manager Class using TLC transients
*
* Implements methods to deal with the soft caching layer
*
* @class WCS_Cache_Manager_TLC
* @version 2.0
* @package WooCommerce Subscriptions/Classes
* @category Class
* @author Gabor Javorszky
*/
class WCS_Cache_Manager_TLC extends WCS_Cache_Manager {
public $logger = null;
public function __construct() {
add_action( 'woocommerce_loaded', array( $this, 'load_logger' ) );
// Add filters for update / delete / trash post to purge cache
add_action( 'trashed_post', array( $this, 'purge_delete' ), 9999 ); // trashed posts aren't included in 'any' queries
add_action( 'untrashed_post', array( $this, 'purge_delete' ), 9999 ); // however untrashed posts are
add_action( 'deleted_post', array( $this, 'purge_delete' ), 9999 ); // if forced delete is enabled
add_action( 'updated_post_meta', array( $this, 'purge_from_metadata' ), 9999, 4 ); // tied to _subscription_renewal
add_action( 'deleted_post_meta', array( $this, 'purge_from_metadata' ), 9999, 4 ); // tied to _subscription_renewal
add_action( 'added_post_meta', array( $this, 'purge_from_metadata' ), 9999, 4 ); // tied to _subscription_renewal
}
/**
* Attaches logger
*/
public function load_logger() {
$this->logger = new WC_Logger();
}
/**
* Wrapper function around WC_Logger->log
*
* @param string $message Message to log
*/
public function log( $message ) {
if ( defined( 'WCS_DEBUG' ) && WCS_DEBUG ) {
$this->logger->add( 'wcs-cache', $message );
}
}
/**
* Wrapper function around our cache library.
*
* @param string $key The key to cache the data with
* @param string|array $callback name of function, or array of class - method that fetches the data
* @param array $params arguments passed to $callback
*
* @return bool|mixed
*/
public function cache_and_get( $key, $callback, $params = array(), $expires = 0 ) {
$expires = absint( $expires );
$transient = tlc_transient( $key )
->updates_with( $callback, $params );
if ( $expires ) {
$transient->expires_in( $expires );
}
return $transient->get();
}
/**
* Clearing for orders / subscriptions with sanitizing bits
*
* @param $post_id integer the ID of an order / subscription
*/
public function purge_subscription_cache_on_update( $post_id ) {
$post_type = get_post_type( $post_id );
if ( 'shop_subscription' !== $post_type && 'shop_order' !== $post_type ) {
return;
}
if ( 'shop_subscription' === $post_type ) {
$this->log( 'ID is subscription, calling wcs_clear_related_order_cache for ' . $post_id );
$this->wcs_clear_related_order_cache( $post_id );
} else {
$this->log( 'ID is order, getting subscription.' );
$subscription = wcs_get_subscriptions_for_order( $post_id );
if ( empty( $subscription ) ) {
$this->log( 'No sub for this ID: ' . $post_id );
return;
}
$subscription = array_shift( $subscription );
$this->log( 'Got subscription, calling wcs_clear_related_order_cache for ' . $subscription->id );
$this->wcs_clear_related_order_cache( $subscription->id );
}
}
/**
* Clearing cache when a post is deleted
*
* @param $post_id integer the ID of a post
*/
public function purge_delete( $post_id ) {
if ( 'shop_order' !== get_post_type( $post_id ) ) {
return;
}
$linked_subscription = get_post_meta( $post_id, '_subscription_renewal', false );
// don't call this if there's nothing to call on
if ( $linked_subscription ) {
$this->log( 'Calling purge from ' . current_filter() . ' on ' . $linked_subscription[0] );
$this->purge_subscription_cache_on_update( $linked_subscription[0] );
}
}
/**
* When the _subscription_renewal metadata is added / deleted / updated on the Order, we need to initiate cache invalidation for both the new
* value of the meta ($_meta_value), and the object it's being added to: $object_id.
*
* @param $meta_id integer the ID of the meta in the meta table
* @param $object_id integer the ID of the post we're updating on
* @param $meta_key string the meta_key in the table
* @param $_meta_value mixed the value we're deleting / adding / updating
*/
public function purge_from_metadata( $meta_id, $object_id, $meta_key, $_meta_value ) {
if ( '_subscription_renewal' !== $meta_key || 'shop_order' !== get_post_type( $object_id ) ) {
return;
}
$this->log( 'Calling purge from ' . current_filter() . ' on object ' . $object_id . ' and meta value ' . $_meta_value . ' due to _subscription_renewal meta.' );
$this->purge_subscription_cache_on_update( $_meta_value );
$this->purge_subscription_cache_on_update( $object_id );
}
/**
* Wrapper function to clear cache that relates to related orders
*
* @param null $id
*/
public function wcs_clear_related_order_cache( $id = null ) {
// if nothing was passed in, there's nothing to delete
if ( null === $id ) {
return;
}
// if it's not a Subscription, we don't deal with it
if ( is_object( $id ) && $id instanceof WC_Subscription ) {
$id = $id->id;
} elseif ( is_numeric( $id ) ) {
$id = absint( $id );
} else {
return;
}
$key = tlc_transient( 'wcs-related-orders-to-' . $id )->key;
$this->log( 'In the clearing, key being purged is this: ' . "\n\n{$key}\n\n" );
$this->delete_cached( $key );
}
/**
* Delete cached data with key
*
* @param string $key Key that needs deleting
*/
public function delete_cached( $key ) {
if ( ! is_string( $key ) || empty( $key ) ) {
return;
}
// have to do this manually for now
delete_transient( 'tlc__' . $key );
delete_transient( 'tlc_up__' . $key );
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Handles the initial payment for a pending subscription via the cart.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Cart_Initial_Payment
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Cart_Initial_Payment extends WCS_Cart_Renewal {
/* The flag used to indicate if a cart item is for a initial payment */
public $cart_item_key = 'subscription_initial_payment';
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
$this->setup_hooks();
}
/**
* Setup the cart for paying for a delayed initial payment for a subscription.
*
* @since 2.0
*/
public function maybe_setup_cart() {
global $wp;
if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && isset( $wp->query_vars['order-pay'] ) ) {
// Pay for existing order
$order_key = $_GET['key'];
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] );
$order = wc_get_order( $wp->query_vars['order-pay'] );
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && ! wcs_order_contains_subscription( $order, array( 'renewal', 'resubscribe' ) ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) );
if ( get_current_user_id() !== $order->get_user_id() ) {
wc_add_notice( __( 'That doesn\'t appear to be your order.', 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) );
exit;
} elseif ( ! empty( $subscriptions ) ) {
// Setup cart with all the original order's line items
$this->setup_cart( $order, array(
'order_id' => $order_id,
) );
WC()->session->set( 'order_awaiting_payment', $order_id );
// Set cart hash for orders paid in WC >= 2.6
$this->set_cart_hash( $order_id );
wp_safe_redirect( WC()->cart->get_checkout_url() );
exit;
}
}
}
}
/**
* Checks the cart to see if it contains an initial payment item.
*
* @return bool | Array The cart item containing the initial payment, else false.
* @since 2.0.13
*/
protected function cart_contains() {
$contains_initial_payment = false;
if ( ! empty( WC()->cart->cart_contents ) ) {
foreach ( WC()->cart->cart_contents as $cart_item ) {
if ( isset( $cart_item[ $this->cart_item_key ] ) ) {
$contains_initial_payment = $cart_item;
break;
}
}
}
return apply_filters( 'wcs_cart_contains_initial_payment', $contains_initial_payment );
}
/**
* Get the order object used to construct the initial payment cart.
*
* @param Array The initial payment cart item.
* @return WC_Order | The order object
* @since 2.0.13
*/
protected function get_order( $cart_item = '' ) {
$order = false;
if ( empty( $cart_item ) ) {
$cart_item = $this->cart_contains();
}
if ( false !== $cart_item && isset( $cart_item[ $this->cart_item_key ] ) ) {
$order = wc_get_order( $cart_item[ $this->cart_item_key ]['order_id'] );
}
return $order;
}
}
new WCS_Cart_Initial_Payment();

View File

@@ -0,0 +1,814 @@
<?php
/**
* Implement renewing to a subscription via the cart.
*
* For manual renewals and the renewal of a subscription after a failed automatic payment, the customer must complete
* the renewal via checkout in order to pay for the renewal. This class handles that.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Cart_Renewal
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Cart_Renewal {
/* The flag used to indicate if a cart item is a renewal */
public $cart_item_key = 'subscription_renewal';
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
$this->setup_hooks();
// Set URL parameter for manual subscription renewals
add_filter( 'woocommerce_get_checkout_payment_url', array( &$this, 'get_checkout_payment_url' ), 10, 2 );
// Remove order action buttons from the My Account page
add_filter( 'woocommerce_my_account_my_orders_actions', array( &$this, 'filter_my_account_my_orders_actions' ), 10, 2 );
// Update customer's address on the subscription if it is changed during renewal
add_filter( 'woocommerce_checkout_update_customer_data', array( &$this, 'maybe_update_subscription_customer_data' ), 10, 2 );
// When a failed renewal order is paid for via checkout, make sure WC_Checkout::create_order() preserves its "failed" status until it is paid
add_filter( 'woocommerce_default_order_status', array( &$this, 'maybe_preserve_order_status' ) );
}
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function setup_hooks() {
// Make sure renewal meta data persists between sessions
add_filter( 'woocommerce_get_cart_item_from_session', array( &$this, 'get_cart_item_from_session' ), 10, 3 );
add_action( 'woocommerce_cart_loaded_from_session', array( &$this, 'cart_items_loaded_from_session' ), 10 );
// Make sure fees are added to the cart
add_action( 'woocommerce_cart_calculate_fees', array( &$this, 'maybe_add_fees' ), 10, 1 );
// Allow renewal of limited subscriptions
add_filter( 'woocommerce_subscription_is_purchasable', array( &$this, 'is_purchasable' ), 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', array( &$this, 'is_purchasable' ), 12, 2 );
// Check if a user is requesting to create a renewal order for a subscription, needs to happen after $wp->query_vars are set
add_action( 'template_redirect', array( &$this, 'maybe_setup_cart' ), 100 );
// Apply renewal discounts as pseudo coupons
add_action( 'wcs_after_renewal_setup_cart_subscription', array( &$this, 'maybe_setup_discounts' ), 10, 1 );
add_filter( 'woocommerce_get_shop_coupon_data', array( &$this, 'renewal_coupon_data' ), 10, 2 );
add_action( 'wcs_before_renewal_setup_cart_subscriptions', array( &$this, 'clear_coupons' ), 10 );
add_action( 'woocommerce_remove_cart_item', array( &$this, 'maybe_remove_items' ), 10, 1 );
add_action( 'woocommerce_before_cart_item_quantity_zero', array( &$this, 'maybe_remove_items' ), 10, 1 );
add_action( 'woocommerce_cart_emptied', array( &$this, 'clear_coupons' ), 10 );
add_filter( 'woocommerce_cart_item_removed_title', array( &$this, 'items_removed_title' ), 10, 2 );
add_action( 'woocommerce_cart_item_restored', array( &$this, 'maybe_restore_items' ), 10, 1 );
// Use original order price when resubscribing to products with addons (to ensure the adds on prices are included)
add_filter( 'woocommerce_product_addons_adjust_price', array( &$this, 'product_addons_adjust_price' ), 10, 2 );
}
/**
* Check if a payment is being made on a renewal order from 'My Account'. If so,
* redirect the order into a cart/checkout payment flow so that the customer can
* choose payment method, apply discounts set shipping and pay for the order.
*
* @since 2.0
*/
public function maybe_setup_cart() {
global $wp;
if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && isset( $wp->query_vars['order-pay'] ) ) {
// Pay for existing order
$order_key = $_GET['key'];
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] );
$order = wc_get_order( $wp->query_vars['order-pay'] );
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_renewal( $order ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
do_action( 'wcs_before_renewal_setup_cart_subscriptions', $subscriptions, $order );
foreach ( $subscriptions as $subscription ) {
do_action( 'wcs_before_renewal_setup_cart_subscription', $subscription, $order );
// Add the existing subscription items to the cart
$this->setup_cart( $subscription, array(
'subscription_id' => $subscription->id,
'renewal_order_id' => $order_id,
) );
do_action( 'wcs_after_renewal_setup_cart_subscription', $subscription, $order );
}
do_action( 'wcs_after_renewal_setup_cart_subscriptions', $subscriptions, $order );
if ( WC()->cart->cart_contents_count != 0 ) {
// Store renewal order's ID in session so it can be re-used after payment
WC()->session->set( 'order_awaiting_payment', $order_id );
// Set cart hash for orders paid in WC >= 2.6
$this->set_cart_hash( $order_id );
}
wp_safe_redirect( WC()->cart->get_checkout_url() );
exit;
}
}
}
/**
* Set up cart item meta data for a to complete a subscription renewal via the cart.
*
* @since 2.0
*/
protected function setup_cart( $subscription, $cart_item_data ) {
WC()->cart->empty_cart( true );
$success = true;
foreach ( $subscription->get_items() as $item_id => $line_item ) {
// Load all product info including variation data
$product_id = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $line_item['product_id'] );
$quantity = (int) $line_item['qty'];
$variation_id = (int) $line_item['variation_id'];
$variations = array();
foreach ( $line_item['item_meta'] as $meta_name => $meta_value ) {
if ( taxonomy_is_product_attribute( $meta_name ) ) {
$variations[ $meta_name ] = $meta_value[0];
} elseif ( meta_is_product_attribute( $meta_name, $meta_value[0], $product_id ) ) {
$variations[ $meta_name ] = $meta_value[0];
}
}
$product = wc_get_product( $line_item['product_id'] );
// The notice displayed when a subscription product has been deleted and the custoemr attempts to manually renew or make a renewal payment for a failed recurring payment for that product/subscription
// translators: placeholder is an item name
$product_deleted_error_message = apply_filters( 'woocommerce_subscriptions_renew_deleted_product_error_message', __( 'The %s product has been deleted and can no longer be renewed. Please choose a new product or contact us for assistance.', 'woocommerce-subscriptions' ) );
// Display error message for deleted products
if ( false === $product ) {
wc_add_notice( sprintf( $product_deleted_error_message, $line_item['name'] ), 'error' );
// Make sure we don't actually need the variation ID (if the product was a variation, it will have a variation ID; however, if the product has changed from a simple subscription to a variable subscription, there will be no variation_id)
} elseif ( $product->is_type( array( 'variable-subscription' ) ) && ! empty( $line_item['variation_id'] ) ) {
$variation = wc_get_product( $variation_id );
// Display error message for deleted product variations
if ( false === $variation ) {
wc_add_notice( sprintf( $product_deleted_error_message, $line_item['name'] ), 'error' );
}
}
if ( wcs_is_subscription( $subscription ) ) {
$cart_item_data['subscription_line_item_id'] = $item_id;
}
$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations, apply_filters( 'woocommerce_order_again_cart_item_data', array( $this->cart_item_key => $cart_item_data ), $line_item, $subscription ) );
$success = $success && (bool) $cart_item_key;
}
// If a product linked to a subscription failed to be added to the cart prevent partially paying for the order by removing all cart items.
if ( ! $success && wcs_is_subscription( $subscription ) ) {
// translators: %s is subscription's number
wc_add_notice( sprintf( esc_html__( 'Subscription #%s has not been added to the cart.', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) , 'error' );
WC()->cart->empty_cart( true );
}
do_action( 'woocommerce_setup_cart_for_' . $this->cart_item_key, $subscription, $cart_item_data );
}
/**
* Check if a renewal order subscription has any coupons applied and if so add pseudo renewal coupon equivalents to ensure the discount is still applied
*
* @param object $subscription subscription
* @since 2.0.10
*/
public function maybe_setup_discounts( $subscription ) {
if ( wcs_is_subscription( $subscription ) ) {
$used_coupons = $subscription->get_used_coupons();
// Add any used coupon discounts to the cart (as best we can) using our pseudo renewal coupons
if ( ! empty( $used_coupons ) ) {
$coupon_items = $subscription->get_items( 'coupon' );
foreach ( $coupon_items as $coupon_item ) {
$coupon = new WC_Coupon( $coupon_item['name'] );
$coupon_code = '';
// If the coupon still exists we can use the existing/available coupon properties
if ( true === $coupon->exists ) {
// But we only want to handle recurring coupons that have been applied to the subscription
if ( in_array( $coupon->type, array( 'recurring_percent', 'recurring_fee' ) ) ) {
// Set the coupon type to be a renewal equivalent for correct validation and calculations
if ( 'recurring_percent' == $coupon->type ) {
$coupon->type = 'renewal_percent';
} elseif ( 'recurring_fee' == $coupon->type ) {
$coupon->type = 'renewal_fee';
}
// Adjust coupon code to reflect that it is being applied to a renewal
$coupon_code = $coupon->code;
}
} else {
// If the coupon doesn't exist we can only really apply the discount amount we know about - so we'll apply a cart style pseudo coupon and then set the amount
$coupon->type = 'renewal_cart';
$coupon->amount = $coupon_item['item_meta']['discount_amount']['0'];
// Adjust coupon code to reflect that it is being applied to a renewal
$coupon_code = $coupon->code;
}
// Now that we have a coupon we know we want to apply
if ( ! empty( $coupon_code ) ) {
// Set renewal order products as the product ids on the coupon
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
$coupon->product_ids = $this->get_products( $subscription );
}
// Store the coupon info for later
$this->store_coupon( $subscription->id, $coupon );
// Add the coupon to the cart - the actually coupon values / data are grabbed when needed later
if ( WC()->cart && ! WC()->cart->has_discount( $coupon_code ) ) {
WC()->cart->add_discount( $coupon_code );
}
}
}
// If there are no coupons but there is still a discount (i.e. it might have been manually added), we need to account for that as well
} elseif ( ! empty( $subscription->cart_discount ) ) {
$coupon = new WC_Coupon( 'discount_renewal' );
// Apply our cart style pseudo coupon and the set the amount
$coupon->type = 'renewal_cart';
$coupon->amount = $subscription->cart_discount;
// Set renewal order products as the product ids on the coupon
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
$coupon->product_ids = $this->get_products( $subscription );
}
// Store the coupon info for later
$this->store_coupon( $subscription->id, $coupon );
// Add the coupon to the cart
if ( WC()->cart && ! WC()->cart->has_discount( 'discount_renewal' ) ) {
WC()->cart->add_discount( 'discount_renewal' );
}
}
}
}
/**
* Does some housekeeping. Fires after the items have been passed through the get items from session filter. Because
* that filter is not good for removing cart items, we need to work around that by doing it later, in the cart
* loaded from session action.
*
* This checks cart items whether underlying subscriptions / renewal orders they depend exist. If not, they are
* removed from the cart.
*
* @param $cart WC_Cart the one we got from session
*/
public function cart_items_loaded_from_session( $cart ) {
$removed_count_subscription = $removed_count_order = 0;
foreach ( $cart->cart_contents as $key => $item ) {
if ( isset( $item[ $this->cart_item_key ]['subscription_id'] ) && ! wcs_is_subscription( $item[ $this->cart_item_key ]['subscription_id'] ) ) {
$cart->remove_cart_item( $key );
$removed_count_subscription++;
continue;
}
if ( isset( $item[ $this->cart_item_key ]['renewal_order_id'] ) && ! 'shop_order' == get_post_type( $item[ $this->cart_item_key ]['renewal_order_id'] ) ) {
$cart->remove_cart_item( $key );
$removed_count_order++;
continue;
}
}
if ( $removed_count_subscription ) {
$error_message = esc_html( _n( 'We couldn\'t find the original subscription for an item in your cart. The item was removed.', 'We couldn\'t find the original subscriptions for items in your cart. The items were removed.', $removed_count_subscription, 'woocommerce-subscriptions' ) );
if ( ! wc_has_notice( $error_message, 'notice' ) ) {
wc_add_notice( $error_message, 'notice' );
}
}
if ( $removed_count_order ) {
$error_message = esc_html( _n( 'We couldn\'t find the original renewal order for an item in your cart. The item was removed.', 'We couldn\'t find the original renewal orders for items in your cart. The items were removed.', $removed_count_order, 'woocommerce-subscriptions' ) );
if ( ! wc_has_notice( $error_message, 'notice' ) ) {
wc_add_notice( $error_message, 'notice' );
}
}
}
/**
* Restore renewal flag when cart is reset and modify Product object with renewal order related info
*
* @since 2.0
*/
public function get_cart_item_from_session( $cart_item_session_data, $cart_item, $key ) {
if ( isset( $cart_item[ $this->cart_item_key ]['subscription_id'] ) ) {
$cart_item_session_data[ $this->cart_item_key ] = $cart_item[ $this->cart_item_key ];
$_product = $cart_item_session_data['data'];
// Need to get the original subscription price, not the current price
$subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
if ( $subscription ) {
$subscription_items = $subscription->get_items();
$item_to_renew = $subscription_items[ $cart_item_session_data[ $this->cart_item_key ]['subscription_line_item_id'] ];
$price = $item_to_renew['line_subtotal'];
if ( wc_prices_include_tax() ) {
$base_tax_rates = WC_Tax::get_base_tax_rates( $_product->tax_class );
$base_taxes_on_item = WC_Tax::calc_tax( $price, $base_tax_rates, false, false );
$price += array_sum( $base_taxes_on_item );
}
$_product->price = $price / $item_to_renew['qty'];
// Don't carry over any sign up fee
$_product->subscription_sign_up_fee = 0;
$_product->post->post_title = apply_filters( 'woocommerce_subscriptions_renewal_product_title', $_product->get_title(), $_product );
// Make sure the same quantity is renewed
$cart_item_session_data['quantity'] = $item_to_renew['qty'];
}
}
return $cart_item_session_data;
}
/**
* When completing checkout for a subscription renewal, update the address on the subscription to use
* the shipping/billing address entered in case it has changed since the subscription was first created.
*
* @since 2.0
*/
public function maybe_update_subscription_customer_data( $update_customer_data, $checkout_object ) {
$cart_renewal_item = $this->cart_contains();
if ( false !== $cart_renewal_item ) {
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
$billing_address = array();
if ( $checkout_object->checkout_fields['billing'] ) {
foreach ( array_keys( $checkout_object->checkout_fields['billing'] ) as $field ) {
$field_name = str_replace( 'billing_', '', $field );
$billing_address[ $field_name ] = $checkout_object->get_posted_address_data( $field_name );
}
}
$shipping_address = array();
if ( $checkout_object->checkout_fields['shipping'] ) {
foreach ( array_keys( $checkout_object->checkout_fields['shipping'] ) as $field ) {
$field_name = str_replace( 'shipping_', '', $field );
$shipping_address[ $field_name ] = $checkout_object->get_posted_address_data( $field_name, 'shipping' );
}
}
$subscription->set_address( $billing_address, 'billing' );
$subscription->set_address( $shipping_address, 'shipping' );
}
return $update_customer_data;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 2.0
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
// If the product is being set as not-purchasable by Subscriptions (due to limiting)
if ( false === $is_purchasable && false === WC_Subscriptions_Product::is_purchasable( $is_purchasable, $product ) ) {
// Adding to cart from the product page or paying for a renewal
if ( isset( $_GET[ $this->cart_item_key ] ) || isset( $_GET['subscription_renewal'] ) || $this->cart_contains() ) {
$is_purchasable = true;
} else if ( WC()->session->cart ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item['subscription_renewal'] ) ) {
$is_purchasable = true;
break;
}
}
}
}
return $is_purchasable;
}
/**
* Flag payment of manual renewal orders via an extra URL param.
*
* This is particularly important to ensure renewals of limited subscriptions can be completed.
*
* @since 2.0
*/
public function get_checkout_payment_url( $pay_url, $order ) {
if ( wcs_order_contains_renewal( $order ) ) {
$pay_url = add_query_arg( array( $this->cart_item_key => 'true' ), $pay_url );
}
return $pay_url;
}
/**
* Customise which actions are shown against a subscription renewal order on the My Account page.
*
* @since 2.0
*/
public function filter_my_account_my_orders_actions( $actions, $order ) {
if ( wcs_order_contains_renewal( $order ) ) {
unset( $actions['cancel'] );
// If the subscription has been deleted or reactivated some other way, don't support payment on the order
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
foreach ( $subscriptions as $subscription ) {
if ( empty( $subscription ) || ! $subscription->has_status( array( 'on-hold', 'pending' ) ) ) {
unset( $actions['pay'] );
break;
}
}
}
return $actions;
}
/**
* When a failed renewal order is being paid for via checkout, make sure WC_Checkout::create_order() preserves its
* status as 'failed' until it is paid. By default, it will always set it to 'pending', but we need it left as 'failed'
* so that we can correctly identify the status change in @see self::maybe_change_subscription_status().
*
* @param string Default order status for orders paid for via checkout. Default 'pending'
* @since 2.0
*/
public function maybe_preserve_order_status( $order_status ) {
if ( null !== WC()->session ) {
$order_id = absint( WC()->session->order_awaiting_payment );
if ( $order_id > 0 && ( $order = wc_get_order( $order_id ) ) && wcs_order_contains_renewal( $order ) && $order->has_status( 'failed' ) ) {
$order_status = 'failed';
}
}
return $order_status;
}
/**
* Removes all the linked renewal/resubscribe items from the cart if a renewal/resubscribe item is removed.
*
* @param string $cart_item_key The cart item key of the item removed from the cart.
* @since 2.0
*/
public function maybe_remove_items( $cart_item_key ) {
if ( isset( WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['subscription_id'] ) ) {
$removed_item_count = 0;
$subscription_id = WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['subscription_id'];
foreach ( WC()->cart->cart_contents as $key => $cart_item ) {
if ( isset( $cart_item[ $this->cart_item_key ] ) && $subscription_id == $cart_item[ $this->cart_item_key ]['subscription_id'] ) {
WC()->cart->removed_cart_contents[ $key ] = WC()->cart->cart_contents[ $key ];
unset( WC()->cart->cart_contents[ $key ] );
$removed_item_count++;
}
}
//remove the renewal order flag
unset( WC()->session->order_awaiting_payment );
//clear renewal coupons
$this->clear_coupons();
if ( $removed_item_count > 1 && 'woocommerce_before_cart_item_quantity_zero' == current_filter() ) {
wc_add_notice( esc_html__( 'All linked subscription items have been removed from the cart.', 'woocommerce-subscriptions' ), 'notice' );
}
}
}
/**
* Checks the cart to see if it contains a subscription renewal item.
*
* @see wcs_cart_contains_renewal()
* @return bool | Array The cart item containing the renewal, else false.
* @since 2.0.10
*/
protected function cart_contains() {
return wcs_cart_contains_renewal();
}
/**
* Formats the title of the product removed from the cart. Because we have removed all
* linked renewal/resubscribe items from the cart we need a product title to reflect that.
*
* @param string $product_title
* @param $cart_item
* @return string $product_title
* @since 2.0
*/
public function items_removed_title( $product_title, $cart_item ) {
if ( isset( $cart_item[ $this->cart_item_key ]['subscription_id'] ) ) {
$subscription = wcs_get_subscription( absint( $cart_item[ $this->cart_item_key ]['subscription_id'] ) );
$product_title = ( count( $subscription->get_items() ) > 1 ) ? esc_html_x( 'All linked subscription items were', 'Used in WooCommerce by removed item notification: "_All linked subscription items were_ removed. Undo?" Filter for item title.', 'woocommerce-subscriptions' ) : $product_title;
}
return $product_title;
}
/**
* Restores all linked renewal/resubscribe items to the cart if the customer has restored one.
*
* @param string $cart_item_key The cart item key of the item being restored to the cart.
* @since 2.0
*/
public function maybe_restore_items( $cart_item_key ) {
if ( isset( WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['subscription_id'] ) ) {
$subscription_id = WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['subscription_id'];
foreach ( WC()->cart->removed_cart_contents as $key => $cart_item ) {
if ( isset( $cart_item[ $this->cart_item_key ] ) && $key != $cart_item_key && $cart_item[ $this->cart_item_key ]['subscription_id'] == $subscription_id ) {
WC()->cart->cart_contents[ $key ] = WC()->cart->removed_cart_contents[ $key ];
unset( WC()->cart->removed_cart_contents[ $key ] );
}
}
//restore the renewal order flag
if ( isset( WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['renewal_order_id'] ) ) {
WC()->session->set( 'order_awaiting_payment', WC()->cart->cart_contents[ $cart_item_key ][ $this->cart_item_key ]['renewal_order_id'] );
}
}
}
/**
* Return our custom pseudo coupon data for renewal coupons
*
* @param array $data the coupon data
* @param string $code the coupon code that data is being requested for
* @return array the custom coupon data
* @since 2.0.10
*/
public function renewal_coupon_data( $data, $code ) {
if ( ! is_object( WC()->session ) ) {
return $data;
}
$renewal_coupons = WC()->session->get( 'wcs_renewal_coupons' );
if ( empty( $renewal_coupons ) ) {
return $data;
}
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
// Tweak the coupon data for renewal coupons
if ( $code == $coupon->code ) {
$data = array(
'discount_type' => $coupon->type,
'coupon_amount' => $coupon->amount,
'individual_use' => ( $coupon->individual_use ) ? $coupon->individual_use : 'no',
'product_ids' => ( $coupon->product_ids ) ? $coupon->product_ids : array(),
'exclude_product_ids' => ( $coupon->exclude_product_ids ) ? $coupon->exclude_product_ids : array(),
'usage_limit' => '',
'usage_count' => '',
'expiry_date' => '',
'free_shipping' => ( $coupon->free_shipping ) ? $coupon->free_shipping : '',
'product_categories' => ( $coupon->product_categories ) ? $coupon->product_categories : array(),
'exclude_product_categories' => ( $coupon->exclude_product_categories ) ? $coupon->exclude_product_categories : array(),
'exclude_sale_items' => ( $coupon->exclude_sale_items ) ? $coupon->exclude_sale_items : 'no',
'minimum_amount' => ( $coupon->minimum_amount ) ? $coupon->minimum_amount : '',
'maximum_amount' => ( $coupon->maximum_amount ) ? $coupon->maximum_amount : '',
'customer_email' => ( $coupon->customer_email ) ? $coupon->customer_email : array(),
);
}
}
}
return $data;
}
/**
* Get original products for a renewal order - so that we can ensure renewal coupons are only applied to those
*
* @param object $subscription subscription
* @return array $product_ids an array of product ids on a subscription renewal order
* @since 2.0.10
*/
protected function get_products( $subscription ) {
$product_ids = array();
if ( wcs_is_subscription( $subscription ) ) {
foreach ( $subscription->get_items() as $item ) {
$product_id = ( $item['variation_id'] ) ? $item['variation_id'] : $item['product_id'];
if ( ! empty( $product_id ) ) {
$product_ids[] = $product_id;
}
}
}
return $product_ids;
}
/**
* Store renewal coupon information in a session variable so we can access it later when coupon data is being retrieved
*
* @param int $subscription_id subscription id
* @param object $coupon coupon
* @since 2.0.10
*/
protected function store_coupon( $subscription_id, $coupon ) {
if ( ! empty( $subscription_id ) && ! empty( $coupon ) ) {
$renewal_coupons = WC()->session->get( 'wcs_renewal_coupons', array() );
// Subscriptions may have multiple coupons, store coupons in array
if ( array_key_exists( $subscription_id, $renewal_coupons ) ) {
$renewal_coupons[ $subscription_id ][] = $coupon;
} else {
$renewal_coupons[ $subscription_id ] = array( $coupon );
}
WC()->session->set( 'wcs_renewal_coupons', $renewal_coupons );
}
}
/**
* Clear renewal coupons - protects against confusing customer facing notices if customers add one renewal order to the cart with a set of coupons and then decide to add another renewal order with a different set of coupons
*
* @since 2.0.10
*/
public function clear_coupons() {
$renewal_coupons = WC()->session->get( 'wcs_renewal_coupons' );
// Remove the coupons from the cart
if ( ! empty( $renewal_coupons ) ) {
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
WC()->cart->remove_coupons( $coupon->code );
}
}
}
// Clear the session information we have stored
WC()->session->set( 'wcs_renewal_coupons', array() );
}
/**
* Add order/subscription fee line items to the cart when a renewal order, initial order or resubscribe is in the cart.
*
* @param WC_Cart $cart
* @since 2.0.13
*/
public function maybe_add_fees( $cart ) {
if ( $cart_item = $this->cart_contains() ) {
$order = $this->get_order( $cart_item );
if ( $order instanceof WC_Order ) {
foreach ( $order->get_fees() as $fee ) {
$cart->add_fee( $fee['name'], $fee['line_total'], abs( $fee['line_tax'] ) > 0, $fee['tax_class'] );
}
}
}
}
/**
* When restoring the cart from the session, if the cart item contains addons, as well as
* a renewal or resubscribe, do not adjust the price because the original order's price will
* be used, and this includes the addons amounts.
*
* @since 2.0
*/
public function product_addons_adjust_price( $adjust_price, $cart_item ) {
if ( true === $adjust_price && isset( $cart_item[ $this->cart_item_key ] ) ) {
$adjust_price = false;
}
return $adjust_price;
}
/**
* Get the order object used to construct the renewal cart.
*
* @param Array The renewal cart item.
* @return WC_Order | The order object
* @since 2.0.13
*/
protected function get_order( $cart_item = '' ) {
$order = false;
if ( empty( $cart_item ) ) {
$cart_item = $this->cart_contains();
}
if ( false !== $cart_item && isset( $cart_item[ $this->cart_item_key ] ) ) {
$order = wc_get_order( $cart_item[ $this->cart_item_key ]['renewal_order_id'] );
}
return $order;
}
/**
* Before allowing payment on an order awaiting payment via checkout, WC >= 2.6 validates
* order items haven't changed by checking for a cart hash on the order, so we need to set
* that here. @see WC_Checkout::create_order()
*
* @since 2.0.14
*/
protected function set_cart_hash( $order_id ) {
update_post_meta( $order_id, '_cart_hash', md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ) );
}
/* Deprecated */
/**
* For subscription renewal via cart, use original order discount
*
* @since 2.0
*/
public function set_renewal_discounts( $cart ) {
_deprecated_function( __METHOD__, '2.0.10', 'Applying original subscription discounts to renewals via cart are now handled within ' . __CLASS__ .'::maybe_setup_cart()' );
}
/**
* For subscription renewal via cart, previously adjust item price by original order discount
*
* No longer required as of 1.3.5 as totals are calculated correctly internally.
*
* @since 2.0
*/
public function get_discounted_price_for_renewal( $price, $cart_item, $cart ) {
_deprecated_function( __METHOD__, '2.0.10', 'No longer required as of 1.3.5 as totals are calculated correctly internally.' );
}
/**
* Add subscription fee line items to the cart when a renewal order or resubscribe is in the cart.
*
* @param WC_Cart $cart
* @since 2.0.10
*/
public function maybe_add_subscription_fees( $cart ) {
_deprecated_function( __METHOD__, '2.0.13', __CLASS__ .'::maybe_add_fees()' );
}
}
new WCS_Cart_Renewal();

View File

@@ -0,0 +1,225 @@
<?php
/**
* Implement resubscribing to a subscription via the cart.
*
* Resubscribing is a similar process to renewal via checkout (which is why this class extends WCS_Cart_Renewal), only it:
* - creates a new subscription with similar terms to the existing subscription, where as a renewal resumes the existing subscription
* - is for an expired or cancelled subscription only.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Cart_Resubscribe
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
/* The flag used to indicate if a cart item is a renewal */
public $cart_item_key = 'subscription_resubscribe';
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
$this->setup_hooks();
// When a resubscribe order is created on checkout, record the resubscribe, attached after WC_Subscriptions_Checkout::process_checkout()
add_action( 'woocommerce_checkout_subscription_created', array( &$this, 'maybe_record_resubscribe' ), 10, 3 );
}
/**
* Checks if the current request is by a user to resubcribe to a subscription, and if it is setup a
* subscription resubcribe process via the cart for the product/variation/s that are being renewed.
*
* @since 2.0
*/
public function maybe_setup_cart() {
global $wp;
if ( isset( $_GET['resubscribe'] ) && isset( $_GET['_wpnonce'] ) ) {
$subscription = wcs_get_subscription( $_GET['resubscribe'] );
$redirect_to = get_permalink( wc_get_page_id( 'myaccount' ) );
if ( wp_verify_nonce( $_GET['_wpnonce'], $subscription->id ) === false ) {
wc_add_notice( __( 'There was an error with your request to resubscribe. Please try again.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( empty( $subscription ) ) {
wc_add_notice( __( 'That subscription does not exist. Has it been deleted?', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! current_user_can( 'subscribe_again', $subscription->id ) ) {
wc_add_notice( __( 'That doesn\'t appear to be one of your subscriptions.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! wcs_can_user_resubscribe_to( $subscription ) ) {
wc_add_notice( __( 'You can not resubscribe to that subscription. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
} else {
$this->setup_cart( $subscription, array(
'subscription_id' => $subscription->id,
) );
if ( WC()->cart->get_cart_contents_count() != 0 ) {
wc_add_notice( __( 'Complete checkout to resubscribe.', 'woocommerce-subscriptions' ), 'success' );
}
$redirect_to = WC()->cart->get_checkout_url();
}
wp_safe_redirect( $redirect_to );
exit;
} elseif ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && isset( $wp->query_vars['order-pay'] ) ) {
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] );
$order = wc_get_order( $wp->query_vars['order-pay'] );
$order_key = $_GET['key'];
if ( $order->order_key == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_resubscribe( $order ) ) {
wc_add_notice( __( 'Complete checkout to resubscribe.', 'woocommerce-subscriptions' ), 'success' );
$subscriptions = wcs_get_subscriptions_for_resubscribe_order( $order );
foreach ( $subscriptions as $subscription ) {
$this->setup_cart( $subscription, array(
'subscription_id' => $subscription->id,
) );
}
$redirect_to = WC()->cart->get_checkout_url();
wp_safe_redirect( $redirect_to );
exit;
}
}
}
/**
* When creating an order at checkout, if the checkout is to resubscribe to an expired or cancelled
* subscription, make sure we record that on the order and new subscription.
*
* @since 2.0
*/
public function maybe_record_resubscribe( $new_subscription, $order, $recurring_cart ) {
$cart_item = $this->cart_contains( $recurring_cart );
if ( false !== $cart_item ) {
update_post_meta( $order->id, '_subscription_resubscribe', $cart_item[ $this->cart_item_key ]['subscription_id'], true );
update_post_meta( $new_subscription->id, '_subscription_resubscribe', $cart_item[ $this->cart_item_key ]['subscription_id'], true );
}
}
/**
* Restore renewal flag when cart is reset and modify Product object with renewal order related info
*
* @since 2.0
*/
public function get_cart_item_from_session( $cart_item_session_data, $cart_item, $key ) {
if ( isset( $cart_item[ $this->cart_item_key ]['subscription_id'] ) ) {
// Setup the cart as if it's a renewal (as the setup process is almost the same)
$cart_item_session_data = parent::get_cart_item_from_session( $cart_item_session_data, $cart_item, $key );
// Need to get the original subscription price, not the current price
$subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
if ( $subscription ) {
// Make sure the original subscription terms perisist
$_product = $cart_item_session_data['data'];
$_product->subscription_period = $subscription->billing_period;
$_product->subscription_period_interval = $subscription->billing_interval;
// And don't give another free trial period
$_product->subscription_trial_length = 0;
}
}
return $cart_item_session_data;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 2.0
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
// If the product is being set as not-purchasable by Subscriptions (due to limiting)
if ( false === $is_purchasable && false === WC_Subscriptions_Product::is_purchasable( $is_purchasable, $product ) ) {
// Validating when restoring cart from session
if ( false !== $this->cart_contains() ) {
$resubscribe_cart_item = $this->cart_contains();
$subscription = wcs_get_subscription( $resubscribe_cart_item['subscription_resubscribe']['subscription_id'] );
if ( $subscription->has_product( $product->id ) ) {
$is_purchasable = true;
}
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart)
} elseif ( isset( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( $product->id == $cart_item['product_id'] && isset( $cart_item[ $this->cart_item_key ] ) ) {
$is_purchasable = true;
break;
}
}
} elseif ( isset( $_GET['resubscribe'] ) ) { // Is a request to resubscribe
$subscription = wcs_get_subscription( absint( $_GET['resubscribe'] ) );
if ( false !== $subscription && $subscription->has_product( $product->id ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$is_purchasable = true;
}
}
}
return $is_purchasable;
}
/**
* Checks the cart to see if it contains a subscription resubscribe item.
*
* @see wcs_cart_contains_resubscribe()
* @param WC_Cart $cart The cart object to search in.
* @return bool | Array The cart item containing the renewal, else false.
* @since 2.0.10
*/
protected function cart_contains( $cart = '' ) {
return wcs_cart_contains_resubscribe( $cart );
}
/**
* Get the subscription object used to construct the resubscribe cart.
*
* @param Array The resubscribe cart item.
* @return WC_Subscription | The subscription object.
* @since 2.0.13
*/
protected function get_order( $cart_item = '' ) {
$subscription = false;
if ( empty( $cart_item ) ) {
$cart_item = $this->cart_contains();
}
if ( false !== $cart_item && isset( $cart_item[ $this->cart_item_key ] ) ) {
$subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
}
return $subscription;
}
}
new WCS_Cart_Resubscribe();

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class to handle everything to do with changing a payment method for a subscription on the
* edit subscription admin page.
*
* @class WCS_Change_Payment_Method_Admin
* @version 2.0
* @package WooCommerce Subscriptions/Includes
* @category Class
* @author Prospress
*/
class WCS_Change_Payment_Method_Admin {
/**
* Display the edit payment gateway option under
*
* @since 2.0
*/
public static function display_fields( $subscription ) {
$payment_method = ! empty( $subscription->payment_method ) ? $subscription->payment_method : '';
$valid_payment_methods = self::get_valid_payment_methods( $subscription );
if ( ! $subscription->is_manual() && ! isset( $valid_payment_methods[ $payment_method ] ) ) {
$subscription_payment_gateway = WC_Subscriptions_Payment_Gateways::get_payment_gateway( $payment_method );
if ( false != $subscription_payment_gateway ) {
$valid_payment_methods[ $payment_method ] = $subscription_payment_gateway->title;
}
}
echo '<p class="form-field form-field-wide">';
if ( count( $valid_payment_methods ) > 1 ) {
$found_method = false;
echo '<label>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':</label>';
echo '<select class="wcs_payment_method_selector" name="_payment_method" id="_payment_method" class="first">';
foreach ( $valid_payment_methods as $gateway_id => $gateway_title ) {
echo '<option value="' . esc_attr( $gateway_id ) . '" ' . selected( $payment_method, $gateway_id, false ) . '>' . esc_html( $gateway_title ) . '</option>';
if ( $payment_method == $gateway_id ) {
$found_method = true;
}
}
echo '</select>';
} elseif ( count( $valid_payment_methods ) == 1 ) {
echo '<strong>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':</strong><br/>' . esc_html( current( $valid_payment_methods ) );
echo '<img class="help_tip" data-tip="Gateway ID: [' . esc_attr( key( $valid_payment_methods ) ) . ']" src="' . esc_url( WC()->plugin_url() ) . '/assets/images/help.png" height="16" width="16" />';
echo '<input type="hidden" value="' . esc_attr( key( $valid_payment_methods ) ) . '" id="_payment_method" name="_payment_method">';
}
echo '</p>';
$payment_method_table = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
if ( is_array( $payment_method_table ) ) {
foreach ( $payment_method_table as $payment_method_id => $payment_method_meta ) {
echo '<div class="wcs_payment_method_meta_fields" id="wcs_' . esc_attr( $payment_method_id ) . '_fields" ' . ( ( $payment_method_id != $payment_method || $subscription->is_manual() ) ? 'style="display:none;"' : '' ) .' >';
foreach ( $payment_method_meta as $meta_table => $meta ) {
foreach ( $meta as $meta_key => $meta_data ) {
$field_id = sprintf( '_payment_method_meta[%s][%s]', $meta_table , $meta_key );
$field_label = ( ! empty( $meta_data['label'] ) ) ? $meta_data['label'] : $meta_key ;
$field_value = ( ! empty( $meta_data['value'] ) ) ? $meta_data['value'] : null ;
$field_disabled = ( isset( $meta_data['disabled'] ) && true == $meta_data['disabled'] ) ? ' readonly' : '';
echo '<p class="form-field form-field-wide">';
echo '<label for="' . esc_attr( $field_id ) . '">' . esc_html( $field_label ) . '</label>';
echo '<input type="text" class="short" name="' . esc_attr( $field_id ) . '" id="' . esc_attr( $field_id ) . '" value="' . esc_attr( $field_value ) . '" placeholder="" ' . esc_attr( $field_disabled ) . '>';
echo '</p>';
}
}
echo '</div>';
}
}
wp_nonce_field( 'wcs_change_payment_method_admin', '_wcsnonce' );
}
/**
* Get the new payment data from POST and check the new payment method supports
* the new admin change hook.
*
* @since 2.0
* @param $subscription WC_Subscription
*/
public static function save_meta( $subscription ) {
if ( empty( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method_admin' ) ) {
return;
}
$payment_gateways = WC()->payment_gateways->payment_gateways();
$payment_method = isset( $_POST['_payment_method'] ) ? wc_clean( $_POST['_payment_method'] ) : '';
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
$payment_method_meta = ( ! empty( $payment_method_meta[ $payment_method ] ) ) ? $payment_method_meta[ $payment_method ] : array();
$valid_payment_methods = self::get_valid_payment_methods( $subscription );
if ( ! isset( $valid_payment_methods[ $payment_method ] ) ) {
throw new Exception( __( 'Please choose a valid payment gateway to change to.', 'woocommerce-subscriptions' ) );
}
if ( ! empty( $payment_method_meta ) ) {
foreach ( $payment_method_meta as $meta_table => &$meta ) {
if ( ! is_array( $meta ) ) {
continue;
}
foreach ( $meta as $meta_key => &$meta_data ) {
$meta_data['value'] = isset( $_POST['_payment_method_meta'][ $meta_table ][ $meta_key ] ) ? $_POST['_payment_method_meta'][ $meta_table ][ $meta_key ] : '';
}
}
}
$payment_gateway = ( 'manual' != $payment_method ) ? $payment_gateways[ $payment_method ] : '';
if ( ! $subscription->is_manual() && property_exists( $subscription->payment_gateway, 'id' ) && ( '' == $payment_gateway || ( $subscription->payment_gateway->id != $payment_gateway->id ) ) ) {
// Before updating to a new payment gateway make sure the subscription status is updated with the current gateway
$gateway_status = apply_filters( 'wcs_gateway_status_payment_changed', 'cancelled', $subscription, $payment_gateway );
WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, $gateway_status );
}
$subscription->set_payment_method( $payment_gateway, $payment_method_meta );
}
/**
* Get a list of possible gateways that a subscription could be changed to by admins.
*
* @since 2.0
* @param $subscription int | WC_Subscription
* @return
*/
public static function get_valid_payment_methods( $subscription ) {
if ( ! $subscription instanceof WC_Subscription ) {
$subscription = wcs_get_subscription( $subscription );
}
$valid_gateways = array( 'manual' => __( 'Manual Renewal', 'woocommerce-subscriptions' ) );
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
foreach ( $available_gateways as $gateway_id => $gateway ) {
if ( $gateway->supports( 'subscription_payment_method_change_admin' ) && 'no' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) || ( ! $subscription->is_manual() && $gateway_id == $subscription->payment_method ) ) {
$valid_gateways[ $gateway_id ] = $gateway->get_title();
}
}
return $valid_gateways;
}
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* Download Handler for WooCommerce Subscriptions
*
* Functions for download related things within the Subscription Extension.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Download_Handler
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Download_Handler {
/**
* Initialize filters and hooks for class.
*
* @since 2.0
*/
public static function init() {
add_filter( 'woocommerce_process_product_file_download_paths_grant_access_to_new_file', __CLASS__ . '::maybe_revoke_immediate_access', 10, 4 );
add_action( 'woocommerce_grant_product_download_permissions', __CLASS__ . '::save_downloadable_product_permissions' );
add_filter( 'woocommerce_get_item_downloads', __CLASS__ . '::get_item_downloads', 10, 3 );
add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::repair_permission_data', 60, 1 );
add_action( 'deleted_post', __CLASS__ . '::delete_subscription_permissions' );
}
/**
* When adding new downloadable content to a subscription product, check if we don't
* want to automatically add the new downloadable files to the subscription or initial and renewal orders.
*
* @param bool $grant_access
* @param string $download_id
* @param int $product_id
* @param WC_Order $order
* @return bool
* @since 2.0
*/
public static function maybe_revoke_immediate_access( $grant_access, $download_id, $product_id, $order ) {
if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_drip_downloadable_content_on_renewal', 'no' ) && ( wcs_is_subscription( $order->id ) || wcs_order_contains_subscription( $order, 'any' ) ) ) {
$grant_access = false;
}
return $grant_access;
}
/**
* Save the download permissions on the individual subscriptions as well as the order. Hooked into
* 'woocommerce_grant_product_download_permissions', which is strictly after the order received all the info
* it needed, so we don't need to play with priorities.
*
* @param integer $order_id the ID of the order. At this point it is guaranteed that it has files in it and that it hasn't been granted permissions before
*/
public static function save_downloadable_product_permissions( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( wcs_order_contains_subscription( $order, 'any' ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => array( 'any' ) ) );
} else {
return;
}
foreach ( $subscriptions as $subscription ) {
if ( sizeof( $subscription->get_items() ) > 0 ) {
foreach ( $subscription->get_items() as $item ) {
$_product = $subscription->get_product_from_item( $item );
if ( $_product && $_product->exists() && $_product->is_downloadable() ) {
$downloads = $_product->get_files();
$product_id = wcs_get_canonical_product_id( $item );
foreach ( array_keys( $downloads ) as $download_id ) {
// grant access on subscription if it does not already exist
if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE `order_id` = %d AND `product_id` = %d AND `download_id` = '%s'", $subscription->id, $product_id, $download_id ) ) ) {
wc_downloadable_file_permission( $download_id, $product_id, $subscription, $item['qty'] );
}
self::revoke_downloadable_file_permission( $product_id, $order_id, $order->user_id );
}
}
}
}
update_post_meta( $subscription->id, '_download_permissions_granted', 1 );
}
}
/**
* Revokes download permissions from permissions table if a file has permissions on a subscription. If a product has
* multiple files, all permissions will be revoked from the original order.
*
* @param int $product_id the ID for the product (the downloadable file)
* @param int $order_id the ID for the original order
* @param int $user_id the user we're removing the permissions from
* @return boolean true on success, false on error
*/
public static function revoke_downloadable_file_permission( $product_id, $order_id, $user_id ) {
global $wpdb;
$table = $wpdb->prefix . 'woocommerce_downloadable_product_permissions';
$where = array(
'product_id' => $product_id,
'order_id' => $order_id,
'user_id' => $user_id,
);
$format = array( '%d', '%d', '%d' );
return $wpdb->delete( $table, $where, $format );
}
/**
* WooCommerce's function receives the original order ID, the item and the list of files. This does not work for
* download permissions stored on the subscription rather than the original order as the URL would have the wrong order
* key. This function takes the same parameters, but queries the database again for download ids belonging to all the
* subscriptions that were in the original order. Then for all subscriptions, it checks all items, and if the item
* passed in here is in that subscription, it creates the correct download link to be passsed to the email.
*
* @param array $files List of files already included in the list
* @param array $item An item (you get it by doing $order->get_items())
* @param WC_Order $order The original order
* @return array List of files with correct download urls
*/
public static function get_item_downloads( $files, $item, $order ) {
global $wpdb;
if ( wcs_order_contains_subscription( $order, 'any' ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => array( 'any' ) ) );
} else {
return $files;
}
$product_id = wcs_get_canonical_product_id( $item );
foreach ( $subscriptions as $subscription ) {
foreach ( $subscription->get_items() as $subscription_item ) {
if ( wcs_get_canonical_product_id( $subscription_item ) === $product_id ) {
$files = $subscription->get_item_downloads( $subscription_item );
}
}
}
return $files;
}
/**
* Repairs a glitch in WordPress's save function. You cannot save a null value on update, see
* https://github.com/woothemes/woocommerce/issues/7861 for more info on this.
*
* @param integer $post_id The ID of the subscription
*/
public static function repair_permission_data( $post_id ) {
if ( absint( $post_id ) !== $post_id ) {
return;
}
if ( 'shop_subscription' !== get_post_type( $post_id ) ) {
return;
}
global $wpdb;
$wpdb->query( $wpdb->prepare( "
UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions
SET access_expires = null
WHERE order_id = %d
AND access_expires = %s
", $post_id, '0000-00-00 00:00:00' ) );
}
/**
* Remove download permissions attached to a subscription when it is permenantly deleted.
*
* @since 2.0
*/
public static function delete_subscription_permissions( $post_id ) {
global $wpdb;
if ( 'shop_subscription' == get_post_type( $post_id ) ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d", $post_id ) );
}
}
}
WCS_Download_Handler::init();

View File

@@ -0,0 +1,169 @@
<?php
/**
* WooCommerce Subscriptions Query Handler
*
* @version 2.0
* @author Prospress
*/
class WCS_Query extends WC_Query {
public function __construct() {
add_action( 'init', array( $this, 'add_endpoints' ) );
add_filter( 'the_title', array( $this, 'change_endpoint_title' ), 11, 1 );
if ( ! is_admin() ) {
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
add_filter( 'woocommerce_get_breadcrumb', array( $this, 'add_breadcrumb' ), 10 );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 );
// Inserting your new tab/page into the My Account page.
add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) );
add_action( 'woocommerce_account_subscriptions_endpoint', array( $this, 'endpoint_content' ) );
}
$this->init_query_vars();
}
/**
* Init query vars by loading options.
*
* @since 2.0
*/
public function init_query_vars() {
$this->query_vars = array(
'view-subscription' => get_option( 'woocommerce_myaccount_view_subscriptions_endpoint', 'view-subscription' ),
'subscriptions' => get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' ),
);
}
/**
* Adds endpoint breadcrumb when viewing subscription
*
* @param array $crumbs already assembled breadcrumb data
* @return array $crumbs if we're on a view-subscription page, then augmented breadcrumb data
*/
public function add_breadcrumb( $crumbs ) {
foreach ( $this->query_vars as $key => $query_var ) {
if ( $this->is_query( $query_var ) ) {
$crumbs[] = array( $this->get_endpoint_title( $key ) );
}
}
return $crumbs;
}
/**
* Changes page title on view subscription page
*
* @param string $title original title
* @return string changed title
*/
public function change_endpoint_title( $title ) {
if ( in_the_loop() ) {
foreach ( $this->query_vars as $key => $query_var ) {
if ( $this->is_query( $query_var ) ) {
$title = $this->get_endpoint_title( $key );
}
}
}
return $title;
}
/**
* Set the subscription page title when viewing a subscription.
*
* @since 2.0
* @param $title
*/
public function get_endpoint_title( $endpoint ) {
global $wp;
switch ( $endpoint ) {
case 'view-subscription':
$subscription = wcs_get_subscription( $wp->query_vars['view-subscription'] );
$title = ( $subscription ) ? sprintf( _x( 'Subscription #%s', 'hash before order number', 'woocommerce-subscriptions' ), $subscription->get_order_number() ) : '';
break;
case 'subscriptions':
$title = __( 'Subscriptions', 'woocommerce-subscriptions' );
break;
default:
$title = '';
break;
}
return $title;
}
/**
* Insert the new endpoint into the My Account menu.
*
* @param array $items
* @return array
*/
public function add_menu_items( $menu_items ) {
// Add our menu item after the Orders tab if it exists, otherwise just add it to the end
if ( array_key_exists( 'orders', $menu_items ) ) {
$menu_items = wcs_array_insert_after( 'orders', $menu_items, 'subscriptions', __( 'Subscriptions', 'woocommerce-subscriptions' ) );
} else {
$menu_items['subscriptions'] = __( 'Subscriptions', 'woocommerce-subscriptions' );
}
return $menu_items;
}
/**
* Endpoint HTML content.
*/
public function endpoint_content() {
wc_get_template( 'myaccount/subscriptions.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
/**
* Check if the current query is for a type we want to override.
*
* @param string $query_var the string for a query to check for
* @return bool
*/
protected function is_query( $query_var ) {
global $wp;
if ( is_main_query() && is_page() && isset( $wp->query_vars[ $query_var ] ) ) {
$is_view_subscription_query = true;
} else {
$is_view_subscription_query = false;
}
return apply_filters( 'wcs_query_is_query', $is_view_subscription_query, $query_var );
}
/**
* Fix for endpoints on the homepage
*
* Based on WC_Query->pre_get_posts(), but only applies the fix for endpoints on the homepage from it
* instead of duplicating all the code to handle the main product query.
*
* @param mixed $q query object
*/
public function pre_get_posts( $q ) {
// We only want to affect the main query
if ( ! $q->is_main_query() ) {
return;
}
if ( $q->is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) !== absint( $q->get( 'page_id' ) ) ) {
$_query = wp_parse_args( $q->query );
if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->query_vars ) ) ) {
$q->is_page = true;
$q->is_home = false;
$q->is_singular = true;
$q->set( 'page_id', (int) get_option( 'page_on_front' ) );
add_filter( 'redirect_canonical', '__return_false' );
}
}
}
}
new WCS_Query();

View File

@@ -0,0 +1,179 @@
<?php
/**
* Subscriptions Remove Item
*
*
* @author Prospress
* @since 2.0
*/
class WCS_Remove_Item {
/**
* Initialise class hooks & filters when the file is loaded
*
* @since 2.0
*/
public static function init() {
// Check if a user is requesting to remove or re-add an item to their subscription
add_action( 'init', __CLASS__ . '::maybe_remove_or_add_item_to_subscription', 100 );
}
/**
* Returns the link used to remove an item from a subscription
*
* @param int $subscription_id
* @param int $order_item_id
* @since 2.0
*/
public static function get_remove_url( $subscription_id, $order_item_id ) {
$remove_link = add_query_arg( array( 'subscription_id' => $subscription_id, 'remove_item' => $order_item_id ) );
$remove_link = wp_nonce_url( $remove_link, $subscription_id );
return $remove_link;
}
/**
* Returns the link to undo removing an item from a subscription
*
* @param int $subscription_id
* @param int $order_item_id
* @param string $base_url
* @since 2.0
*/
public static function get_undo_remove_url( $subscription_id, $order_item_id, $base_url ) {
$undo_link = add_query_arg( array( 'subscription_id' => $subscription_id, 'undo_remove_item' => $order_item_id ), $base_url );
$undo_link = wp_nonce_url( $undo_link, $subscription_id );
return $undo_link;
}
/**
* Process the remove or re-add a line item from a subscription request.
*
* @since 2.0
*/
public static function maybe_remove_or_add_item_to_subscription() {
if ( isset( $_GET['subscription_id'] ) && ( isset( $_GET['remove_item'] ) || isset( $_GET['undo_remove_item'] ) ) && isset( $_GET['_wpnonce'] ) ) {
$subscription = ( wcs_is_subscription( $_GET['subscription_id'] ) ) ? wcs_get_subscription( $_GET['subscription_id'] ) : false;
$undo_request = ( isset( $_GET['undo_remove_item'] ) ) ? true : false;
$item_id = ( $undo_request ) ? $_GET['undo_remove_item'] : $_GET['remove_item'];
if ( false === $subscription ) {
wc_add_notice( sprintf( _x( 'Subscription #%d does not exist.', 'hash before subscription ID', 'woocommerce-subscriptions' ), $_GET['subscription_id'] ), 'error' );
wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
exit;
}
if ( self::validate_remove_items_request( $subscription, $item_id, $undo_request ) ) {
if ( $undo_request ) {
// handle undo request
$removed_item = WC()->session->get( 'removed_subscription_items', array() );
if ( ! empty( $removed_item[ $item_id ] ) && $subscription->id == $removed_item[ $item_id ] ) {
// restore the item
wc_update_order_item( $item_id, array( 'order_item_type' => 'line_item' ) );
unset( $removed_item[ $item_id ] );
WC()->session->set( 'removed_subscription_items', $removed_item );
// restore download permissions for this item
$line_items = $subscription->get_items();
$line_item = $line_items[ $item_id ];
$_product = $subscription->get_product_from_item( $line_item );
$product_id = wcs_get_canonical_product_id( $line_item );
if ( $_product && $_product->exists() && $_product->is_downloadable() ) {
$downloads = $_product->get_files();
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $subscription, $line_item['qty'] );
}
}
// translators: 1$: product name, 2$: product id
$subscription->add_order_note( sprintf( _x( 'Customer added "%1$s" (Product ID: #%2$d) via the My Account page.', 'used in order note', 'woocommerce-subscriptions' ), wcs_get_line_item_name( $line_item ), $product_id ) );
} else {
wc_add_notice( __( 'Your request to undo your previous action was unsuccessful.', 'woocommerce-subscriptions' ) );
}
} else {
// handle remove item requests
WC()->session->set( 'removed_subscription_items', array( $item_id => $subscription->id ) );
// remove download access for the item
$line_items = $subscription->get_items();
$line_item = $line_items[ $item_id ];
$product_id = wcs_get_canonical_product_id( $line_item );
WCS_Download_Handler::revoke_downloadable_file_permission( $product_id, $subscription->id, $subscription->get_user_id() );
// remove the line item from subscription but preserve its data in the DB
wc_update_order_item( $item_id, array( 'order_item_type' => 'line_item_removed' ) );
// translators: 1$: product name, 2$: product id
$subscription->add_order_note( sprintf( _x( 'Customer removed "%1$s" (Product ID: #%2$d) via the My Account page.', 'used in order note', 'woocommerce-subscriptions' ), wcs_get_line_item_name( $line_item ), $product_id ) );
// translators: placeholders are 1$: item name, and, 2$: opening and, 3$: closing link tags
wc_add_notice( sprintf( __( 'You have successfully removed "%1$s" from your subscription. %2$sUndo?%3$s', 'woocommerce-subscriptions' ), $line_item['name'], '<a href="' . esc_url( self::get_undo_remove_url( $subscription->id, $item_id, $subscription->get_view_order_url() ) ) . '" >', '</a>' ) );
}
}
$subscription->calculate_totals();
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
}
}
/**
* Validate the incoming request to either remove an item or add and item back to a subscription that was previously removed.
* Add an descriptive notice to the page whether or not the request was validated or not.
*
* @since 2.0
* @param WC_Subscription $subscription
* @param int $order_item_id
* @param bool $undo_request bool
* @return bool
*/
private static function validate_remove_items_request( $subscription, $order_item_id, $undo_request = false ) {
$subscription_items = $subscription->get_items();
$response = false;
if ( ! wp_verify_nonce( $_GET['_wpnonce'], $_GET['subscription_id'] ) ) {
wc_add_notice( __( 'Security error. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! current_user_can( 'edit_shop_subscription_line_items', $subscription->id ) ) {
wc_add_notice( __( 'You cannot modify a subscription that does not belong to you.', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! $undo_request && ! isset( $subscription_items[ $order_item_id ] ) ) { // only need to validate the order item id when removing
wc_add_notice( __( 'You cannot remove an item that does not exist. ', 'woocommerce-subscriptions' ), 'error' );
} elseif ( ! $subscription->payment_method_supports( 'subscription_amount_changes' ) ) {
wc_add_notice( __( 'The item was not removed because this Subscription\'s payment method does not support removing an item.', 'woocommerce-subscriptions' ) );
} else {
$response = true;
}
return $response;
}
}
WCS_Remove_Item::init();

View File

@@ -0,0 +1,45 @@
<?php
/**
* WC Subscriptions Template Loader
*
* @version 2.0
* @author Prospress
*/
class WCS_Template_Loader {
public static function init() {
add_filter( 'wc_get_template', __CLASS__ . '::add_view_subscription_template', 10, 5 );
add_action( 'woocommerce_account_view-subscription_endpoint', __CLASS__ . '::get_view_subscription_template' );
}
/**
* Show the subscription template when view a subscription instead of loading the default order template.
*
* @param $located
* @param $template_name
* @param $args
* @param $template_path
* @param $default_path
* @since 2.0
*/
public static function add_view_subscription_template( $located, $template_name, $args, $template_path, $default_path ) {
global $wp;
if ( 'myaccount/my-account.php' == $template_name && ! empty( $wp->query_vars['view-subscription'] ) && WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
$located = wc_locate_template( 'myaccount/view-subscription.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
return $located;
}
/**
* Get the view subscription template. A post WC v2.6 compatible version of @see WCS_Template_Loader::add_view_subscription_template()
*
* @since 2.0.17
*/
public static function get_view_subscription_template() {
wc_get_template( 'myaccount/view-subscription.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
}
WCS_Template_Loader::init();

View File

@@ -0,0 +1,115 @@
<?php
/**
* WooCommerce Subscriptions User Change Status Handler Class
*
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_User_Change_Status_Handler {
public static function init() {
// Check if a user is requesting to cancel their subscription
add_action( 'wp_loaded', __CLASS__ . '::maybe_change_users_subscription', 100 );
}
/**
* Checks if the current request is by a user to change the status of their subscription, and if it is,
* validate the request and proceed to change to the subscription.
*
* @since 2.0
*/
public static function maybe_change_users_subscription() {
if ( isset( $_GET['change_subscription_to'] ) && isset( $_GET['subscription_id'] ) && isset( $_GET['_wpnonce'] ) ) {
$user_id = get_current_user_id();
$subscription = wcs_get_subscription( $_GET['subscription_id'] );
$new_status = $_GET['change_subscription_to'];
if ( self::validate_request( $user_id, $subscription, $new_status, $_GET['_wpnonce'] ) ) {
self::change_users_subscription( $subscription, $new_status );
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
}
}
}
/**
* Change the status of a subscription and show a notice to the user if there was an issue.
*
* @since 2.0
*/
public static function change_users_subscription( $subscription, $new_status ) {
$subscription = ( ! is_object( $subscription ) ) ? wcs_get_subscription( $subscription ) : $subscription;
$changed = false;
switch ( $new_status ) {
case 'active' :
if ( ! $subscription->needs_payment() ) {
$subscription->update_status( $new_status );
$subscription->add_order_note( _x( 'Subscription reactivated by the subscriber from their account page.', 'order note left on subscription after user action', 'woocommerce-subscriptions' ) );
WC_Subscriptions::add_notice( _x( 'Your subscription has been reactivated.', 'Notice displayed to user confirming their action.', 'woocommerce-subscriptions' ), 'success' );
$changed = true;
} else {
WC_Subscriptions::add_notice( __( 'You can not reactivate that subscription until paying to renew it. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
}
break;
case 'on-hold' :
if ( wcs_can_user_put_subscription_on_hold( $subscription ) ) {
$subscription->update_status( $new_status );
$subscription->add_order_note( _x( 'Subscription put on hold by the subscriber from their account page.', 'order note left on subscription after user action', 'woocommerce-subscriptions' ) );
WC_Subscriptions::add_notice( _x( 'Your subscription has been put on hold.', 'Notice displayed to user confirming their action.', 'woocommerce-subscriptions' ), 'success' );
$changed = true;
} else {
WC_Subscriptions::add_notice( __( 'You can not suspend that subscription - the suspension limit has been reached. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
}
break;
case 'cancelled' :
$subscription->cancel_order();
$subscription->add_order_note( _x( 'Subscription cancelled by the subscriber from their account page.', 'order note left on subscription after user action', 'woocommerce-subscriptions' ) );
WC_Subscriptions::add_notice( _x( 'Your subscription has been cancelled.', 'Notice displayed to user confirming their action.', 'woocommerce-subscriptions' ), 'success' );
$changed = true;
break;
}
if ( $changed ) {
do_action( 'woocommerce_customer_changed_subscription_to_' . $new_status, $subscription );
}
}
/**
* Checks if the user's current request to change the status of their subscription is valid.
*
* @since 2.0
*/
public static function validate_request( $user_id, $subscription, $new_status, $wpnonce = '' ) {
$subscription = ( ! is_object( $subscription ) ) ? wcs_get_subscription( $subscription ) : $subscription;
if ( ! wcs_is_subscription( $subscription ) ) {
WC_Subscriptions::add_notice( __( 'That subscription does not exist. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
return false;
} elseif ( ! empty( $wpnonce ) && wp_verify_nonce( $wpnonce, $subscription->id . $subscription->get_status() ) === false ) {
WC_Subscriptions::add_notice( __( 'Security error. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), 'error' );
return false;
} elseif ( ! user_can( $user_id, 'edit_shop_subscription_status', $subscription->id ) ) {
WC_Subscriptions::add_notice( __( 'That doesn\'t appear to be one of your subscriptions.', 'woocommerce-subscriptions' ), 'error' );
return false;
} elseif ( ! $subscription->can_be_updated_to( $new_status ) ) {
// translators: placeholder is subscription's new status, translated
WC_Subscriptions::add_notice( sprintf( __( 'That subscription can not be changed to %s. Please contact us if you need assistance.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $new_status ) ), 'error' );
return false;
}
return true;
}
}
WCS_User_Change_Status_Handler::init();

View File

@@ -0,0 +1,149 @@
<?php
/**
* WooCommerce Subscriptions Webhook class
*
* This class introduces webhooks to, storing and retrieving webhook data from the associated
* `shop_webhook` custom post type, as well as delivery logs from the `webhook_delivery`
* comment type.
*
* Subscription Webhooks are enqueued to their associated actions, delivered, and logged.
*
* @author Prospress
* @category Webhooks
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Webhooks {
/**
* Setup webhook for subscriptions
*
* @since 2.0
*/
public static function init() {
add_filter( 'woocommerce_webhook_topic_hooks', __CLASS__ . '::add_topics', 10, 2 );
add_filter( 'woocommerce_webhook_payload', __CLASS__ . '::create_payload', 10, 4 );
add_filter( 'woocommerce_valid_webhook_resources', __CLASS__ . '::add_resource', 10, 1 );
add_action( 'woocommerce_checkout_subscription_created', __CLASS__ . '::add_subscription_created_callback', 10, 1 );
add_action( 'woocommerce_subscription_date_updated', __CLASS__ . '::add_subscription_updated_callback', 10, 1 );
add_filter( 'woocommerce_webhook_topics' , __CLASS__ . '::add_topics_admin_menu', 10, 1 );
}
/**
* Add Subscription webhook topics
*
* @param array $topic_hooks
* @since 2.0
*/
public static function add_topics( $topic_hooks, $webhook ) {
if ( 'subscription' == $webhook->get_resource() ) {
$topic_hooks = apply_filters( 'woocommerce_subscriptions_webhook_topics', array(
'subscription.created' => array(
'wcs_api_subscription_created',
'wcs_webhook_subscription_created',
'woocommerce_process_shop_subscription_meta',
),
'subscription.updated' => array(
'wc_api_subscription_updated',
'woocommerce_subscription_status_changed',
'wcs_webhook_subscription_updated',
'woocommerce_process_shop_subscription_meta',
),
'subscription.deleted' => array(
'woocommerce_subscription_trashed',
'woocommerce_subscription_deleted',
'woocommerce_api_delete_subscription',
),
), $webhook );
}
return $topic_hooks;
}
/**
* Add Subscription topics to the Webhooks dropdown menu in when creating a new webhook.
*
* @since 2.0
*/
public static function add_topics_admin_menu( $topics ) {
$front_end_topics = array(
'subscription.created' => __( ' Subscription Created', 'woocommerce-subscriptions' ),
'subscription.updated' => __( ' Subscription Updated', 'woocommerce-subscriptions' ),
'subscription.deleted' => __( ' Subscription Deleted', 'woocommerce-subscriptions' ),
);
return array_merge( $topics, $front_end_topics );
}
/**
* Setup payload for subscription webhook delivery.
*
* @since 2.0
*/
public static function create_payload( $payload, $resource, $resource_id, $id ) {
if ( 'subscription' == $resource && empty( $payload ) && wcs_is_subscription( $resource_id ) ) {
$webhook = new WC_Webhook( $id );
$event = $webhook->get_event();
$current_user = get_current_user_id();
wp_set_current_user( $webhook->get_user_id() );
WC()->api->WC_API_Subscriptions->register_routes( array() );
$payload = WC()->api->WC_API_Subscriptions->get_subscription( $resource_id );
wp_set_current_user( $current_user );
}
return $payload;
}
/**
* Add webhook resource for subscription.
*
* @param array $resources
* @since 2.0
*/
public static function add_resource( $resources ) {
$resources[] = 'subscription';
return $resources;
}
/**
* Call a "subscription created" action hook with the first parameter being a subscription id so that it can be used
* for webhooks.
*
* @since 2.0
*/
public static function add_subscription_created_callback( $subscription ) {
do_action( 'wcs_webhook_subscription_created', $subscription->id );
}
/**
* Call a "subscription updated" action hook with a subscription id as the first parameter to be used for webhooks payloads.
*
* @since 2.0
*/
public static function add_subscription_updated_callback( $subscription ) {
do_action( 'wcs_webhook_subscription_updated', $subscription->id );
}
}
WCS_Webhooks::init();

View File

@@ -0,0 +1,128 @@
<?php
/**
* Handle deprecated actions.
*
* When triggering an action which has a deprecated equivalient from Subscriptions v1.n, check if the old
* action had any callbacks attached to it, and if so, log a notice and trigger the old action with a set
* of parameters in the deprecated format.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Action_Deprecator extends WCS_Hook_Deprecator {
/* The actions that have been deprecated, 'new_hook' => 'old_hook' */
protected $deprecated_hooks = array(
'woocommerce_scheduled_subscription_payment' => 'scheduled_subscription_payment',
'woocommerce_subscription_payment_complete' => 'processed_subscription_payment',
'woocommerce_subscription_renewal_payment_complete' => 'processed_subscription_renewal_payment',
'woocommerce_subscriptions_paid_for_failed_renewal_order' => 'woocommerce_subscriptions_processed_failed_renewal_order_payment',
'woocommerce_subscriptions_pre_update_payment_method' => 'woocommerce_subscriptions_pre_update_recurring_payment_method',
'woocommerce_subscription_payment_method_updated' => 'woocommerce_subscriptions_updated_recurring_payment_method',
'woocommerce_subscription_failing_payment_method_updated' => 'woocommerce_subscriptions_changed_failing_payment_method',
'woocommerce_subscription_payment_failed' => 'processed_subscription_payment_failure',
'woocommerce_subscription_change_payment_method_via_pay_shortcode' => 'woocommerce_subscriptions_change_payment_method_via_pay_shortcode',
'subscriptions_put_on_hold_for_order' => 'subscriptions_suspended_for_order',
'woocommerce_subscription_status_active' => 'activated_subscription',
'woocommerce_subscription_status_on-hold' => array( 'suspended_subscription', 'subscription_put_on-hold' ),
'woocommerce_subscription_status_cancelled' => 'cancelled_subscription',
'woocommerce_subscription_status_on-hold_to_active' => 'reactivated_subscription',
'woocommerce_subscription_status_expired' => 'subscription_expired',
'woocommerce_scheduled_subscription_trial_end' => 'subscription_trial_end',
'woocommerce_scheduled_subscription_end_of_prepaid_term' => 'subscription_end_of_prepaid_term',
);
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
parent::__construct();
}
/**
* Trigger the old action with the original callback parameters
*
* @since 2.0
*/
protected function trigger_hook( $old_hook, $new_callback_args ) {
switch ( $old_hook ) {
// New arg spec: $subscription_id
// Old arg spec: $user_id, $subscription_key
case 'scheduled_subscription_payment' :
case 'subscription_end_of_prepaid_term' :
case 'subscription_trial_end' :
$subscription = wcs_get_subscription( $new_callback_args[0] );
do_action( $old_hook, $subscription->get_user_id(), wcs_get_old_subscription_key( $subscription ) );
break;
// New arg spec: $subscription
// Old arg spec: $user_id, $subscription_key
case 'processed_subscription_payment' :
case 'processed_subscription_renewal_payment' :
case 'processed_subscription_payment_failure' :
$subscription = $new_callback_args[0];
do_action( $old_hook, $subscription->get_user_id(), wcs_get_old_subscription_key( $subscription ) );
break;
// New arg spec: $renewal_order, $subscription
// Old arg spec: $subscription_key, $original_order
case 'woocommerce_subscriptions_processed_failed_renewal_order_payment' :
$renewal_order = $new_callback_args[0];
$subscription = $new_callback_args[1];
do_action( $old_hook, wcs_get_old_subscription_key( $subscription ), self::get_order( $subscription ) );
break;
// New arg spec: $subscription, $new_payment_method, $old_payment_method
// Old arg spec: $order, $subscription_key, $new_payment_method, $old_payment_method
case 'woocommerce_subscriptions_pre_update_recurring_payment_method' :
case 'woocommerce_subscriptions_updated_recurring_payment_method' :
$subscription = $new_callback_args[0];
$new_payment_method = $new_callback_args[1];
$old_payment_method = $new_callback_args[2];
do_action( $old_hook, self::get_order( $subscription ), wcs_get_old_subscription_key( $subscription ), $new_payment_method, $old_payment_method );
break;
// New arg spec: $subscription, $renewal_order
// Old arg spec: $original_order, $renewal_order, $subscription_key
case 'woocommerce_subscriptions_changed_failing_payment_method' :
$subscription = $new_callback_args[0];
$renewal_order = $new_callback_args[1];
do_action( $old_hook, self::get_order( $subscription ), $renewal_order, wcs_get_old_subscription_key( $subscription ) );
break;
// New arg spec: $order
// Old arg spec: $order
case 'subscriptions_suspended_for_order' :
do_action( $old_hook, $new_callback_args[0] );
break;
// New arg spec: $subscription
// Old arg spec: $subscription_key, $order
case 'woocommerce_subscriptions_change_payment_method_via_pay_shortcode' :
$subscription = $new_callback_args[0];
do_action( $old_hook, wcs_get_old_subscription_key( $subscription ), self::get_order( $subscription ) );
break;
// New arg spec: $subscription
// Old arg spec: $user_id, $subscription_key
case 'activated_subscription' :
case 'subscription_put_on-hold' :
case 'suspended_subscription' :
case 'cancelled_subscription' :
case 'reactivated_subscription' :
case 'subscription_expired' :
$subscription = $new_callback_args[0];
do_action( $old_hook, $subscription->get_user_id(), wcs_get_old_subscription_key( $subscription ) );
break;
}
}
}
new WCS_Action_Deprecator();

View File

@@ -0,0 +1,109 @@
<?php
/**
* Deprecate actions that use a dynamic hook by appending a variable, like a payment gateway's name.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Dynamic_Action_Deprecator extends WCS_Dynamic_Hook_Deprecator {
/* The prefixes of hooks that have been deprecated, 'new_hook' => 'old_hook_prefix' */
protected $deprecated_hook_prefixes = array(
'woocommerce_admin_changed_subscription_to_' => 'admin_changed_subscription_to_',
'woocommerce_scheduled_subscription_payment_' => 'scheduled_subscription_payment_',
'woocommerce_customer_changed_subscription_to_' => 'customer_changed_subscription_to_',
'woocommerce_subscription_payment_method_updated_to_' => 'woocommerce_subscriptions_updated_recurring_payment_method_to_',
'woocommerce_subscription_payment_method_updated_from_' => 'woocommerce_subscriptions_updated_recurring_payment_method_from_',
'woocommerce_subscription_failing_payment_method_updated_' => 'woocommerce_subscriptions_changed_failing_payment_method_',
// Gateway status change hooks
'woocommerce_subscription_activated_' => array(
'activated_subscription_',
'reactivated_subscription_',
),
'woocommerce_subscription_on-hold_' => 'subscription_put_on-hold_',
'woocommerce_subscription_cancelled_' => 'cancelled_subscription_',
'woocommerce_subscription_expired_' => 'subscription_expired_',
);
/**
* Bootstraps the class and hooks required actions & filters.
*
* We need to use the special 'all' hook here because we don't actually know the full hook names
* in advance, just their prefix.
*
* @since 2.0
*/
public function __construct() {
parent::__construct();
}
/**
* Display a notice if functions are hooked to the old filter and apply the old filters args
*
* @since 2.0
*/
protected function trigger_hook( $old_hook, $new_callback_args ) {
if ( 0 === strpos( $old_hook, 'admin_changed_subscription_to_' ) ) {
// New arg spec: $subscription_id
// Old arg spec: $subscription_key
$subscription = wcs_get_subscription( $new_callback_args[0] );
do_action( $old_hook, wcs_get_old_subscription_key( $subscription ) );
} elseif ( 0 === strpos( $old_hook, 'scheduled_subscription_payment_' ) ) {
// New arg spec: $amount, $renewal_order
// Old arg spec: $amount, $original_order, $product_id
$subscription = $new_callback_args[0];
$subscriptions = wcs_get_subscriptions_for_renewal_order( $new_callback_args[1] );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
do_action( $old_hook, $new_callback_args[0], self::get_order( $subscription ), self::get_product_id( $subscription ) );
}
} elseif ( 0 === strpos( $old_hook, 'activated_subscription_' ) || 0 === strpos( $old_hook, 'reactivated_subscription_' ) || 0 === strpos( $old_hook, 'subscription_put_on-hold_' ) || 0 === strpos( $old_hook, 'cancelled_subscription_' ) || 0 === strpos( $old_hook, 'subscription_expired_' ) ) {
// New arg spec: $subscription
// Old arg spec: $order, $product_id
$subscription = $new_callback_args[0];
do_action( $old_hook, self::get_order( $subscription ), self::get_product_id( $subscription ) );
} elseif ( 0 === strpos( $old_hook, 'customer_changed_subscription_to_' ) ) {
// New arg spec: $subscription
// Old arg spec: $subscription_key
do_action( $old_hook, wcs_get_old_subscription_key( $new_callback_args[0] ) );
} elseif ( 0 === strpos( $old_hook, 'woocommerce_subscriptions_updated_recurring_payment_method_to_' ) ) {
// New arg spec: $subscription, $old_payment_method
// Old arg spec: $order, $subscription_key, $old_payment_method
$subscription = $new_callback_args[0];
$old_payment_method = $new_callback_args[2];
do_action( $old_hook, self::get_order( $subscription ), wcs_get_old_subscription_key( $subscription ), $old_payment_method );
} elseif ( 0 === strpos( $old_hook, 'woocommerce_subscriptions_updated_recurring_payment_method_from_' ) ) {
// New arg spec: $subscription, $new_payment_method
// Old arg spec: $order, $subscription_key, $new_payment_method
$subscription = $new_callback_args[0];
$new_payment_method = $new_callback_args[1];
do_action( $old_hook, self::get_order( $subscription ), wcs_get_old_subscription_key( $subscription ), $new_payment_method );
} elseif ( 0 === strpos( $old_hook, 'woocommerce_subscriptions_changed_failing_payment_method_' ) ) {
// New arg spec: $subscription, $renewal_order
// Old arg spec: $original_order, $renewal_order, $subscription_key
$subscription = $new_callback_args[0];
do_action( $old_hook, self::get_order( $subscription ), $new_callback_args[1], wcs_get_old_subscription_key( $subscription ) );
}
}
}
new WCS_Dynamic_Action_Deprecator();

View File

@@ -0,0 +1,51 @@
<?php
/**
* Deprecate filters that use a dynamic hook by appending a variable, like a payment gateway's name.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Dynamic_Filter_Deprecator extends WCS_Dynamic_Hook_Deprecator {
/* The prefixes of hooks that have been deprecated, 'new_hook' => 'old_hook_prefix' */
protected $deprecated_hook_prefixes = array(
'woocommerce_can_subscription_be_updated_to_' => 'woocommerce_subscription_can_be_changed_to_',
);
/**
* Bootstraps the class and hooks required actions & filters.
*
* We need to use the special 'all' hook here because we don't actually know the full hook names
* in advance, just their prefix.
*
* @since 2.0
*/
public function __construct() {
parent::__construct();
}
/**
* Display a notice if functions are hooked to the old filter and apply the old filters args
*
* @since 2.0
*/
protected function trigger_hook( $old_hook, $new_callback_args ) {
// Return value is always the first param
$return_value = $new_callback_args[0];
if ( 0 === strpos( $old_hook, 'woocommerce_subscription_can_be_changed_to_' ) ) {
// New arg spec: $can_be_updated, $subscription
// Old arg spec: $can_be_changed, $subscription, $order
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $subscription ), self::get_order( $subscription ) );
}
return $return_value;
}
}
new WCS_Dynamic_Filter_Deprecator();

View File

@@ -0,0 +1,338 @@
<?php
/**
* Handle deprecated filters.
*
* When triggering a filter which has a deprecated equivalient from Subscriptions v1.n, check if the old
* filter had any callbacks attached to it, and if so, log a notice and trigger the old filter with a set
* of parameters in the deprecated format so that the current return value also has the old filters applied
* (whereever possible that is).
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Hook_Deprecator
* @category Class
* @author Prospress
* @since 2.0
*/
class WCS_Filter_Deprecator extends WCS_Hook_Deprecator {
/* The filters that have been deprecated, 'new_hook' => 'old_hook' */
protected $deprecated_hooks = array(
// Subscription Meta Filters
'woocommerce_subscription_payment_failed_count' => 'woocommerce_subscription_failed_payment_count',
'woocommerce_subscription_payment_completed_count' => 'woocommerce_subscription_completed_payment_count',
'woocommerce_subscription_get_end_date' => 'woocommerce_subscription_expiration_date',
'woocommerce_subscription_get_trial_end_date' => 'woocommerce_subscription_trial_expiration_date',
'woocommerce_subscription_date_updated' => 'woocommerce_subscriptions_set_expiration_date',
'woocommerce_subscriptions_product_expiration_date' => 'woocommerce_subscription_calculated_expiration_date',
'woocommerce_subscription_date_updated' => 'woocommerce_subscription_set_next_payment_date',
'woocommerce_subscription_get_last_payment_date' => 'woocommerce_subscription_last_payment_date',
'woocommerce_subscription_calculated_next_payment_date' => 'woocommerce_subscriptions_calculated_next_payment_date',
'woocommerce_subscription_date_updated' => 'woocommerce_subscriptions_set_trial_expiration_date',
'wcs_subscription_statuses' => array(
'woocommerce_subscriptions_custom_status_string', //no replacement as Subscriptions now uses wcs_get_subscription_statuses() for everything (the deprecator could use 'wc_subscription_statuses' and loop over all statuses to set it in the returned value)
'woocommerce_subscriptions_status_string',
),
// Renewal Filters
'wcs_renewal_order_items' => 'woocommerce_subscriptions_renewal_order_items',
'wcs_renewal_order_meta_query' => 'woocommerce_subscriptions_renewal_order_meta_query',
'wcs_renewal_order_meta' => 'woocommerce_subscriptions_renewal_order_meta',
'wcs_renewal_order_item_name' => 'woocommerce_subscriptions_renewal_order_item_name',
'wcs_users_resubscribe_link' => 'woocommerce_subscriptions_users_renewal_link',
'wcs_can_user_resubscribe_to_subscription' => 'woocommerce_can_subscription_be_renewed',
'wcs_renewal_order_created' => array(
'woocommerce_subscriptions_renewal_order_created', // Even though 'woocommerce_subscriptions_renewal_order_created' is an action, as it is attached to a filter, we need to handle it in here
'woocommerce_subscriptions_renewal_order_id',
),
// List Table Filters
'woocommerce_subscription_list_table_actions' => 'woocommerce_subscriptions_list_table_actions',
'woocommerce_subscription_list_table_column_status_content' => 'woocommerce_subscriptions_list_table_column_status_content',
'woocommerce_subscription_list_table_column_content' => 'woocommerce_subscriptions_list_table_column_content',
// User Filters
'wcs_can_user_put_subscription_on_hold' => 'woocommerce_subscriptions_can_current_user_suspend',
'wcs_view_subscription_actions' => 'woocommerce_my_account_my_subscriptions_actions',
'wcs_get_users_subscriptions' => 'woocommerce_users_subscriptions',
'wcs_users_change_status_link' => 'woocommerce_subscriptions_users_action_link',
'wcs_user_has_subscription' => 'woocommerce_user_has_subscription',
// Misc Filters
'woocommerce_subscription_max_failed_payments_exceeded' => 'woocommerce_subscriptions_max_failed_payments_exceeded',
'woocommerce_my_subscriptions_payment_method' => 'woocommerce_my_subscriptions_recurring_payment_method',
'woocommerce_subscriptions_update_payment_via_pay_shortcode' => 'woocommerce_subscriptions_update_recurring_payment_via_pay_shortcode',
'woocommerce_can_subscription_be_updated_to' => 'woocommerce_can_subscription_be_changed_to',
);
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public function __construct() {
parent::__construct();
}
/**
* Trigger the old filter with the original callback parameters and make sure the return value is passed on (when possible).
*
* @since 2.0
*/
protected function trigger_hook( $old_hook, $new_callback_args ) {
// Return value is always the first param
$return_value = $new_callback_args[0];
switch ( $old_hook ) {
// New arg spec: $subscription_statuses
// Old arg spec: $status, $subscription_key, $user_id
case 'woocommerce_subscriptions_custom_status_string' :
// Need to loop over the status and apply the old hook to each, we don't have a subscription or user for them anymore though
foreach ( $return_value as $status_key => $status_string ) {
$return_value[ $status_key ] = apply_filters( $old_hook, $status_string, '', 0 );
}
break;
// New arg spec: $subscription_statuses
// Old arg spec: $status_string, $status, $subscription_key, $user_id
case 'woocommerce_subscriptions_status_string' :
// Need to loop over the status and apply the old hook to each, we don't have a subscription or user for them anymore though
foreach ( $return_value as $status_key => $status_string ) {
$return_value[ $status_key ] = apply_filters( $old_hook, $status_string, $status_key, '', 0 );
}
break;
// New arg spec: $count, $subscription
// Old arg spec: $count, $user_id, $subscription_key
case 'woocommerce_subscription_failed_payment_count' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, $subscription->get_user_id(), wcs_get_old_subscription_key( $subscription ) );
break;
// New arg spec: $date, $subscription, $timezone
// Old arg spec: $date, $subscription_key, $user_id
case 'woocommerce_subscription_completed_payment_count' :
case 'woocommerce_subscription_expiration_date' :
case 'woocommerce_subscription_trial_expiration_date' :
case 'woocommerce_subscription_last_payment_date' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, wcs_get_old_subscription_key( $subscription ), $subscription->get_user_id(), 'mysql' );
break;
// New arg spec: $expiration_date, $product_id, $from_date
// Old arg spec: $expiration_date, $subscription_key, $user_id
case 'woocommerce_subscription_calculated_expiration_date' :
$return_value = apply_filters( $old_hook, $return_value, '', 0 );
break;
// New arg spec: $next_payment_date, $subscription
// Old arg spec: $next_payment_date, $order, $product_id, $type, $from_date, $from_date_arg
case 'woocommerce_subscriptions_calculated_next_payment_date' :
$subscription = $new_callback_args[1];
$last_payment = $subscription->get_date( 'last_payment' );
$return_value = apply_filters( $old_hook, $return_value, self::get_order( $subscription ), self::get_product_id( $subscription ), 'mysql', $last_payment, $last_payment );
break;
// New arg spec: $subscription, $date_type, $datetime
// Old arg spec: $is_set, $expiration_date, $subscription_key, $user_id
case 'woocommerce_subscription_set_next_payment_date' :
case 'woocommerce_subscriptions_set_trial_expiration_date' :
case 'woocommerce_subscriptions_set_expiration_date' :
$subscription = $new_callback_args[0];
$date_type = $new_callback_args[1];
if ( ( 'next_payment' == $date_type && in_array( $old_hook, array( 'woocommerce_subscriptions_set_trial_expiration_date', 'woocommerce_subscription_set_next_payment_date' ) ) ) || ( 'end_date' == $date_type && 'woocommerce_subscriptions_set_expiration_date' == $old_hook ) ) {
// Here the old return value was a boolean where as now there is no equivalent filter, so we apply the filter to the action (which is only triggered when the old filter's value would have been true) and ignore the return value
apply_filters( $old_hook, true, wcs_get_old_subscription_key( $subscription ), $subscription->get_user_id() );
}
break;
// New arg spec: $order_items, $renewal_order, $subscription
// Old arg spec: $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role
case 'woocommerce_subscriptions_renewal_order_meta' :
case 'woocommerce_subscriptions_renewal_order_meta_query' :
// Old arg spec: $order_items, $original_order_id, $renewal_order_id, $product_id, $new_order_role
case 'woocommerce_subscriptions_renewal_order_items' :
$renewal_order = $new_callback_args[1];
$subscription = $new_callback_args[2];
$original_id = self::get_order_id( $subscription );
// Now we need to find the new orders role, if the calling function is wcs_create_resubscribe_order(), the role is parent, otherwise it's child
$backtrace = debug_backtrace();
$order_role = ( 'wcs_create_resubscribe_order' == $backtrace[1]['function'] ) ? 'parent' : 'child';
// Old arg spec: $order_items, $original_order_id, $renewal_order_id, $product_id, $new_order_role
if ( 'woocommerce_subscriptions_renewal_order_items' == $old_hook ) {
$return_value = apply_filters( $old_hook, $return_value, $original_id, $renewal_order->id, self::get_product_id( $subscription ), $order_role );
} else {
// Old arg spec: $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role
$return_value = apply_filters( $old_hook, $return_value, $original_id, $renewal_order->id, $order_role );
}
break;
// New arg spec: $item_name, $order_item, $subscription
// Old arg spec: $item_name, $order_item, $original_order
case 'woocommerce_subscriptions_renewal_order_item_name' :
$return_value = apply_filters( $old_hook, $return_value, $new_callback_args[1], self::get_order( $new_callback_args[2] ) );
break;
// New arg spec: $renewal_order, $subscription
// Old arg spec: $renewal_order, $original_order, $product_id, $new_order_role
case 'woocommerce_subscriptions_renewal_order_created' :
do_action( $old_hook, $return_value, self::get_order( $new_callback_args[1] ), self::get_product_id( $new_callback_args[1] ), 'child' );
break;
// New arg spec: $renewal_order, $subscription
// Old arg spec: $renewal_order_id, $original_order, $product_id, $new_order_role
case 'woocommerce_subscriptions_renewal_order_id' :
$renewal_order = $new_callback_args[0];
$subscription = $new_callback_args[1];
// Now we need to find the new orders role, if the calling function is wcs_create_resubscribe_order(), the role is parent, otherwise it's child
$backtrace = debug_backtrace();
$order_role = ( 'wcs_create_resubscribe_order' == $backtrace[1]['function'] ) ? 'parent' : 'child';
$renewal_order_id = apply_filters( $old_hook, $return_value->id, self::get_order( $subscription ), self::get_product_id( $subscription ), $order_role );
// Only change the return value if a new filter was returned by the hook
if ( $renewal_order_id !== $renewal_order->id ) {
$return_value = wc_get_order( $renewal_order_id );
}
break;
// New arg spec: $resubscribe_link, $subscription_id
// Old arg spec: $renewal_url, $subscription_key
case 'woocommerce_subscriptions_users_renewal_link' :
$return_value = apply_filters( $old_hook, $return_value, wcs_get_old_subscription_key( wcs_get_subscription( $new_callback_args[1] ) ) );
break;
// New arg spec: $can_user_resubscribe, $subscription, $user_id
// Old arg spec: $subscription_can_be_renewed, $subscription, $subscription_key, $user_id
case 'woocommerce_can_subscription_be_renewed' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $subscription ), wcs_get_old_subscription_key( $subscription ), $subscription->get_user_id() );
break;
// New arg spec: $actions, $subscription
// Old arg spec: $actions, $subscription_array
case 'woocommerce_subscriptions_list_table_actions' :
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $new_callback_args[1] ) );
break;
// New arg spec: $column_content, $subscription, $actions
// Old arg spec: $column_content, $subscription_array, $actions, $list_table
case 'woocommerce_subscriptions_list_table_column_status_content' :
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $new_callback_args[1] ) );
break;
// New arg spec: $column_content, $the_subscription, $column
// Old arg spec: $column_content, $subscription_array, $column
case 'woocommerce_subscriptions_list_table_column_content' :
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $new_callback_args[1] ), $new_callback_args[2] );
break;
// New arg spec: $user_can_suspend, $subscription
// Old arg spec: $user_can_suspend, $subscription_key
case 'woocommerce_subscriptions_can_current_user_suspend' :
$return_value = apply_filters( $old_hook, $return_value, wcs_get_old_subscription_key( $new_callback_args[1] ) );
break;
// New arg spec: $actions, $subscription (individual subscription object)
// Old arg spec: $all_actions, $subscriptions (array of subscription arrays)
case 'woocommerce_my_account_my_subscriptions_actions' :
$subscription = $new_callback_args[1];
$old_key = wcs_get_old_subscription_key( $subscription );
$subscription_in_deprecated_structure = array(
$old_key => wcs_get_subscription_in_deprecated_structure( $subscription ),
);
$all_actions = apply_filters( $old_hook, $return_value, $subscription_in_deprecated_structure );
// Only change the return value if a new value was returned by the filter
if ( $all_actions !== $return_value ) {
$return_value = $all_actions[ $old_key ];
}
break;
// New arg spec: $action_link, $subscription_id, $status
// Old arg spec: $action_link, $subscription_key, $status
case 'woocommerce_subscriptions_users_action_link' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, wcs_get_old_subscription_key( wcs_get_subscription( $new_callback_args[1] ) ), $new_callback_args[2] );
break;
// New arg spec: failed_payments_exceeded, $subscription
// Old arg spec: $failed_payments_exceeded, $user_id, $subscription_key
case 'woocommerce_subscriptions_max_failed_payments_exceeded' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, $subscription->get_user_id(), wcs_get_old_subscription_key( $subscription ) );
break;
// New arg spec: $payment_method_to_display, $subscription
// Old arg spec: $payment_method_to_display, $subscription_details, $order
case 'woocommerce_my_subscriptions_recurring_payment_method' :
$subscription = $new_callback_args[1];
$return_value = apply_filters( $old_hook, $return_value, wcs_get_subscription_in_deprecated_structure( $subscription ), self::get_order( $subscription ) );
break;
// New arg spec: $allow_update, $new_payment_method, $subscription
// Old arg spec: $allow_update, $new_payment_method
case 'woocommerce_subscriptions_update_recurring_payment_via_pay_shortcode' :
$return_value = apply_filters( $old_hook, $return_value, $new_callback_args[1] );
break;
// New arg spec: $has_subscription, $user_id, $product_id, $status
// Old arg spec: $has_subscription, $user_id, $product_id
case 'woocommerce_user_has_subscription' :
$return_value = apply_filters( $old_hook, $return_value, $new_callback_args[0], $new_callback_args[1] );
break;
// New arg spec: $subscriptions (array of objects), $user_id
// Old arg spec: $subscriptions (array of arrays), $user_id
case 'woocommerce_users_subscriptions' :
// For this hook, the old return value is incompatible with the new return value, so we will trigger another, more urgent notice
trigger_error( 'Callbacks on the "woocommerce_users_subscriptions" filter must be updated immediately. Attach callbacks to the new "wcs_get_users_subscriptions" filter instead. Since version 2.0 of WooCommerce Subscriptions, the "woocommerce_users_subscriptions" filter does not affect the list of a user\'s subscriptions as the subscription data structure has changed.' );
// But still trigger the old hook, even if we can't map the old data to the new return value
$subscriptions = $new_callback_args[0];
$old_subscriptions = array();
foreach ( $subscriptions as $subscription ) {
$old_subscriptions[ wcs_get_old_subscription_key( $subscription ) ] = wcs_get_subscription_in_deprecated_structure( $subscription );
}
apply_filters( $old_hook, $old_subscriptions, $new_callback_args[1] );
break;
// New arg spec: $can_be_updated, $new_status_or_meta, $subscription
// Old arg spec: $can_be_changed, $new_status_or_meta, $args
case 'woocommerce_can_subscription_be_changed_to' :
$subscription = $new_callback_args[2];
// Build the old $arg object
$args = new stdClass();
$args->subscription_key = wcs_get_old_subscription_key( $subscription );
$args->subscription = wcs_get_subscription_in_deprecated_structure( $subscription );
$args->user_id = $subscription->get_user_id();
$args->order = self::get_order( $subscription );
$args->payment_gateway = $subscription->payment_method;
$args->order_uses_manual_payments = $subscription->is_manual();
$return_value = apply_filters( $old_hook, $return_value, $args );
break;
}
return $return_value;
}
}
new WCS_Filter_Deprecator();

View File

@@ -0,0 +1,161 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Cancelled Subscription Email
*
* An email sent to the admin when a subscription is cancelled (either by a store manager, or the customer).
*
* @class WCS_Email_Cancelled_Subscription
* @version 1.4
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Brent Shepherd
* @extends WC_Email
*/
class WCS_Email_Cancelled_Subscription extends WC_Email {
/**
* Create an instance of the class.
*
* @access public
* @return void
*/
function __construct() {
$this->id = 'cancelled_subscription';
$this->title = __( 'Cancelled Subscription', 'woocommerce-subscriptions' );
$this->description = __( 'Cancelled Subscription emails are sent when a customer\'s subscription is cancelled (either by a store manager, or the customer).', 'woocommerce-subscriptions' );
$this->heading = __( 'Subscription Cancelled', 'woocommerce-subscriptions' );
// translators: placeholder is {blogname}, a variable that will be substituted when email is sent out
$this->subject = sprintf( _x( '[%s] Subscription Cancelled', 'default email subject for cancelled emails sent to the admin', 'woocommerce-subscriptions' ), '{blogname}' );
$this->template_html = 'emails/cancelled-subscription.php';
$this->template_plain = 'emails/plain/cancelled-subscription.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
add_action( 'cancelled_subscription_notification', array( $this, 'trigger' ) );
parent::__construct();
$this->recipient = $this->get_option( 'recipient' );
if ( ! $this->recipient ) {
$this->recipient = get_option( 'admin_email' );
}
}
/**
* trigger function.
*
* @access public
* @return void
*/
function trigger( $subscription ) {
$this->object = $subscription;
if ( ! is_object( $subscription ) ) {
_deprecated_argument( __METHOD__, '2.0', 'The subscription key is deprecated. Use a subscription post ID' );
$subscription = wcs_get_subscription_from_key( $subscription );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
update_post_meta( $subscription->id, '_cancelled_email_sent', 'true' );
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'subscription' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Initialise Settings Form Fields
*
* @access public
* @return void
*/
function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => _x( 'Enable/Disable', 'an email notification', 'woocommerce-subscriptions' ),
'type' => 'checkbox',
'label' => __( 'Enable this email notification', 'woocommerce-subscriptions' ),
'default' => 'no',
),
'recipient' => array(
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ),
'placeholder' => '',
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ),
'placeholder' => '',
'default' => '',
),
'email_type' => array(
'title' => _x( 'Email type', 'text, html or multipart', 'woocommerce-subscriptions' ),
'type' => 'select',
'description' => __( 'Choose which format of email to send.', 'woocommerce-subscriptions' ),
'default' => 'html',
'class' => 'email_type',
'options' => array(
'plain' => _x( 'Plain text', 'email type', 'woocommerce-subscriptions' ),
'html' => _x( 'HTML', 'email type', 'woocommerce-subscriptions' ),
'multipart' => _x( 'Multipart', 'email type', 'woocommerce-subscriptions' ),
),
),
);
}
}

View File

@@ -0,0 +1,147 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Customer Completed Order Email
*
* Order complete emails are sent to the customer when the order is marked complete and usual indicates that the order has been shipped.
*
* @class WC_Email_Customer_Completed_Order
* @version 2.0.0
* @package WooCommerce/Classes/Emails
* @author WooThemes
* @extends WC_Email
*/
class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Order {
/**
* Constructor
*/
function __construct() {
// Call override values
$this->id = 'customer_completed_renewal_order';
$this->title = __( 'Completed Renewal Order', 'woocommerce-subscriptions' );
$this->description = __( 'Renewal order complete emails are sent to the customer when a subscription renewal order is marked complete and usually indicates that the item for that renewal period has been shipped.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = _x( 'Your renewal order is complete', 'Default email heading for email to customer on completed renewal order', 'woocommerce-subscriptions' );
// translators: $1: {blogname}, $2: {order_date}, variables that will be substituted when email is sent out
$this->subject = sprintf( _x( 'Your %1$s renewal order from %2$s is complete', 'Default email subject for email to customer on completed renewal order', 'woocommerce-subscriptions' ), '{blogname}', '{order_date}' );
$this->template_html = 'emails/customer-completed-renewal-order.php';
$this->template_plain = 'emails/plain/customer-completed-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// Other settings
$this->heading_downloadable = $this->get_option( 'heading_downloadable', _x( 'Your subscription renewal order is complete - download your files', 'Default email heading for email with downloadable files in it', 'woocommerce-subscriptions' ) );
// translators: $1: {blogname}, $2: {order_date}, variables will be substituted when email is sent out
$this->subject_downloadable = $this->get_option( 'subject_downloadable', sprintf( _x( 'Your %1$s subscription renewal order from %2$s is complete - download your files', 'Default email subject for email with downloadable files in it', 'woocommerce-subscriptions' ), '{blogname}', '{order_date}' ) );
// Triggers for this email
add_action( 'woocommerce_order_status_completed_renewal_notification', array( $this, 'trigger' ) );
// We want most of the parent's methods, with none of its properties, so call its parent's constructor
WC_Email::__construct();
}
/**
* trigger function.
*
* We need to override WC_Email_Customer_Completed_Order's trigger method because it expects to be run only once
* per request (but multiple subscription renewal orders can be generated per request).
*
* @access public
* @return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = new WC_Order( $order_id );
$this->recipient = $this->object->billing_email;
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_subject function.
*
* @access public
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_customer_completed_renewal_order', parent::get_subject(), $this->object );
}
/**
* get_heading function.
*
* @access public
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', parent::get_heading(), $this->object );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,150 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Customer Completed Switch Order Email
*
* Order switch email sent to customer when a subscription is switched successfully.
*
* @class WCS_Email_Completed_Switch_Order
* @version 2.0.0
* @package WooCommerce/Classes/Emails
* @author WooThemes
* @extends WC_Email
*/
class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order {
/**
* Constructor
*/
function __construct() {
// Call override values
$this->id = 'customer_completed_switch_order';
$this->title = __( 'Subscription Switch Complete', 'woocommerce-subscriptions' );
$this->description = __( 'Subscription switch complete emails are sent to the customer when a subscription is switched successfully.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Your subscription change is complete', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} subscription change from {order_date} is complete', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-completed-switch-order.php';
$this->template_plain = 'emails/plain/customer-completed-switch-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// Other settings
$this->heading_downloadable = $this->get_option( 'heading_downloadable', __( 'Your subscription change is complete - download your files', 'woocommerce-subscriptions' ) );
$this->subject_downloadable = $this->get_option( 'subject_downloadable', __( 'Your {blogname} subscription change from {order_date} is complete - download your files', 'woocommerce-subscriptions' ) );
// Triggers for this email
add_action( 'woocommerce_order_status_completed_switch_notification', array( $this, 'trigger' ) );
// We want most of the parent's methods, with none of its properties, so call its parent's constructor
WC_Email::__construct();
}
/**
* trigger function.
*
* We need to override WC_Email_Customer_Completed_Order's trigger method because it expects to be run only once
* per request (but multiple subscription switch orders can be generated per request).
*
* @access public
* @return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = new WC_Order( $order_id );
$this->recipient = $this->object->billing_email;
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
$this->subscriptions = wcs_get_subscriptions_for_switch_order( $this->object );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_subject function.
*
* @access public
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_customer_completed_switch_order', parent::get_subject(), $this->object );
}
/**
* get_heading function.
*
* @access public
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_switch_order', parent::get_heading(), $this->object );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,141 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Customer Completed Order Email
*
* Order complete emails are sent to the customer when the order is marked complete and usual indicates that the order has been shipped.
*
* @class WC_Email_Customer_Completed_Order
* @version 2.0.0
* @package WooCommerce/Classes/Emails
* @author WooThemes
* @extends WC_Email
*/
class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Order {
/**
* Constructor
*/
function __construct() {
$this->id = 'customer_processing_renewal_order';
$this->title = __( 'Processing Renewal order', 'woocommerce-subscriptions' );
$this->description = __( 'This is an order notification sent to the customer after payment for a subscription renewal order is completed. It contains the renewal order details.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Thank you for your order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-processing-renewal-order.php';
$this->template_plain = 'emails/plain/customer-processing-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// Triggers for this email
add_action( 'woocommerce_order_status_pending_to_processing_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_on-hold_renewal_notification', array( $this, 'trigger' ) );
// We want all the parent's methods, with none of its properties, so call its parent's constructor
WC_Email::__construct();
}
/**
* trigger function.
*
* We need to override WC_Email_Customer_Processing_Order's trigger method because it expects to be run only once
* per request (but multiple subscription renewal orders can be generated per request).
*
* @access public
* @return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = new WC_Order( $order_id );
$this->recipient = $this->object->billing_email;
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_subject function.
*
* @access public
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_customer_processing_renewal_order', parent::get_subject(), $this->object );
}
/**
* get_heading function.
*
* @access public
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', parent::get_heading(), $this->object );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,175 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Customer Invoice
*
* An email sent to the customer via admin.
*
* @class WC_Email_Customer_Invoice
* @version 2.0.0
* @package WooCommerce/Classes/Emails
* @author WooThemes
* @extends WC_Email
*/
class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
var $find;
var $replace;
/**
* Constructor
*/
function __construct() {
$this->id = 'customer_renewal_invoice';
$this->title = __( 'Customer Renewal Invoice', 'woocommerce-subscriptions' );
$this->description = __( 'Sent to a customer when the subscription is due for renewal and the renewal requires a manual payment, either because it uses manual renewals or the automatic recurring payment failed. The email contains renewal order information and payment links.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->template_html = 'emails/customer-renewal-invoice.php';
$this->template_plain = 'emails/plain/customer-renewal-invoice.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
$this->subject = __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
$this->heading = __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
$this->subject_paid = __( 'Your {blogname} renewal order from {order_date}', 'woocommerce-subscriptions' );
$this->heading_paid = __( 'Renewal order {order_number} details', 'woocommerce-subscriptions' );
// Triggers for this email
add_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_renewal_notification', array( $this, 'trigger' ) );
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
}
/**
* trigger function.
*
* We need to override WC_Email_Customer_Invoice's trigger method because it expects to be run only once
* per request (but multiple subscription renewal orders can be generated per request).
*
* @access public
* @return void
*/
function trigger( $order ) {
if ( ! is_object( $order ) ) {
$order = new WC_Order( absint( $order ) );
}
if ( $order ) {
$this->object = $order;
$this->recipient = $this->object->billing_email;
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_subject function.
*
* @access public
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', parent::get_subject(), $this->object );
}
/**
* get_heading function.
*
* @access public
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', parent::get_heading(), $this->object );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Initialise Settings Form Fields, but add an enable/disable field
* to this email as WC doesn't include that for customer Invoices.
*
* @access public
* @return void
*/
function init_form_fields() {
parent::init_form_fields();
$this->form_fields = array_merge(
array(
'enabled' => array(
'title' => _x( 'Enable/Disable', 'an email notification', 'woocommerce-subscriptions' ),
'type' => 'checkbox',
'label' => __( 'Enable this email notification', 'woocommerce-subscriptions' ),
'default' => 'yes',
),
),
$this->form_fields
);
}
}

View File

@@ -0,0 +1,128 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* New Order Email
*
* An email sent to the admin when a new order is received/paid for.
*
* @class WCS_Email_New_Renewal_Order
* @version 1.4
* @extends WC_Email_New_Order
*/
class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
/**
* Constructor
*/
function __construct() {
$this->id = 'new_renewal_order';
$this->title = __( 'New Renewal Order', 'woocommerce-subscriptions' );
$this->description = __( 'New renewal order emails are sent when a subscription renewal payment is processed.', 'woocommerce-subscriptions' );
$this->heading = __( 'New subscription renewal order', 'woocommerce-subscriptions' );
$this->subject = __( '[{blogname}] New subscription renewal order ({order_number}) - {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/admin-new-renewal-order.php';
$this->template_plain = 'emails/plain/admin-new-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// Triggers for this email
add_action( 'woocommerce_order_status_pending_to_processing_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_completed_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_on-hold_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_processing_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_completed_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_on-hold_renewal_notification', array( $this, 'trigger' ) );
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
// Other settings
$this->recipient = $this->get_option( 'recipient' );
if ( ! $this->recipient ) {
$this->recipient = get_option( 'admin_email' );
}
}
/**
* trigger function.
*
* We need to override WC_Email_New_Order's trigger method because it expects to be run only once
* per request (but multiple subscription renewal orders can be generated per request).
*
* @access public
* @return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = new WC_Order( $order_id );
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,132 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Subscription Switched Email
*
* An email sent to the admin when a customer switches their subscription.
*
* @class WCS_Email_New_Switch_Order
* @version 1.5
* @extends WC_Email_New_Order
*/
class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
/**
* Constructor
*/
function __construct() {
$this->id = 'new_switch_order';
$this->title = __( 'Subscription Switched', 'woocommerce-subscriptions' );
$this->description = __( 'Subscription switched emails are sent when a customer switches a subscription.', 'woocommerce-subscriptions' );
$this->heading = __( 'Subscription Switched', 'woocommerce-subscriptions' );
$this->subject = __( '[{blogname}] Subscription Switched ({order_number}) - {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/admin-new-switch-order.php';
$this->template_plain = 'emails/plain/admin-new-switch-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/';
// Triggers for this email
add_action( 'woocommerce_order_status_pending_to_processing_switch_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_completed_switch_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_on-hold_switch_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_processing_switch_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_completed_switch_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_to_on-hold_switch_notification', array( $this, 'trigger' ) );
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
// Other settings
$this->recipient = $this->get_option( 'recipient' );
if ( ! $this->recipient ) {
$this->recipient = get_option( 'admin_email' );
}
}
/**
* trigger function.
*
* We need to override WC_Email_New_Order's trigger method because it expects to be run only once
* per request.
*
* @access public
* @return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = new WC_Order( $order_id );
$order_date_index = array_search( '{order_date}', $this->find );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
} else {
$this->replace[ $order_date_index ] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
}
$order_number_index = array_search( '{order_number}', $this->find );
if ( false === $order_number_index ) {
$this->find[] = '{order_number}';
$this->replace[] = $this->object->get_order_number();
} else {
$this->replace[ $order_number_index ] = $this->object->get_order_number();
}
$this->subscriptions = wcs_get_subscriptions_for_switch_order( $this->object );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* get_content_html function.
*
* @access public
* @return string
*/
function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* get_content_plain function.
*
* @access public
* @return string
*/
function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'order' => $this->object,
'subscriptions' => $this->subscriptions,
'email_heading' => $this->get_heading(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* Subscriptions Payment Gateways
*
* Hooks into the WooCommerce payment gateways class to add subscription specific functionality.
*
* @package WooCommerce Subscriptions
* @subpackage WC_Subscriptions_Payment_Gateways
* @category Class
* @author Brent Shepherd
* @since 1.0
*/
class WC_Subscriptions_Payment_Gateways {
protected static $one_gateway_supports = array();
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 1.0
*/
public static function init() {
add_action( 'init', __CLASS__ . '::init_paypal', 10 );
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' );
add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' );
// Create a custom hook for gateways that need to manually charge recurring payments
add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 );
// Create a gateway specific hooks for subscription events
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::trigger_gateway_status_updated_hook', 10, 2 );
}
/**
* Instantiate our custom PayPal class
*
* @since 2.0
*/
public static function init_paypal() {
require_once( 'paypal/class-wcs-paypal.php' );
WCS_PayPal::init();
}
/**
* Returns a payment gateway object by gateway's ID, or false if it could not find the gateway.
*
* @since 1.2.4
*/
public static function get_payment_gateway( $gateway_id ) {
$found_gateway = false;
if ( WC()->payment_gateways ) {
foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) {
if ( $gateway_id == $gateway->id ) {
$found_gateway = $gateway;
}
}
}
return $found_gateway;
}
/**
* Only display the gateways which support subscriptions if manual payments are not allowed.
*
* @since 1.0
*/
public static function get_available_payment_gateways( $available_gateways ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() || ( isset( $_GET['order_id'] ) && wcs_order_contains_subscription( $_GET['order_id'] ) ) ) {
$accept_manual_renewals = ( 'no' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', 'no' ) ) ? true : false;
$subscriptions_in_cart = count( WC()->cart->recurring_carts );
foreach ( $available_gateways as $gateway_id => $gateway ) {
$supports_subscriptions = $gateway->supports( 'subscriptions' );
// Remove the payment gateway if there are multiple subscriptions in the cart and this gateway either doesn't support multiple subscriptions or isn't manual (all manual gateways support multiple subscriptions)
if ( $subscriptions_in_cart > 1 && $gateway->supports( 'multiple_subscriptions' ) !== true && ( $supports_subscriptions || ! $accept_manual_renewals ) ) {
unset( $available_gateways[ $gateway_id ] );
// If there is just the one subscription the cart, remove the payment gateway if manual renewals are disabled and this gateway doesn't support automatic payments
} elseif ( ! $supports_subscriptions && ! $accept_manual_renewals ) {
unset( $available_gateways[ $gateway_id ] );
}
}
}
return $available_gateways;
}
/**
* Helper function to check if at least one payment gateway on the site supports a certain subscription feature.
*
* @since 2.0
*/
public static function one_gateway_supports( $supports_flag ) {
// Only check if we haven't already run the check
if ( ! isset( self::$one_gateway_supports[ $supports_flag ] ) ) {
self::$one_gateway_supports[ $supports_flag ] = false;
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway ) {
if ( $gateway->supports( $supports_flag ) ) {
self::$one_gateway_supports[ $supports_flag ] = true;
break;
}
}
}
return self::$one_gateway_supports[ $supports_flag ];
}
/**
* Improve message displayed on checkout when a subscription is in the cart but not gateways support subscriptions.
*
* @since 1.5.2
*/
public static function no_available_payment_methods_message( $no_gateways_message ) {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && 'no' == get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', 'no' ) ) {
$no_gateways_message = __( 'Sorry, it seems there are no available payment methods which support subscriptions. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce-subscriptions' );
}
return $no_gateways_message;
}
/**
* Fire a gateway specific whenever a subscription's status is changed.
*
* @since 2.0
*/
public static function trigger_gateway_status_updated_hook( $subscription, $new_status ) {
if ( $subscription->is_manual() ) {
return;
}
switch ( $new_status ) {
case 'active' :
$hook_prefix = 'woocommerce_subscription_activated_';
break;
case 'on-hold' :
$hook_prefix = 'woocommerce_subscription_on-hold_';
break;
case 'pending-cancel' :
$hook_prefix = 'woocommerce_subscription_pending-cancel_';
break;
case 'cancelled' :
$hook_prefix = 'woocommerce_subscription_cancelled_';
break;
case 'expired' :
$hook_prefix = 'woocommerce_subscription_expired_';
break;
default :
$hook_prefix = apply_filters( 'woocommerce_subscriptions_gateway_status_updated_hook_prefix', 'woocommerce_subscription_status_updated_', $subscription, $new_status );
break;
}
do_action( $hook_prefix . $subscription->payment_method, $subscription );
}
/**
* Fire a gateway specific hook for when a subscription payment is due.
*
* @since 1.0
*/
public static function gateway_scheduled_subscription_payment( $subscription_id, $deprecated = null ) {
// Passing the old $user_id/$subscription_key parameters
if ( null != $deprecated ) {
_deprecated_argument( __METHOD__, '2.0', 'Second parameter is deprecated' );
$subscription = wcs_get_subscription_from_key( $deprecated );
} else {
$subscription = wcs_get_subscription( $subscription_id );
}
if ( false === $subscription ) {
throw new InvalidArgumentException( sprintf( __( 'Subscription doesn\'t exist in scheduled action: %d', 'woocommerce-subscriptions' ), $subscription_id ) );
}
if ( ! $subscription->is_manual() && $subscription->get_total() > 0 && ! empty( $subscription->payment_method ) ) {
do_action( 'woocommerce_scheduled_subscription_payment_' . $subscription->payment_method, $subscription->get_total(), $subscription->get_last_order( 'all' ) );
}
}
/**
* Fire a gateway specific hook for when a subscription is activated.
*
* @since 1.0
*/
public static function trigger_gateway_activated_subscription_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'active' );
}
/**
* Fire a gateway specific hook for when a subscription is activated.
*
* @since 1.0
*/
public static function trigger_gateway_reactivated_subscription_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'active' );
}
/**
* Fire a gateway specific hook for when a subscription is on-hold.
*
* @since 1.2
*/
public static function trigger_gateway_subscription_put_on_hold_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'on-hold' );
}
/**
* Fire a gateway specific when a subscription is cancelled.
*
* @since 1.0
*/
public static function trigger_gateway_cancelled_subscription_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'cancelled' );
}
/**
* Fire a gateway specific hook when a subscription expires.
*
* @since 1.0
*/
public static function trigger_gateway_subscription_expired_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '2.0', __CLASS__ . '::trigger_gateway_status_updated_hook()' );
self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'expired' );
}
/**
* Fired a gateway specific when a subscription was suspended. Suspended status was changed in 1.2 to match
* WooCommerce with the "on-hold" status.
*
* @deprecated 1.2
* @since 1.0
*/
public static function trigger_gateway_suspended_subscription_hook( $user_id, $subscription_key ) {
_deprecated_function( __METHOD__, '1.2', __CLASS__ . '::trigger_gateway_subscription_put_on_hold_hook( $subscription_key, $user_id )' );
self::trigger_gateway_subscription_put_on_hold_hook( $subscription_key, $user_id );
}
}
WC_Subscriptions_Payment_Gateways::init();

View File

@@ -0,0 +1,558 @@
<?php
/**
* PayPal Subscription Class.
*
* Filters necessary functions in the WC_Paypal class to allow for subscriptions, either via PayPal Standard (default)
* or PayPal Express Checkout using Reference Transactions (preferred)
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once( 'includes/wcs-paypal-functions.php' );
require_once( 'includes/class-wcs-paypal-supports.php' );
require_once( 'includes/class-wcs-paypal-status-manager.php' );
require_once( 'includes/class-wcs-paypal-standard-switcher.php' );
require_once( 'includes/class-wcs-paypal-standard-request.php' );
require_once( 'includes/class-wcs-paypal-standard-change-payment-method.php' );
require_once( 'includes/admin/class-wcs-paypal-admin.php' );
require_once( 'includes/admin/class-wcs-paypal-change-payment-method-admin.php' );
require_once( 'includes/deprecated/class-wc-paypal-standard-subscriptions.php' );
class WCS_PayPal {
/** @var WCS_PayPal_Express_API for communicating with PayPal */
protected static $api;
/** @var WCS_PayPal single instance of this class */
protected static $instance;
/** @var Array cache of PayPal IPN Handler */
protected static $ipn_handlers;
/** @var Array cache of PayPal Standard settings in WooCommerce */
protected static $paypal_settings;
/**
* Main PayPal Instance, ensures only one instance is/can be loaded
*
* @see wc_paypal_express()
* @return WC_PayPal_Express
* @since 2.0
*/
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
self::$paypal_settings = self::get_options();
// wc-api handler for express checkout transactions
if ( ! has_action( 'woocommerce_api_wcs_paypal' ) ) {
add_action( 'woocommerce_api_wcs_paypal', __CLASS__ . '::handle_wc_api' );
}
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_action( 'woocommerce_update_options_payment_gateways_paypal', __CLASS__ . '::reload_options', 100 );
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_action( 'woocommerce_update_options_payment_gateways_paypal', __CLASS__ . '::are_reference_transactions_enabled', 100 );
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_filter( 'woocommerce_paypal_args', __CLASS__ . '::get_paypal_args', 10, 2 );
// Check a valid PayPal IPN request to see if it's a subscription *before* WCS_Gateway_Paypal::successful_request()
add_action( 'valid-paypal-standard-ipn-request', __CLASS__ . '::process_ipn_request', 0 );
add_action( 'woocommerce_scheduled_subscription_payment_paypal', __CLASS__ . '::process_subscription_payment', 10, 2 );
// Don't copy over PayPal details to Resubscribe Orders
add_filter( 'wcs_resubscribe_order_created', __CLASS__ . '::remove_resubscribe_order_meta', 10, 2 );
// Triggered by WCS_SV_API_Base::broadcast_request() whenever an API request is made
add_action( 'wc_paypal_api_request_performed', __CLASS__ . '::log_api_requests', 10, 2 );
add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' );
WCS_PayPal_Supports::init();
WCS_PayPal_Status_Manager::init();
WCS_PayPal_Standard_Switcher::init();
if ( is_admin() ) {
WCS_PayPal_Admin::init();
WCS_PayPal_Change_Payment_Method_Admin::init();
}
}
/**
* Get a WooCommerce setting value for the PayPal Standard Gateway
*
* @since 2.0
*/
public static function get_option( $setting_key ) {
return ( isset( self::$paypal_settings[ $setting_key ] ) ) ? self::$paypal_settings[ $setting_key ] : '';
}
/**
* Checks if the PayPal API credentials are set.
*
* @since 2.0
*/
public static function are_credentials_set() {
$credentials_are_set = false;
if ( '' !== self::get_option( 'api_username' ) && '' !== self::get_option( 'api_password' ) && '' !== self::get_option( 'api_signature' ) ) {
$credentials_are_set = true;
}
return apply_filters( 'wooocommerce_paypal_credentials_are_set', $credentials_are_set );
}
/**
* Checks if the PayPal account has reference transactions setup
*
* Subscriptions keeps a record of all accounts where reference transactions were found to be enabled just in case the
* store manager switches to and from accounts. This record is stored as a JSON encoded array in the options table.
*
* @since 2.0
*/
public static function are_reference_transactions_enabled( $bypass_cache = '' ) {
$api_username = self::get_option( 'api_username' );
$transient_key = 'wcs_paypal_rt_enabled';
$reference_transactions_enabled = false;
if ( self::are_credentials_set() ) {
$accounts_with_reference_transactions_enabled = json_decode( get_option( 'wcs_paypal_rt_enabled_accounts' , wcs_json_encode( array() ) ) );
if ( in_array( $api_username, $accounts_with_reference_transactions_enabled ) ) {
$reference_transactions_enabled = true;
} elseif ( 'bypass_cache' === $bypass_cache || get_transient( $transient_key ) !== $api_username ) {
if ( self::get_api()->are_reference_transactions_enabled() ) {
$accounts_with_reference_transactions_enabled[] = $api_username;
update_option( 'wcs_paypal_rt_enabled_accounts', wcs_json_encode( $accounts_with_reference_transactions_enabled ) );
$reference_transactions_enabled = true;
} else {
set_transient( $transient_key, $api_username, DAY_IN_SECONDS );
}
}
}
return apply_filters( 'wooocommerce_subscriptions_paypal_reference_transactions_enabled', $reference_transactions_enabled );
}
/**
* Handle WC API requests where we need to run a reference transaction API operation
*
* @since 2.0
*/
public static function handle_wc_api() {
if ( ! isset( $_GET['action'] ) ) {
return;
}
switch ( $_GET['action'] ) {
// called when the customer is returned from PayPal after authorizing their payment, used for retrieving the customer's checkout details
case 'create_billing_agreement' :
// bail if no token
if ( ! isset( $_GET['token'] ) ) {
return;
}
// get token to retrieve checkout details with
$token = esc_attr( $_GET['token'] );
try {
$express_checkout_details_response = self::get_api()->get_express_checkout_details( $token );
// Make sure the billing agreement was accepted
if ( 1 == $express_checkout_details_response->get_billing_agreement_status() ) {
$order = $express_checkout_details_response->get_order();
if ( is_null( $order ) ) {
throw new Exception( __( 'Unable to find order for PayPal billing agreement.', 'woocommerce-subscriptions' ) );
}
// we need to process an initial payment
if ( $order->get_total() > 0 && ! wcs_is_subscription( $order ) ) {
$billing_agreement_response = self::get_api()->do_express_checkout( $token, $order, array(
'payment_action' => 'Sale',
'payer_id' => $express_checkout_details_response->get_payer_id(),
) );
} else {
$billing_agreement_response = self::get_api()->create_billing_agreement( $token );
}
if ( $billing_agreement_response->has_api_error() ) {
throw new Exception( $billing_agreement_response->get_api_error_message(), $billing_agreement_response->get_api_error_code() );
}
// We're changing the payment method for a subscription, make sure we update it before updating the billing agreement ID so that an old PayPal subscription can be cancelled if the existing payment method is also PayPal
if ( wcs_is_subscription( $order ) ) {
WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, 'paypal' );
$redirect_url = add_query_arg( 'utm_nooverride', '1', $order->get_view_order_url() );
}
// Make sure PayPal is set as the payment method on the order and subscription
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$payment_method = isset( $available_gateways[ self::instance()->get_id() ] ) ? $available_gateways[ self::instance()->get_id() ] : false;
$order->set_payment_method( $payment_method );
// Store the billing agreement ID on the order and subscriptions
wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() );
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) {
$subscription->set_payment_method( $payment_method );
wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() );
}
if ( ! wcs_is_subscription( $order ) ) {
if ( 0 == $order->get_total() ) {
$order->payment_complete();
} else {
self::process_subscription_payment_response( $order, $billing_agreement_response );
}
$redirect_url = add_query_arg( 'utm_nooverride', '1', $order->get_checkout_order_received_url() );
}
// redirect customer to order received page
wp_safe_redirect( esc_url_raw( $redirect_url ) );
} else {
wp_safe_redirect( WC()->cart->get_cart_url() );
}
} catch ( Exception $e ) {
wc_add_notice( __( 'An error occurred, please try again or try an alternate form of payment.', 'woocommerce-subscriptions' ), 'error' );
wp_redirect( WC()->cart->get_cart_url() );
}
exit;
case 'reference_transaction_account_check' :
exit;
}
}
/**
* Override the default PayPal standard args in WooCommerce for subscription purchases when
* automatic payments are enabled and when the recurring order totals is over $0.00 (because
* PayPal doesn't support subscriptions with a $0 recurring total, we need to circumvent it and
* manage it entirely ourselves.)
*
* @since 2.0
*/
public static function get_paypal_args( $paypal_args, $order ) {
if ( wcs_order_contains_subscription( $order, array( 'parent', 'renewal', 'resubscribe', 'switch' ) ) || wcs_is_subscription( $order ) ) {
if ( self::are_reference_transactions_enabled() ) {
$paypal_args = self::get_api()->get_paypal_args( $paypal_args, $order );
} else {
$paypal_args = WCS_PayPal_Standard_Request::get_paypal_args( $paypal_args, $order );
}
}
return $paypal_args;
}
/**
* When a PayPal IPN messaged is received for a subscription transaction,
* check the transaction details and
*
* @link https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*
* @since 2.0
*/
public static function process_ipn_request( $transaction_details ) {
require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' );
require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' );
if ( ! isset( $transaction_details['txn_type'] ) || ! in_array( $transaction_details['txn_type'], array_merge( self::get_ipn_handler( 'standard' )->get_transaction_types(), self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) ) {
return;
}
WC_Gateway_Paypal::log( 'Subscription Transaction Type: ' . $transaction_details['txn_type'] );
WC_Gateway_Paypal::log( 'Subscription Transaction Details: ' . print_r( $transaction_details, true ) );
if ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'standard' )->get_transaction_types() ) ) {
self::get_ipn_handler( 'standard' )->valid_response( $transaction_details );
} elseif ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) {
self::get_ipn_handler( 'reference' )->valid_response( $transaction_details );
}
}
/**
* Check whether a given subscription is using reference transactions and if so process the payment.
*
* @since 2.0
*/
public static function process_subscription_payment( $amount, $order ) {
// If the subscription is using reference transactions, we can process the payment ourselves
$paypal_profile_id = wcs_get_paypal_id( $order->id );
if ( wcs_is_paypal_profile_a( $paypal_profile_id, 'billing_agreement' ) ) {
if ( 0 == $amount ) {
$order->payment_complete();
return;
}
$response = self::get_api()->do_reference_transaction( $paypal_profile_id, $order, array(
'amount' => $amount,
'invoice_number' => self::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
) );
self::process_subscription_payment_response( $order, $response );
}
}
/**
* Process a payment based on a response
*
* @since 2.0.9
*/
public static function process_subscription_payment_response( $order, $response ) {
if ( $response->has_api_error() ) {
$error_message = $response->get_api_error_message();
// Some PayPal error messages end with a fullstop, others do not, we prefer our punctuation consistent, so add one if we don't already have one.
if ( '.' !== substr( $error_message, -1 ) ) {
$error_message .= '.';
}
// translators: placeholders are PayPal API error code and PayPal API error message
$order->update_status( 'failed', sprintf( __( 'PayPal API error: (%d) %s', 'woocommerce-subscriptions' ), $response->get_api_error_code(), $error_message ) );
} elseif ( $response->transaction_held() ) {
// translators: placeholder is PayPal transaction status message
$order_note = sprintf( __( 'PayPal Transaction Held: %s', 'woocommerce-subscriptions' ), $response->get_status_message() );
$order_status = apply_filters( 'wcs_paypal_held_payment_order_status', 'on-hold', $order, $response );
// mark order as held
if ( ! $order->has_status( $order_status ) ) {
$order->update_status( $order_status, $order_note );
} else {
$order->add_order_note( $order_note );
}
} elseif ( ! $response->transaction_approved() ) {
// translators: placeholder is PayPal transaction status message
$order->update_status( 'failed', sprintf( __( 'PayPal payment declined: %s', 'woocommerce-subscriptions' ), $response->get_status_message() ) );
} elseif ( $response->transaction_approved() ) {
$order->add_order_note( sprintf( __( 'PayPal payment approved (ID: %s)', 'woocommerce-subscriptions' ), $response->get_transaction_id() ) );
$order->payment_complete( $response->get_transaction_id() );
}
}
/**
* Don't transfer PayPal meta to resubscribe orders.
*
* @param object $resubscribe_order The order created for resubscribing the subscription
* @param object $subscription The subscription to which the resubscribe order relates
* @return object
* @since 2.0
*/
public static function remove_resubscribe_order_meta( $resubscribe_order, $subscription ) {
$post_meta_keys = array(
'Transaction ID',
'Payer first name',
'Payer last name',
'Payer PayPal address',
'Payer PayPal first name',
'Payer PayPal last name',
'PayPal Subscriber ID',
'Payment type',
);
foreach ( $post_meta_keys as $post_meta_key ) {
delete_post_meta( $resubscribe_order->id, $post_meta_key );
}
return $resubscribe_order;
}
/**
* Maybe adds a warning message to subscription script parameters which is used in a Javascript dialog if the
* payment method of the subscription is set to be changed. The warning message is only added if the subscriptions
* payment gateway is PayPal Standard.
*
* @param array $script_parameters The script parameters used in subscription meta boxes.
* @return array $script_parameters
* @since 2.0
*/
public static function maybe_add_change_payment_method_warning( $script_parameters ) {
global $post;
$subscription = wcs_get_subscription( $post );
if ( 'paypal' === $subscription->payment_method ) {
$paypal_profile_id = wcs_get_paypal_id( $subscription->id );
$is_paypal_standard = ! wcs_is_paypal_profile_a( $paypal_profile_id, 'billing_agreement' );
if ( $is_paypal_standard ) {
$script_parameters['change_payment_method_warning'] = __( "Are you sure you want to change the payment method from PayPal standard?\n\nThis will suspend the subscription at PayPal.", 'woocommerce-subscriptions' );
}
}
return $script_parameters;
}
/** Getters ******************************************************/
/**
* Get the API object
*
* @see SV_WC_Payment_Gateway::get_api()
* @return WC_PayPal_Express_API API instance
* @since 2.0
*/
protected static function get_ipn_handler( $ipn_type = 'standard' ) {
$use_sandbox = ( 'yes' === self::get_option( 'testmode' ) ) ? true : false;
if ( 'reference' === $ipn_type ) {
if ( ! isset( self::$ipn_handlers['reference'] ) ) {
require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' );
self::$ipn_handlers['reference'] = new WCS_Paypal_Reference_Transaction_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) );
}
$ipn_handler = self::$ipn_handlers['reference'];
} else {
if ( ! isset( self::$ipn_handlers['standard'] ) ) {
require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' );
self::$ipn_handlers['standard'] = new WCS_Paypal_Standard_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) );
}
$ipn_handler = self::$ipn_handlers['standard'];
}
return $ipn_handler;
}
/**
* Get the API object
*
* @return WCS_PayPal_Express_API API instance
* @since 2.0
*/
public static function get_api() {
if ( is_object( self::$api ) ) {
return self::$api;
}
if ( ! class_exists( 'WC_Gateway_Paypal_Response' ) ) {
require_once( WC()->plugin_path() . '/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php' );
}
$classes = array(
'api',
'api-request',
'api-response',
'api-response-checkout',
'api-response-billing-agreement',
'api-response-payment',
'api-response-recurring-payment',
);
foreach ( $classes as $class ) {
require_once( "includes/class-wcs-paypal-reference-transaction-{$class}.php" );
}
$environment = ( 'yes' === self::get_option( 'testmode' ) ) ? 'sandbox' : 'production';
return self::$api = new WCS_PayPal_Reference_Transaction_API( 'paypal', $environment, self::get_option( 'api_username' ), self::get_option( 'api_password' ), self::get_option( 'api_signature' ) );
}
/**
* Return the default WC PayPal gateway's settings.
*
* @since 2.0
*/
public static function reload_options() {
self::get_options();
}
/**
* Return the default WC PayPal gateway's settings.
*
* @since 2.0
*/
protected static function get_options() {
self::$paypal_settings = get_option( 'woocommerce_paypal_settings' );
return self::$paypal_settings;
}
/** Logging **/
/**
* Log API request/response data
*
* @since 2.0
*/
public static function log_api_requests( $request_data, $response_data ) {
WC_Gateway_Paypal::log( 'Subscription Request Parameters: ' . print_r( $request_data, true ) );
WC_Gateway_Paypal::log( 'Subscription Request Response: ' . print_r( $response_data, true ) );
}
/** Method required by WCS_SV_API_Base, which normally requires an instance of SV_WC_Plugin **/
public function get_plugin_name() {
return _x( 'WooCommerce Subscriptions PayPal', 'used in User Agent data sent to PayPal to help identify where a payment came from', 'woocommerce-subscriptions' );
}
public function get_version() {
return WC_Subscriptions::$version;
}
public function get_id() {
return 'paypal';
}
}

View File

@@ -0,0 +1,634 @@
<?php
/**
* WooCommerce Plugin Framework API Base Class
*
* This class provides a standardized framework for constructing an API wrapper
* to external services. It is designed to be extremely flexible.
*
* Namespaced copy of the SV_WC_API_Base class developed by the masterful SkyVerge team
*
* This source file is subject to the GNU General Public License v3.0
* that is bundled with this package in the file license.txt.
* It is also available through the world-wide-web at this URL:
* http://www.gnu.org/licenses/gpl-3.0.html
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@skyverge.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade the plugin to newer
* versions in the future. If you wish to customize the plugin for your
* needs please refer to http://www.skyverge.com
*
* @package SkyVerge/WooCommerce/API
* @author SkyVerge
* @copyright Copyright (c) 2013-2015, SkyVerge, Inc.
* @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
* @version 2.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* # WooCommerce Plugin Framework API Base Class
*
* This class provides a standardized framework for constructing an API wrapper
* to external services. It is designed to be extremely flexible.
*
* @version 2.2.0
*/
abstract class WCS_SV_API_Base {
/** @var string request method, defaults to POST */
protected $request_method = 'POST';
/** @var string URI used for the request */
protected $request_uri;
/** @var array request headers */
protected $request_headers = array();
/** @var string request user-agent */
protected $request_user_agent;
/** @var string request HTTP version, defaults to 1.0 */
protected $request_http_version = '1.0';
/** @var string request duration */
protected $request_duration;
/** @var object request */
protected $request;
/** @var string response code */
protected $response_code;
/** @var string response message */
protected $response_message;
/** @var array response headers */
protected $response_headers;
/** @var string raw response body */
protected $raw_response_body;
/** @var string response handler class name */
protected $response_handler;
/** @var object response */
protected $response;
/**
* Perform the request and return the parsed response
*
* @since 2.2.0
* @param object $request class instance which implements \SV_WC_API_Request
* @throws Exception
* @return object class instance which implements \SV_WC_API_Response
*/
protected function perform_request( $request ) {
// ensure API is in its default state
$this->reset_response();
// save the request object
$this->request = $request;
$start_time = microtime( true );
// perform the request
$response = $this->do_remote_request( $this->get_request_uri(), $this->get_request_args() );
// calculate request duration
$this->request_duration = round( microtime( true ) - $start_time, 5 );
try {
// parse & validate response
$response = $this->handle_response( $response );
} catch ( Exception $e ) {
// alert other actors that a request has been made
$this->broadcast_request();
throw $e;
}
return $response;
}
/**
* Simple wrapper for wp_remote_request() so child classes can override this
* and provide their own transport mechanism if needed, e.g. a custom
* cURL implementation
*
* @since 2.2.0
* @param string $request_uri
* @param string $request_args
* @return array|WP_Error
*/
protected function do_remote_request( $request_uri, $request_args ) {
return wp_safe_remote_request( $request_uri, $request_args );
}
/**
* Handle and parse the response
*
* @since 2.2.0
* @param array|WP_Error $response response data
* @throws Exception network issues, timeouts, API errors, etc
* @return object request class instance that implements SV_WC_API_Request
*/
protected function handle_response( $response ) {
// check for WP HTTP API specific errors (network timeout, etc)
if ( is_wp_error( $response ) ) {
throw new Exception( $response->get_error_message(), (int) $response->get_error_code() );
}
// set response data
$this->response_code = wp_remote_retrieve_response_code( $response );
$this->response_message = wp_remote_retrieve_response_message( $response );
$this->response_headers = wp_remote_retrieve_headers( $response );
$this->raw_response_body = wp_remote_retrieve_body( $response );
// allow child classes to validate response prior to parsing -- this is useful
// for checking HTTP status codes, etc.
$this->do_pre_parse_response_validation();
// parse the response body and tie it to the request
$this->response = $this->get_parsed_response( $this->raw_response_body );
// allow child classes to validate response after parsing -- this is useful
// for checking error codes/messages included in a parsed response
$this->do_post_parse_response_validation();
// fire do_action() so other actors can act on request/response data,
// primarily used for logging
$this->broadcast_request();
return $this->response;
}
/**
* Allow child classes to validate a response prior to instantiating the
* response object. Useful for checking response codes or messages, e.g.
* throw an exception if the response code is not 200.
*
* A child class implementing this method should simply return true if the response
* processing should continue, or throw a \SV_WC_API_Exception with a
* relevant error message & code to stop processing.
*
* Note: Child classes *must* sanitize the raw response body before throwing
* an exception, as it will be included in the broadcast_request() method
* which is typically used to log requests.
*
* @since 2.2.0
*/
protected function do_pre_parse_response_validation() {
// stub method
}
/**
* Allow child classes to validate a response after it has been parsed
* and instantiated. This is useful for check error codes or messages that
* exist in the parsed response.
*
* A child class implementing this method should simply return true if the response
* processing should continue, or throw an Exception with a
* relevant error message & code to stop processing.
*
* Note: Response body sanitization is handled automatically
*
* @since 2.2.0
*/
protected function do_post_parse_response_validation() {
// stub method
}
/**
* Return the parsed response object for the request
*
* @since 2.2.0
* @param string $raw_response_body
* @return object response class instance which implements SV_WC_API_Request
*/
protected function get_parsed_response( $raw_response_body ) {
$handler_class = $this->get_response_handler();
return new $handler_class( $raw_response_body );
}
/**
* Alert other actors that a request has been performed. This is primarily used
* for request logging.
*
* @since 2.2.0
*/
protected function broadcast_request() {
$request_data = array(
'method' => $this->get_request_method(),
'uri' => $this->get_request_uri(),
'user-agent' => $this->get_request_user_agent(),
'headers' => $this->get_sanitized_request_headers(),
'body' => $this->request->to_string_safe(),
'duration' => $this->get_request_duration() . 's', // seconds
);
$response_data = array(
'code' => $this->get_response_code(),
'message' => $this->get_response_message(),
'headers' => $this->get_response_headers(),
'body' => $this->get_sanitized_response_body() ? $this->get_sanitized_response_body() : $this->get_raw_response_body(),
);
do_action( 'wc_' . $this->get_api_id() . '_api_request_performed', $request_data, $response_data, $this );
}
/**
* Reset the API response members to their
*
* @since 1.0.0
*/
protected function reset_response() {
$this->response_code = null;
$this->response_message = null;
$this->response_headers = null;
$this->raw_response_body = null;
$this->response = null;
$this->request_duration = null;
}
/** Request Getters *******************************************************/
/**
* Get the request URI
*
* @since 2.2.0
* @return string
*/
protected function get_request_uri() {
// API base request URI + any request-specific path
$uri = $this->request_uri . ( $this->get_request() ? $this->get_request()->get_path() : '' );
/**
* Request URI Filter.
*
* Allow actors to filter the request URI. Note that child classes can override
* this method, which means this filter may be invoked prior to the overridden
* method.
*
* @since 4.1.0
* @param string $uri current request URI
* @param \WCS_SV_API_Base class instance
*/
return apply_filters( 'wc_' . $this->get_api_id() . '_api_request_uri', $uri, $this );
}
/**
* Get the request arguments in the format required by wp_remote_request()
*
* @since 2.2.0
* @return mixed|void
*/
protected function get_request_args() {
$args = array(
'method' => $this->get_request_method(),
'timeout' => MINUTE_IN_SECONDS,
'redirection' => 0,
'httpversion' => $this->get_request_http_version(),
'sslverify' => true,
'blocking' => true,
'user-agent' => $this->get_request_user_agent(),
'headers' => $this->get_request_headers(),
'body' => $this->get_request()->to_string(),
'cookies' => array(),
);
/**
* Request arguments.
*
* Allow other actors to filter the request arguments. Note that
* child classes can override this method, which means this filter may
* not be invoked, or may be invoked prior to the overridden method
*
* @since 2.2.0
* @param array $args request arguments
* @param \WCS_SV_API_Base class instance
*/
return apply_filters( 'wc_' . $this->get_api_id() . '_http_request_args', $args, $this );
}
/**
* Get the request method, POST by default
*
* @since 2.2.0
* @return string
*/
protected function get_request_method() {
// if the request object specifies the method to use, use that, otherwise use the API default
return $this->get_request() && $this->get_request()->get_method() ? $this->get_request()->get_method() : $this->request_method;
}
/**
* Get the request HTTP version, 1.1 by default
*
* @since 2.2.0
* @return string
*/
protected function get_request_http_version() {
return $this->request_http_version;
}
/**
* Get the request headers
*
* @since 2.2.0
* @return array
*/
protected function get_request_headers() {
return $this->request_headers;
}
/**
* Get sanitized request headers suitable for logging, stripped of any
* confidential information
*
* The `Authorization` header is sanitized automatically.
*
* Child classes that implement any custom authorization headers should
* override this method to perform sanitization.
*
* @since 2.2.0
* @return array
*/
protected function get_sanitized_request_headers() {
$headers = $this->get_request_headers();
if ( ! empty( $headers['Authorization'] ) ) {
$headers['Authorization'] = str_repeat( '*', strlen( $headers['Authorization'] ) );
}
return $headers;
}
/**
* Get the request user agent, defaults to:
*
* Dasherized-Plugin-Name/Plugin-Version (WooCommerce/WC-Version; WordPress/WP-Version)
*
* @since 2.2.0
* @return string
*/
protected function get_request_user_agent() {
return sprintf( '%s/%s (WooCommerce/%s; WordPress/%s)', str_replace( ' ', '-', $this->get_plugin()->get_plugin_name() ), $this->get_plugin()->get_version(), WC_VERSION, $GLOBALS['wp_version'] );
}
/**
* Get the request duration in seconds, rounded to the 5th decimal place
*
* @since 2.2.0
* @return string
*/
protected function get_request_duration() {
return $this->request_duration;
}
/** Response Getters ******************************************************/
/**
* Get the response handler class name
*
* @since 2.2.0
* @return string
*/
protected function get_response_handler() {
return $this->response_handler;
}
/**
* Get the response code
*
* @since 2.2.0
* @return string
*/
protected function get_response_code() {
return $this->response_code;
}
/**
* Get the response message
*
* @since 2.2.0
* @return string
*/
protected function get_response_message() {
return $this->response_message;
}
/**
* Get the response headers
*
* @since 2.2.0
* @return array
*/
protected function get_response_headers() {
return $this->response_headers;
}
/**
* Get the raw response body, prior to any parsing or sanitization
*
* @since 2.2.0
* @return string
*/
protected function get_raw_response_body() {
return $this->raw_response_body;
}
/**
* Get the sanitized response body, provided by the response class
* to_string_safe() method
*
* @since 2.2.0
* @return string|null
*/
protected function get_sanitized_response_body() {
return is_callable( array( $this->get_response(), 'to_string_safe' ) ) ? $this->get_response()->to_string_safe() : null;
}
/** Misc Getters ******************************************************/
/**
* Returns the most recent request object
*
* @since 2.2.0
* @see \SV_WC_API_Request
* @return object the most recent request object
*/
public function get_request() {
return $this->request;
}
/**
* Returns the most recent response object
*
* @since 2.2.0
* @see \SV_WC_API_Response
* @return object the most recent response object
*/
public function get_response() {
return $this->response;
}
/**
* Get the ID for the API, used primarily to namespace the action name
* for broadcasting requests
*
* @since 2.2.0
* @return string
*/
protected function get_api_id() {
return $this->get_plugin()->get_id();
}
/**
* Return a new request object
*
* Child classes must implement this to return an object that implements
* \SV_WC_API_Request which should be used in the child class API methods
* to build the request. The returned SV_WC_API_Request should be passed
* to self::perform_request() by your concrete API methods
*
* @since 2.2.0
* @param array $args optional request arguments
* @return SV_WC_API_Request
*/
abstract protected function get_new_request( $args = array() );
/**
* Return the plugin class instance associated with this API
*
* Child classes must implement this to return their plugin class instance
*
* This is used for defining the plugin ID used in filter names, as well
* as the plugin name used for the default user agent.
*
* @since 2.2.0
* @return SV_WC_Plugin
*/
abstract protected function get_plugin();
/** Setters ***************************************************************/
/**
* Set a header request
*
* @since 2.2.0
* @param string $name header name
* @param string $value header value
* @return string
*/
protected function set_request_header( $name, $value ) {
$this->request_headers[ $name ] = $value;
}
/**
* Set HTTP basic auth for the request
*
* Since 2.2.0
* @param string $username
* @param string $password
*/
protected function set_http_basic_auth( $username, $password ) {
$this->request_headers['Authorization'] = sprintf( 'Basic %s', base64_encode( "{$username}:{$password}" ) );
}
/**
* Set the Content-Type request header
*
* @since 2.2.0
* @param string $content_type
*/
protected function set_request_content_type_header( $content_type ) {
$this->request_headers['content-type'] = $content_type;
}
/**
* Set the Accept request header
*
* @since 2.2.0
* @param string $type the request accept type
*/
protected function set_request_accept_header( $type ) {
$this->request_headers['accept'] = $type;
}
/**
* Set the response handler class name. This class will be instantiated
* to parse the response for the request.
*
* Note the class should implement SV_WC_API
*
* @since 2.2.0
* @param string $handler handle class name
* @return array
*/
protected function set_response_handler( $handler ) {
$this->response_handler = $handler;
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* WooCommerce Subscriptions PayPal Administration Class.
*
* Hooks into WooCommerce's core PayPal class to display fields and notices relating to subscriptions.
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Admin {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// Add PayPal API fields to PayPal form fields as required
add_action( 'woocommerce_settings_start', __CLASS__ . '::add_form_fields', 100 );
add_action( 'woocommerce_api_wc_gateway_paypal', __CLASS__ . '::add_form_fields', 100 );
// Handle requests to check whether a PayPal account has Reference Transactions enabled
add_action( 'admin_init', __CLASS__ . '::maybe_check_account' );
// Maybe show notice to enter PayPal API credentials
add_action( 'admin_notices', __CLASS__ . '::maybe_show_admin_notices' );
}
/**
* Adds extra PayPal credential fields required to manage subscriptions.
*
* @since 2.0
*/
public static function add_form_fields() {
foreach ( WC()->payment_gateways->payment_gateways as $key => $gateway ) {
if ( WC()->payment_gateways->payment_gateways[ $key ]->id !== 'paypal' ) {
continue;
}
// Warn store managers not to change their PayPal Email address as it can break existing Subscriptions in WC2.0+
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['desc_tip'] = false;
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= ' </p><p class="description">' . __( 'It is <strong>strongly recommended you do not change the Receiver Email address</strong> if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' );
}
}
/**
* Handle requests to check whether a PayPal account has Reference Transactions enabled
*
* @since 2.0
*/
public static function maybe_check_account() {
if ( isset( $_GET['wcs_paypal'] ) && 'check_reference_transaction_support' === $_GET['wcs_paypal'] && wp_verify_nonce( $_GET['_wpnonce'], __CLASS__ ) ) {
$redirect_url = remove_query_arg( array( 'wcs_paypal', '_wpnonce' ) );
if ( WCS_PayPal::are_reference_transactions_enabled( 'bypass_cache' ) ) {
$redirect_url = add_query_arg( array( 'wcs_paypal' => 'rt_enabled' ), $redirect_url );
} else {
$redirect_url = add_query_arg( array( 'wcs_paypal' => 'rt_not_enabled' ), $redirect_url );
}
wp_safe_redirect( $redirect_url );
}
}
/**
* Display an assortment of notices to administrators to encourage them to get PayPal setup right.
*
* @since 2.0
*/
public static function maybe_show_admin_notices() {
self::maybe_disable_invalid_profile_notice();
self::maybe_update_credentials_error_flag();
if ( ! in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB' ) ) ) ) {
$valid_for_use = false;
} else {
$valid_for_use = true;
}
$payment_gateway_tab_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=wc_gateway_paypal' );
$notices = array();
if ( $valid_for_use && 'yes' == WCS_PayPal::get_option( 'enabled' ) && ! has_action( 'admin_notices', 'WC_Subscriptions_Admin::admin_installed_notice' ) && current_user_can( 'manage_options' ) ) {
if ( ! WCS_PayPal::are_credentials_set() ) {
$notices[] = array(
'type' => 'warning',
// translators: placeholders are opening and closing link tags. 1$-2$: to docs on woothemes, 3$-4$ to gateway settings on the site
'text' => sprintf( esc_html__( 'PayPal is inactive for subscription transactions. Please %1$sset up the PayPal IPN%2$s and %3$senter your API credentials%4$s to enable PayPal for Subscriptions.', 'woocommerce-subscriptions' ),
'<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'</a>',
'<a href="' . esc_url( $payment_gateway_tab_url ) . '">',
'</a>'
),
);
} elseif ( 'woocommerce_page_wc-settings' === get_current_screen()->base && isset( $_GET['tab'] ) && in_array( $_GET['tab'], array( 'subscriptions', 'checkout' ) ) && ! WCS_PayPal::are_reference_transactions_enabled() ) {
$notices[] = array(
'type' => 'warning',
// translators: placeholders are opening and closing strong and link tags. 1$-2$: strong tags, 3$-8$ link to docs on woothemes
'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are not enabled on your account%2$s, some subscription management features are not enabled. Please contact PayPal and request they %3$senable PayPal Reference Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %7$sLearn more %8$s', 'woocommerce-subscriptions' ),
'<strong>',
'</strong>',
'<a href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'</a>',
'</p><p><a class="button" href="' . esc_url( wp_nonce_url( add_query_arg( 'wcs_paypal', 'check_reference_transaction_support' ), __CLASS__ ) ) . '">',
'</a>',
'<a class="button button-primary" href="http://docs.woothemes.com/document/subscriptions/store-manager-guide/#section-4" target="_blank">',
'&raquo;</a>'
),
);
}
if ( isset( $_GET['wcs_paypal'] ) && 'rt_enabled' === $_GET['wcs_paypal'] ) {
$notices[] = array(
'type' => 'confirmation',
// translators: placeholders are opening and closing strong tags.
'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are enabled on your account%2$s. All subscription management features are now enabled. Happy selling!', 'woocommerce-subscriptions' ),
'<strong>',
'</strong>'
),
);
}
if ( false !== get_option( 'wcs_paypal_credentials_error' ) ) {
$notices[] = array(
'type' => 'error',
// translators: placeholders are link opening and closing tags. 1$-2$: to gateway settings, 3$-4$: support docs on woothemes.com
'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your API credentials may be incorrect. Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s.', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( $payment_gateway_tab_url ) . '">',
'</a>',
'<a href="https://support.woothemes.com/hc/en-us/articles/202882473#paypal-credentials" target="_blank">',
'</a>'
),
);
}
if ( 'yes' == get_option( 'wcs_paypal_invalid_profile_id' ) ) {
$notices[] = array(
'type' => 'error',
// translators: placeholders are opening and closing link tags. 1$-2$: docs on woothemes, 3$-4$: dismiss link
'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your PayPal account is issuing out-of-date subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s.', 'woocommerce-subscriptions' ),
'<a href="https://support.woothemes.com/hc/en-us/articles/202882473#old-paypal-account" target="_blank">',
'</a>',
'<a href="' . esc_url( add_query_arg( 'wcs_disable_paypal_invalid_profile_id_notice', 'true' ) ) . '">',
'</a>'
),
);
}
}
if ( ! empty( $notices ) ) {
include_once( dirname( __FILE__ ) . '/../templates/admin-notices.php' );
}
}
/**
* Disable the invalid profile notice when requested.
*
* @since 2.0
*/
protected static function maybe_disable_invalid_profile_notice() {
if ( isset( $_GET['wcs_disable_paypal_invalid_profile_id_notice'] ) ) {
update_option( 'wcs_paypal_invalid_profile_id', 'disabled' );
}
}
/**
* Remove the invalid credentials error flag whenever a new set of API credentials are saved.
*
* @since 2.0
*/
protected static function maybe_update_credentials_error_flag() {
// Check if the API credentials are being saved - we can't do this on the 'woocommerce_update_options_payment_gateways_paypal' hook because it is triggered after 'admin_notices'
if ( ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-settings' ) && isset( $_POST['woocommerce_paypal_api_username'] ) || isset( $_POST['woocommerce_paypal_api_password'] ) || isset( $_POST['woocommerce_paypal_api_signature'] ) ) {
$credentials_updated = false;
if ( isset( $_POST['woocommerce_paypal_api_username'] ) && WCS_PayPal::get_option( 'api_username' ) != $_POST['woocommerce_paypal_api_username'] ) {
$credentials_updated = true;
} elseif ( isset( $_POST['woocommerce_paypal_api_password'] ) && WCS_PayPal::get_option( 'api_password' ) != $_POST['woocommerce_paypal_api_password'] ) {
$credentials_updated = true;
} elseif ( isset( $_POST['woocommerce_paypal_api_signature'] ) && WCS_PayPal::get_option( 'api_signature' ) != $_POST['woocommerce_paypal_api_signature'] ) {
$credentials_updated = true;
}
if ( $credentials_updated ) {
delete_option( 'wcs_paypal_credentials_error' );
}
}
do_action( 'wcs_paypal_admin_update_credentials' );
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* PayPal Subscription Change Payment Method Admin Class
*
* Allow store managers to manually set PayPal as the payment method on a subscription if reference transactions are enabled
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Change_Payment_Method_Admin {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// Include the PayPal billing agreement ID meta key in the required meta data for setting PayPal as the payment method
add_filter( 'woocommerce_subscription_payment_meta', __CLASS__ . '::add_payment_meta_details', 10, 2 );
// Validate the PayPal billing agreement ID meta value when attempting to set PayPal as the payment method
add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 );
}
/**
* Include the PayPal payment meta data required to process automatic recurring payments so that store managers can
* manually set up automatic recurring payments for a customer via the Edit Subscription screen.
*
* @param array $payment_meta associative array of meta data required for automatic payments
* @param WC_Subscription $subscription An instance of a subscription object
* @return array
* @since 2.0
*/
public static function add_payment_meta_details( $payment_meta, $subscription ) {
$subscription_id = get_post_meta( $subscription->id, '_paypal_subscription_id', true );
if ( wcs_is_paypal_profile_a( $subscription_id, 'billing_agreement' ) || empty( $subscription_id ) ) {
$label = 'PayPal Billing Agreement ID';
$disabled = false;
} else {
$label = 'PayPal Standard Subscription ID';
$disabled = true;
}
$payment_meta['paypal'] = array(
'post_meta' => array(
'_paypal_subscription_id' => array(
'value' => $subscription_id,
'label' => $label,
'disabled' => $disabled,
),
),
);
return $payment_meta;
}
/**
* Validate the payment meta data required to process automatic recurring payments so that store managers can
* manually set up automatic recurring payments for a customer via the Edit Subscription screen.
*
* @param string $payment_method_id The ID of the payment method to validate
* @param array $payment_meta associative array of meta data required for automatic payments
* @return array
* @since 2.0
*/
public static function validate_payment_meta( $payment_meta, $subscription ) {
if ( empty( $payment_meta['post_meta']['_paypal_subscription_id']['value'] ) ) {
throw new Exception( 'A valid PayPal Billing Agreement ID value is required.' );
} elseif ( $subscription->paypal_subscription_id !== $payment_meta['post_meta']['_paypal_subscription_id']['value'] && 0 !== strpos( $payment_meta['post_meta']['_paypal_subscription_id']['value'], 'B-' ) ) {
throw new Exception( 'Invalid Billing Agreemend ID. A valid PayPal Billing Agreement ID must begin with "B-".' );
}
}
}

View File

@@ -0,0 +1,659 @@
<?php
/**
* PayPal Reference Transaction API Request Class
*
* Generates request data to send to the PayPal Express Checkout API for Reference Transaction related API calls
*
* Heavily inspired by the WC_Paypal_Express_API_Request class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Request {
/** auth/capture transaction type */
const AUTH_CAPTURE = 'Sale';
/** @var array the request parameters */
private $parameters = array();
/**
* Construct an PayPal Express request object
*
* @param string $api_username the API username
* @param string $api_password the API password
* @param string $api_signature the API signature
* @param string $api_version the API version
* @since 2.0
*/
public function __construct( $api_username, $api_password, $api_signature, $api_version ) {
$this->add_parameters( array(
'USER' => $api_username,
'PWD' => $api_password,
'SIGNATURE' => $api_signature,
'VERSION' => $api_version,
) );
}
/**
* Sets up the express checkout transaction
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECGettingStarted/#id084RN060BPF
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/
*
* @param array $args {
* @type string 'currency' (Optional) A 3-character currency code (default is store's currency).
* @type string 'billing_type' (Optional) Type of billing agreement for reference transactions. You must have permission from PayPal to use this field. This field must be set to one of the following values: MerchantInitiatedBilling - PayPal creates a billing agreement for each transaction associated with buyer. You must specify version 54.0 or higher to use this option; MerchantInitiatedBillingSingleAgreement - PayPal creates a single billing agreement for all transactions associated with buyer. Use this value unless you need per-transaction billing agreements. You must specify version 58.0 or higher to use this option.
* @type string 'billing_description' (Optional) Description of goods or services associated with the billing agreement. This field is required for each recurring payment billing agreement if using MerchantInitiatedBilling as the billing type, that means you can use a different agreement for each subscription/order. PayPal recommends that the description contain a brief summary of the billing agreement terms and conditions (but this only makes sense when the billing type is MerchantInitiatedBilling, otherwise the terms will be incorrectly displayed for all agreements). For example, buyer is billed at "9.99 per month for 2 years".
* @type string 'maximum_amount' (Optional) The expected maximum total amount of the complete order and future payments, including shipping cost and tax charges. If you pass the expected average transaction amount (default 25.00). PayPal uses this value to validate the buyer's funding source.
* @type string 'no_shipping' (Optional) Determines where or not PayPal displays shipping address fields on the PayPal pages. For digital goods, this field is required, and you must set it to 1. It is one of the following values: 0 PayPal displays the shipping address on the PayPal pages; 1 PayPal does not display shipping address fields whatsoever (default); 2 If you do not pass the shipping address, PayPal obtains it from the buyer's account profile.
* @type string 'page_style' (Optional) Name of the Custom Payment Page Style for payment pages associated with this button or link. It corresponds to the HTML variable page_style for customizing payment pages. It is the same name as the Page Style Name you chose to add or edit the page style in your PayPal Account profile.
* @type string 'brand_name' (Optional) A label that overrides the business name in the PayPal account on the PayPal hosted checkout pages. Default: store name.
* @type string 'landing_page' (Optional) Type of PayPal page to display. It is one of the following values: 'login' PayPal account login (default); 'Billing' Non-PayPal account.
* @type string 'payment_action' (Optional) How you want to obtain payment. If the transaction does not include a one-time purchase, this field is ignored. Default 'Sale' This is a final sale for which you are requesting payment (default). Alternative: 'Authorization' This payment is a basic authorization subject to settlement with PayPal Authorization and Capture. You cannot set this field to Sale in SetExpressCheckout request and then change the value to Authorization or Order in the DoExpressCheckoutPayment request. If you set the field to Authorization or Order in SetExpressCheckout, you may set the field to Sale.
* @type string 'return_url' (Required) URL to which the buyer's browser is returned after choosing to pay with PayPal.
* @type string 'cancel_url' (Required) URL to which the buyer is returned if the buyer does not approve the use of PayPal to pay you.
* @type string 'custom' (Optional) A free-form field for up to 256 single-byte alphanumeric characters
* }
* @since 2.0
*/
public function set_express_checkout( $args ) {
// translators: placeholder is blogname
$default_description = sprintf( _x( 'Orders with %s', 'data sent to paypal', 'woocommerce-subscriptions' ), get_bloginfo( 'name' ) );
$defaults = array(
'currency' => get_woocommerce_currency(),
'billing_type' => apply_filters( 'woocommerce_subscriptions_paypal_billing_agreement_type', 'MerchantInitiatedBillingSingleAgreement', $args ),
// translators: placeholder is for blog name
'billing_description' => html_entity_decode( apply_filters( 'woocommerce_subscriptions_paypal_billing_agreement_description', $default_description, $args ), ENT_NOQUOTES, 'UTF-8' ),
'maximum_amount' => null,
'no_shipping' => 1,
'page_style' => null,
'brand_name' => html_entity_decode( get_bloginfo( 'name' ), ENT_NOQUOTES, 'UTF-8' ),
'landing_page' => 'login',
'payment_action' => 'Sale',
'custom' => '',
);
$args = wp_parse_args( $args, $defaults );
$this->set_method( 'SetExpressCheckout' );
$this->add_parameters( array(
'L_BILLINGTYPE0' => $args['billing_type'],
'L_BILLINGAGREEMENTDESCRIPTION0' => wcs_get_paypal_item_name( $args['billing_description'] ),
'L_BILLINGAGREEMENTCUSTOM0' => $args['custom'],
'RETURNURL' => $args['return_url'],
'CANCELURL' => $args['cancel_url'],
'PAGESTYLE' => $args['page_style'],
'BRANDNAME' => $args['brand_name'],
'LANDINGPAGE' => ( 'login' == $args['landing_page'] ) ? 'Login' : 'Billing',
'NOSHIPPING' => $args['no_shipping'],
'MAXAMT' => $args['maximum_amount'],
) );
// if we have an order, the request is to create a subscription/process a payment (not just check if the PayPal account supports Reference Transactions)
if ( isset( $args['order'] ) ) {
if ( 0 == $args['order']->get_total() ) {
$this->add_parameters( array(
'PAYMENTREQUEST_0_AMT' => 0, // a zero amount is use so that no DoExpressCheckout action is required and instead CreateBillingAgreement is used to first create a billing agreement not attached to any order and then DoReferenceTransaction is used to charge both the initial order and renewal order amounts
'PAYMENTREQUEST_0_ITEMAMT' => 0,
'PAYMENTREQUEST_0_SHIPPINGAMT' => 0,
'PAYMENTREQUEST_0_TAXAMT' => 0,
'PAYMENTREQUEST_0_CURRENCYCODE' => $args['currency'],
'PAYMENTREQUEST_0_CUSTOM' => $args['custom'],
'PAYMENTREQUEST_0_PAYMENTACTION' => $args['payment_action'],
) );
} else {
$this->add_payment_details_parameters( $args['order'], $args['payment_action'] );
}
}
}
/**
* Set up the DoExpressCheckoutPayment request
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECGettingStarted/#id084RN060BPF
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/
*
* @since 2.0.9
* @param string $token PayPal Express Checkout token returned by SetExpressCheckout operation
* @param WC_Order $order order object
* @param string $type
*/
public function do_express_checkout( $token, WC_Order $order, $args ) {
$this->set_method( 'DoExpressCheckoutPayment' );
// set base params
$this->add_parameters( array(
'TOKEN' => $token,
'PAYERID' => $args['payer_id'],
'BUTTONSOURCE' => 'WooThemes_Cart',
'RETURNFMFDETAILS' => 1,
) );
$this->add_payment_details_parameters( $order, $args['payment_action'] );
}
/**
* Get info about the buyer & transaction from PayPal
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECGettingStarted/#id084RN060BPF
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/GetExpressCheckoutDetails_API_Operation_NVP/
*
* @param string $token token from SetExpressCheckout response
* @since 2.0
*/
public function get_express_checkout_details( $token ) {
$this->set_method( 'GetExpressCheckoutDetails' );
$this->add_parameter( 'TOKEN', $token );
}
/**
* Create a billing agreement, required when a subscription sign-up has no initial payment
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECReferenceTxns/#id094TB0Y0J5Z__id094TB4003HS
* @link https://developer.paypal.com/docs/classic/api/merchant/CreateBillingAgreement_API_Operation_NVP/
*
* @param string $token token from SetExpressCheckout response
* @since 2.0
*/
public function create_billing_agreement( $token ) {
$this->set_method( 'CreateBillingAgreement' );
$this->add_parameter( 'TOKEN', $token );
}
/**
* Charge a payment against a reference token
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECReferenceTxns/#id094UM0DA0HS
* @link https://developer.paypal.com/docs/classic/api/merchant/DoReferenceTransaction_API_Operation_NVP/
*
* @param string $reference_id the ID of a refrence object, e.g. billing agreement ID.
* @param WC_Order $order order object
* @param array $args {
* @type string 'payment_type' (Optional) Specifies type of PayPal payment you require for the billing agreement. It is one of the following values. 'Any' or 'InstantOnly'. Echeck is not supported for DoReferenceTransaction requests.
* @type string 'payment_action' How you want to obtain payment. It is one of the following values: 'Authorization' - this payment is a basic authorization subject to settlement with PayPal Authorization and Capture; or 'Sale' - This is a final sale for which you are requesting payment.
* @type string 'return_fraud_filters' (Optional) Flag to indicate whether you want the results returned by Fraud Management Filters. By default, you do not receive this information.
* }
* @since 2.0
*/
public function do_reference_transaction( $reference_id, $order, $args = array() ) {
$defaults = array(
'amount' => $order->get_total(),
'payment_type' => 'Any',
'payment_action' => 'Sale',
'return_fraud_filters' => 1,
'notify_url' => WC()->api_request_url( 'WC_Gateway_Paypal' ),
'invoice_number' => WCS_PayPal::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
'custom' => wcs_json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key ) ),
);
$args = wp_parse_args( $args, $defaults );
$this->set_method( 'DoReferenceTransaction' );
// set base params
$this->add_parameters( array(
'REFERENCEID' => $reference_id,
'BUTTONSOURCE' => 'WooThemes_Cart',
'RETURNFMFDETAILS' => $args['return_fraud_filters'],
'NOTIFYURL' => $args['notify_url'],
) );
$this->add_payment_details_parameters( $order, $args['payment_action'], true );
}
/**
* Set up the payment details for a DoExpressCheckoutPayment or DoReferenceTransaction request
*
* @since 2.0.9
* @param WC_Order $order order object
* @param string $type the type of transaction for the payment
* @param bool $use_deprecated_params whether to use deprecated PayPal NVP parameters (required for DoReferenceTransaction API calls)
*/
protected function add_payment_details_parameters( WC_Order $order, $type, $use_deprecated_params = false ) {
$calculated_total = 0;
$order_subtotal = 0;
$item_count = 0;
$order_items = array();
// add line items
foreach ( $order->get_items() as $item ) {
$product = new WC_Product( $item['product_id'] );
$order_items[] = array(
'NAME' => wcs_get_paypal_item_name( $product->get_title() ),
'DESC' => $this->get_item_description( $item, $product ),
'AMT' => $this->round( $order->get_item_subtotal( $item ) ),
'QTY' => ( ! empty( $item['qty'] ) ) ? absint( $item['qty'] ) : 1,
'ITEMURL' => $product->get_permalink(),
);
$order_subtotal += $item['line_total'];
}
// add fees
foreach ( $order->get_fees() as $fee ) {
$order_items[] = array(
'NAME' => wcs_get_paypal_item_name( $fee['name'] ),
'AMT' => $this->round( $fee['line_total'] ),
'QTY' => 1,
);
$order_subtotal += $fee['line_total'];
}
// add discounts
if ( $order->get_total_discount() > 0 ) {
$order_items[] = array(
'NAME' => __( 'Total Discount', 'woocommerce-subscriptions' ),
'QTY' => 1,
'AMT' => - $this->round( $order->get_total_discount() ),
);
}
if ( $this->skip_line_items( $order ) ) {
$total_amount = $this->round( $order->get_total() );
// calculate the total as PayPal would
$calculated_total += $this->round( $order_subtotal + $order->get_cart_tax() ) + $this->round( $order->get_total_shipping() + $order->get_shipping_tax() );
// offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the order subtotal
if ( $total_amount !== $calculated_total ) {
$order_subtotal = $order_subtotal - ( $calculated_total - $total_amount );
}
$item_names = array();
foreach ( $order_items as $item ) {
$item_names[] = sprintf( '%1$s x %2$s', $item['NAME'], $item['QTY'] );
}
// add a single item for the entire order
$this->add_line_item_parameters( array(
// translators: placeholder is blogname
'NAME' => sprintf( __( '%s - Order', 'woocommerce-subscriptions' ), get_option( 'blogname' ) ),
'DESC' => wcs_get_paypal_item_name( implode( ', ', $item_names ) ),
'AMT' => $this->round( $order_subtotal + $order->get_cart_tax() ),
'QTY' => 1,
), 0, $use_deprecated_params );
// add order-level parameters
// - Do not sent the TAXAMT due to rounding errors
if ( $use_deprecated_params ) {
$this->add_parameters( array(
'AMT' => $total_amount,
'CURRENCYCODE' => $order->get_order_currency(),
'ITEMAMT' => $this->round( $order_subtotal + $order->get_cart_tax() ),
'SHIPPINGAMT' => $this->round( $order->get_total_shipping() + $order->get_shipping_tax() ),
'INVNUM' => WCS_PayPal::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
'PAYMENTACTION' => $type,
'PAYMENTREQUESTID' => $order->id,
'CUSTOM' => json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key ) ),
) );
} else {
$this->add_payment_parameters( array(
'AMT' => $total_amount,
'CURRENCYCODE' => $order->get_order_currency(),
'ITEMAMT' => $this->round( $order_subtotal + $order->get_cart_tax() ),
'SHIPPINGAMT' => $this->round( $order->get_total_shipping() + $order->get_shipping_tax() ),
'INVNUM' => WCS_PayPal::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
'PAYMENTACTION' => $type,
'PAYMENTREQUESTID' => $order->id,
'CUSTOM' => json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key ) ),
) );
}
} else {
// add individual order items
foreach ( $order_items as $item ) {
$this->add_line_item_parameters( $item, $item_count++, $use_deprecated_params );
$calculated_total += $this->round( $item['AMT'] * $item['QTY'] );
}
// add shipping and tax to calculated total
$calculated_total += $this->round( $order->get_total_shipping() ) + $this->round( $order->get_total_tax() );
$total_amount = $this->round( $order->get_total() );
// add order-level parameters
if ( $use_deprecated_params ) {
$this->add_parameters( array(
'AMT' => $total_amount,
'CURRENCYCODE' => $order->get_order_currency(),
'ITEMAMT' => $this->round( $order_subtotal ),
'SHIPPINGAMT' => $this->round( $order->get_total_shipping() ),
'TAXAMT' => $this->round( $order->get_total_tax() ),
'INVNUM' => WCS_PayPal::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
'PAYMENTACTION' => $type,
'PAYMENTREQUESTID' => $order->id,
'CUSTOM' => json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key ) ),
) );
} else {
$this->add_payment_parameters( array(
'AMT' => $total_amount,
'CURRENCYCODE' => $order->get_order_currency(),
'ITEMAMT' => $this->round( $order_subtotal ),
'SHIPPINGAMT' => $this->round( $order->get_total_shipping() ),
'TAXAMT' => $this->round( $order->get_total_tax() ),
'INVNUM' => WCS_PayPal::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
'PAYMENTACTION' => $type,
'PAYMENTREQUESTID' => $order->id,
'CUSTOM' => json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key ) ),
) );
}
// offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the cost of the first item
if ( $total_amount !== $calculated_total ) {
$this->parameters['L_PAYMENTREQUEST_0_AMT0'] = $this->parameters['L_PAYMENTREQUEST_0_AMT0'] - ( $calculated_total - $total_amount );
}
}
}
/**
* Performs an Express Checkout NVP API operation as passed in $api_method.
*
* Although the PayPal Standard API provides no facility for cancelling a subscription, the PayPal
* Express Checkout NVP API can be used.
*
* @since 2.0
*/
public function manage_recurring_payments_profile_status( $profile_id, $new_status, $order = null ) {
$this->set_method( 'ManageRecurringPaymentsProfileStatus' );
// We need to get merge the existing params to ensure the method and API credentials are passed to the filter for backward compatibility
$this->add_parameters( apply_filters( 'woocommerce_subscriptions_paypal_change_status_data', array_merge( $this->get_parameters(), array(
'PROFILEID' => $profile_id,
'ACTION' => $new_status,
// translators: 1$: new status (e.g. "Cancel"), 2$: blog name
'NOTE' => html_entity_decode( sprintf( _x( '%1$s subscription event triggered at %2$s', 'data sent to paypal', 'woocommerce-subscriptions' ), $new_status, get_bloginfo( 'name' ) ), ENT_NOQUOTES, 'UTF-8' ),
) ), $new_status, $order, $profile_id ) );
}
/** Helper Methods ******************************************************/
/**
* Add a parameter
*
* @param string $key
* @param string|int $value
* @since 2.0
*/
private function add_parameter( $key, $value ) {
$this->parameters[ $key ] = $value;
}
/**
* Add multiple parameters
*
* @param array $params
* @since 2.0
*/
private function add_parameters( array $params ) {
foreach ( $params as $key => $value ) {
$this->add_parameter( $key, $value );
}
}
/**
* Set the method for the request, currently using:
*
* + `SetExpressCheckout` - setup transaction
* + `GetExpressCheckout` - gets buyers info from PayPal
* + `DoExpressCheckoutPayment` - completes the transaction
* + `DoCapture` - captures a previously authorized transaction
*
* @param string $method
* @since 2.0
*/
private function set_method( $method ) {
$this->add_parameter( 'METHOD', $method );
}
/**
* Add payment parameters, auto-prefixes the parameter key with `PAYMENTREQUEST_0_`
* for convenience and readability
*
* @param array $params
* @since 2.0
*/
private function add_payment_parameters( array $params ) {
foreach ( $params as $key => $value ) {
$this->add_parameter( "PAYMENTREQUEST_0_{$key}", $value );
}
}
/**
* Adds a line item parameters to the request, auto-prefixes the parameter key
* with `L_PAYMENTREQUEST_0_` for convenience and readability
*
* @param array $params
* @param int $item_count current item count
* @since 2.0
*/
private function add_line_item_parameters( array $params, $item_count, $use_deprecated_params = false ) {
foreach ( $params as $key => $value ) {
if ( $use_deprecated_params ) {
$this->add_parameter( "L_{$key}{$item_count}", $value );
} else {
$this->add_parameter( "L_PAYMENTREQUEST_0_{$key}{$item_count}", $value );
}
}
}
/**
* Helper method to return the item description, which is composed of item
* meta flattened into a comma-separated string, if available. Otherwise the
* product SKU is included.
*
* The description is automatically truncated to the 127 char limit.
*
* @param array $item cart or order item
* @param \WC_Product $product product data
* @return string
* @since 2.0
*/
private function get_item_description( $item, $product ) {
if ( empty( $item['item_meta'] ) ) {
// cart item
$item_desc = WC()->cart->get_item_data( $item, true );
$item_desc = str_replace( "\n", ', ', rtrim( $item_desc ) );
} else {
// order item
$item_meta = new WC_Order_Item_Meta( $item );
$item_meta = $item_meta->get_formatted();
if ( ! empty( $item_meta ) ) {
$item_desc = array();
foreach ( $item_meta as $meta ) {
$item_desc[] = sprintf( '%s: %s', $meta['label'], $meta['value'] );
}
$item_desc = implode( ', ', $item_desc );
} else {
$item_desc = is_callable( array( $product, 'get_sku' ) ) && $product->get_sku() ? sprintf( __( 'SKU: %s', 'woocommerce-subscriptions' ), $product->get_sku() ) : null;
}
}
return wcs_get_paypal_item_name( $item_desc );
}
/**
* Returns the string representation of this request
*
* @see SV_WC_Payment_Gateway_API_Request::to_string()
* @return string the request query string
* @since 2.0
*/
public function to_string() {
return http_build_query( $this->get_parameters() );
}
/**
* Returns the string representation of this request with any and all
* sensitive elements masked or removed
*
* @see SV_WC_Payment_Gateway_API_Request::to_string_safe()
* @return string the pretty-printed request array string representation, safe for logging
* @since 2.0
*/
public function to_string_safe() {
$request = $this->get_parameters();
$sensitive_fields = array( 'USER', 'PWD', 'SIGNATURE' );
foreach ( $sensitive_fields as $field ) {
if ( isset( $request[ $field ] ) ) {
$request[ $field ] = str_repeat( '*', strlen( $request[ $field ] ) );
}
}
return print_r( $request, true );
}
/**
* Returns the request parameters after validation & filtering
*
* @throws \SV_WC_Payment_Gateway_Exception invalid amount
* @return array request parameters
* @since 2.0
*/
public function get_parameters() {
/**
* Filter PPE request parameters.
*
* Use this to modify the PayPal request parameters prior to validation
*
* @param array $parameters
* @param \WC_PayPal_Express_API_Request $this instance
*/
$this->parameters = apply_filters( 'wcs_paypal_request_params', $this->parameters, $this );
// validate parameters
foreach ( $this->parameters as $key => $value ) {
// remove unused params
if ( '' === $value || is_null( $value ) ) {
unset( $this->parameters[ $key ] );
}
// format and check amounts
if ( false !== strpos( $key, 'AMT' ) ) {
// amounts must be 10,000.00 or less for USD
if ( isset( $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] ) && 'USD' == $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] && $value > 10000 ) {
throw new SV_WC_Payment_Gateway_Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, $value ) );
}
// PayPal requires locale-specific number formats (e.g. USD is 123.45)
// PayPal requires the decimal separator to be a period (.)
$this->parameters[ $key ] = number_format( $value, 2, '.', '' );
}
}
return $this->parameters;
}
/**
* Returns the method for this request. PPE uses the API default request
* method (POST)
*
* @return null
* @since 2.0
*/
public function get_method() { }
/**
* Returns the request path for this request. PPE request paths do not
* vary per request
*
* @return string
* @since 2.0
*/
public function get_path() {
return '';
}
/**
* PayPal cannot properly calculate order totals when prices include tax (due
* to rounding issues), so line items are skipped and the order is sent as
* a single item
*
* @since 2.0.9
* @param WC_Order $order Optional. The WC_Order object. Default null.
* @return bool true if line items should be skipped, false otherwise
*/
private function skip_line_items( $order = null ) {
if ( isset( $order->prices_include_tax ) ) {
$skip_line_items = $order->prices_include_tax;
} else {
$skip_line_items = wc_prices_include_tax();
}
/**
* Filter whether line items should be skipped or not
*
* @since 3.3.0
* @param bool $skip_line_items True if line items should be skipped, false otherwise
* @param WC_Order/null $order The WC_Order object or null.
*/
return apply_filters( 'wcs_paypal_reference_transaction_skip_line_items', $skip_line_items, $order );
}
/**
* Round a float
*
* @since 2.0.9
* @param float $number
* @param int $precision Optional. The number of decimal digits to round to.
*/
private function round( $number, $precision = 2 ) {
return round( (float) $number, $precision );
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* PayPal Reference Transaction API Response Class for Express Checkout API calls to create a billing agreement
*
* @link https://developer.paypal.com/docs/classic/api/merchant/CreateBillingAgreement_API_Operation_NVP/
*
* Heavily inspired by the WC_Paypal_Express_API_Checkout_Response class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Response_Billing_Agreement extends WCS_PayPal_Reference_Transaction_API_Response {
/**
* Get the billing agreement ID which is returned after a successful CreateBillingAgreement API call
*
* @return string|null
* @since 2.0.0
*/
public function get_billing_agreement_id() {
return $this->get_parameter( 'BILLINGAGREEMENTID' );
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* PayPal Reference Transaction API Response Class for Express Checkout API calls
*
* Parses response string received from PayPal Express Checkout API, which is simply a URL-encoded string of parameters
*
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/
* @link https://developer.paypal.com/docs/classic/api/merchant/ManageRecurringPaymentsProfileStatus_API_Operation_NVP/
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/GetExpressCheckoutDetails_API_Operation_NVP/
*
* Heavily inspired by the WC_Paypal_Express_API_Checkout_Response class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Response_Checkout extends WCS_PayPal_Reference_Transaction_API_Response {
/**
* Get the token which is returned after a successful SetExpressCheckout
* API call
*
* @return string|null
* @since 2.0
*/
public function get_token() {
return $this->get_parameter( 'TOKEN' );
}
/**
* Get the billing agreement status for a successful SetExpressCheckout
*
* @since 3.0.0
* @return string|null
* @since 2.0
*/
public function get_billing_agreement_status() {
return $this->get_parameter( 'BILLINGAGREEMENTACCEPTEDSTATUS' );
}
/**
* Get the shipping details from GetExpressCheckoutDetails response mapped to the WC shipping address format
*
* @since 3.0.0
* @return array
* @since 2.0
*/
public function get_shipping_details() {
$details = array();
if ( $this->has_parameter( 'FIRSTNAME' ) ) {
$details = array(
'first_name' => $this->get_parameter( 'FIRSTNAME' ),
'last_name' => $this->get_parameter( 'LASTNAME' ),
'company' => $this->get_parameter( 'BUSINESS' ),
'email' => $this->get_parameter( 'EMAIL' ),
'phone' => $this->get_parameter( 'PHONENUM' ),
'address_1' => $this->get_parameter( 'SHIPTOSTREET' ),
'address_2' => $this->get_parameter( 'SHIPTOSTREET2' ),
'city' => $this->get_parameter( 'SHIPTOCITY' ),
'postcode' => $this->get_parameter( 'SHIPTOZIP' ),
'country' => $this->get_parameter( 'SHIPTOCOUNTRYCODE' ),
'state' => $this->get_state_code( $this->get_parameter( 'SHIPTOCOUNTRYCODE' ), $this->get_parameter( 'SHIPTOSTATE' ) ),
);
}
return $details;
}
/**
* Get the note text from checkout details
*
* @return string
* @since 2.0
*/
public function get_note_text() {
return $this->get_parameter( 'PAYMENTREQUEST_0_NOTETEXT' );
}
/**
* Gets the payer ID from checkout details, a payer ID is a Unique PayPal Customer Account identification number
*
* @return string
* @since 2.0
*/
public function get_payer_id() {
return $this->get_parameter( 'PAYERID' );
}
/**
* Get state code given a full state name and country code
*
* @param string $country_code country code sent by PayPal
* @param string $state state name or code sent by PayPal
* @return string state code
* @since 2.0
*/
private function get_state_code( $country_code, $state ) {
// if not a US address, then convert state to abbreviation
if ( 'US' !== $country_code && isset( WC()->countries->states[ $country_code ] ) ) {
$local_states = WC()->countries->states[ $country_code ];
if ( ! empty( $local_states ) && in_array( $state, $local_states ) ) {
foreach ( $local_states as $key => $val ) {
if ( $val === $state ) {
return $key;
}
}
}
}
return $state;
}
}

View File

@@ -0,0 +1,358 @@
<?php
/**
* PayPal Reference Transaction API Do Express Checkout Response Class
*
* Parses DoExpressCheckout response which are used to process initial payments (if any) when checking out.
*
* @link https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/
*
* Heavily inspired by the WC_Paypal_Express_API_Payment_Response class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Response_Payment extends WCS_PayPal_Reference_Transaction_API_Response_Billing_Agreement {
/** approved transaction response payment status */
const TRANSACTION_COMPLETED = 'Completed';
/** in progress transaction response payment status */
const TRANSACTION_INPROGRESS = 'In-Progress';
/** in progress transaction response payment status */
const TRANSACTION_PROCESSED = 'Processed';
/** pending transaction response payment status */
const TRANSACTION_PENDING = 'Pending';
/** @var array URL-decoded and parsed parameters */
protected $successful_statuses = array();
/**
* Parse the payment response
*
* @see WC_PayPal_Express_API_Response::__construct()
* @param string $response the raw URL-encoded response string
* @since 2.0
*/
public function __construct( $response ) {
parent::__construct( $response );
$this->successful_statuses = array(
self::TRANSACTION_COMPLETED,
self::TRANSACTION_PROCESSED,
self::TRANSACTION_INPROGRESS,
);
}
/**
* Checks if the transaction was successful
*
* @return bool true if approved, false otherwise
* @since 2.0
*/
public function transaction_approved() {
return in_array( $this->get_payment_status(), $this->successful_statuses );
}
/**
* Returns true if the payment is pending, for instance if the payment was authorized, but not captured. There are many other
* possible reasons
*
* @link https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/#id105CAM003Y4__id116RI0UF0YK
*
* @return bool true if the transaction was held, false otherwise
* @since 2.0
*/
public function transaction_held() {
return self::TRANSACTION_PENDING === $this->get_payment_status();
}
/**
* Gets the response status code, or null if there is no status code associated with this transaction.
*
* @link https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/#id105CAM003Y4__id116RI0UF0YK
*
* @return string status code
* @since 2.0
*/
public function get_status_code() {
return $this->get_payment_status();
}
/**
* Gets the response status message, or null if there is no status message associated with this transaction.
*
* PayPal provides additional info only for Pending or Completed-Funds-Held transactions.
*
* @return string status message
* @since 2.0
*/
public function get_status_message() {
$message = '';
if ( $this->transaction_held() ) {
// PayPal's "pending" is our Held
$message = $this->get_pending_reason();
} elseif ( 'echeck' == $this->get_payment_type() ) {
// add some additional info for eCheck payments
// translators: placeholder is localised datetime
$message = sprintf( __( 'expected clearing date %s', 'woocommerce-subscriptions' ), date_i18n( wc_date_format(), strtotime( $this->get_payment_parameter( 'EXPECTEDECHECKCLEARDATE' ) ) ) );
}
// add fraud filters
if ( $filters = $this->get_fraud_filters() ) {
foreach ( $filters as $filter ) {
$message .= sprintf( ' %s: %s', $filter['name'], $filter['id'] );
}
}
return $message;
}
/**
* Gets the response transaction id, or null if there is no transaction id associated with this transaction.
*
* @return string transaction id
* @since 2.0
*/
public function get_transaction_id() {
return $this->get_payment_parameter( 'TRANSACTIONID' );
}
/**
* Return true if the response has a payment type other than `none`
*
* @return bool
* @since 2.0
*/
public function has_payment_type() {
return 'none' !== $this->get_payment_type();
}
/**
* Get the PayPal payment type, either `none`, `echeck`, or `instant`
*
* @since 2.0.9
* @return string
*/
public function get_payment_type() {
return $this->get_payment_parameter( 'PAYMENTTYPE' );
}
/**
* Gets payment status
*
* @return string
* @since 2.0
*/
private function get_payment_status() {
return $this->has_payment_parameter( 'PAYMENTSTATUS' ) ? $this->get_payment_parameter( 'PAYMENTSTATUS' ) : 'N/A';
}
/**
* Gets the pending reason
*
* @return string
* @since 2.0
*/
private function get_pending_reason() {
return $this->has_payment_parameter( 'PENDINGREASON' ) ? $this->get_payment_parameter( 'PENDINGREASON' ) : 'N/A';
}
/** AVS/CSC Methods *******************************************************/
/**
* PayPal Express does not return an authorization code
*
* @return string credit card authorization code
* @since 2.0
*/
public function get_authorization_code() {
return false;
}
/**
* Returns the result of the AVS check
*
* @return string result of the AVS check, if any
* @since 2.0
*/
public function get_avs_result() {
if ( $filters = $this->get_fraud_filters() ) {
foreach ( $filters as $filter ) {
if ( in_array( $filter['id'], range( 1, 3 ) ) ) {
return $filter['id'];
}
}
}
return null;
}
/**
* Returns the result of the CSC check
*
* @return string result of CSC check
* @since 2.0
*/
public function get_csc_result() {
if ( $filters = $this->get_fraud_filters() ) {
foreach ( $filters as $filter ) {
if ( '4' == $filter['id'] ) {
return $filter['id'];
}
}
}
return null;
}
/**
* Returns true if the CSC check was successful
*
* @return boolean true if the CSC check was successful
* @since 2.0
*/
public function csc_match() {
return is_null( $this->get_csc_result() );
}
/**
* Return any fraud management data available. This data is explicitly
* enabled in the request, but PayPal recommends checking certain error
* conditions prior to accessing this data.
*
* This data provides additional context for why a transaction was held for
* review or declined.
*
* @link https://developer.paypal.com/webapps/developer/docs/classic/fmf/integration-guide/FMFProgramming/#id091UNG0065Z
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/DoReferenceTransaction_API_Operation_NVP/#id09BUI01L0K3__id0861GA0N07U (L_FMFfilterIDn Type Fields)
*
* @return array $filters {
* @type string $id filter ID, integer from 1-17
* @type string name filter name, short description for filter
* }
* @since 2.0
*/
private function get_fraud_filters() {
$filters = array();
if ( '11610' == $this->get_api_error_code() ) {
$type = 'PENDING';
} elseif ( '11611' == $this->get_api_error_code() ) {
$type = 'DENY';
} else {
// not supporting REPORT type yet
return $filters;
}
foreach ( range( 0, 9 ) as $index ) {
if ( $this->has_parameter( "L_FMF{$type}ID{$index}" ) && $this->has_parameter( "L_FMF{$type}NAME{$index}" ) ) {
$filters[] = array(
'id' => $this->get_parameter( "L_FMF{$type}ID{$index}" ),
'name' => $this->get_parameter( "L_FMF{$type}NAME{$index}" ),
);
}
}
return $filters;
}
/**
* Check if the response has a specific payment parameter.
*
* A wrapper around @see WCS_PayPal_Reference_Transaction_API_Response::has_parameter()
* that prepends the @see self::get_payment_parameter_prefix().
*
* @since 2.0.9
* @param string $name parameter name
* @return bool
*/
protected function has_payment_parameter( $name ) {
return $this->has_parameter( $this->get_payment_parameter_prefix() . $name );
}
/**
* Gets a given payment parameter's value, or null if parameter is not set or empty.
*
* A wrapper around @see WCS_PayPal_Reference_Transaction_API_Response::get_parameter()
* that prepends the @see self::get_payment_parameter_prefix().
*
* @since 2.0.9
* @param string $name parameter name
* @return string|null
*/
protected function get_payment_parameter( $name ) {
return $this->get_parameter( $this->get_payment_parameter_prefix() . $name );
}
/**
* DoExpressCheckoutPayment API responses have a prefix for the payment
* parameters. Parallels payments are not used, so the numeric portion of
* the prefix is always '0'
*
* @since 2.0.9
* @return string
*/
protected function get_payment_parameter_prefix() {
return 'PAYMENTINFO_0_';
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* PayPal Reference Transaction API Do Reference Transaction Response Class
*
* Parses DoReferenceTransaction responses
*
* The response parameters returned by payments initiated by DoExpressCheckout requests differ to the response parameters
* returned by DoReferenceTransaction requests in that the former have a payment prefix 'PAYMENTINFO_n_' (for our purposes
* that is always 'PAYMENTINFO_0_'). Because of this, we need a special class to handle the DoReferenceTransaction request
* response. However, the logic is identical so we can extend @see WCS_PayPal_Reference_Transaction_API_Response_Payment
* and only change the few payment prefix to be ''.
*
* @link https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/DoReferenceTransaction_API_Operation_NVP/
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0.9
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Response_Recurring_Payment extends WCS_PayPal_Reference_Transaction_API_Response_Payment {
/**
* Parse the payment response
*
* @see WC_PayPal_Express_API_Response::__construct()
* @param string $response the raw URL-encoded response string
* @since 2.0
*/
public function __construct( $response ) {
parent::__construct( $response );
}
/**
* DoExpressCheckoutPayment API responses have a prefix for the payment
* parameters. Parallels payments are not used, so the numeric portion of
* the prefix is always '0'
*
* @since 2.0.9
* @return string
*/
protected function get_payment_parameter_prefix() {
return '';
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* PayPal Reference Transaction API Response Class
*
* Parses response string received from the PayPal Express Checkout API for Reference Transaction related requests, which is simply a URL-encoded string of parameters
*
* @link https://developer.paypal.com/docs/classic/api/NVPAPIOverview/#id084DN080HY4
*
* Heavily inspired by the WC_Paypal_Express_API_Payment_Response class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_API_Response extends WC_Gateway_Paypal_Response {
/** @var array URL-decoded and parsed parameters */
protected $parameters = array();
/**
* Parse the response parameters from the raw URL-encoded response string
*
* @link https://developer.paypal.com/docs/classic/api/NVPAPIOverview/#id084FBM0M0HS
*
* @param string $response the raw URL-encoded response string
* @since 2.0
*/
public function __construct( $response ) {
// URL decode the response string and parse it
wp_parse_str( urldecode( $response ), $this->parameters );
}
/**
* Checks if response contains an API error code
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/
*
* @return bool true if has API error, false otherwise
* @since 2.0
*/
public function has_api_error() {
// assume something went wrong if ACK is missing
if ( ! $this->has_parameter( 'ACK' ) ) {
return true;
}
// any non-success ACK is considered an error, see
// https://developer.paypal.com/docs/classic/api/NVPAPIOverview/#id09C2F0K30L7
return ( 'Success' !== $this->get_parameter( 'ACK' ) && 'SuccessWithWarning' !== $this->get_parameter( 'ACK' ) );
}
/**
* Gets the API error code
*
* Note that PayPal can return multiple error codes, which are merged here
* for convenience
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/
*
* @return string
* @since 2.0
*/
public function get_api_error_code() {
$error_codes = array();
foreach ( range( 0, 9 ) as $index ) {
if ( $this->has_parameter( "L_ERRORCODE{$index}" ) ) {
$error_codes[] = $this->get_parameter( "L_ERRORCODE{$index}" );
}
}
return empty( $error_codes ) ? 'N/A' : trim( implode( ', ', $error_codes ) );
}
/**
* Gets the API error message
*
* Note that PayPal can return multiple error messages, which are merged here
* for convenience
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/
*
* @return string
* @since 2.0
*/
public function get_api_error_message() {
$error_messages = array();
foreach ( range( 0, 9 ) as $index ) {
if ( $this->has_parameter( "L_SHORTMESSAGE{$index}" ) ) {
$error_message = sprintf( '%s: %s - %s',
$this->has_parameter( "L_SEVERITYCODE{$index}" ) ? $this->get_parameter( "L_SEVERITYCODE{$index}" ) : _x( 'Error', 'used in api error message if there is no severity code from PayPal', 'woocommerce-subscriptions' ),
$this->get_parameter( "L_SHORTMESSAGE{$index}" ),
$this->has_parameter( "L_LONGMESSAGE{$index}" ) ? $this->get_parameter( "L_LONGMESSAGE{$index}" ) : _x( 'Unknown error', 'used in api error message if there is no long message', 'woocommerce-subscriptions' )
);
// append additional info if available
if ( $this->has_parameter( "L_ERRORPARAMID{$index}" ) && $this->has_parameter( "L_ERRORPARAMVALUE{$index}" ) ) {
$error_message .= sprintf( ' (%s - %s)', $this->get_parameter( "L_ERRORPARAMID{$index}" ), $this->get_parameter( "L_ERRORPARAMVALUE{$index}" ) );
}
$error_messages[] = $error_message;
}
}
return empty( $error_messages ) ? _x( 'N/A', 'no information about something', 'woocommerce-subscriptions' ) : trim( implode( ', ', $error_messages ) );
}
/**
* Returns true if the parameter is not empty
*
* @param string $name parameter name
* @return bool
* @since 2.0
*/
protected function has_parameter( $name ) {
return ! empty( $this->parameters[ $name ] );
}
/**
* Gets the parameter value, or null if parameter is not set or empty
*
* @param string $name parameter name
* @return string|null
* @since 2.0
*/
protected function get_parameter( $name ) {
return $this->has_parameter( $name ) ? $this->parameters[ $name ] : null;
}
/**
* Returns a message appropriate for a frontend user. This should be used
* to provide enough information to a user to allow them to resolve an
* issue on their own, but not enough to help nefarious folks fishing for
* info.
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/
*
* @return string user message, if there is one
* @since 2.0
*/
public function get_user_message() {
$allowed_user_error_message_codes = array(
'10445',
'10474',
'12126',
'13113',
'13122',
'13112',
);
return in_array( $this->get_api_error_code(), $allowed_user_error_message_codes ) ? $this->get_api_error_message() : null;
}
/**
* Returns the string representation of this response
*
* @return string response
* @since 2.0
*/
public function to_string() {
return print_r( $this->parameters, true );
}
/**
* Returns the string representation of this response with any and all
* sensitive elements masked or removed
*
* @return string response safe for logging/displaying
* @since 2.0
*/
public function to_string_safe() {
// no sensitive data to mask
return $this->to_string();
}
/**
* Get the order for a request based on the 'custom' response field
*
* @see WC_Gateway_Paypal_Response::get_paypal_order()
* @param string $response the raw URL-encoded response string
* @since 2.0
*/
public function get_order() {
// assume something went wrong if ACK is missing
if ( $this->has_parameter( 'CUSTOM' ) ) {
return $this->get_paypal_order( $this->get_parameter( 'CUSTOM' ) );
}
}
}

View File

@@ -0,0 +1,299 @@
<?php
/**
* PayPal Reference Transaction API Class
*
* Performs reference transaction related transactions requests via the PayPal Express Checkout API,
* including the creation of a billing agreement and processing renewal payments using that billing
* agremeent's ID in a reference tranasction.
*
* Also hijacks checkout when PayPal Standard is chosen as the payment method, but Reference Transactions
* are enabled on the store's PayPal account, to go via Express Checkout approval flow instead of the
* PayPal Standard checkout flow.
*
* Heavily inspired by the WC_Paypal_Express_API class developed by the masterful SkyVerge team
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once( 'abstracts/abstract-wcs-sv-api-base.php' );
class WCS_PayPal_Reference_Transaction_API extends WCS_SV_API_Base {
/** the production endpoint */
const PRODUCTION_ENDPOINT = 'https://api-3t.paypal.com/nvp';
/** the sandbox endpoint */
const SANDBOX_ENDPOINT = 'https://api-3t.sandbox.paypal.com/nvp';
/** NVP API version */
const VERSION = '124';
/** @var array the request parameters */
private $parameters = array();
/**
* Constructor - setup request object and set endpoint
*
* @param string $gateway_id gateway ID for this request
* @param string $api_environment the API environment
* @param string $api_username the API username
* @param string $api_password the API password
* @param string $api_signature the API signature
* @since 2.0
*/
public function __construct( $gateway_id, $api_environment, $api_username, $api_password, $api_signature ) {
// tie API to gateway
$this->gateway_id = $gateway_id;
// request URI does not vary per-request
$this->request_uri = ( 'production' === $api_environment ) ? self::PRODUCTION_ENDPOINT : self::SANDBOX_ENDPOINT;
// PayPal requires HTTP 1.1
$this->request_http_version = '1.1';
$this->api_username = $api_username;
$this->api_password = $api_password;
$this->api_signature = $api_signature;
}
/**
* Get PayPal URL parameters for the checkout URL
*
* @param array $paypal_args
* @param WC_Order $order
* @return array
* @since 2.0
*/
public function get_paypal_args( $paypal_args, $order ) {
$request = $this->get_new_request();
// First we need to request an express checkout token for setting up a billing agreement, to do that, we need to pull details of the transaction from the PayPal Standard args and massage them into the Express Checkout params
$response = $this->set_express_checkout( array(
'currency' => $paypal_args['currency_code'],
'return_url' => $this->get_callback_url( 'create_billing_agreement' ),
'cancel_url' => $paypal_args['cancel_return'],
'notify_url' => $paypal_args['notify_url'],
'custom' => $paypal_args['custom'],
'order' => $order,
) );
$paypal_args = array(
'cmd' => '_express-checkout',
'token' => $response->get_token(),
);
return $paypal_args;
}
/**
* Check account for reference transaction support
*
* For reference transactions to be enabled, we need to be able to setup a dummy SetExpressCheckout request without receiving any APIs errors.
* This ensures there are no API credentials errors (e.g. error code 10008: "Security header is not valid") as well as testing the account for
* reference transaction support. If the account does not have reference transaction support enabled, PayPal will return the error code
* error code 11452: "Merchant not enabled for reference transactions".
*
* @link https://developer.paypal.com/docs/classic/api/errorcodes/#id09C3G0PJ0N9__id5e8c50e9-4f1b-462a-8586-399b63b07f1a
*
* @return bool
* @since 2.0
*/
public function are_reference_transactions_enabled() {
$request = $this->get_new_request();
$request->set_express_checkout( array(
// As we are only testing for whether billing agreements are allowed, we don't need to se any other details
'currency' => get_woocommerce_currency(),
'return_url' => $this->get_callback_url( 'reference_transaction_account_check' ),
'cancel_url' => $this->get_callback_url( 'reference_transaction_account_check' ),
'notify_url' => $this->get_callback_url( 'reference_transaction_account_check' ),
) );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response' );
try {
$response = $this->perform_request( $request );
if ( ! $response->has_api_error() ) {
$reference_transactions_enabled = true;
} else {
$reference_transactions_enabled = false;
}
} catch ( Exception $e ) {
$reference_transactions_enabled = false;
}
return $reference_transactions_enabled;
}
/**
* Set Express Checkout
*
* @param array $args @see WCS_PayPal_Reference_Transaction_API_Request::set_express_checkout() for details
* @throws Exception network timeouts, etc
* @return WCS_PayPal_Reference_Transaction_API_Response_Checkout response object
* @since 2.0
*/
public function set_express_checkout( $args ) {
$request = $this->get_new_request();
$request->set_express_checkout( $args );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Checkout' );
return $this->perform_request( $request );
}
/**
* Create a billing agreement, required when a subscription sign-up has no initial payment
*
* @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECReferenceTxns/#id094TB0Y0J5Z__id094TB4003HS
* @link https://developer.paypal.com/docs/classic/api/merchant/CreateBillingAgreement_API_Operation_NVP/
*
* @param string $token token from SetExpressCheckout response
* @return WCS_PayPal_Reference_Transaction_API_Response_Billing_Agreement response object
* @since 2.0
*/
public function create_billing_agreement( $token ) {
$request = $this->get_new_request();
$request->create_billing_agreement( $token );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Billing_Agreement' );
return $this->perform_request( $request );
}
/**
* Get Express Checkout Details
*
* @param string $token Token from set_express_checkout response
* @return WC_PayPal_Reference_Transaction_API_Checkout_Response response object
* @throws Exception network timeouts, etc
* @since 2.0
*/
public function get_express_checkout_details( $token ) {
$request = $this->get_new_request();
$request->get_express_checkout_details( $token );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Checkout' );
return $this->perform_request( $request );
}
/**
* Process an express checkout payment and billing agreement creation
*
* @param string $token PayPal Express Checkout token returned by SetExpressCheckout operation
* @param WC_Order $order order object
* @param array $args
* @return WCS_PayPal_Reference_Transaction_API_Response_Payment refund response
* @since 2.0.9
*/
public function do_express_checkout( $token, $order, $args ) {
$this->order = $order;
$request = $this->get_new_request();
$request->do_express_checkout( $token, $order, $args );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Payment' );
return $this->perform_request( $request );
}
/**
* Perform a reference transaction for the given order
*
* @see SV_WC_Payment_Gateway_API::refund()
* @param WC_Order $order order object
* @return SV_WC_Payment_Gateway_API_Response refund response
* @throws SV_WC_Payment_Gateway_Exception network timeouts, etc
* @since 2.0
*/
public function do_reference_transaction( $reference_id, $order, $args ) {
$this->order = $order;
$request = $this->get_new_request();
$request->do_reference_transaction( $reference_id, $order, $args );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Recurring_Payment' );
return $this->perform_request( $request );
}
/**
* Change the status of a subscription for a given order/profile ID
*
* @since 2.0.0
* @see SV_WC_Payment_Gateway_API::refund()
* @param WC_Order $order order object
* @return SV_WC_Payment_Gateway_API_Response refund response
* @throws SV_WC_Payment_Gateway_Exception network timeouts, etc
*/
public function manage_recurring_payments_profile_status( $profile_id, $new_status, $order ) {
$this->order = $order;
$request = $this->get_new_request();
$request->manage_recurring_payments_profile_status( $profile_id, $new_status, $order );
$this->set_response_handler( 'WCS_PayPal_Reference_Transaction_API_Response_Checkout' );
return $this->perform_request( $request );
}
/** Helper methods ******************************************************/
/**
* Get the wc-api URL to redirect to
*
* @param string $action checkout action, either `set_express_checkout or `get_express_checkout_details`
* @return string URL
* @since 2.0
*/
public function get_callback_url( $action ) {
return add_query_arg( 'action', $action, WC()->api_request_url( 'wcs_paypal' ) );
}
/**
* Builds and returns a new API request object
*
* @see \WCS_SV_API_Base::get_new_request()
* @param array $args
* @return WC_PayPal_Reference_Transaction_API_Request API request object
* @since 2.0
*/
protected function get_new_request( $args = array() ) {
return new WCS_PayPal_Reference_Transaction_API_Request( $this->api_username, $this->api_password, $this->api_signature, self::VERSION );
}
/**
* Supposed to return the main gatewya plugin class, but we don't have one of those
*
* @see \WCS_SV_API_Base::get_plugin()
* @return object
* @since 2.0
*/
protected function get_plugin() {
return WCS_PayPal::instance();
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Handles responses from PayPal IPN for Reference Transactions
*
* Example IPN payloads: https://gist.github.com/thenbrent/95b6b0c0aaa3ab787b71
*
* @link https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/#id08CTB0S055Z
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Reference_Transaction_IPN_Handler extends WCS_PayPal_Standard_IPN_Handler {
/** @var Array transaction types this class can handle */
protected $transaction_types = array(
'mp_signup', // Created a billing agreement
'mp_cancel', // Billing agreement cancelled
'merch_pmt', // Reference transaction payment
);
/**
* Constructor
*/
public function __construct( $sandbox = false, $receiver_email = '' ) {
$this->receiver_email = $receiver_email;
$this->sandbox = $sandbox;
}
/**
* There was a valid response
*
* Based on the IPN Variables documented here: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/#id091EB0901HT
*
* @param array $posted Post data after wp_unslash
*/
public function valid_response( $transaction_details ) {
if ( ! $this->validate_transaction_type( $transaction_details['txn_type'] ) ) {
return;
}
switch ( $transaction_details['txn_type'] ) {
case 'mp_cancel':
$this->cancel_subscriptions( $transaction_details['mp_id'] );
break;
case 'merch_pmt' :
if ( ! empty( $transaction_details['custom'] ) && ( $order = $this->get_paypal_order( $transaction_details['custom'] ) ) ) {
$transaction_details['payment_status'] = strtolower( $transaction_details['payment_status'] );
// Sandbox fix
if ( isset( $transaction_details['test_ipn'] ) && 1 == $transaction_details['test_ipn'] && 'pending' == $transaction_details['payment_status'] ) {
$transaction_details['payment_status'] = 'completed';
}
WC_Gateway_Paypal::log( 'Found order #' . $order->id );
WC_Gateway_Paypal::log( 'Payment status: ' . $transaction_details['payment_status'] );
if ( method_exists( $this, 'payment_status_' . $transaction_details['payment_status'] ) ) {
call_user_func( array( $this, 'payment_status_' . $transaction_details['payment_status'] ), $order, $transaction_details );
} else {
WC_Gateway_Paypal::log( 'Unknown payment status: ' . $transaction_details['payment_status'] );
}
}
break;
case 'mp_signup' :
// Silence is Golden
break;
}
exit;
}
/**
* Find all subscription with a given billing agreement ID and cancel them becasue that billing agreement has been
* cancelled at PayPal, and therefore, no future payments can be charged.
*
* @since 2.0
*/
protected function cancel_subscriptions( $billing_agreement_id ) {
$subscription_ids = get_posts( array(
'posts_per_page' => -1,
'post_type' => 'shop_subscription',
'post_status' => 'any',
'fields' => 'ids',
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => array(
array(
'key' => '_paypal_subscription_id',
'compare' => '=',
'value' => $billing_agreement_id,
),
),
) );
if ( empty( $subscription_ids ) ) {
return;
}
$note = esc_html__( 'Billing agreement cancelled at PayPal.', 'woocommerce-subscriptions' );
foreach ( $subscription_ids as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
try {
if ( false !== $subscription && ! $subscription->has_status( wcs_get_subscription_ended_statuses() ) ) {
$subscription->cancel_order( $note );
WC_Gateway_Paypal::log( sprintf( 'Subscription %s Cancelled: %s', $subscription_id, $note ) );
}
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( sprintf( 'Unable to cancel subscription %s: %s', $subscription_id, $e->getMessage() ) );
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* PayPal Standard Change Subscription Payment Method Class.
*
* Handles the process of a customer changing the payment method on a subscription via their My Account page from or two PayPal Standard.
*
* @link http://docs.woothemes.com/document/subscriptions/customers-view/#section-5
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Standard_Change_Payment_Method {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// Don't automatically cancel a subscription with PayPal on payment method change - we'll cancel it ourselves
add_action( 'woocommerce_subscriptions_pre_update_payment_method', __CLASS__ . '::maybe_remove_subscription_cancelled_callback', 10, 3 );
add_action( 'woocommerce_subscription_payment_method_updated', __CLASS__ . '::maybe_reattach_subscription_cancelled_callback', 10, 3 );
// Don't update payment methods immediately when changing to PayPal - wait for the IPN notification
add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', __CLASS__ . '::maybe_dont_update_payment_method', 10, 3 );
add_filter( 'wcs_gateway_change_payment_button_text', __CLASS__ . '::change_payment_button_text', 10 , 2 );
}
/**
* If changing a subscriptions payment method from and to PayPal, wait until an appropriate IPN message
* has come in before deciding to cancel the old subscription.
*
* @since 2.0
*/
public static function maybe_remove_subscription_cancelled_callback( $subscription, $new_payment_method, $old_payment_method ) {
if ( 'paypal' == $new_payment_method && 'paypal' == $old_payment_method && ! WCS_PayPal::are_reference_transactions_enabled() ) {
remove_action( 'woocommerce_subscription_cancelled_paypal', 'WCS_PayPal_Status_Manager::cancel_subscription' );
}
}
/**
* If changing a subscriptions payment method from and to PayPal, the cancelled subscription hook was removed in
* @see self::maybe_remove_cancelled_subscription_hook() so we want to add it again for other subscriptions.
*
* @since 2.0
*/
public static function maybe_reattach_subscription_cancelled_callback( $subscription, $new_payment_method, $old_payment_method ) {
if ( 'paypal' == $new_payment_method && 'paypal' == $old_payment_method && ! WCS_PayPal::are_reference_transactions_enabled() ) {
add_action( 'woocommerce_subscription_cancelled_paypal', 'WCS_PayPal_Status_Manager::cancel_subscription' );
}
}
/**
* Don't update the payment method on checkout when switching to PayPal - wait until we have the IPN message.
*
* @param string $item_name
* @return string
* @since 1.5.14
*/
public static function maybe_dont_update_payment_method( $update, $new_payment_method, $subscription ) {
if ( 'paypal' == $new_payment_method ) {
$update = false;
}
return $update;
}
/**
* Change the "Change Payment Method" button for PayPal
*
* @param string $change_button_text
* @param WC_Payment_Gateway $gateway
* @since 2.0.8
*/
public static function change_payment_button_text( $change_button_text, $gateway ) {
if ( is_object( $gateway ) && isset( $gateway->id ) && 'paypal' == $gateway->id && ! empty( $gateway->order_button_text ) ) {
$change_button_text = $gateway->order_button_text;
}
return $change_button_text;
}
}
WCS_PayPal_Standard_Change_Payment_Method::init();

View File

@@ -0,0 +1,657 @@
<?php
/**
* PayPal Standard IPN Handler
*
* Handles IPN requests from PayPal for PayPal Standard Subscription transactions
*
* Example IPN payloads https://gist.github.com/thenbrent/3037967
*
* @link https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/#id08CTB0S055Z
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
/** @var Array transaction types this class can handle */
protected $transaction_types = array(
'subscr_signup', // Subscription started
'subscr_payment', // Subscription payment received
'subscr_cancel', // Subscription canceled
'subscr_eot', // Subscription expired
'subscr_failed', // Subscription payment failed
'subscr_modify', // Subscription modified
// The PayPal docs say these are for Express Checkout recurring payments but they are also sent for PayPal Standard subscriptions
'recurring_payment_skipped', // Recurring payment skipped; it will be retried up to 3 times, 5 days apart
'recurring_payment_suspended', // Recurring payment suspended. This transaction type is sent if PayPal tried to collect a recurring payment, but the related recurring payments profile has been suspended.
'recurring_payment_suspended_due_to_max_failed_payment', // Recurring payment failed and the related recurring payment profile has been suspended
);
/**
* Constructor from WC_Gateway_Paypal_IPN_Handler
*/
public function __construct( $sandbox = false, $receiver_email = '' ) {
$this->receiver_email = $receiver_email;
$this->sandbox = $sandbox;
}
/**
* There was a valid response
*
* Based on the IPN Variables documented here: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/#id091EB0901HT
*
* @param array $transaction_details Post data after wp_unslash
* @since 2.0
*/
public function valid_response( $transaction_details ) {
global $wpdb;
$transaction_details = stripslashes_deep( $transaction_details );
if ( ! $this->validate_transaction_type( $transaction_details['txn_type'] ) ) {
return;
}
$transaction_details['txn_type'] = strtolower( $transaction_details['txn_type'] );
$this->process_ipn_request( $transaction_details );
}
/**
* Process a PayPal Standard Subscription IPN request
*
* @param array $transaction_details Post data after wp_unslash
* @since 2.0
*/
protected function process_ipn_request( $transaction_details ) {
// Get the subscription ID and order_key with backward compatibility
$subscription_id_and_key = self::get_order_id_and_key( $transaction_details, 'shop_subscription' );
$subscription = wcs_get_subscription( $subscription_id_and_key['order_id'] );
$subscription_key = $subscription_id_and_key['order_key'];
// We have an invalid $subscription, probably because invoice_prefix has changed since the subscription was first created, so get the subscription by order key
if ( ! isset( $subscription->id ) ) {
$subscription = wcs_get_subscription( wc_get_order_id_by_order_key( $subscription_key ) );
}
if ( 'recurring_payment_suspended_due_to_max_failed_payment' == $transaction_details['txn_type'] && empty( $subscription ) ) {
WC_Gateway_Paypal::log( 'Returning as "recurring_payment_suspended_due_to_max_failed_payment" transaction is for a subscription created with Express Checkout' );
return;
}
if ( empty( $subscription ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: Could not find matching Subscription.' );
exit;
}
if ( $subscription->order_key != $subscription_key ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: Subscription Key does not match invoice.' );
exit;
}
if ( isset( $transaction_details['ipn_track_id'] ) ) {
// Make sure the IPN request has not already been handled
$handled_ipn_requests = get_post_meta( $subscription->id, '_paypal_ipn_tracking_ids', true );
if ( empty( $handled_ipn_requests ) ) {
$handled_ipn_requests = array();
}
// The 'ipn_track_id' is not a unique ID and is shared between different transaction types, so create a unique ID by prepending the transaction type
$ipn_id = $transaction_details['txn_type'] . '_' . $transaction_details['ipn_track_id'];
if ( in_array( $ipn_id, $handled_ipn_requests ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: IPN ' . $ipn_id . ' message has already been correctly handled.' );
exit;
}
// Make sure we're not in the process of handling this IPN request on a server under extreme load and therefore, taking more than a minute to process it (which is the amount of time PayPal allows before resending the IPN request)
$ipn_lock_transient_name = 'wcs_pp_' . $ipn_id; // transient names need to be less than 45 characters and the $ipn_id will be around 30 characters, e.g. subscr_payment_5ab4c38e1f39d
if ( 'in-progress' == get_transient( $ipn_lock_transient_name ) && 'recurring_payment_suspended_due_to_max_failed_payment' !== $transaction_details['txn_type'] ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: an older IPN request with ID ' . $ipn_id . ' is still in progress.' );
// We need to send an error code to make sure PayPal does retry the IPN after our lock expires, in case something is actually going wrong and the server isn't just taking a long time to process the request
status_header( 503 );
exit;
}
// Set a transient to block IPNs with this transaction ID for the next 5 minutes
set_transient( $ipn_lock_transient_name, 'in-progress', apply_filters( 'woocommerce_subscriptions_paypal_ipn_request_lock_time', 5 * MINUTE_IN_SECONDS ) );
}
if ( isset( $transaction_details['txn_id'] ) ) {
// Make sure the IPN request has not already been handled
$handled_transactions = get_post_meta( $subscription->id, '_paypal_transaction_ids', true );
if ( empty( $handled_transactions ) ) {
$handled_transactions = array();
}
$transaction_id = $transaction_details['txn_id'];
if ( isset( $transaction_details['txn_type'] ) ) {
$transaction_id .= '_' . $transaction_details['txn_type'];
}
// The same transaction ID is used for different payment statuses, so make sure we handle it only once. See: http://stackoverflow.com/questions/9240235/paypal-ipn-unique-identifier
if ( isset( $transaction_details['payment_status'] ) ) {
$transaction_id .= '_' . $transaction_details['payment_status'];
}
if ( in_array( $transaction_id, $handled_transactions ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: transaction ' . $transaction_id . ' has already been correctly handled.' );
exit;
}
}
WC_Gateway_Paypal::log( 'Subscription transaction details: ' . print_r( $transaction_details, true ) );
WC_Gateway_Paypal::log( 'Subscription Transaction Type: ' . $transaction_details['txn_type'] );
$is_renewal_sign_up_after_failure = false;
// If the invoice ID doesn't match the default invoice ID and contains the string '-wcsfrp-', the IPN is for a subscription payment to fix up a failed payment
if ( in_array( $transaction_details['txn_type'], array( 'subscr_signup', 'subscr_payment' ) ) && false !== strpos( $transaction_details['invoice'], '-wcsfrp-' ) ) {
$renewal_order = wc_get_order( substr( $transaction_details['invoice'], strrpos( $transaction_details['invoice'], '-' ) + 1 ) );
// check if the failed signup has been previously recorded
if ( $renewal_order->id != get_post_meta( $subscription->id, '_paypal_failed_sign_up_recorded', true ) ) {
$is_renewal_sign_up_after_failure = true;
}
}
// If the invoice ID doesn't match the default invoice ID and contains the string '-wcscpm-', the IPN is for a subscription payment method change
if ( 'subscr_signup' == $transaction_details['txn_type'] && false !== strpos( $transaction_details['invoice'], '-wcscpm-' ) ) {
$is_payment_change = true;
} else {
$is_payment_change = false;
}
// Ignore IPN messages when the payment method isn't PayPal
if ( 'paypal' != $subscription->payment_method ) {
// The 'recurring_payment_suspended' transaction is actually an Express Checkout transaction type, but PayPal also send it for PayPal Standard Subscriptions suspended by admins at PayPal, so we need to handle it *if* the subscription has PayPal as the payment method, or leave it if the subscription is using a different payment method (because it might be using PayPal Express Checkout or PayPal Digital Goods)
if ( 'recurring_payment_suspended' == $transaction_details['txn_type'] ) {
WC_Gateway_Paypal::log( '"recurring_payment_suspended" IPN ignored: recurring payment method is not "PayPal". Returning to allow another extension to process the IPN, like PayPal Digital Goods.' );
return;
} elseif ( false === $is_renewal_sign_up_after_failure && false === $is_payment_change ) {
WC_Gateway_Paypal::log( 'IPN ignored, recurring payment method has changed.' );
exit;
}
}
if ( $is_renewal_sign_up_after_failure || $is_payment_change ) {
// Store the old profile ID on the order (for the first IPN message that comes through)
$existing_profile_id = wcs_get_paypal_id( $subscription );
if ( empty( $existing_profile_id ) || $existing_profile_id !== $transaction_details['subscr_id'] ) {
update_post_meta( $subscription->id, '_old_paypal_subscriber_id', $existing_profile_id );
update_post_meta( $subscription->id, '_old_payment_method', $subscription->payment_method );
}
}
// Save the profile ID if it's not a cancellation/expiration request
if ( isset( $transaction_details['subscr_id'] ) && ! in_array( $transaction_details['txn_type'], array( 'subscr_cancel', 'subscr_eot' ) ) ) {
wcs_set_paypal_id( $subscription, $transaction_details['subscr_id'] );
if ( wcs_is_paypal_profile_a( $transaction_details['subscr_id'], 'out_of_date_id' ) && 'disabled' != get_option( 'wcs_paypal_invalid_profile_id' ) ) {
update_option( 'wcs_paypal_invalid_profile_id', 'yes' );
}
}
$is_first_payment = ( $subscription->get_completed_payment_count() < 1 ) ? true : false;
if ( $subscription->has_status( 'switched' ) ) {
WC_Gateway_Paypal::log( 'IPN ignored, subscription has been switched.' );
exit;
}
switch ( $transaction_details['txn_type'] ) {
case 'subscr_signup':
// Store PayPal Details on Subscription and Order
$this->save_paypal_meta_data( $subscription, $transaction_details );
$this->save_paypal_meta_data( $subscription->order, $transaction_details );
// When there is a free trial & no initial payment amount, we need to mark the order as paid and activate the subscription
if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $subscription->order->get_total() ) {
// Safe to assume the subscription has an order here because otherwise we wouldn't get a 'subscr_signup' IPN
$subscription->order->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages
update_post_meta( $subscription->id, '_paypal_first_ipn_ignored_for_pdt', 'true' );
}
// Payment completed
if ( $is_payment_change ) {
// Set PayPal as the new payment method
WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $subscription, 'paypal' );
// We need to cancel the subscription now that the method has been changed successfully
if ( 'paypal' == get_post_meta( $subscription->id, '_old_payment_method', true ) ) {
self::cancel_subscription( $subscription, get_post_meta( $subscription->id, '_old_paypal_subscriber_id', true ) );
}
$subscription->add_order_note( _x( 'IPN subscription payment method changed to PayPal.', 'when it is a payment change, and there is a subscr_signup message, this will be a confirmation message that PayPal accepted it being the new payment method', 'woocommerce-subscriptions' ) );
} else {
$subscription->add_order_note( __( 'IPN subscription sign up completed.', 'woocommerce-subscriptions' ) );
}
if ( $is_payment_change ) {
WC_Gateway_Paypal::log( 'IPN subscription payment method changed for subscription ' . $subscription->id );
} else {
WC_Gateway_Paypal::log( 'IPN subscription sign up completed for subscription ' . $subscription->id );
}
break;
case 'subscr_payment':
if ( 0.01 == $transaction_details['mc_gross'] && 1 == $subscription->get_completed_payment_count() ) {
WC_Gateway_Paypal::log( 'IPN ignored, treating IPN as secondary trial period.' );
exit;
}
if ( ! $is_first_payment && ! $is_renewal_sign_up_after_failure ) {
if ( $subscription->has_status( 'active' ) ) {
remove_action( 'woocommerce_subscription_on-hold_paypal', 'WCS_PayPal_Status_Manager::suspend_subscription' );
$subscription->update_status( 'on-hold' );
add_action( 'woocommerce_subscription_on-hold_paypal', 'WCS_PayPal_Status_Manager::suspend_subscription' );
}
// Generate a renewal order to record the payment (and determine how much is due)
$renewal_order = wcs_create_renewal_order( $subscription );
// Set PayPal as the payment method (we can't use $renewal_order->set_payment_method() here as it requires an object we don't have)
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$renewal_order->set_payment_method( $available_gateways['paypal'] );
}
if ( 'completed' == strtolower( $transaction_details['payment_status'] ) ) {
// Store PayPal Details
$this->save_paypal_meta_data( $subscription, $transaction_details );
// Subscription Payment completed
$subscription->add_order_note( __( 'IPN subscription payment completed.', 'woocommerce-subscriptions' ) );
WC_Gateway_Paypal::log( 'IPN subscription payment completed for subscription ' . $subscription->id );
// First payment on order, process payment & activate subscription
if ( $is_first_payment ) {
$subscription->order->payment_complete( $transaction_details['txn_id'] );
// Store PayPal Details on Order
$this->save_paypal_meta_data( $subscription->order, $transaction_details );
// IPN got here first or PDT will never arrive. Normally PDT would have arrived, so the first IPN would not be the first payment. In case the the first payment is an IPN, we need to make sure to not ignore the second one
update_post_meta( $subscription->id, '_paypal_first_ipn_ignored_for_pdt', 'true' );
// Ignore the first IPN message if the PDT should have handled it (if it didn't handle it, it will have been dealt with as first payment), but set a flag to make sure we only ignore it once
} elseif ( $subscription->get_completed_payment_count() == 1 && '' !== WCS_PayPal::get_option( 'identity_token' ) && 'true' != get_post_meta( $subscription->id, '_paypal_first_ipn_ignored_for_pdt', true ) && false === $is_renewal_sign_up_after_failure ) {
WC_Gateway_Paypal::log( 'IPN subscription payment ignored for subscription ' . $subscription->id . ' due to PDT previously handling the payment.' );
update_post_meta( $subscription->id, '_paypal_first_ipn_ignored_for_pdt', 'true' );
// Process the payment if the subscription is active
} elseif ( ! $subscription->has_status( array( 'cancelled', 'expired', 'switched', 'trash' ) ) ) {
if ( true === $is_renewal_sign_up_after_failure && is_object( $renewal_order ) ) {
update_post_meta( $subscription->id, '_paypal_failed_sign_up_recorded', $renewal_order->id );
// We need to cancel the old subscription now that the method has been changed successfully
if ( 'paypal' == get_post_meta( $subscription->id, '_old_payment_method', true ) ) {
$profile_id = get_post_meta( $subscription->id, '_old_paypal_subscriber_id', true );
// Make sure we don't cancel the current profile
if ( $profile_id !== $transaction_details['subscr_id'] ) {
self::cancel_subscription( $subscription, $profile_id );
}
$subscription->add_order_note( __( 'IPN subscription failing payment method changed.', 'woocommerce-subscriptions' ) );
}
}
try {
// to cover the case when PayPal drank too much coffee and sent IPNs early - needs to happen before $renewal_order->payment_complete
$update_dates = array();
if ( $subscription->get_time( 'trial_end' ) > gmdate( 'U' ) ) {
$update_dates['trial_end'] = gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) - 1 );
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment for subscription %d: trial_end is in futute (date: %s) setting to %s.', $subscription->id, $subscription->get_date( 'trial_end' ), $update_dates['trial_end'] ) );
} else {
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment for subscription %d: trial_end is in past (date: %s).', $subscription->id, $subscription->get_date( 'trial_end' ) ) );
}
if ( $subscription->get_time( 'next_payment' ) > gmdate( 'U' ) ) {
$update_dates['next_payment'] = gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) - 1 );
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment for subscription %d: next_payment is in future (date: %s) setting to %s.', $subscription->id, $subscription->get_date( 'next_payment' ), $update_dates['next_payment'] ) );
} else {
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment for subscription %d: next_payment is in past (date: %s).', $subscription->id, $subscription->get_date( 'next_payment' ) ) );
}
if ( ! empty( $update_dates ) ) {
$subscription->update_dates( $update_dates );
}
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment exception subscription %d: %s.', $subscription->id, $e->getMessage() ) );
}
remove_action( 'woocommerce_subscription_activated_paypal', 'WCS_PayPal_Status_Manager::reactivate_subscription' );
try {
$renewal_order->payment_complete( $transaction_details['txn_id'] );
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment exception calling $renewal_order->payment_complete() for subscription %d: %s.', $subscription->id, $e->getMessage() ) );
}
$renewal_order->add_order_note( __( 'IPN subscription payment completed.', 'woocommerce-subscriptions' ) );
add_action( 'woocommerce_subscription_activated_paypal', 'WCS_PayPal_Status_Manager::reactivate_subscription' );
wcs_set_paypal_id( $renewal_order, $transaction_details['subscr_id'] );
}
} elseif ( in_array( strtolower( $transaction_details['payment_status'] ), array( 'pending', 'failed' ) ) ) {
// Subscription Payment completed
// translators: placeholder is payment status (e.g. "completed")
$subscription->add_order_note( sprintf( _x( 'IPN subscription payment %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'] ) );
if ( ! $is_first_payment ) {
update_post_meta( $renewal_order->id, '_transaction_id', $transaction_details['txn_id'] );
// translators: placeholder is payment status (e.g. "completed")
$renewal_order->add_order_note( sprintf( _x( 'IPN subscription payment %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'] ) );
$subscription->payment_failed();
}
WC_Gateway_Paypal::log( 'IPN subscription payment failed for subscription ' . $subscription->id );
} else {
WC_Gateway_Paypal::log( 'IPN subscription payment notification received for subscription ' . $subscription->id . ' with status ' . $transaction_details['payment_status'] );
}
break;
// Admins can suspend subscription at PayPal triggering this IPN
case 'recurring_payment_suspended':
if ( $subscription->has_status( 'active' ) ) {
// We don't need to suspend the subscription at PayPal because it's already on-hold there
remove_action( 'woocommerce_subscription_on-hold_paypal', 'WCS_PayPal_Status_Manager::suspend_subscription' );
$subscription->update_status( 'on-hold', __( 'IPN subscription suspended.', 'woocommerce-subscriptions' ) );
add_action( 'woocommerce_subscription_on-hold_paypal', 'WCS_PayPal_Status_Manager::suspend_subscription' );
WC_Gateway_Paypal::log( 'IPN subscription suspended for subscription ' . $subscription->id );
} else {
WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d. Subscription already %s.', $subscription->id, $subscription->get_status() ) );
}
break;
case 'subscr_cancel':
// Make sure the subscription hasn't been linked to a new payment method
if ( wcs_get_paypal_id( $subscription ) != $transaction_details['subscr_id'] ) {
WC_Gateway_Paypal::log( 'IPN subscription cancellation request ignored - new PayPal Profile ID linked to this subscription, for subscription ' . $subscription->id );
} else {
$subscription->cancel_order( __( 'IPN subscription cancelled.', 'woocommerce-subscriptions' ) );
WC_Gateway_Paypal::log( 'IPN subscription cancelled for subscription ' . $subscription->id );
}
break;
case 'subscr_eot': // Subscription ended, either due to failed payments or expiration
WC_Gateway_Paypal::log( 'IPN EOT request ignored for subscription ' . $subscription->id );
break;
case 'subscr_failed': // Subscription sign up failed
case 'recurring_payment_suspended_due_to_max_failed_payment': // Recurring payment failed
$ipn_failure_note = __( 'IPN subscription payment failure.', 'woocommerce-subscriptions' );
if ( ! $is_first_payment && ! $is_renewal_sign_up_after_failure && $subscription->has_status( 'active' ) ) {
// Generate a renewal order to record the failed payment
$renewal_order = wcs_create_renewal_order( $subscription );
// Set PayPal as the payment method
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$renewal_order->set_payment_method( $available_gateways['paypal'] );
$renewal_order->add_order_note( $ipn_failure_note );
}
WC_Gateway_Paypal::log( 'IPN subscription payment failure for subscription ' . $subscription->id );
// Subscription Payment completed
$subscription->add_order_note( $ipn_failure_note );
try {
$subscription->payment_failed();
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment failure, unable to process payment failure. Exception: %s ', $e->getMessage() ) );
}
break;
}
// Store the transaction IDs to avoid handling requests duplicated by PayPal
if ( isset( $transaction_details['ipn_track_id'] ) ) {
$handled_ipn_requests[] = $ipn_id;
update_post_meta( $subscription->id, '_paypal_ipn_tracking_ids', $handled_ipn_requests );
}
if ( isset( $transaction_details['txn_id'] ) ) {
$handled_transactions[] = $transaction_id;
update_post_meta( $subscription->id, '_paypal_transaction_ids', $handled_transactions );
}
// And delete the transient that's preventing other IPN's being processed
if ( isset( $ipn_lock_transient_name ) ) {
delete_transient( $ipn_lock_transient_name );
}
// Log completion
$log_message = 'IPN subscription request processed for ' . $subscription->id;
if ( isset( $ipn_id ) && ! empty( $ipn_id ) ) {
$log_message .= sprintf( ' (%s)', $ipn_id );
}
WC_Gateway_Paypal::log( $log_message );
// Prevent default IPN handling for subscription txn_types
exit;
}
/**
* Return valid transaction types
*
* @since 2.0
*/
public function get_transaction_types() {
return $this->transaction_types;
}
/**
* Checks a set of args and derives an Order ID with backward compatibility for WC < 1.7 where 'custom' was the Order ID.
*
* @since 2.0
*/
public static function get_order_id_and_key( $args, $order_type = 'shop_order' ) {
$order_id = $order_key = '';
if ( isset( $args['subscr_id'] ) ) { // PayPal Standard IPN message
$subscription_id = $args['subscr_id'];
} elseif ( isset( $args['recurring_payment_id'] ) ) { // PayPal Express Checkout IPN, most likely 'recurring_payment_suspended_due_to_max_failed_payment', for a PayPal Standard Subscription
$subscription_id = $args['recurring_payment_id'];
} else {
$subscription_id = '';
}
// First try and get the order ID by the subscription ID
if ( ! empty( $subscription_id ) ) {
$posts = get_posts( array(
'numberposts' => 1,
'orderby' => 'ID',
'order' => 'ASC',
'meta_key' => '_paypal_subscription_id',
'meta_value' => $subscription_id,
'post_type' => $order_type,
'post_status' => 'any',
'suppress_filters' => true,
) );
if ( ! empty( $posts ) ) {
$order_id = $posts[0]->ID;
$order_key = get_post_meta( $order_id, '_order_key', true );
}
}
// Couldn't find the order ID by subscr_id, so it's either not set on the order yet or the $args doesn't have a subscr_id, either way, let's get it from the args
if ( empty( $order_id ) && isset( $args['custom'] ) ) {
// WC < 1.6.5
if ( is_numeric( $args['custom'] ) && 'shop_order' == $order_type ) {
$order_id = $args['custom'];
$order_key = $args['invoice'];
} else {
$order_details = json_decode( $args['custom'] );
if ( is_object( $order_details ) ) { // WC 2.3.11+ converted the custom value to JSON, if we have an object, we've got valid JSON
if ( 'shop_order' == $order_type ) {
$order_id = $order_details->order_id;
$order_key = $order_details->order_key;
} elseif ( isset( $order_details->subscription_id ) ) {
// Subscription created with Subscriptions 2.0+
$order_id = $order_details->subscription_id;
$order_key = $order_details->subscription_key;
} else {
// Subscription created with Subscriptions < 2.0
$subscriptions = wcs_get_subscriptions_for_order( $order_details->order_id, array( 'order_type' => array( 'parent' ) ) );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
$order_id = $subscription->id;
$order_key = $subscription->order_key;
}
}
} elseif ( preg_match( '/^a:2:{/', $args['custom'] ) && ! preg_match( '/[CO]:\+?[0-9]+:"/', $args['custom'] ) && ( $order_details = maybe_unserialize( $args['custom'] ) ) ) { // WC 2.0 - WC 2.3.11, only allow serialized data in the expected format, do not allow objects or anything nasty to sneak in
if ( 'shop_order' == $order_type ) {
$order_id = $order_details[0];
$order_key = $order_details[1];
} else {
// Subscription, but we didn't have the subscription data in old, serialized value, so we need to pull it based on the order
$subscriptions = wcs_get_subscriptions_for_order( $order_details[0], array( 'order_type' => array( 'parent' ) ) );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
$order_id = $subscription->id;
$order_key = $subscription->order_key;
}
}
} else { // WC 1.6.5 - WC 2.0 or invalid data
$order_id = str_replace( WCS_PayPal::get_option( 'invoice_prefix' ), '', $args['invoice'] );
$order_key = $args['custom'];
}
}
}
return array( 'order_id' => (int) $order_id, 'order_key' => $order_key );
}
/**
* Cancel a specific PayPal Standard Subscription Profile with PayPal.
*
* Used when switching payment methods with PayPal Standard to make sure that
* the old subscription's profile ID is cancelled, not the new one.
*
* @param WC_Subscription A subscription object
* @param string A PayPal Subscription Profile ID
* @since 2.0
*/
protected static function cancel_subscription( $subscription, $old_paypal_subscriber_id ) {
// No need to cancel billing agreements
if ( wcs_is_paypal_profile_a( $old_paypal_subscriber_id, 'billing_agreement' ) ) {
return;
}
$current_profile_id = wcs_get_paypal_id( $subscription->id );
// Update the subscription using the old profile ID
wcs_set_paypal_id( $subscription, $old_paypal_subscriber_id );
// Call update_subscription_status() directly as we don't want the notes added by WCS_PayPal_Status_Manager::cancel_subscription()
WCS_PayPal_Status_Manager::update_subscription_status( $subscription, 'Cancel' );
// Restore the current profile ID
wcs_set_paypal_id( $subscription, $current_profile_id );
}
/**
* Check for a valid transaction type
*
* @param string $txn_type
* @since 2.0
*/
protected function validate_transaction_type( $txn_type ) {
if ( in_array( strtolower( $txn_type ), $this->get_transaction_types() ) ) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,274 @@
<?php
/**
* WooCommerce Subscriptions PayPal Standard Request Class.
*
* Generates URL parameters to send to PayPal to create a subscription with PayPal Standard
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Standard_Request {
/**
* Get PayPal Args for passing to PP
*
* Based on the HTML Variables documented here: https://developer.paypal.com/webapps/developer/docs/classic/paypal-payments-standard/integration-guide/Appx_websitestandard_htmlvariables/#id08A6HI00JQU
*
* @param WC_Order $order
* @return array
*/
public static function get_paypal_args( $paypal_args, $order ) {
$is_payment_change = WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment;
$order_contains_failed_renewal = false;
// Payment method changes act on the subscription not the original order
if ( $is_payment_change ) {
$subscriptions = array( wcs_get_subscription( $order->id ) );
$subscription = array_pop( $subscriptions );
$order = $subscription->order;
// We need the subscription's total
remove_filter( 'woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
} else {
// Otherwise the order is the $order
if ( $cart_item = wcs_cart_contains_failed_renewal_order_payment() || false !== WC_Subscriptions_Renewal_Order::get_failed_order_replaced_by( $order->id ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
$order_contains_failed_renewal = true;
} else {
$subscriptions = wcs_get_subscriptions_for_order( $order );
}
// Only one subscription allowed per order with PayPal
$subscription = array_pop( $subscriptions );
}
if ( $order_contains_failed_renewal || ( ! empty( $subscription ) && $subscription->get_total() > 0 && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) ) {
// It's a subscription
$paypal_args['cmd'] = '_xclick-subscriptions';
// Store the subscription ID in the args sent to PayPal so we can access them later
$paypal_args['custom'] = wcs_json_encode( array( 'order_id' => $order->id, 'order_key' => $order->order_key, 'subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key ) );
foreach ( $subscription->get_items() as $item ) {
if ( $item['qty'] > 1 ) {
$item_names[] = $item['qty'] . ' x ' . wcs_get_paypal_item_name( $item['name'] );
} elseif ( $item['qty'] > 0 ) {
$item_names[] = wcs_get_paypal_item_name( $item['name'] );
}
}
// translators: 1$: subscription ID, 2$: order ID, 3$: names of items, comma separated
$paypal_args['item_name'] = wcs_get_paypal_item_name( sprintf( _x( 'Subscription %1$s (Order %2$s) - %3$s', 'item name sent to paypal', 'woocommerce-subscriptions' ), $subscription->get_order_number(), $order->get_order_number(), implode( ', ', $item_names ) ) );
$unconverted_periods = array(
'billing_period' => $subscription->billing_period,
'trial_period' => $subscription->trial_period,
);
$converted_periods = array();
// Convert period strings into PayPay's format
foreach ( $unconverted_periods as $key => $period ) {
switch ( strtolower( $period ) ) {
case 'day':
$converted_periods[ $key ] = 'D';
break;
case 'week':
$converted_periods[ $key ] = 'W';
break;
case 'year':
$converted_periods[ $key ] = 'Y';
break;
case 'month':
default:
$converted_periods[ $key ] = 'M';
break;
}
}
$price_per_period = $subscription->get_total();
$subscription_interval = $subscription->billing_interval;
$start_timestamp = $subscription->get_time( 'start' );
$trial_end_timestamp = $subscription->get_time( 'trial_end' );
$next_payment_timestamp = $subscription->get_time( 'next_payment' );
$is_synced_subscription = WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription->id );
if ( $is_synced_subscription ) {
$length_from_timestamp = $next_payment_timestamp;
} elseif ( $trial_end_timestamp > 0 ) {
$length_from_timestamp = $trial_end_timestamp;
} else {
$length_from_timestamp = $start_timestamp;
}
$subscription_length = wcs_estimate_periods_between( $length_from_timestamp, $subscription->get_time( 'end' ), $subscription->billing_period );
$subscription_installments = $subscription_length / $subscription_interval;
$initial_payment = ( $is_payment_change ) ? 0 : $order->get_total();
if ( $order_contains_failed_renewal || $is_payment_change ) {
if ( $is_payment_change ) {
// Add a nonce to the order ID to avoid "This invoice has already been paid" error when changing payment method to PayPal when it was previously PayPal
$suffix = '-wcscpm-' . wp_create_nonce();
} else {
// Failed renewal order, append a descriptor and renewal order's ID
$suffix = '-wcsfrp-' . $order->id;
}
// Change the 'invoice' and the 'custom' values to be for the original order (if there is one)
if ( false === $subscription->order ) {
// No original order so we need to use the subscriptions values instead
$order_number = ltrim( $subscription->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) . '-subscription';
$order_id_key = array( 'order_id' => $subscription->id, 'order_key' => $subscription->order_key );
} else {
$order_number = ltrim( $subscription->order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) );
$order_id_key = array( 'order_id' => $subscription->order->id, 'order_key' => $subscription->order->order_key );
}
$order_details = ( false !== $subscription->order ) ? $subscription->order : $subscription;
// Set the invoice details to the original order's invoice but also append a special string and this renewal orders ID so that we can match it up as a failed renewal order payment later
$paypal_args['invoice'] = WCS_PayPal::get_option( 'invoice_prefix' ) . $order_number . $suffix;
$paypal_args['custom'] = wcs_json_encode( array_merge( $order_id_key, array( 'subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key ) ) );
}
if ( $order_contains_failed_renewal ) {
$subscription_trial_length = 0;
$subscription_installments = max( $subscription_installments - $subscription->get_completed_payment_count(), 0 );
// If we're changing the payment date or switching subs, we need to set the trial period to the next payment date & installments to be the number of installments left
} elseif ( $is_payment_change || $is_synced_subscription ) {
$next_payment_timestamp = $subscription->get_time( 'next_payment' );
// When the subscription is on hold
if ( false != $next_payment_timestamp && ! empty( $next_payment_timestamp ) ) {
$trial_until = wcs_calculate_paypal_trial_periods_until( $next_payment_timestamp );
$subscription_trial_length = $trial_until['first_trial_length'];
$converted_periods['trial_period'] = $trial_until['first_trial_period'];
$second_trial_length = $trial_until['second_trial_length'];
$second_trial_period = $trial_until['second_trial_period'];
} else {
$subscription_trial_length = 0;
}
// If this is a payment change, we need to account for completed payments on the number of installments owing
if ( $is_payment_change && $subscription_length > 0 ) {
$subscription_installments = max( $subscription_installments - $subscription->get_completed_payment_count(), 0 );
}
} else {
$subscription_trial_length = wcs_estimate_periods_between( $start_timestamp, $trial_end_timestamp, $subscription->trial_period );
}
if ( $subscription_trial_length > 0 ) { // Specify a free trial period
$paypal_args['a1'] = ( $initial_payment > 0 ) ? $initial_payment : 0;
// Trial period length
$paypal_args['p1'] = $subscription_trial_length;
// Trial period
$paypal_args['t1'] = $converted_periods['trial_period'];
// We need to use a second trial period before we have more than 90 days until the next payment
if ( isset( $second_trial_length ) && $second_trial_length > 0 ) {
$paypal_args['a2'] = 0.01; // Alas, although it's undocumented, PayPal appears to require a non-zero value in order to allow a second trial period
$paypal_args['p2'] = $second_trial_length;
$paypal_args['t2'] = $second_trial_period;
}
} elseif ( $initial_payment != $price_per_period ) { // No trial period, but initial amount includes a sign-up fee and/or other items, so charge it as a separate period
if ( 1 == $subscription_installments ) {
$param_number = 3;
} else {
$param_number = 1;
}
$paypal_args[ 'a' . $param_number ] = $initial_payment;
// Sign Up interval
$paypal_args[ 'p' . $param_number ] = $subscription_interval;
// Sign Up unit of duration
$paypal_args[ 't' . $param_number ] = $converted_periods['billing_period'];
}
// We have a recurring payment
if ( ! isset( $param_number ) || 1 == $param_number ) {
// Subscription price
$paypal_args['a3'] = $price_per_period;
// Subscription duration
$paypal_args['p3'] = $subscription_interval;
// Subscription period
$paypal_args['t3'] = $converted_periods['billing_period'];
}
// Recurring payments
if ( 1 == $subscription_installments || ( $initial_payment != $price_per_period && 0 == $subscription_trial_length && 2 == $subscription_installments ) ) {
// Non-recurring payments
$paypal_args['src'] = 0;
} else {
$paypal_args['src'] = 1;
if ( $subscription_installments > 0 ) {
// An initial period is being used to charge a sign-up fee
if ( $initial_payment != $price_per_period && 0 == $subscription_trial_length ) {
$subscription_installments--;
}
$paypal_args['srt'] = $subscription_installments;
}
}
// Don't reattempt failed payments, instead let Subscriptions handle the failed payment
$paypal_args['sra'] = 0;
// Force return URL so that order description & instructions display
$paypal_args['rm'] = 2;
// Reattach the filter we removed earlier
if ( $is_payment_change ) {
add_filter( 'woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
}
}
return $paypal_args;
}
}

View File

@@ -0,0 +1,232 @@
<?php
/**
* PayPal Subscription Switcher Class.
*
* Because PayPal Standard does not support recurring amount or date changes, items can not be switched when the subscription is using a
* profile ID for PayPal Standard. However, PayPal Reference Transactions do allow these to be updated and because switching uses the checkout
* process, we can migrate a subscription from PayPal Standard to Reference Transactions when the customer switches.
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Standard_Switcher {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// Allow items on PayPal Standard Subscriptions to be switch when the PayPal account supports Reference Transactions
add_filter( 'woocommerce_subscriptions_can_item_be_switched', __CLASS__ . '::can_item_be_switched', 10, 3 );
// Sometimes, even if the order total is $0, the cart still needs payment
add_filter( 'woocommerce_cart_needs_payment', __CLASS__ . '::cart_needs_payment' , 100, 2 );
// Update the new payment method if switching from PayPal Standard and not creating a new subscription
add_filter( 'woocommerce_payment_successful_result', __CLASS__ . '::maybe_set_payment_method' , 10, 2 );
// Save old PP standand id on switched orders so that PP recurring payments can be cancelled after successful switch
add_action( 'woocommerce_checkout_update_order_meta', __CLASS__ . '::save_old_paypal_meta', 15, 2 );
// Try to cancel a paypal once the switch has been successfully completed
add_action( 'woocommerce_order_status_changed', __CLASS__ . '::maybe_cancel_paypal_after_switch', 10, 3 );
// Do not allow subscriptions to be switched using PayPal Standard as the payment method
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways', 12, 1 );
}
/**
* Allow items on PayPal Standard Subscriptions to be switch when the PayPal account supports Reference Transactions
*
* Because PayPal Standard does not support recurring amount or date changes, items can not be switched when the subscription is using a
* profile ID for PayPal Standard. However, PayPal Reference Transactions do allow these to be updated and because switching uses the checkout
* process, we can migrate a subscription from PayPal Standard to Reference Transactions when the customer switches, so we will allow that.
*
* @since 2.0
*/
public static function can_item_be_switched( $item_can_be_switch, $item, $subscription ) {
if ( false === $item_can_be_switch && 'paypal' === $subscription->payment_method && WCS_PayPal::are_reference_transactions_enabled() ) {
$is_billing_agreement = wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' );
if ( 'line_item' == $item['type'] && wcs_is_product_switchable_type( $item['product_id'] ) ) {
$is_product_switchable = true;
} else {
$is_product_switchable = false;
}
if ( $subscription->has_status( 'active' ) && 0 !== $subscription->get_date( 'last_payment' ) ) {
$is_subscription_switchable = true;
} else {
$is_subscription_switchable = false;
}
// If the only reason the subscription isn't switchable is because the PayPal profile ID is not a billing agreement, allow it to be switched
if ( false === $is_billing_agreement && $is_product_switchable && $is_subscription_switchable ) {
$item_can_be_switch = true;
}
}
return $item_can_be_switch;
}
/**
* Check whether the cart needs payment even if the order total is $0 because it's a subscription switch request for a subscription using
* PayPal Standard as the subscription.
*
* @param bool $needs_payment The existing flag for whether the cart needs payment or not.
* @param WC_Cart $cart The WooCommerce cart object.
* @return bool
*/
public static function cart_needs_payment( $needs_payment, $cart ) {
$cart_switch_items = WC_Subscriptions_Switcher::cart_contains_switches();
if ( false === $needs_payment && 0 == $cart->total && false !== $cart_switch_items && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
foreach ( $cart_switch_items as $cart_switch_details ) {
$subscription = wcs_get_subscription( $cart_switch_details['subscription_id'] );
if ( 'paypal' === $subscription->payment_method && ! wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' ) ) {
$needs_payment = true;
break;
}
}
}
return $needs_payment;
}
/**
* If switching a subscription using PayPal Standard as the payment method and the customer has entered
* in a payment method other than PayPal (which would be using Reference Transactions), make sure to update
* the payment method on the subscription (this is hooked to 'woocommerce_payment_successful_result' to make
* sure it happens after the payment succeeds).
*
* @param array $payment_processing_result The result of the process payment gateway extension request.
* @param int $order_id The ID of an order potentially recording a switch.
* @return array
*/
public static function maybe_set_payment_method( $payment_processing_result, $order_id ) {
if ( wcs_order_contains_switch( $order_id ) ) {
$order = wc_get_order( $order_id );
foreach ( wcs_get_subscriptions_for_switch_order( $order_id ) as $subscription ) {
if ( 'paypal' === $subscription->payment_method && $subscription->payment_method !== $order->payment_method && false === wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' ) ) {
// Set the new payment method on the subscription
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( isset( $available_gateways[ $order->payment_method ] ) ) {
$subscription->set_payment_method( $available_gateways[ $order->payment_method ] );
}
}
}
}
return $payment_processing_result;
}
/**
* Stores the old paypal standard subscription id on the switch order so that it can be used later to cancel the recurring payment.
*
* Strictly hooked on after WC_Subscriptions_Switcher::add_order_meta()
*
* @param int $order_id
* @param array $posted
* @since 2.0.15
*/
public static function save_old_paypal_meta( $order_id, $posted ) {
if ( wcs_order_contains_switch( $order_id ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'switch' ) );
foreach ( $subscriptions as $subscription ) {
if ( 'paypal' === $subscription->payment_method ) {
$paypal_id = wcs_get_paypal_id( $subscription->id );
if ( ! wcs_is_paypal_profile_a( $paypal_id, 'billing_agreement' ) ) {
update_post_meta( $order_id, '_old_payment_method', 'paypal_standard' );
update_post_meta( $order_id, '_old_paypal_subscription_id', $paypal_id );
}
}
}
}
}
/**
* Cancel subscriptions with PayPal Standard after the order has been successfully switched.
*
* @param int $order_id
* @param string $old_status
* @param string $new_status
* @since 2.0.15
*/
public static function maybe_cancel_paypal_after_switch( $order_id, $old_status, $new_status ) {
$order_completed = in_array( $new_status, array( apply_filters( 'woocommerce_payment_complete_order_status', 'processing', $order_id ), 'processing', 'completed' ) ) && in_array( $old_status, apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'on-hold', 'failed' ) ) );
if ( $order_completed && wcs_order_contains_switch( $order_id ) && 'paypal_standard' == get_post_meta( $order_id, '_old_payment_method', true ) ) {
$old_profile_id = get_post_meta( $order_id, '_old_paypal_subscription_id', true );
if ( ! empty( $old_profile_id ) ) {
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'switch' ) );
foreach ( $subscriptions as $subscription ) {
if ( ! wcs_is_paypal_profile_a( $old_profile_id, 'billing_agreement' ) ) {
$new_payment_method = $subscription->payment_method;
$new_profile_id = get_post_meta( $subscription->id, '_paypal_subscription_id', true ); // grab the current paypal subscription id in case it's a billing agreement
update_post_meta( $subscription->id, '_payment_method', 'paypal' );
update_post_meta( $subscription->id, '_paypal_subscription_id', $old_profile_id );
WCS_PayPal_Status_Manager::suspend_subscription( $subscription );
// restore payment meta to the new data
update_post_meta( $subscription->id, '_payment_method', $new_payment_method );
update_post_meta( $subscription->id, '_paypal_subscription_id', $new_profile_id );
}
}
}
}
}
/**
* Do not allow subscriptions to be switched using PayPal Standard as the payment method
*
* @since 2.0.16
*/
public static function get_available_payment_gateways( $available_gateways ) {
if ( WC_Subscriptions_Switcher::cart_contains_switches() || ( isset( $_GET['order_id'] ) && wcs_order_contains_switch( $_GET['order_id'] ) ) ) {
foreach ( $available_gateways as $gateway_id => $gateway ) {
if ( 'paypal' == $gateway_id && false == WCS_PayPal::are_reference_transactions_enabled() ) {
unset( $available_gateways[ $gateway_id ] );
}
}
}
return $available_gateways;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* PayPal Subscription Status Manager Class.
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Status_Manager extends WCS_PayPal {
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// When a subscriber or store manager changes a subscription's status in the store, change the status with PayPal
add_action( 'woocommerce_subscription_cancelled_paypal', __CLASS__ . '::cancel_subscription' );
add_action( 'woocommerce_subscription_pending-cancel_paypal', __CLASS__ . '::suspend_subscription' );
add_action( 'woocommerce_subscription_expired_paypal', __CLASS__ . '::suspend_subscription' );
add_action( 'woocommerce_subscription_on-hold_paypal', __CLASS__ . '::suspend_subscription' );
add_action( 'woocommerce_subscription_activated_paypal', __CLASS__ . '::reactivate_subscription' );
add_filter( 'wcs_gateway_status_payment_changed', __CLASS__ . '::suspend_subscription_on_payment_changed', 10, 2 );
}
/**
* When a store manager or user cancels a subscription in the store, also cancel the subscription with PayPal.
*
* @since 2.0
*/
public static function cancel_subscription( $subscription ) {
if ( ! wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' ) && self::update_subscription_status( $subscription, 'Cancel' ) ) {
$subscription->add_order_note( __( 'Subscription cancelled with PayPal', 'woocommerce-subscriptions' ) );
}
}
/**
* When a store manager or user suspends a subscription in the store, also suspend the subscription with PayPal.
*
* @since 2.0
*/
public static function suspend_subscription( $subscription ) {
if ( ! wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' ) && self::update_subscription_status( $subscription, 'Suspend' ) ) {
$subscription->add_order_note( __( 'Subscription suspended with PayPal', 'woocommerce-subscriptions' ) );
}
}
/**
* When a store manager or user reactivates a subscription in the store, also reactivate the subscription with PayPal.
*
* How PayPal Handles suspension is discussed here: https://www.x.com/developers/paypal/forums/nvp/reactivate-recurring-profile
*
* @since 2.0
*/
public static function reactivate_subscription( $subscription ) {
if ( ! wcs_is_paypal_profile_a( wcs_get_paypal_id( $subscription->id ), 'billing_agreement' ) && self::update_subscription_status( $subscription, 'Reactivate' ) ) {
$subscription->add_order_note( __( 'Subscription reactivated with PayPal', 'woocommerce-subscriptions' ) );
}
}
/**
* Performs an Express Checkout NVP API operation as passed in $api_method.
*
* Although the PayPal Standard API provides no facility for cancelling a subscription, the PayPal
* Express Checkout NVP API can be used.
*
* @since 2.0
*/
public static function update_subscription_status( $subscription, $new_status ) {
$profile_id = wcs_get_paypal_id( $subscription->id );
if ( wcs_is_paypal_profile_a( $profile_id, 'billing_agreement' ) ) {
// Nothing to do here, leave the billing agreement active at PayPal for use with other subscriptions and just change the status in the store
$status_updated = true;
} elseif ( ! empty( $profile_id ) ) {
// We need to change the subscriptions status at PayPal, which is doing via Express Checkout APIs, despite the subscription having been created with PayPal Standard
$response = self::get_api()->manage_recurring_payments_profile_status( $profile_id, $new_status, $subscription );
if ( ! $response->has_api_error() ) {
$status_updated = true;
} else {
$status_updated = false;
}
} else {
$status_updated = false;
}
return $status_updated;
}
/**
* When changing the payment method on edit subscription screen from PayPal, only suspend the subscription rather
* than cancelling it.
*
* @param string $status The subscription status sent to the current payment gateway before changing subscription payment method.
* @return object $subscription
* @since 2.0
*/
public static function suspend_subscription_on_payment_changed( $status, $subscription ) {
return ( 'paypal' == $subscription->payment_gateway->id ) ? 'on-hold' : $status;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* PayPal Subscription Support Class.
*
* Hook into WooCommerce and Subscriptions to declare support for different subscription features depending
* on the PayPal flavour in use. Site wide, the feature support is based on whether the PayPal account has
* reference transactions enabled or not. However, because we use two flavours of PayPal, both identified with
* the same gateway ID, we also need to hook in to check for feature support on a subscription specific basis.
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_PayPal_Supports {
protected static $standard_supported_features = array(
'subscriptions',
'gateway_scheduled_payments',
'subscription_payment_method_change_customer',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
);
protected static $reference_transaction_supported_features = array(
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'subscription_amount_changes',
'subscription_date_changes',
'multiple_subscriptions',
);
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
// Set the PayPal Standard gateway to support subscriptions after it is added to the woocommerce_payment_gateways array
add_filter( 'woocommerce_payment_gateway_supports', __CLASS__ . '::add_feature_support_for_gateway', 10, 3 );
// Check for specific subscription support based on whether the subscription is using a billing agreement or subscription for recurring payments with PayPal
add_filter( 'woocommerce_subscription_payment_gateway_supports', __CLASS__ . '::add_feature_support_for_subscription', 10, 3 );
}
/**
* Add subscription support to the PayPal Standard gateway only when credentials are set
*
* @since 2.0
*/
public static function add_feature_support_for_gateway( $is_supported, $feature, $gateway ) {
if ( 'paypal' === $gateway->id && WCS_PayPal::are_credentials_set() ) {
if ( in_array( $feature, self::$standard_supported_features ) ) {
$is_supported = true;
} elseif ( in_array( $feature, self::$reference_transaction_supported_features ) && WCS_PayPal::are_reference_transactions_enabled() ) {
$is_supported = true;
}
}
return $is_supported;
}
/**
* Add additional feature support at the subscription level instead of just the gateway level because some subscriptions may have been
* setup with PayPal Standard while others may have been setup with Billing Agreements to use with Reference Transactions.
*
* @since 2.0
*/
public static function add_feature_support_for_subscription( $is_supported, $feature, $subscription ) {
if ( 'paypal' === $subscription->payment_method && WCS_PayPal::are_credentials_set() ) {
$paypal_profile_id = wcs_get_paypal_id( $subscription->id );
$is_billing_agreement = wcs_is_paypal_profile_a( $paypal_profile_id, 'billing_agreement' );
if ( 'gateway_scheduled_payments' === $feature && $is_billing_agreement ) {
$is_supported = false;
} elseif ( in_array( $feature, self::$standard_supported_features ) ) {
if ( wcs_is_paypal_profile_a( $paypal_profile_id, 'out_of_date_id' ) ) {
$is_supported = false;
} else {
$is_supported = true;
}
} elseif ( in_array( $feature, self::$reference_transaction_supported_features ) ) {
if ( $is_billing_agreement ) {
$is_supported = true;
} else {
$is_supported = false;
}
}
}
return $is_supported;
}
}

View File

@@ -0,0 +1,280 @@
<?php
/**
* The old PayPal Standard Subscription Class.
*
* Filtered necessary functions in the WC_Paypal class to allow for subscriptions.
*
* Replaced by WCS_PayPal.
*
* @package WooCommerce Subscriptions
* @subpackage WC_PayPal_Standard_Subscriptions
* @category Class
* @author Brent Shepherd
* @since 1.0
*/
class WC_PayPal_Standard_Subscriptions {
public static $api_username;
public static $api_password;
public static $api_signature;
public static $api_endpoint;
private static $request_handler;
/**
* Set the public properties to make sure we don't trigger any fatal errors even though the class is deprecated.
*
* @since 1.0
*/
public static function init() {
self::$api_username = WCS_PayPal::get_option( 'api_username' );
self::$api_password = WCS_PayPal::get_option( 'api_password' );
self::$api_signature = WCS_PayPal::get_option( 'api_signature' );
self::$api_endpoint = ( 'no' == WCS_PayPal::get_option( 'testmode' ) ) ? 'https://api-3t.paypal.com/nvp' : 'https://api-3t.sandbox.paypal.com/nvp';
}
/**
* Checks if the PayPal API credentials are set.
*
* @since 1.0
*/
public static function are_credentials_set() {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal::are_credentials_set()' );
return WCS_PayPal::are_credentials_set();
}
/**
* Add subscription support to the PayPal Standard gateway only when credentials are set
*
* @since 1.0
*/
public static function add_paypal_standard_subscription_support( $is_supported, $feature, $gateway ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal_Supports::add_feature_support( $is_supported, $feature, $gateway )' );
return WCS_PayPal_Supports::add_feature_support( $is_supported, $feature, $gateway );
}
/**
* When a PayPal IPN messaged is received for a subscription transaction,
* check the transaction details and
*
* @since 1.0
*/
public static function process_paypal_ipn_request( $transaction_details ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal::process_ipn_request( $transaction_details )' );
WCS_PayPal::process_ipn_request( $transaction_details );
}
/**
* Override the default PayPal standard args in WooCommerce for subscription purchases when
* automatic payments are enabled and when the recurring order totals is over $0.00 (because
* PayPal doesn't support subscriptions with a $0 recurring total, we need to circumvent it and
* manage it entirely ourselves.)
*
* @since 1.0
*/
public static function paypal_standard_subscription_args( $paypal_args, $order = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal_Standard_Request::get_paypal_args( $paypal_args, $order )' );
return WCS_PayPal_Standard_Request::get_paypal_args( $paypal_args, $order );
}
/**
* Adds extra PayPal credential fields required to manage subscriptions.
*
* @since 1.0
*/
public static function add_subscription_form_fields() {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal_Admin::add_form_fields()' );
WCS_PayPal_Admin::add_form_fields();
}
/**
* Returns a PayPal Subscription ID/Recurring Payment Profile ID based on a user ID and subscription key
*
* @param WC_Order|WC_Subscription A WC_Order object or child object (i.e. WC_Subscription)
* @since 1.1
*/
public static function get_subscriptions_paypal_id( $order_id, $product_id = '' ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_get_paypal_id( $order_id )' );
return wcs_get_paypal_id( $order_id );
}
/**
* Performs an Express Checkout NVP API operation as passed in $api_method.
*
* Although the PayPal Standard API provides no facility for cancelling a subscription, the PayPal
* Express Checkout NVP API can be used.
*
* @since 1.1
*/
public static function change_subscription_status( $profile_id, $new_status, $order = null ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal::get_api()->manage_recurring_payments_profile_status()' );
return WCS_PayPal::get_api()->manage_recurring_payments_profile_status( $profile_id, $new_status, $order );
}
/**
* Checks a set of args and derives an Order ID with backward compatibility for WC < 1.7 where 'custom' was the Order ID.
*
* @since 1.2
*/
public static function get_order_id_and_key( $args ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_Paypal_Standard_IPN_Handler::get_order_id_and_key()' );
return WCS_Paypal_Standard_IPN_Handler::get_order_id_and_key( $args, 'shop_order' );
}
/**
* If changing a subscriptions payment method from and to PayPal, wait until an appropriate IPN message
* has come in before deciding to cancel the old subscription.
*
* @since 2.0
*/
public static function maybe_remove_subscription_cancelled_callback( $subscription, $new_payment_method, $old_payment_method ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* If changing a subscriptions payment method from and to PayPal, the cancelled subscription hook was removed in
* @see self::maybe_remove_cancelled_subscription_hook() so we want to add it again for other subscriptions.
*
* @since 2.0
*/
public static function maybe_reattach_subscription_cancelled_callback( $subscription, $new_payment_method, $old_payment_method ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Don't update the payment method on checkout when switching to PayPal - wait until we have the IPN message.
*
* @param string $item_name
* @return string
* @since 1.5.14
*/
public static function maybe_dont_update_payment_method( $update, $new_payment_method ) {
_deprecated_function( __METHOD__, '2.0' );
return $update;
}
/**
* In typical PayPal style, there are a couple of important limitations we need to work around:
*
* @since 1.4.3
*/
public static function scheduled_subscription_payment() {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Prompt the store manager to enter their PayPal API credentials if they are using
* PayPal and have yet not entered their API credentials.
*
* @return void
*/
public static function maybe_show_admin_notice() {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* When a store manager or user cancels a subscription in the store, also cancel the subscription with PayPal.
*
* @since 1.1
*/
public static function cancel_subscription_with_paypal( $order, $product_id = '', $profile_id = '' ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal_Status_Manager::cancel_subscription( $subscription )' );
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) ) as $subscription ) {
self::change_subscription_status( $profile_id, 'Cancel', $subscription );
}
}
/**
* When a store manager or user suspends a subscription in the store, also suspend the subscription with PayPal.
*
* @since 1.1
*/
public static function suspend_subscription_with_paypal( $order, $product_id ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal_Status_Manager::suspend_subscription( $subscription )' );
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) ) as $subscription ) {
WCS_PayPal_Status_Manager::suspend_subscription( $subscription );
}
}
/**
* When a store manager or user reactivates a subscription in the store, also reactivate the subscription with PayPal.
*
* How PayPal Handles suspension is discussed here: https://www.x.com/developers/paypal/forums/nvp/reactivate-recurring-profile
*
* @since 1.1
*/
public static function reactivate_subscription_with_paypal( $order, $product_id ) {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal::reactivate_subscription( $subscription )' );
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) ) as $subscription ) {
WCS_PayPal_Status_Manager::reactivate_subscription( $subscription );
}
}
/**
* Don't transfer PayPal customer/token meta when creating a parent renewal order.
*
* @access public
* @param array $order_meta_query MySQL query for pulling the metadata
* @param int $original_order_id Post ID of the order being used to purchased the subscription being renewed
* @param int $renewal_order_id Post ID of the order created for renewing the subscription
* @param string $new_order_role The role the renewal order is taking, one of 'parent' or 'child'
* @return void
*/
public static function remove_renewal_order_meta( $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role ) {
_deprecated_function( __METHOD__, '2.0' );
if ( 'parent' == $new_order_role ) {
$order_meta_query .= ' AND `meta_key` NOT IN ('
. "'Transaction ID', "
. "'Payer first name', "
. "'Payer last name', "
. "'Payment type', "
. "'Payer PayPal address', "
. "'Payer PayPal first name', "
. "'Payer PayPal last name', "
. "'PayPal Subscriber ID' )";
}
return $order_meta_query;
}
/**
* If changing a subscriptions payment method from and to PayPal, wait until an appropriate IPN message
* has come in before deciding to cancel the old subscription.
*
* @since 1.4.6
*/
public static function maybe_remove_cancelled_subscription_hook( $order, $subscription_key, $new_payment_method, $old_payment_method ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* If changing a subscriptions payment method from and to PayPal, the cancelled subscription hook was removed in
* @see self::maybe_remove_cancelled_subscription_hook() so we want to add it again for other subscriptions.
*
* @since 1.4.6
*/
public static function maybe_readd_cancelled_subscription_hook( $order, $subscription_key, $new_payment_method, $old_payment_method ) {
_deprecated_function( __METHOD__, '2.0' );
}
/**
* Takes a timestamp for a date in the future and calculates the number of days between now and then
*
* @since 1.4
*/
public static function calculate_trial_periods_until( $future_timestamp ) {
_deprecated_function( __METHOD__, '2.0', 'wcs_calculate_paypal_trial_periods_until()' );
return wcs_calculate_paypal_trial_periods_until( $future_timestamp );
}
}
add_action( 'init', 'WC_PayPal_Standard_Subscriptions::init', 11 );
/**
* Needs to be called after init so that $woocommerce global is setup
**/
function create_paypal_standard_subscriptions() {
_deprecated_function( __METHOD__, '2.0', 'WCS_PayPal::init()' );
WC_PayPal_Standard_Subscriptions::init();
}

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