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()?
Related
Since the latest releases of elementor and woocommerce i seem to be having a problem i cant fix.
Basically im adding multiple products to cart by url manipulation with data coming from a form calculator. This worked well for over a year and now suddenly doesnt.
1. Description of the function
When i press the button on the calculator page, it calculates the values which are then parsed into a field and added to the current url.
document.location.href='?add-to-cart=' +fieldname51+':'+fieldname15+',' +fieldname52+':'+fieldname16+',' +fieldname53+':'+fieldname17+',' , ....
example of the data of such field where following applies: <productid1>:<product quantitiy1> , <productid2>:<product quantitiy2> , ...
?add-to-cart=11439:4,0:0,0:0,0:0,0:0,11469:2,0:0,0:0,0:0,5213:1,11425:1,5246:1,5275:2,5304:2,5329:6,0:0,0:0,0:0,0:0,0:0,0:0,0:0,0:0,0:0,0:0,11705:6
I've used and adapted some code snippets to add multiple product variations to cart through url in woocommerce and added it to my functions.php file in wordpress:
/** ADD PRODUCTS THROUGH URL CODE **/
function webroom_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>)
$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', 'webroom_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 );
$args = array_merge( array( $reflection ), $args );
return call_user_func_array( array( $method, 'invoke' ), $args );
}
2. The problem
Now for some reason when pressing the calculation button, it indeed does as expected and adds said products to cart, however the field parameters stay stuck in the url.
What happens is as soon as you press another link on the website, or the cart or whatever, it "refreshes" the page and parses the same field values again, causing the cart to have twice the amount of products than intended.
3. Tried the following
i've tried redirecting the field data after calculation with window.location.replace but it redirects before it has time to calculate and add the products to cart
setTimeout(function(){window.location.replace("url or url/fielddata");},3000); also didnt work when added to an onclick event from the calculation button, because then it no longer sees the field data or makes the calculations, adding nothing to cart
attempts to "remove the parametes" after clicking failed and caused the same issues as above.
4. Goal
The goal is either to clean the URL after pressing the button and adding products to cart or to redirect to the basket or any custom URL without parsing the existing parameters from the field data again. either solution would work but maybe it is not possible to do.
I would like to understand the sequence various classes are loaded in Wordpress.
There are many plugins available in Wordpress, then who will be loaded earlier than another.
Consider I would like to develop a plugin that will use some existing class of Woocommerce. Basically my custom plugin will extend some class of Woocommerce (for example : WC_Gateway_COD)
How I can ensure the existing class ‘WC_Gateway_COD’ is already defined & loaded before it execute the below statement ?
if (class_exists('WC_Gateway_COD')) {
class WC_my_custom_class extends WC_Gateway_COD {
…..
….
}
}
To make a custom gateway based on an existing WooCommerce payment method as COD, It's recommended to copy the source code from WC_Gateway_COD Class in a plugin (adapting the code for your needs) like:
defined( 'ABSPATH' ) or exit;
// Make sure WooCommerce is active
if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
return;
}
add_filter( 'woocommerce_payment_gateways', 'wc_custom_add_to_gateways' );
function wc_custom_add_to_gateways( $gateways ) {
$gateways[] = 'WC_Gateway_COD2';
return $gateways;
}
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'wc_gateway_custom_plugin_links' );
function wc_gateway_custom_plugin_links( $links ) {
$plugin_links = array(
'' . __( 'Configure', 'payment_cod2' ) . ''
);
return array_merge( $plugin_links, $links );
}
add_action( 'plugins_loaded', 'wc_gateway_cod2_init', 11 );
function wc_gateway_cod2_init() {
class WC_Gateway_COD2 extends WC_Payment_Gateway {
public $domain; // The text domain (optional)
/**
* Constructor for the gateway.
*/
public function __construct() {
$this->domain = 'payment_cod2'; // text domain name (for translations)
// Setup general properties.
$this->setup_properties();
// Load the settings.
$this->init_form_fields();
$this->init_settings();
// Get settings.
$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
$this->instructions = $this->get_option( 'instructions' );
$this->enable_for_methods = $this->get_option( 'enable_for_methods', array() );
$this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes';
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) );
add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 );
// Customer Emails.
add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
}
/**
* Setup general properties for the gateway.
*/
protected function setup_properties() {
$this->id = 'cod2';
$this->icon = apply_filters( 'woocommerce_cod2_icon', '' );
$this->method_title = __( 'Cash on delivery', 'woocommerce' );
$this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' );
$this->has_fields = false;
}
// and so on (the rest of the code)…
}
}
You can unset / remove original COD payment gateway changing the 1st function like:
add_filter( 'woocommerce_payment_gateways', 'wc_custom_add_to_gateways' );
function wc_custom_add_to_gateways( $gateways ) {
$gateways[] = 'WC_Gateway_COD2';
unset($gateways['WC_Gateway_COD']; // Remove COD gateway
return $gateways;
}
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
I added an override function of the extended class of WC_Gateway_BACS. The function will update the status of the order from on-hold to processing. The problem is the email is missing the bank details now. Before I have the bank account number included on the email but after the customization, the email does not include it and I think it is because the order status is now processing.
Has anyone here did the same thing and come up with a solution? I included some images here of on-hold and processing emails. I like to add the account number to processing-email
class WC_Gateway_BACS_custom extends WC_Gateway_BACS {
/**
* Process the payment and return the result
*
* #access public
* #param int $order_id
* #return array
*/
function process_payment( $order_id ) {
global $woocommerce;
$order = new WC_Order( $order_id );
// Mark as processing (or anything you want)
$order->update_status('processing', __( 'Awaiting BACS payment', 'woocommerce' ));
// Reduce stock levels
$order->reduce_order_stock();
// Remove cart
$woocommerce->cart->empty_cart();
// Return thankyou redirect
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
}
/**
* Add content to the WC emails.
*
* #param WC_Order $order
* #param bool $sent_to_admin
* #param bool $plain_text
*/
// public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
// if ( ! $sent_to_admin && 'bacs' === $order->payment_method && ($order->has_status( 'on-hold' ) || $order->has_status( 'processing' )) ) {
// if ( $this->instructions ) {
// echo wpautop( wptexturize( $this->instructions ) ) . PHP_EOL;
// }
// $this->bank_details( $order->id );
// }
// }
}
on-hold email
processing email
I actually ran into the same issue today. I came up with two possible solutions:
Extending the class, as you did above:
/* override gateway for BACS */
function my_core_gateways($methods)
{
foreach ($methods as &$method){
if($method == 'WC_Gateway_BACS')
{
$method = 'WC_Gateway_BACS_custom';
}
}
return $methods;
}
/* custom gateway processor for BACS */
class WC_Gateway_BACS_custom extends WC_Gateway_BACS
{
/**
* Add content to the WC emails.
*
* #param WC_Order $order
* #param bool $sent_to_admin
* #param bool $plain_text
*/
public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
if ( ! $sent_to_admin && 'bacs' === $order->payment_method && $order->has_status( 'processing' ) ) {
if ( $this->instructions ) {
echo wpautop( wptexturize( $this->instructions ) ) . PHP_EOL;
}
/* dirty hack to get access to bank_details */
$reflector = new ReflectionObject($this);
$method = $reflector->getMethod('bank_details');
$method->setAccessible(true);
$result = $method->invoke($this, $order->id);
}
}
/**
* Process the payment and return the result.
*
* #param int $order_id
* #return array
*/
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
// Mark as on-hold (we're awaiting the payment)
$order->update_status( 'processing', __( 'Awaiting BACS payment', 'woocommerce' ) );
// Reduce stock levels
$order->reduce_order_stock();
// Remove cart
WC()->cart->empty_cart();
// Return thankyou redirect
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
}
}
Or, cleaner in my opinion, by adding two actions
add_action( 'woocommerce_email_before_order_table', 'add_order_email_instructions', 10, 2 );
add_action( 'woocommerce_thankyou', 'bacs_order_payment_processing_order_status', 10, 1 );
function bacs_order_payment_processing_order_status( $order_id )
{
if ( ! $order_id ) {
return;
}
$order = new WC_Order( $order_id );
if ('bacs' === $order->payment_method && ('on-hold' == $order->status || 'pending' == $order->status)) {
$order->update_status('processing');
} else {
return;
}
}
function add_order_email_instructions( $order, $sent_to_admin ) {
if ( ! $sent_to_admin && 'bacs' === $order->payment_method && $order->has_status( 'processing' ) ) {
$gw = new WC_Gateway_BACS();
$reflector = new ReflectionObject($gw);
$method = $reflector->getMethod('bank_details');
$method->setAccessible(true);
$result = $method->invoke($gw, $order->id);
}
}
The second solution has the least maintenance requirements in the long run.
Well I am trying to modify a small plugin. This plugin calculate the deposit of an item, after the user paid the deposit then the plugin send to the user an email saying that the deposit has been paid.
I want to add a button on the admin panel in the woocommerce order that once is clicked send and email to that user saying how much left to pay.
Firstly, I added the button:
//New action button to send the remaining email
add_action('woocommerce_admin_order_totals_after_total', array($this, 'pay_remaining_email'));
public function pay_remaining_email(){
?>
<tr>
<button type="button" class="button pay_remaining button-primary" title="Send">
<span>Send remaining email</span>
</button>
</tr>
<?php
}
Then I get this by jquery:
/* This is will get the button from the admin panel to send the remianing email */
$('.pay_remaining').click(function(){
var test = 2;
if (test =! 0){
alert('It works');
} else {
alert('It isnt work');
}
});
Obviously this is a test, I want to call in here the function to a custom email:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
if ( ! class_exists( 'WC_Deposits_Email_Customer_Partially_Paid_Complete' ) ) :
/**
* Customer Partially Paid Email
*
* An email sent to the customer when an order is not been paid completed seven days before.
*
*/
class WC_Deposits_Email_Customer_Partially_Paid_Complete extends WC_Email {
/**
* Constructor
*/
function __construct() {
$this->id = 'customer_partially_paid';
$this->title = __( 'Partial Payment Received', 'woocommerce-deposits' );
$this->description = __( 'This is an order notification sent to the customer after payment the deposit, containing order details and a link to pay the remaining balance.', 'woocommerce-deposits' );
$this->heading = __( 'Thank you for your order', 'woocommerce-deposits' );
$this->subject = __( 'Your {site_title} order receipt from {order_date}', 'woocommerce-deposits' );
$this->template_html = 'emails/customer-order-partially-paid.php';
$this->template_plain = 'emails/plain/customer-order-partially-paid.php';
// Triggers for this email
add_action( 'pay_remaining_email', array( $this, 'trigger' ) );
// Call parent constructor
parent::__construct();
$this->template_base = WC_DEPOSITS_TEMPLATE_PATH;
}
/**
* trigger function.
*
* #access public
* #return void
*/
function trigger( $order_id ) {
if ( $order_id ) {
$this->object = wc_get_order( $order_id );
$this->recipient = $this->object->billing_email;
$this->find['order-date'] = '{order_date}';
$this->find['order-number'] = '{order_number}';
$this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) );
$this->replace['order-number'] = $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(),
'sent_to_admin' => false,
'plain_text' => false
), '', $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(),
'sent_to_admin' => false,
'plain_text' => true
), '', $this->template_base );
return ob_get_clean();
}
}
endif;
return new WC_Deposits_Email_Customer_Partially_Paid_Complete();
Any ideas how can I accomplish this?
In simple terms:
Register an Ajax endpoint that launches your email function within your plugin
When the button is clicked, fire a request to the Ajax endpoint
Your email is sent
Return what ever you want to the Ajax call and do what ever you want with the returned result.
Registering an Ajax endpoint:
https://codex.wordpress.org/Plugin_API/Action_Reference/wp_ajax_(action)
Sending Ajax requests with jQuery:
http://api.jquery.com/jquery.ajax/
Sending emails from a Wordpress plugin:
https://developer.wordpress.org/reference/functions/wp_mail/