How can I get the payment details from Paypal like PaymentID, PaymentFirstName/LastName, and other details?
The code PayPal Standard integration uses valid-paypal-standard-ipn-request action to process the valid IPN response. You can use the same action to hook into the IPN and get/store any information you want.
To save additional information:
// Hook before the code has processed the order
add_action( 'valid-paypal-standard-ipn-request', 'prefix_process_valid_ipn_response', 9 );
function prefix_process_valid_ipn_response( $posted ) {
if ( ! empty( $posted['custom'] ) && ( $order = prefix_get_paypal_order( $posted['custom'] ) ) ) {
// Lowercase returned variables.
$posted['payment_status'] = strtolower( $posted['payment_status'] );
// Any status can be checked here
if ( 'completed' == $posted['payment_status'] ) {
// Save additional information you want
}
}
}
/**
* From the Abstract "WC_Gateway_Paypal_Response" class
*
* #param $raw_custom
*
* #return bool|WC_Order|WC_Refund
*/
function prefix_get_paypal_order( $raw_custom ) {
// We have the data in the correct format, so get the order.
if ( ( $custom = json_decode( $raw_custom ) ) && is_object( $custom ) ) {
$order_id = $custom->order_id;
$order_key = $custom->order_key;
// Nothing was found.
} else {
return false;
}
if ( ! $order = wc_get_order( $order_id ) ) {
// We have an invalid $order_id, probably because invoice_prefix has changed.
$order_id = wc_get_order_id_by_order_key( $order_key );
$order = wc_get_order( $order_id );
}
if ( ! $order || $order->get_order_key() !== $order_key ) {
return false;
}
return $order;
}
You can find the PayPal variables here: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/#id08CKFJ00JYK
The WC core also saves to the order a lot of the IPN data already. All data is saved to the order meta, so you can access it using get_post_meta or $order->get_meta('meta_key').
List by meta_key:
'Payer PayPal address' - The payer address
'Payer first name' - Payer first name
'Payer last name' - Payer last name
'Payment type' - Payment Type
'_paypal_status' - PayPal payment status
Related
So I'm building a custom payment gateway for WooCommerce which has been... challenging, mostly because I'm not a PHP developer so I have been learning as I go. I've started by dissecting the PayPal Standard Gateway which ships with WooCommerce. That gateway has an included class for handling IPN callbacks and I'm having trouble understanding why their code is set up the following way:
/**
* Constructor.
*
* #param bool $sandbox Use sandbox or not.
* #param string $receiver_email Email to receive IPN from.
*/
public function __construct( $sandbox = false, $receiver_email = '' ) {
add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_response' ) );
add_action( 'valid-paypal-standard-ipn-request', array( $this, 'valid_response' ) );
$this->receiver_email = $receiver_email;
$this->sandbox = $sandbox;
}
/**
* Check for PayPal IPN Response.
*/
public function check_response() {
if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok.
$posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
do_action( 'valid-paypal-standard-ipn-request', $posted );
exit;
}
wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) );
}
/**
* There was a valid response.
*
* #param array $posted Post data after wp_unslash.
*/
public function valid_response( $posted ) {
$order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false;
if ( $order ) {
// Lowercase returned variables.
$posted['payment_status'] = strtolower( $posted['payment_status'] );
WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() );
WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] );
if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) {
call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted );
}
}
}
The first add_action registers a hook with a WooCommerce action and I totally get that - it's a way to extend functionality in an external package. However, the second add_action( 'valid-paypal-standard-ipn-request'...) seems to simply call valid_response() - which is a local function - inside the next function check_response(). Why couldn't they just call valid_response() directly within check_response()?
I get several orders where a customer selects "Direct Bank Transfer" and then they change their mind and want to pay by Credit Card. This is quite annoying because I have to manually change the order from "On Hold" to "Pending Payment" so they can pay by card via the "order-pay" endpoint which is found in "My Account" under "Orders".
I've been using the WooCommerce change order status BACS processing to automatically change the order status from "On Hold" to "Pending Payment".
// WooCommerce Change Order Status BACS Pending
add_action( 'woocommerce_thankyou', 'bacs_order_payment_pending_order_status', 10, 1 );
function bacs_order_payment_pending_order_status( $order_id ) {
if ( ! $order_id ) {
return;
}
// Get an instance of the WC_Order object
$order = new WC_Order( $order_id );
if ( ( get_post_meta($order->id, '_payment_method', true) == 'bacs' ) && ('on-hold' == $order->status ) ) {
$order->update_status('pending');
} else {
return;
}
}
But since I have several user profiles (I sell B2B as well), this is not practical for my shop. I'm trying to expand this snippet to also check for the user role. I've used the following in my other snippets. Is it possible to add the below logic to the snippet above?
$user = wp_get_current_user();
$roles = (array) $user->roles;
$roles_to_check = array('administrator', 'customer', 'shop_manager');
$compare = array_diff($roles, $roles_to_check);
if (empty($compare)){
This is my attempt.
// WooCommerce Change Order Status BACS Pending
add_action( 'woocommerce_thankyou', 'bacs_order_payment_pending_order_status', 10, 1 );
function bacs_order_payment_pending_order_status( $order_id ) {
if ( ! $order_id ) {
return;
}
// Get an instance of the WC_Order object
$order = new WC_Order( $order_id );
$user = wp_get_current_user();
$roles = (array) $user->roles;
$roles_to_check = array('administrator', 'customer', 'shop_manager');
$compare = array_diff($roles, $roles_to_check);
if (empty($compare)){
if ( ( get_post_meta($order->id, '_payment_method', true) == 'bacs' ) && ('on-hold' == $order->status ) ) {
$order->update_status('pending');
} else {
return;
}
}
You can use this as follows, comment with explanation added in the code
function bacs_order_payment_pending_order_status( $order_id ) {
// Get $order object
$order = wc_get_order( $order_id );
// Is a WC_Order
if ( is_a( $order, 'WC_Order' ) ) {
// Get user
$user = $order->get_user();
// Roles
$roles = (array) $user->roles;
// Roles to check
$roles_to_check = array( 'administrator', 'customer', 'shop_manager' );
// Compare
$compare = array_diff( $roles, $roles_to_check );
// Result is empty
if ( empty ( $compare ) ) {
if ( $order->get_payment_method() == 'bacs' && $order->has_status( 'on-hold' ) ) {
$order->update_status( 'pending' );
}
}
}
}
add_action( 'woocommerce_thankyou', 'bacs_order_payment_pending_order_status', 10, 1 );
Might Come in handy: WooCommerce: Get Order Info (total, items, etc) From $order Object
Woocommerce version 3.4.0 has introduced a much better hook that allows to change the default status for BACS payment gateway which is set to "on-hold".
Using this hook will:
Lighten your code,
Avoid "on-hold" notification to the customer when a BACS order is placed.
Here is that code:
add_filter( 'woocommerce_bacs_process_payment_order_status','filter_process_payment_order_status_callback', 10, 2 );
function filter_process_payment_order_status_callback( $status, $order ) {
// Here set the user roles to check
$roles_to_check = array( 'administrator', 'customer', 'shop_manager' );
$user = $order->get_user(); // Get the WP_User Object
$compare = array_diff( $user->roles, $roles_to_check ); // compare
if ( empty ( $compare ) ) {
return 'pending';
}
return $status;
}
Code goes in functions.php file of the active child theme (or active theme). Tested and works.
Since WooCommerce 5+: Allow re-sending New Order Notification in WooCommerce 5+
Enabling New Order email notification (sent to the admin) for BACS payments:
As pending Orders doesn't send email notifications, you can enable that with the following
add_action( 'woocommerce_checkout_order_processed', 'pending_new_order_notification', 20, 1 );
function pending_new_order_notification( $order_id ) {
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Only for "pending" order status and BACS payments
if( $order->has_status( 'pending' ) && $order->get_payment_method() === 'bacs' )
{
// Send "New Email" notification (to admin)
WC()->mailer()->get_emails()['WC_Email_New_Order']->trigger( $order_id );
}
}
Code goes in functions.php file of the active child theme (or active theme). Tested and works.
Related: Send an Email notification to the admin for pending order status in WooCommerce
Useful: How to get WooCommerce order details
Related: Change default WooCommerce order status to processing for cheque and bacs payments
Freshly updated answer thread: WooCommerce change order status BACS processing
I am running a function whereby I check if a user has a refunded item/s on their previous order and if they do then apply a credit as a negative cart fee at checkout. The function is working for users who have placed an order before but is causing a critical error on the site for new users.
Fatal error: Uncaught Error: Call to a member function get_refunds() on bool in /wp-content/themes/my-theme/refunds.php:20 Stack trace:
#0 /wp-includes/class-wp-hook.php(287): add_last_order_refund_total_at_checkout(Object(WC_Cart))
#1 /wp-includes/class-wp-hook.php(311): WP_Hook->apply_filters('', Array)
#2 /wp-includes/plugin.php(478): WP_Hook->do_action(Array)
#3 /wp-content/plugins/woocommerce/includes/class-wc-cart.php(1714): do_action('woocommerce_car...', Object(WC_Cart))
#4 /wp-content/plugins/woocommerce/includes/class-wc-cart-totals.php(270): WC_Cart->calculate_fees()
#5 /wp-content/plugins/woocommerce/includes/class-wc-cart-totals.php(829): WC_Cart_Totals->get_fees_from_cart()
#6 /wp-content/plugins/woocommerce/includes/class-wc- in /wp-content/themes/my-theme/refunds.php on line 20
Here is the code to check for any refunds on the previous order:
//Check total of refunded items from last order - add as a fee at checkout
function add_last_order_refund_total_at_checkout($cart_object){
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
$user_id = get_current_user_id(); // The current user ID
$customer = new WC_Customer( $user_id );
$last_order = $customer->get_last_order();
$order_id = $last_order;
// Get the WC_Order Object instance (from the order ID)
$order = wc_get_order( $order_id );
// Get the Order refunds (array of refunds)
$order_refunds = $order->get_refunds();
$total_to_refund = $order->get_total_refunded()*(-1);//WIP need to check which items have tax
if (!empty($order_refunds)) WC()->cart->add_fee( 'Refund', $total_to_refund );
}
add_action( 'woocommerce_cart_calculate_fees', 'add_last_order_refund_total_at_checkout', 10, 1 );
I believe I need to first check to see if a user has any previous orders otherwise the get_refund() is causing an error as their aren't any orders to check? How would I safely do this?
If you look to WC_Customer get_last_order() method documentation or source code, you will see:
/*
* #return WC_Order|false
*/
which means that get_last_order() method can return alternatively:
the WC_Order object
or false boolean value.
So you can just use in your code:
$last_order = $customer->get_last_order();
if ( ! $last_order ) {
return; // Exit (Not an order)
}
To avoid this error.
Now you can use is_a() php conditional functions to check that a variable is a from a specific Object class and not something else like:
$last_order = $customer->get_last_order();
if ( ! is_a( $last_order, 'WC_Order' ) ) {
return; // Exit (Not an order)
}
// Your other code…
Or you can use method_exists() php conditional functions for WC_Order get_refunds() method, to check that the a variable is a from a specific Object class and not something else:
$last_order = $customer->get_last_order();
if ( ! method_exists( $last_order, 'get_refunds' ) ) {
return; // Exit (Not an order)
}
// Your other code…
The three cases work nicely, avoiding an error
Give it a try this way, multiple controls have been added, see comments
// Check total of refunded items from last order - add as a fee at checkout
function add_last_order_refund_total_at_checkout( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
// The current user ID
$user_id = get_current_user_id();
// User id exists
if ( $user_id > 0 ) {
$customer = new WC_Customer( $user_id );
// Get last order
$last_order = $customer->get_last_order();
// True
if ( $last_order ) {
// Get the order id
$order_id = $last_order->get_id();
// Order ID exists
if ( is_numeric( $order_id ) ) {
// Get the WC_Order Object instance (from the order ID)
$order = wc_get_order( $order_id );
// Get the Order refunds (array of refunds)
$order_refunds = $order->get_refunds();
// NOT empty
if ( ! empty( $order_refunds) ) {
// WIP need to check which items have tax
$total_to_refund = $order->get_total_refunded();
// Add fee
$cart->add_fee( 'Refund', $total_to_refund );
}
}
}
}
}
add_action( 'woocommerce_cart_calculate_fees', 'add_last_order_refund_total_at_checkout', 10, 1 );
Information
WooCommerce version: 3.6.2
WordPress version: 5.1.1
When creating and saving a manual order via Admin, I am trying to replace the wc-item-meta-label with a prefix (meta key label Prefix is RAQ_ )
Screenshots:
During order creation I would enter
I am trying to get it updated with a prefix for each Order Item Meta label on save
Code tried so far:
add_action( 'save_post_shop_order', 'add_prefix_order_item_meta_data', 1000);
function add_prefix_order_item_meta_data( $order_id ){
// Ensure that this is a manual new order
if( $created = get_post_meta( $order_id, '_created_via', true ) ) {
return $order_id;
}
// Check the user’s permissions (for 'shop_manager' and 'administrator' user roles)
if ( ! current_user_can( 'edit_shop_order', $order_id ) ) {
return $order_id;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return $order_id; // Exit if it's an autosave
// Get the WC_Order object
$order = wc_get_order($order_id);
foreach( $order->get_items() as $item_id => $item ){
$item->update_meta_data( 'RAQ_'. $item_id);
$order->save(); // Save order data
}
}
But I am unable to make it work and update the prefix RAQ_.
Any help on this is welcome.
The idea here is that when an order comes in with an "express delivery" as Shipping Method, the order status is updated to On-Hold.
As there I have some different "express delivery" Shipping Method rates I thought that by using stristr() to see if the word 'express' appears anywhere in the formatted shipping method title. But I seem to be missing something as I don't get anything.
How can I check if the Order shipping method is an "express delivery" to be able to update the order status?
Here is the code that I have:
add_action( 'woocommerce_thankyou', 'express_orders_4865', 10, 1 );
function express_orders_4865( $order_id ) {
global $woocommerce;
$order = new WC_Order( $order_id );
$shipping_method = $order->get_shipping_method();
if (stristr($shipping_method, 'express') === TRUE) {
$order->update_status('on-hold');
} else {
return;
}
}
EDIT-----------------------------------------------------------
For anyone using Woocommerce Table Rate Shipping the get_method_id returns the table rate id so i used get_method_title instead as below, if there is a better way please comment...
add_action( 'woocommerce_thankyou', 'express_shipping_update_order_status', 10, 1 );
function express_shipping_update_order_status( $order_id ) {
if ( ! $order_id ) return;
$search = 'Express'; // The needle to search in the shipping method ID
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the WC_Order_Item_Shipping object data
foreach($order->get_shipping_methods() as $shipping_item ){
// When "express delivery" method is used, we change the order to "on-hold" status
if( strpos( $shipping_item->get_method_title(), $search ) !== false ){
$order->update_status('on-hold');
break;
}
}
}
I prefer to use the faster and less memory intensive function strpos() instead as the shipping method ID is alway in lowercase (like a kind of slug).
So is better the get the WC_Order_Item_Shipping object data for this case, using the available methods.
So the code should be:
add_action( 'woocommerce_thankyou', 'express_shipping_update_order_status', 10, 1 );
function express_shipping_update_order_status( $order_id ) {
if ( ! $order_id ) return;
$search = 'express'; // The needle to search in the shipping method ID
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the WC_Order_Item_Shipping object data
foreach($order->get_shipping_methods() as $shipping_item ){
// When "express delivery" method is used, we change the order to "on-hold" status
if( strpos( $shipping_item->get_method_title(), $search ) !== false && ! $order->has_status('on-hold')){
$order->update_status('on-hold');
break;
}
}
}
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
Tested and works…
So the code above didn't work for me, i had someone in a FB group help me debug it and this was the final one that worked for me
add_action( 'woocommerce_thankyou', 'express_shipping_update_order_status', 10, 1 );
function express_shipping_update_order_status( $order_id ) {
if ( ! $order_id ) return;
$search = 'express'; // The needle to search in the shipping method ID
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the WC_Order_Item_Shipping object data
foreach($order->get_shipping_methods() as $shipping_item ){
// When "express delivery" method is used, we change the order to "on-hold" status
if( strpos( $shipping_item->get_method_title(). $search ) !== false ){
$order->update_status('on-hold');
$order->save();
break;
}
}
}
My solution assumes that the normal status for a new order is PROCESSING.
So when an order is changed to PROCESSING, check the shipping method and if it matches, then change it to ON-HOLD.
add_action('woocommerce_order_status_changed', 'jds_auto_change_status_by_shipping_method');
function jds_auto_change_status_by_shipping_method($order_id) {
// If the status of an order is changed to PROCESSING and the shipping method contains specific text then change the status.
if ( ! $order_id ) {
return;
}
global $product;
$order = wc_get_order( $order_id );
if ($order->data['status'] == 'processing') { // if order status is processing
$shipping_method = $order->get_shipping_method();
if ( strpos($shipping_method, 'express') !== false ) { // if shipping method CONTAINS this text
$order->update_status('on-hold'); // change status to this
}
}
}
Note that $shipping_method returns the human readbale version of the shipping method that the customer sees, so you need to match exactly how the word 'express' appears to customer... is it 'express' or 'Express' or 'EXPRESS'