Allow Multiple Payments In The Same WooCommerce Order

All deposit / split / partial payment plugins generate an additional order for paying the balance. Today, we change that.

Hosted by Rodolfo Melogli

Masterclass overview

It may happen you need to enable deposit payments, partial payments, multiple payment methods per order (e.g. half PayPal, half Bank), installments, split an order between more customers, or whatever requires the customer to pay a percentage of the order total and then come back to pay the difference, possibly with a different payment method.

Most, maybe all, plugins out there, complete the current order upon partial payment and create another order for the balance (a “sub-order” i.e. an order linked to the one that was partially paid), so that the customer can complete that one at a given date.

This is because WooCommerce requires a 1:1 relationship between orders and payments, and does not allow you to use multiple “transactions” for the same order.

Speaking of which, welcome to the world of “transactions“. By using this custom post type, we can save each payment on its own (partial, deposit, partial refund, installment), and then associate multiple transactions to a single order, so that the order status and total are dynamically set based on the outstanding balance.

A customer can then pay for the same orders multiple times, and each time the order amount will be defined by the total sum of its associated transactions.

In this class, we will take a look at some PHP code and demo it live, so that you can get the full picture of this – really! – revolutionary WooCommerce customization.

This is an amazing opportunity to see how a simple yet effective functionality can be coded, so that you can learn a new thing or two for your WooCommerce website or your WooCommerce clients. It’s also a great opportunity to hang out with like-minded professionals during the live class.

Recording & Materials

Video recording

Sorry, this video recording is only visible to logged in Business Bloomer Club members.
If you are a member, please log in.
Otherwise, here is why you should join the Club.

Code used during the class

add_action( 'init', function() {
    $supports_array = array( 'title' );
    $args = array(
        'labels' => array(
            'name' => 'Payments',
            'singular_name' => 'Payments'
        ),
        'capability_type' => 'shop_order',
        'public' => false,
        'supports' => $supports_array,
        'rewrite' => false,
        'show_ui' => true,
        'show_in_menu' => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : false,
        'menu_position' => 3,
    );    
    register_post_type( 'payment', $args );
});

add_action( 'woocommerce_order_after_calculate_totals', function( $and_taxes, $order ) {

    $payments = get_posts( [ 'post_type' => 'payment', 'post_status' => 'publish', 'numberposts' => -1, 'title' => $order->get_id(), 'fields' => 'ids' ] );
	
    if ( $payments ) {

        $total = $order->get_total();
        $paid = 0.00;
        foreach ( $payments as $payment_id ) {
            $amount = get_field( "amount", $payment_id );
            $paid += $amount;
        }
        $new_total = $total - $paid;
        $order->set_total( $new_total );
        if ( $new_total > 0 ) $order->set_status( 'pending' );
        $order->save();
		
	}
	
}, 9999, 2 );

add_action( 'acf/save_post', function( $post_id ) {
    if ( get_post_type( $post_id ) !== 'payment' && get_post_status( $post_id ) !== 'publish' ) return;
    $order = wc_get_order( get_the_title( $post_id ) );
    if ( ! $order ) return;
    $order->calculate_totals();
});

add_action( 'woocommerce_thankyou', function( $order_id ) {

    $order = wc_get_order( $order_id );
    if ( $order->has_status( 'failed' ) ) return;
	
    $my_post = array(
        'post_title' => $order_id,
        'post_status' => 'publish',
        'post_type' => 'payment',
    );
    $new_post_id = wp_insert_post( $my_post );
	
	update_field( 'amount', $order->get_total(), $new_post_id );
	$order->calculate_totals();
	
});

add_action( 'woocommerce_review_order_before_payment', function() {
    
    $chosen = WC()->session->get( 'deposit_chosen' );
    $chosen = empty( $chosen ) ? WC()->checkout->get_value( 'deposit' ) : $chosen;
    $chosen = empty( $chosen ) ? '100' : $chosen;
            
    $args = array(
        'type' => 'radio',
        'class' => array( 'form-row-wide', 'update_totals_on_change' ),
        'options' => array(
            '100' => 'Pay 100%',
            '30' => 'Pay 30% Deposit',
        ),
        'default' => $chosen,
    );
    
    echo '<p><b>Payment Option</b></p>';
    woocommerce_form_field( 'deposit', $args, $chosen );

}, 1 );

add_action( 'woocommerce_checkout_update_order_review', function( $posted_data ) {
    parse_str( $posted_data, $output );
    if ( isset( $output['deposit'] ) ) {
        WC()->session->set( 'deposit_chosen', $output['deposit'] );
    }
});

add_filter( 'woocommerce_calculated_total', function( $total, $cart ) {
	if ( ! WC()->session->get( 'deposit_chosen' ) || WC()->session->get( 'deposit_chosen' ) == '100' ) return $total;
    return $total * 0.3;
}, 9999, 2 );

add_action( 'woocommerce_admin_order_totals_after_total', function ( $order_id ) {
    echo '<tr><td class="label">Payments:</td><td width="1%"></td><td class="total"><ul style="margin:0">';
    $payments = get_posts( [ 'post_type' => 'payment', 'post_status' => 'publish', 'numberposts' => -1, 'title' => $order_id, 'fields' => 'ids' ] );
    if ( $payments ) {
        foreach ( $payments as $payment_id ) {
            $amount = get_field( "amount", $payment_id );
            echo '<li style="margin:0"><a href="' . get_edit_post_link( $payment_id ) . '">' . wc_price( $amount ) . '</a> (' . get_the_time( 'd-m-y', $payment_id ) . ')</li>';
        }
    }
    echo '</ul></td></tr>';
});

What you’ll learn

Why most deposit / partial payment plugins do it wrong
How to code a checkbok to pay a % of the cart total on the Woo checkout
How to create a new custom post type and programmatically create a post upon order payment
How to set order status and total based on the related transactions

Requirements

Basic knowledge of PHP and WordPress hooks

Upcoming masterclasses

Available recordings

1 2 3
IT Monks is a leading WordPress development agency with over 15 years of experience in custom WooCommerce design and development, delivering 500+ successful eCommerce projects.

6 thoughts on “Allow Multiple Payments In The Same WooCommerce Order

  1. That’s awesome. I was on clueless how to accept multiple payments.

    Thank you mate

  2. Great tutorial, Rodolfo!

  3. This was helpful, but felt like it was missing the major piece to tie it all together, which would be to send a order request to a customer with a preset deposit amount. While this accomplished the deposit by first using the checkout page, it doesn’t let shop owners send a pay order request (invoice) to a customer with the down payment request.

    I would have loved to see the deposit radio fields added to the order page on possibly the ‘before_woocommerce_pay_form’ hook, or even better allow the site owner to choose the deposit amount to send the order, instead of requiring a end user checkout to trigger this.

    Any advice on how to achieve this? Overall great video.

    1. Thank you Bryan!

      send a order request to a customer with a preset deposit amount

      Good point! My solution only applies to checkout orders and not orders created via admin. However, you can always display the radio buttons (“Pay deposit” / “Pay in full”) on the Order Pay page as well, and that will let the customer choose between a set amount and a 100% payment.

      I’m sure there’s also a way to implement the preset deposit amount right from the admin edit order page – this can be custom coded: you enter an amount, click “request payment”, and the Order Pay page asks for the preset deposit instead of the full amount.

      The smooth alternative may be a URL parameter! Let’s say you want to force a $5 deposit – simply add “&dep=5” to the Order Pay URL (e.g. example.com/checkout/order-pay/123456/?pay_for_order=true&key=wc_order_xyz&dep=5) and install this snippet:

      add_filter( 'woocommerce_order_get_total', function( $total, $order ) {
      	if ( is_checkout_pay_page() && isset( $_GET['dep'] ) ) {
          		return (int) $_GET['dep'];
      	}
      	return $total;
      }, 9999, 2 );
      

      Let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *