WooCommerce Subscriptions - Only Allow user one active subscription - wordpress

I am in the process of building a membership / subscription based site for a client of mine and we are using Woocommerce Subscriptions and Woocommerce Memberships plugin.
Now the problem is the my client is building a few promo pages which basically allows the user to purchase an upgrade. This is fine but my client only wants one unique subscription by customer (with its associated membership).
So the agreed solution is that, on a purchase of any new subscription product, all other subscriptions should be cancelled. All associated membership deleted/cancelled and only the latest subscription should remain active with its accompanying membership.
So I have tried to build this solution but it is just not working, so any advise/direction would be most welcome.
What I have tried:
function wp56908_new_order_housekeeping ($order_id)
{
$args = array(
'subscriptions_per_page' => -1,
'customer_id' => get_current_user_id(),
);
$subscriptions = wcs_get_subscriptions($args);
foreach ($subscriptions as $subscription) {
$s_order_id = method_exists( $subscription, 'get_parent_id' ) ? $subscription->get_parent_id() : $subscription->order->id;
if ($s_order_id != $order_id) {
$cancel_note = 'Customer purchased new subscription in order #' . $order_id;
$subscription->update_status( 'cancelled', $cancel_note );
}
}
}
add_action( 'woocommerce_thankyou', 'wp56908_new_order_housekeeping', 10, 1 );

This is the support email I got from WooCommerce regarding the issue with the function wcs_get_subscriptions().
Thanks for contacting support and report us this issue!
I've tried it in a local install and I confirm that this seems to be a
bug! I've already reported it to our development team and a patch
should be included in next updates. By now, if you're in a real hurry
and want to get it working, you could search for this code (in
wcs-functions.php line 483):
// We need to restrict subscriptions to those which contain a certain
product/variation if ( ( 0 != $args['product_id'] && is_numeric(
$args['product_id'] ) ) || ( 0 != $args['variation_id'] && is_numeric(
$args['variation_id'] ) ) ) { $query_args['post__in'] =
wcs_get_subscriptions_for_product( array( $args['product_id'],
$args['variation_id'] ) ); }
And replace it with something like:
// We need to restrict subscriptions to those which contain a certain
product/variation if ( ( 0 != $args['product_id'] && is_numeric(
$args['product_id'] ) ) || ( 0 != $args['variation_id'] && is_numeric(
$args['variation_id'] ) ) ) { $prod_args = array( $args['product_id'],
$args['variation_id'] ); $prod_args = array_filter($prod_args,
function($prod_args) { return ($prod_args !== 0); });
$query_args['post__in'] = wcs_get_subscriptions_for_product(
$prod_args ); }
Please take in mind that this is not the ideal solution because you'll
be modifying our plugin's code directly and these changes can be lost
after updating, but if you feel comfortable with code and want to take
the risk as a temporary workaround until we add the proper patch in
our plugin, you can do it. ;)
Let me know if you need any help with this!
Thanks, Bernat

There's a new setting in the WC Subscriptions plugin which lets you limit to one active subscription. You can find more information here: https://woocommerce.com/document/subscriptions/store-manager-guide/#:~:text=Limit%20subscriptions,the%20subscription%20from%20the%20dropdown.
The end user will get the message:
“You have a subscription to this product. Choosing a new subscription will replace your existing subscription.”

Related

Hook "wp_trash_post" executing multiple time in case of bulk trash products

I am currently using hook wp_trash_post to trigger my custom API when a WooCommerce Product is trashed.
This is working fine for the case of single trash.
But, in case of Bulk trash this event is executing multiple times. for example if 3 products are selected to trash then this event will trigger 3 times.
I need different event for Bulk Trash which will execute one time but having all of product ids.
Hopefully it is clear to understand.
Please Help!
Your API call must be withing the foreach loop and is therefore being run foreach post ID. You can capture the IDs in a variable and execute your API call after the if and foreach statements are complete:
function wpdocs_trash_multiple_posts( $post_id = '' ) {
$trashed_ids = [];
if ( isset( $_GET['post'] ) && is_array( $_GET['post'] ) ) {
foreach ( $_GET['post'] as $post_id ) {
$trashed_ids[] = $post_id;
}
} else {
$trashed_ids[] = $post_id;
}
// Do something here, $trashed_ids array
// contains either one, or several IDs
}
add_action( 'wp_trash_post', 'wpdocs_trash_multiple_posts' );

woocommerce_before_order_object_save hook not work [duplicate]

Hello I'm trying to create a function in the mu-plugins to prevent certain users to change order status from specific order statuses to specific order statuses.
I've been looking everywhere and I have tried many different ways, but nothing seems to work.
Actually the function is running using woocommerce_order_status_changed action hook. The thing is that this hook runs after the order status has already been changed, what's causing an infinite loop.
The most useful hook I found seems to be woocommerce_before_order_object_save.
I found "Add an additional argument to prevent 'woocommerce_order_status_changed' from being called" useful related thread on WooCommerce Github.
I tried using #kloon code snippet solution:
add_filter( 'woocommerce_before_order_object_save', 'prevent_order_status_change', 10, 2 );
function prevent_order_status_change( $order, $data_store ) {
$changes = $order->get_changes();
if ( isset( $changes['status'] ) ) {
$data = $order->get_data();
$from_status = $data['status'];
$to_status = $changes['status'];
// Do your logic here and update statuses with CRUD eg $order->set_status( 'completed' );
// Be sure to return the order object
}
return $order;
}
but $changes variable is always an empty array.
I tried to use wp_insert_post_data Wordpress hook, but when I set:
$data['post_status'] = "some status";
it just prevents the whole update (the whole new data) from being saved.
This is the code I would like to run is:
function($data){
if($data['order_status'] == 'comlpeted' && $data['new_order_status'] == 'proccessing'){
// prevent the order status from being changed
$data['new_order_status'] = $data['order_status'];
}
few more if conditions...
return $data;
}
Any help or advise is appreciated.
Based on #kloon code snippet, I have been able to get the old order status and the new order status. Then I can disable any status change from a specific defined order status to a specific defined order status.
With the following code, specific defined user roles can't change order status from "processing" to "on-hold":
add_filter( 'woocommerce_before_order_object_save', 'prevent_order_status_change', 10, 2 );
function prevent_order_status_change( $order, $data_store ) {
// Below define the disallowed user roles
$disallowed_user_roles = array( 'shop_manager');
$changes = $order->get_changes();
if( ! empty($changes) && isset($changes['status']) ) {
$old_status = str_replace( 'wc-', '', get_post_status($order->get_id()) );
$new_status = $changes['status'];
$user = wp_get_current_user();
$matched_roles = array_intersect($user->roles, $disallowed_user_roles);
// Avoid status change from "processing" to "on-hold"
if ( 'processing' === $old_status && 'on-hold' === $new_status && ! empty($matched_roles) ) {
throw new Exception( sprintf( __("You are not allowed to change order from %s to %s.", "woocommerce" ), $old_status, $new_status ) );
return false;
}
}
return $order;
}
Code goes in functions.php file of the active child theme (or active theme). Tested and works.

Woocommerce - Edit account issue

My Woocommerce is setup to generate username automatically. I'm trying to use the code below to change username before save. I would like to change user name to be equal a custom field filled in billing form.
My code is:
function wc_cpf_as_username ( $user_login ) {
if( !empty($_POST['billing_cpf'] ) ) {
$user_login = $_POST['billing_cpf'];
}
elseif (!empty( $_POST['billing_cnpj'] )){
$user_login = $_POST['billing_cnpj'];
}
else{
$user_login = $_POST['billing_email'];
}
return $user_login;
}
add_filter( 'pre_user_login' , 'wc_cpf_as_username' );
The code work to create user, but this code do not work to edit user in my account page (/my-account/edit-account). Woocommerce show success message (Account details changed successfully.), but data is not changed.
I do not know what is the issue.
Could you help me?
Why you are making that complex function if you have a hook available for this. edit_user_profile_update hook i.e. located in /wp-admin/user-edit.php.
update_user_meta($user_id, 'custom_meta_key', $_POST['custom_meta_key']).
update_user_meta thats for update user meta field based on user ID.
add_action('edit_user_profile_update', 'update_extra_profile_fields');
function update_extra_profile_fields($user_id) {
if ( current_user_can('edit_user',$user_id) )
update_user_meta($user_id, 'Custom_field', $_POST['your_field']);
}

WooCommerce Eway plugin not creating customer token for new customer

I have setup a fresh docker container with Wordpress 5.0.3 and the latest WC and WC Eway plugin (WooCommerce eWAY Gateway).
Created a store with some products, hooked up my Eway sandbox environment, enabled Save Cards (which would enable the token) and created an order.
After checking the post_meta in my DB for the order, I didn't see a _eway_token_customer_id field. While being logged in as a customer, I tried again and with the new order I still do not get a token.
The reason for this tests is that I got this strange behaviour in my real, new website, where the first order with a NEW customer, doesn't result in a token.
However, when I create a second order whilst being logged in, I do get a _eway_token_customer_id value within the order_meta.
It is imperative for me to get that token with the first order, because after that I will auto renew the product using the tokenp ayment option.
Debugging this issue is hell, and I find it very disconcerting that on my fresh WP installation I get no token at all.
Is there anyone that has a bright idea?
**update
After some digging around in the Eway Plugin, I found out that the first time I do an order, the function request_access_code() from the class WC_Gateway_EWAY is checking if there is a token in the database for this user.
The function body:
protected function request_access_code( $order ) {
$token_payment = $this->get_token_customer_id( $order );
if ( $token_payment && 'new' === $token_payment ) {
$result = json_decode( $this->get_api()->request_access_code( $order, 'TokenPayment', 'Recurring' ) );
} elseif ( 0 === $order->get_total() && 'shop_subscription' === ( version_compare( WC_VERSION, '3.0', '<' ) ? $order->order_type : $order->get_type() ) ) {
$result = json_decode( $this->get_api()->request_access_code( $order, 'CreateTokenCustomer', 'Recurring' ) );
} else {
$result = json_decode( $this->get_api()->request_access_code( $order ) );
}
if ( isset( $result->Errors ) && ! is_null( $result->Errors ) ) {
throw new Exception( $this->response_message_lookup( $result->Errors ) );
}
return $result;
}
The function handles three possible outcomes:
1) new customer: results in calling `$this->get_api()->request_access_code( $order, 'TokenPayment', 'Recurring' )` <-- this is the one we are after!
2) shop_subscription: calls `$this->get_api()->request_access_code( $order, 'CreateTokenCustomer', 'Recurring' )`
3) else..: calls `$this->get_api()->request_access_code( $order )`
What is happening during debugging, is that the $token_payment variable has the value of an empty string for a new customer, instead of new.
So I will attempt to fix this, either via a filter/action hook, or figure out why this is happening.
When I forced the function the always use the first if block, I got my token. :)
**Update 2:
I tested with an existing user account, created a new order.
When I look in the post_meta table:
Voila, the new value is present.
However, when I am not logged in and I create an account, the new value is not added and that is where it goes wrong.
A temp fix would be to use a hook and add the new value to the order so that when get_token_customer_id is called it retrieves a new value and not an empty string.
I think this is a bug, since this value should be added. It explains why the second transactions get the token but not the first.
If only Woocommerce Eway plugin had a git repo.... I could flag an issue or fork it.
***Solution without hack
Added this to my plugin (or functions.php if you like):
add_action( 'woocommerce_checkout_order_processed', function( $order_id, $posted_data, $order ) {
update_post_meta( $order_id, '_eway_token_customer_id', 'new' );
}, 10, 3);
This will add the new value when you checkout with a non-existent user.
The token was added nicely after adding my creditcard details.
The matter of the fact stays that the plugin still has a bug, which you can work around.

Regenerating WooCommerce Download Permissions on older orders

I am trying to add some download permissions to all previous orders via a script to do them in batch. The script seems to work fine expect for one thing. Here is the script…
function update_download_permissions(){
$orders = get_posts( array(
'post_type' => 'shop_order',
'post_status' => 'wc-completed',
'posts_per_page' => -1
) );
foreach ( $orders as $order ) {
wc_downloadable_product_permissions( $order->ID, true );
}
}
The problem is the wc_downloadable_product_permissions function is producing duplicate entries in the wp_woocommerce_downloadable_product_permissions table.
I tried to set the second argument to false (the default) but that resulted in no permissions being created.
Does anybody have any ideas as to why duplicate download permissions are being set?
Cheers!
I came across your question after digging through some of the WooCommerce source code, while attempting to add an item to an existing order and then regenerate the permissions.
The reason wc_downloadable_product_permissions() will create duplicate permission entries is because it does not check for any existing permissions. It simply inserts another entry into the permissions table for every item in the order, which is no good because this will then show up as another download in both the admin and user account frontend.
The second force parameter (poorly documented), is related to a boolean flag that indicates whether wc_downloadable_product_permissions() has run before. The boolean is set to true at the end of the function via the set_download_permissions_granted method. If force is true, it will ignore the boolean. If force is false, and the boolean is true, the function will return near the start.
I created this function which uses the same functions as used by the admin Order action "Regenerate download permissions":
/**
* Regenerate the WooCommerce download permissions for an order
* #param Integer $order_id
*/
function regen_woo_downloadable_product_permissions( $order_id ){
// Remove all existing download permissions for this order.
// This uses the same code as the "regenerate download permissions" action in the WP admin (https://github.com/woocommerce/woocommerce/blob/3.5.2/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php#L129-L131)
// An instance of the download's Data Store (WC_Customer_Download_Data_Store) is created and
// uses its method to delete a download permission from the database by order ID.
$data_store = WC_Data_Store::load( 'customer-download' );
$data_store->delete_by_order_id( $order_id );
// Run WooCommerce's built in function to create the permissions for an order (https://docs.woocommerce.com/wc-apidocs/function-wc_downloadable_product_permissions.html)
// Setting the second "force" argument to true makes sure that this ignores the fact that permissions
// have already been generated on the order.
wc_downloadable_product_permissions( $order_id, true );
}
I found the best way to update order download is to hook into the save_post action hook and check if it's a product that's being updated
there you can get order ids by product id and update just orders that relate to that specific product.
it's more efficient
function get_orders_ids_by_product_id($product_id) {
global $wpdb;
$orders_statuses = "'wc-completed', 'wc-processing', 'wc-on-hold'";
return $wpdb->get_col(
"
SELECT DISTINCT woi.order_id
FROM {$wpdb->prefix}woocommerce_order_itemmeta as woim,
{$wpdb->prefix}woocommerce_order_items as woi,
{$wpdb->prefix}posts as p
WHERE woi.order_item_id = woim.order_item_id
AND woi.order_id = p.ID
AND p.post_status IN ( $orders_statuses )
AND woim.meta_key IN ( '_product_id', '_variation_id' )
AND woim.meta_value LIKE '$product_id'
ORDER BY woi.order_item_id DESC"
);
}
// if you don't add 3 as as 4th argument, this will not work as expected
add_action('save_post', 'prefix_on_post_update', 10, 3);
function prefix_on_post_update($post_id, $post, $update) {
if ($post->post_type == 'product') {
$orders_ids = get_orders_ids_by_product_id($post_id);
foreach ($orders_ids as $order_id) {
$data_store = WC_Data_Store::load('customer-download');
$data_store->delete_by_order_id($order_id);
wc_downloadable_product_permissions($order_id, true);
}
}
}

Resources