Woocommerce action hook to execute function on subscription renewal - wordpress

I want to know if there is a action hook that can check if the subscription is successfully renewed in woocommerce ? BTW I am using woocommerce subscription plugin. I have created a functionality that records the date of the subscription order and add it to a CSV file, the function is working perfectly for the first purchase I mean when the user purchase a subscription it is recorded successfully in the CSV because I am firing up the function on woocommerce_thankyou action hook, The only issue I am facing is that I can't seem to find a hook which can execute this function on successful subscription renewal. I tried to use woocommerce_subscription_renewal_payment_complete action hook but it didn't worked below is the function that I have created.
/**
* Add subscriptions to csv.
*/
add_action( 'woocommerce_subscription_renewal_payment_complete', 'add_subs_to_csv' );
add_action( 'woocommerce_thankyou', 'add_subs_to_csv' );
function add_subs_to_csv( $order_id ) {
$order = wc_get_order( $order_id );
$items = $order->get_items();
foreach ( $items as $key => $value ) {
$meta_values = $value->get_data();
foreach ( $meta_values as $meta_key => $meta_value ) {
if ( $meta_key == 'product_id' && $meta_value == 875 ) {
$paid_date = explode( " ", get_post_meta( $order_id, '_paid_date', true ) );
$subs_paid_date = date( 'd F, Y', strtotime( $paid_date[0] ) );
wc_add_order_item_meta( $key, 'Delivery Date', $subs_paid_date );
}
}
}
}

Could the wcs_renewal_order_created hook be what you're looking for? The docs say:
WooCommerce Subscriptions stores all details of each subscription
renewal in a standard WooCommerce order, only with a special meta flag
linking it to a subscription.
These orders are always created through the wcs_create_renewal_order()
function, regardless of whether they are created for a scheduled
renewal event, manually via the WooCommerce > Edit Subscription
administration screen, or via the Subscriptions endpoints for the
WooCommerce REST API. Because of this, it’s possible to add, remove or
update the value of anything on that renewal order using this filter.
For example, this can be used to add a discount to specific renewal
orders, like the 12th order each year. It could also be used to add
one-time fee for a certain renewal order, like a special annual extra
fee on a monthly subscription.
So the above hook should trigger after payment, you'd probably just need to check if it was completed status which you could also do in your current hooks:
/**
* After WooCommerce Subscriptions Creates Renewal Order
*
* #param WC_Order Object $order
* #param Integer|WC_Subscription Object $subscription
*
* #return WC_Order $order
*/
function add_subs_to_csv( $order, $subscription ) {
if( 'completed' === $order->get_status() ) {
$items = $order->get_items();
foreach ( $items as $key => $value ) {
$meta_values = $value->get_data();
foreach ( $meta_values as $meta_key => $meta_value ) {
if ( $meta_key == 'product_id' && $meta_value == 875 ) {
$paid_date = explode( " ", get_post_meta( $order_id, '_paid_date', true ) );
$subs_paid_date = date( 'd F, Y', strtotime( $paid_date[0] ) );
wc_add_order_item_meta( $key, 'Delivery Date', $subs_paid_date );
}
}
}
}
return $order
}
add_filter( 'wcs_renewal_order_created', 'add_subs_to_csv', 10, 2 );

I had a issue at Subscription renewal and I got it fixed with below code:
/*
* FIXED : Membership got PAUSED everytime at automatic subscription renewal
*/
function change_membership_status_active( $subscription , $order ) {
global $wpdb;
if( 'completed' === $order->get_status() ) {
$membership = $wpdb->get_row( "SELECT * FROM wp_postmeta WHERE meta_key = '_subscription_id' AND meta_value = $subscription->ID" );
$mem_id = $membership->post_id;
$status = 'wcm-active';
$update_args = array( 'ID' => $mem_id, 'post_status' => $status );
wp_update_post($update_args);
}
}
add_action( 'woocommerce_subscription_renewal_payment_complete', 'change_membership_status_active', 10, 2 );

Related

Send Email notifications when a user completes a successful order using a certain code l Woocommerce [duplicate]

How to send order notification to a business partner when a specific coupon is used?
I found a solution for the instance when the coupon is applied here :
Send an email notification when a specific coupon code is applied in WooCommerce
However, I need to find a solution for when after order is submitted, because the order is not always submitted after coupon is applied.
Each coupon will have its own email address.
First we add a setting field to admin Coupon pages, to set an email recipient for for a coupon:
// Add a custom field to Admin coupon settings pages
add_action( 'woocommerce_coupon_options', 'add_coupon_text_field', 10 );
function add_coupon_text_field() {
woocommerce_wp_text_input( array(
'id' => 'email_recipient',
'label' => __( 'Email recipient', 'woocommerce' ),
'placeholder' => '',
'description' => __( 'Send an email notification to a defined recipient' ),
'desc_tip' => true, // Or false
) );
}
// Save the custom field value from Admin coupon settings pages
add_action( 'woocommerce_coupon_options_save', 'save_coupon_text_field', 10, 2 );
function save_coupon_text_field( $post_id, $coupon ) {
if( isset( $_POST['email_recipient'] ) ) {
$coupon->update_meta_data( 'email_recipient', sanitize_text_field( $_POST['email_recipient'] ) );
$coupon->save();
}
}
Then email is sent for each applied coupon to a submitted order, if the email recipient has been set for the applied coupon.
Caution! choose only one of the following functions:
For woocommerce versions Up to 4.3 (new hook)
// For Woocommerce version 4.3+
add_action( 'woocommerce_checkout_order_created', 'custom_email_for_orders_with_applied_coupon' );
function custom_email_for_orders_with_applied_coupon( $order ){
$used_coupons = $order->get_used_coupons();
if( ! empty($used_coupons) ){
foreach ( $used_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code ); // WC_Coupon Object
$recipient = $coupon->get_meta('email_recipient'); // get recipient
if( ! empty($recipient) ) {
$subject = sprintf( __('Coupon "%s" has been applied'), $coupon_code );
$content = sprintf( __('The coupon code "%s" has been applied by a customer'), $coupon_code );
wp_mail( $recipient, $subject, $content ); // Send email
}
}
}
}
Or for all WooCommerce versions (since version 3.0)
// For all Woocommerce versions (since 3.0)
add_action( 'woocommerce_checkout_update_order_meta', 'custom_email_for_orders_with_applied_coupon' );
function custom_email_for_orders_with_applied_coupon( $order_id ){
$order = wc_get_order( $order_id );
$used_coupons = $order->get_used_coupons();
if( ! empty($used_coupons) ){
foreach ( $used_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code ); // WC_Coupon Object
$recipient = $coupon->get_meta('email_recipient'); // get recipient
if( ! empty($recipient) ) {
$subject = sprintf( __('Coupon "%s" has been applied'), $coupon_code );
$content = sprintf( __('The coupon code "%s" has been applied by a customer'), $coupon_code );
wp_mail( $recipient, $subject, $content ); // Send email
}
}
}
}
Code goes in functions.php file of your active child theme (or active theme). Tested and works.

Large WooCommerce query throws fatal memory errors

I have a site that has tens of thousands of orders, which I need to compare the billing and customer/user emails and show a flag if they don't match. One of the stipulations is that I'm unable to add any metadata to the orders. So my solution is to just add a custom column, and compare the emails on the fly when the orders list is rendered. That works just fine.
add_filter( 'manage_edit-shop_order_columns', 'mismatched_orders_column' );
function mismatched_orders_column( $columns ) {
$columns['mismatched'] = 'Mismatched';
return $columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'mismatched_orders_column_data' );
function mismatched_orders_column_data( $column ) {
global $post;
if ( 'mismatched' === $column ) {
$order = new WC_Order( $post->ID );
$customer = $order->get_user();
$result = '';
$billing_email = strtolower ( $order->get_billing_email() );
$customer_email = '';
if ($customer) $customer_email = strtolower ( $customer->user_email );
if ( $customer && ( $billing_email != $customer_email ) ) {
$result = '<span class="mismatched-order" title="Possible order mismatch">Yes</span>';
}
echo $result;
}
}
My issue is when trying to add sorting. Because I'm not accessing any post metadata, I don't have any easy data to sort via the main query. My solution here was originally to hook into pre_get_posts, grab all the orders in a new WP_Query, then loop through them and add the ones that had mismatched emails to an array for use in post__in.
This works/worked fine on my small dev site, but throws fatal memory errors when trying to loop over any more than about 8 or 9 thousand posts (out of a total of 30-40 thousand). Increasing memory isn't really an option.
add_filter( 'manage_edit-shop_order_sortable_columns', 'mismatched_orders_column_sortable');
function mismatched_orders_column_sortable( $columns ) {
$columns['mismatched'] = 'mismatched';
return $columns;
}
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
function mismatched_emails_posts_orderby( $query ) {
if( ! is_admin() || ! $query->is_main_query() ) {
return;
}
//Remove the pre_get_posts hook so we don't get stuck in a loop
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
//Make sure we're only looking at our custom column
if ( 'mismatched' === $query->get( 'orderby') ) {
//Set our initial array for 'post__in'
$mismatched = array();
$orders_list = get_posts(array(
'post_type' => 'shop_order',
'posts_per_page' => -1,
'post_status' => 'any',
'fields' => 'ids'
));
//And here is our problem
foreach( $orders_list as $order_post ) :
//Get our order and customer/user object
$order_object = new WC_Order( $order_post );
$customer = $order_object->get_user();
//Check that billing and customer emails don't match, and also that we're not dealing with a guest order
if ( ( $order_object->get_billing_email() != $customer->user_email ) && $order_object->get_user() != false ) {
$mismatched[] = $order_post;
}
endforeach; wp_reset_postdata();
$query->set( 'post__in', $mismatched );
}
}
I would seriously appreciate any insight into how I could either reduce the expense of the query I'm trying to run, or an alternate approach. Again, just for clarification, adding metadata to the orders isn't an option.
Thanks!

How to send "Customer Invoice" emails with Pay Later (pending status) gateway

I have created a new payment gateway plugin that allows customers to pay later. I will be using it with a woocommerce POS system.
I cannot get the 'customer invoice' email to send automatically - I believe it has something to do with the "pending" status of the order (which it must have in order to allow the customer to pay later). I have analysed many other answers but none have worked for me.
I have gently modified the code from this link (only - changed names and set up as plugin): https://github.com/creativelittledots/woocommerce-pay-later.
It uses a series of functions to trigger the email that I don't fully understand - but it doesn't seem to work: The default status is set to "on- hold" to allow the emails to trigger then the order is set to "pending" to allow the customer to pay for the order.
Here are the relevant snippets:
add_filter( 'woocommerce_default_order_status', array($this, 'default_order_status') );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
add_filter( 'woocommerce_email_format_string_find', array($this, 'order_status_format_string_find') );
add_filter( 'woocommerce_email_format_string_replace', array($this, 'order_status_format_string_replace'), 10, 2 );
add_action( 'woocommerce_order_status_pending', array($this, 'send_pending_order_emails') );
add_filter( 'woocommerce_valid_order_statuses_for_payment', array($this, 'valid_order_statuses_for_payment' ), 10, 2 );
add_action( 'wp', array($this, 'change_order_to_pending_on_order_received'), 8 );`
public function valid_order_statuses_for_payment($statuses, $order) {
if( $order->is_pay_later ) {
$statuses[] = 'on-hold';
}
return $statuses;
}
/**
* Change the default order status to on-hold so that pending order emails can be triggered
*/
public function default_order_status($default) {
if( ! is_admin() && WC()->session->set( 'chosen_payment_method') == $this->id ) {
$default = 'on-hold';
}
return $default;
}
/**
* Allow Order status to be accessible from emails
*/
public function order_status_format_string_find( $find ) {
$find['order-status'] = '{order_status}';
return $find;
}
/**
* Replace Order status in emails
*/
public function order_status_format_string_replace( $replace, $email ) {
if( $email->object ) {
$replace['order-status'] = wc_get_order_status_name( $email->object->get_status() );
}
return $replace;
}
/**
* Trigger pending order emails and invoice email
*/
public function send_pending_order_emails( $order_id ) {
$emails = new WC_Emails();
$order = wc_get_order( $order_id );
$emails->customer_invoice( $order_id );
$emails->emails['WC_Email_New_Order']->trigger( $order_id );
$order->set_payment_method( $this );
}
/**
* WC Shop As Customer support on Order Received, because the default status is on hold we need to change these orders to pending
*/
public function change_order_to_pending_on_order_received() {
if( class_exists('WC_Shop_As_Customer') && ! empty( $_GET['order_on_behalf'] ) && ! empty( $_GET['key'] ) && ! empty( $_GET['send_invoice'] ) ) {
global $wp;
if ( ! isset( $wp->query_vars['order-received'] ) )
return;
// Bail if we're not shopping-as - don't display the special interface.
if ( ! WC_Shop_As_Customer::get_original_user() )
return;
$order_id = $wp->query_vars['order-received'];
if ( ! empty( $order_id ) ) {
$order = new WC_Order( absint( $order_id) );
$order->update_status( 'pending' );
}
unset( $_GET['send_invoice'] );
}
}
I also read that woocommerce_order_status_pending may not trigger emails so I have tried adding this code (no successes):
add_filter( 'woocommerce_email_actions', 'filter_woocommerce_email_actions' );
function filter_woocommerce_email_actions( $actions ){
$actions[] = 'woocommerce_order_status_pending';
return $actions;
}
I suspect either:
The code to put the order 'on hold' then trigger then back to 'pending' is not set up correctly
woocommerce_order_status_pending hook does not trigger emails
I have a little work around to trigger from the "woocommerce_thankyou" hook in functions.php but I believe that this should be done in the gateway (plus I don't want to rely on woocommerce_thankyou - this email should be based on order creation).
Thanks in advance!!!!!!

WooCommerce New order email manually trigger for some products

Please i find this code as solution for WooCommerce New order email manually trigger for some products but I don't know where exactly putting this code in functions.php or else thanks.
/**
* Modified from https://www.skyverge.com/blog/add-woocommerce-email-recipients-conditionally/
*
* Add another email recipient for admin New Order emails if a product from a specific category or with a specific tag is ordered
*
* #param string $recipient a comma-separated string of email recipients (will turn into an array after this filter!)
* #param \WC_Order $order the order object for which the email is sent
* #return string $recipient the updated list of email recipients
*/
function sv_conditional_email_recipient( $recipient, $order ) {
// Bail on WC settings pages since the order object isn't yet set yet
// Not sure why this is even a thing, but shikata ga nai
$page = $_GET['page'] = isset( $_GET['page'] ) ? $_GET['page'] : '';
if ( 'wc-settings' === $page ) {
return $recipient;
}
// just in case
if ( ! $order instanceof WC_Order ) {
return $recipient;
}
$items = $order->get_items();
// check if product from category or with tag is in order
foreach ( $items as $item ) {
$product = $order->get_product_from_item( $item );
$args = array('orderby' => 'name', 'order' => 'ASC', 'fields' => 'names');
$product_cats = wp_get_post_terms( $product->get_id, 'product_cat', $args ); // could swap product_cat for product_tag
// add our extra recipient if there's a product from the category with slug "dieta" - commas needed!
// we can bail if we've found one, no need to add the recipient more than once
if ( $product && in_array( "dieta", $product_cats ) ) {
$recipient .= ', notify#example.com';
return $recipient;
}
}
return $recipient;
}
add_filter( 'woocommerce_email_recipient_new_order', 'sv_conditional_email_recipient', 10, 2 );
I code this one and it works on my website, don't forget to replace the_name_of_the_cat by the name of the category and first_email#domain.com,second_email#domain.com by the two emails.
function change_email_recipient_depending_of_cat ( $recipient, $order ) {
global $woocommerce;
$items = $order->get_items();
foreach ( $items as $item ) {
$product_id = $item['product_id'];
if ( has_term( 'the_name_of_the_cat', 'product_cat', $product_id ) ) {
$recipient = 'first_email#domain.com,second_email#domain.com';
}
return $recipient;
}
}
add_filter( 'woocommerce_email_recipient_new_order', 'change_email_recipient_depending_of_cat', 10, 2 );

WooCommerce: how to add multiple products to cart at once?

I need a "get products A, B and C for $xxx" special offer, products A, B and C must be available on their own, and the bundle is a special offer accessible through a coupon code.
On a marketing page hosting outside my site, I would like a button leading to my site that carries a query string like ?add-to-cart=244,249,200 so that once on my site, all bundle products are already added to the cart (instead of adding them one by one which sounds unacceptably tedious).
If not possible, then at least I'd like a landing page on my site with a single button adding all bundle products to cart at once.
I couldn't find working solutions googling around (here's one example). Any suggestion?
After some research I found that DsgnWrks wrote a hook that does exactly this. For your convenience, and in case the blog goes offline, I bluntly copied his code to this answer:
function woocommerce_maybe_add_multiple_products_to_cart( $url = false ) {
// Make sure WC is installed, and add-to-cart qauery arg exists, and contains at least one comma.
if ( ! class_exists( 'WC_Form_Handler' ) || empty( $_REQUEST['add-to-cart'] ) || false === strpos( $_REQUEST['add-to-cart'], ',' ) ) {
return;
}
// Remove WooCommerce's hook, as it's useless (doesn't handle multiple products).
remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'add_to_cart_action' ), 20 );
$product_ids = explode( ',', $_REQUEST['add-to-cart'] );
$count = count( $product_ids );
$number = 0;
foreach ( $product_ids as $id_and_quantity ) {
// Check for quantities defined in curie notation (<product_id>:<product_quantity>)
// https://dsgnwrks.pro/snippets/woocommerce-allow-adding-multiple-products-to-the-cart-via-the-add-to-cart-query-string/#comment-12236
$id_and_quantity = explode( ':', $id_and_quantity );
$product_id = $id_and_quantity[0];
$_REQUEST['quantity'] = ! empty( $id_and_quantity[1] ) ? absint( $id_and_quantity[1] ) : 1;
if ( ++$number === $count ) {
// Ok, final item, let's send it back to woocommerce's add_to_cart_action method for handling.
$_REQUEST['add-to-cart'] = $product_id;
return WC_Form_Handler::add_to_cart_action( $url );
}
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $product_id ) );
$was_added_to_cart = false;
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
continue;
}
$add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart );
// Variable product handling
if ( 'variable' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_variable', $product_id );
// Grouped Products
} elseif ( 'grouped' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_grouped', $product_id );
// Custom Handler
} elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ){
do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url );
// Simple Products
} else {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_simple', $product_id );
}
}
}
// Fire before the WC_Form_Handler::add_to_cart_action callback.
add_action( 'wp_loaded', 'woocommerce_maybe_add_multiple_products_to_cart', 15 );
/**
* Invoke class private method
*
* #since 0.1.0
*
* #param string $class_name
* #param string $methodName
*
* #return mixed
*/
function woo_hack_invoke_private_method( $class_name, $methodName ) {
if ( version_compare( phpversion(), '5.3', '<' ) ) {
throw new Exception( 'PHP version does not support ReflectionClass::setAccessible()', __LINE__ );
}
$args = func_get_args();
unset( $args[0], $args[1] );
$reflection = new ReflectionClass( $class_name );
$method = $reflection->getMethod( $methodName );
$method->setAccessible( true );
$args = array_merge( array( $class_name ), $args );
return call_user_func_array( array( $method, 'invoke' ), $args );
}
It works just like you'd expect, by providing a comma separated list of products. It even works with quantities using ?add-to-cart=63833:2,221916:4
I was, and am still looking for a 'pure' solution that allows to add multiple products to the cart without having to install a plugin or add custom actions. But for many, the above might be an appropriate solution

Resources