Pay Later option for booking

Hey,

Forgive me if this is already possible. I’m possibly just overthinking things, but if not, I’d like to suggest an additional option for the Bookings / Marketplace extensions.

On the ‘Request to Book’ form that’s added to individual listing pages, it would be nice if there was a setting we could enable on the backend which would add a checkbox underneath our Tiers on the form, which said something like “Pay Later” and allowed making the booking without requiring payment.

Essentially, I’m trying to allow clients to pay vendors with either online and offline payments methods, whilst encouraging the use of the booking form.

(Also, I did try creating a £0 priced tier, but on the next page of the booking form the price showed as £10 - this could be something to do with my WooCommerce settings, so I’m not sure if that’s a bug or not.)

I’m not sure if this would be possible with a snippet or whatever, but any guidance would be appreciated as always!

Cheers,
Chris :victory_hand:

Hi,

Unfortunately, there is no such feature currently available, but thanks for the suggestion. We will consider adding it in the future.

Thanks for the reply, @kseniia.

I wonder if @ihor could kindly help with some guidance on how I may be able to try and implement this myself.

So far, I’ve created a new ‘Payment Options’ select type attribute under Bookings > Attributes.

Would you be able to offer some advice on how I can create a custom function so that if:

  • The end-user selects ‘Pay Now’ - nothing happens, the form continues to checkout as usual.
  • The end-user selects ‘Pay Later’ the bookings form will skip the checkout page and submit the booking request.

Cheers,
Chris :victory_hand:

Hi,

I recommend integrating with one of the available “pay later” extensions for WooCommerce. Then the order would get Processing status even before the payment and the extension would handle requesting the payment later and cancelling the order if there’s no payment. On the HivePress Bookings side, everything would work as expected since Processing status triggers the booking confirmation.

Thanks for the reply, @ihor.

To clarify, I’m not trying to offer a traditional Pay Later option like Klarna - Instead, what I’m trying to do is have the option of paying online or paying in cash (my vendors and buyers will always meet).

I’ve since removed my custom attribute, and instead enabled cash on delivery in WooCommerce Payments which on initial testing seems to work as I’d like…

When checking out, buyers can pay by card or select the cash option which lets the checkout complete / booking process.

I’m also using another Woo extension to hide the cash on delivery option if the product type is a subscription, or a website related fee.

However, for buyer’s who select the cash on delivery option, I’m not sure how the hivepress and or WooCommerce order/booking statuses should be handled.

I’m aiming for as much automation as possible, and don’t currently have the settings enabled for vendors or buyers where they have to mark an order as complete. But, perhaps, with a snippet it could do something like:

IF cash on delivery is selected at checkout THEN require vendors to mark the order/booking as complete?

Or perhaps, leaving it as is, with say a 14 day completion window until it’s auto marked as complete?

I hope that makes sense!

Cheers,
Chris :victory_hand:

In this case you can try using the Cash payment method with some solution to switch the order status automatically (with a code snippet or third-party plugin), just for this payment method. The issue is that if you use Manual payouts then the order amount will possibly be added to the vendor balance anyway, even if the Cash method is selected, in this case customizations in the Marketplace code are needed.

Thanks for your reply, Ihor.

Ideally, I won’t need to use manual payouts at all. I would like the site to run as automated as possible using Stripe Connect for online payments, and naturally Cash payments would resolve themselves when Vendors and Buyers meet up.

I currently have the auto-completion period set to 14 days. However, if a Buyer books a Vendor appointment for say 3 weeks time, then the auto-completion period will occur before the actual booking has taken place.

I considered making the auto-completion period 30 days, and mentioning somewhere that my site pays Vendors once monthly. However, I don’t want to make Vendors wait ages to get paid.

I’ve thought about this in more detail, and what I think would resolve things would be a snippet that made the HivePress auto-completion period ‘date-aware’. So, essentially it takes the booking end date + auto-completion period into consideration.

If you could provide a snippet that would achieve that, I would be highly grateful!

Cheers,
Chris :victory_hand:

I recommend disabling auto-completion as this feature is related to Marketplace and is not date-aware. By default, if you leave the auto-completion period field empty, bookings will auto complete within 12 hours after the end date, this is enabled by default.

Regarding the Stripe payouts, please note that HivePress may attempt to create a Stripe payout for the cash order as there’s no exception for this payment method in the code (yet).

1 Like

Thanks again for the reply, Ihor - it’s greatly appreciated.

I’ll go with your recommendation and turn off the auto-completion feature, and just rely on the default settings in that case.

Would you be able to provide a temporary snippet to help exclude cash orders?

Also, you said, it “may attempt to create a Stripe payout” - however, since a buyer isn’t required to enter card information if they choose cash at checkout, surely Stripe wouldn’t have be able to process a charge?

Perhaps, I’m still confused about the payment flow in regards to Stripe? If you wouldn’t mind clarifying that it would be much appreciated. I thought I’d seen another setting in Stripe that essentially let me choose if payments came to me first, or if Stripe initiated the transfer between the vendor and seller.

Much appreciated if you can confirm the flow, and, if it can be customised, if needed, as well. I need to have a crystal clear idea of what’s going on with the money, as the legal liabilities heavily revolve around this.

I’m not entirety sure if I should have HivePress > Payouts > Requests enabled or disabled, too? Like I mentioned, I’m turning to turn on full automation for payment to vendors, etc.

Cheers,
Chris :victory_hand:

Hey,

Sorry - I’m driving myself crazy trying to understand all of this, but from reading older topics - it appears the way HivePress is designed means buyer funds land in the platform account before being sent onto the vendor.

For legal reasons, I need the funds to directly go from the buyer to vendor. (I imagine a lot of site owners would also prefer this implementation if they looked into the legal side of things.)

Whilst the ‘Separate Charges & Transfers’ method does allow me to set the on_behalf_of parameter - this doesn’t change the flow of funds, and so, I’m still considered the Merchant of Record and therefore liable for the Vendor’s tax, etc. And, so I believe I need to switch method to use Destination Charges instead.

I’ve been discussing my options with AI, and they’ve presented some code that tries to change the flow, and achieve what I’m describing, but as a very under qualified developer, I’ve no idea if this will work as intended - or break any of the HivePress flow.

<?php
/**
 * Plugin Name: WooPayments Destination Charges for HivePress
 * Description: Routes funds directly from buyer to vendor in WooPayments by setting on_behalf_of + transfer_data[destination]. Guards for single-vendor orders. Disables HivePress transfers to prevent double-payouts. 0% fee.
 * Version: 1.0.0
 * Author: You
 * License: GPL-2.0+
 */

if ( ! defined( 'ABSPATH' ) ) exit;

/** ───────────────────────── Helpers ───────────────────────── **/

/** Add a private order note (never throws). */
function wdfh_add_note( $order, $message ) {
	if ( is_numeric( $order ) ) { $order = wc_get_order( (int) $order ); }
	if ( $order instanceof WC_Order ) { $order->add_order_note( $message, false ); }
}

/** Normalize an order param that might be WC_Order or array ['id'=>..]. */
function wdfh_get_order( $order ) {
	if ( $order instanceof WC_Order ) return $order;
	if ( is_array( $order ) && isset( $order['id'] ) ) return wc_get_order( (int) $order['id'] );
	return null;
}

/**
 * Determine the single vendor for an order by inspecting line items.
 * Returns [ 'vendor_id' => int, 'vendor' => HivePress\Models\Vendor|NULL, 'error' => string|NULL ].
 * - Requires HivePress classes to exist.
 * - Requires all line items to belong to the same vendor.
 */
function wdfh_resolve_order_vendor( WC_Order $order ) {
	$out = [ 'vendor_id' => 0, 'vendor' => null, 'error' => null ];

	// Ensure HivePress Listing model is available.
	if ( ! class_exists( '\HivePress\Models\Listing' ) ) {
		$out['error'] = 'HivePress Listing model not found.';
		return $out;
	}

	$vendor_ids = [];

	foreach ( $order->get_items( 'line_item' ) as $item ) {
		$product_id = $item->get_product_id();
		if ( ! $product_id ) { continue; }

		// Find the associated Listing by product ID.
		$listing = \HivePress\Models\Listing::query()->filter( [ 'product' => $product_id ] )->get_first();
		if ( ! $listing ) {
			$out['error'] = 'Listing not found for product ID ' . $product_id . '.';
			return $out;
		}

		$vid = (int) $listing->get_vendor__id();
		if ( ! $vid ) {
			$out['error'] = 'Vendor not found for listing with product ID ' . $product_id . '.';
			return $out;
		}

		$vendor_ids[] = $vid;
	}

	$vendor_ids = array_values( array_unique( array_filter( $vendor_ids ) ) );

	if ( count( $vendor_ids ) !== 1 ) {
		$out['error'] = 'Order contains zero or multiple vendors.';
		return $out;
	}

	$vendor_id = (int) $vendor_ids[0];
	$out['vendor_id'] = $vendor_id;

	// Load Vendor model if available.
	if ( class_exists( '\HivePress\Models\Vendor' ) ) {
		$vendor = \HivePress\Models\Vendor::query()->get_by_id( $vendor_id );
		if ( $vendor ) {
			$out['vendor'] = $vendor;
		} else {
			$out['error'] = 'Vendor model could not be loaded.';
		}
	} else {
		$out['error'] = 'HivePress Vendor model not found.';
	}

	return $out;
}

/** ───────────────── Disable HivePress transfers (avoid double-pay) ───────────────── **/

add_action( 'plugins_loaded', function() {
	// Unhook Marketplace payout scheduler if present.
	if ( function_exists( 'hivepress' ) && isset( hivepress()->payout ) ) {
		remove_action( 'woocommerce_order_status_changed', [ hivepress()->payout, 'schedule_payout' ], 100 );
	}
}, 20 );

/**
 * Safety net: If any Stripe Transfer is still attempted by HivePress, neutralize it and leave an audit note.
 */
add_filter( 'hivepress/v1/components/stripe/create_transfer', function( $args, $context ) {
	$order_id = 0;
	if ( is_array( $context ) && isset( $context['order_id'] ) ) { $order_id = (int) $context['order_id']; }
	if ( $order_id ) {
		wdfh_add_note( $order_id, 'Destination Charges: prevented a post-payment Transfer (funds already routed to vendor at payment time).' );
	}
	$args['amount']   = 0;
	$args['metadata'] = array_merge( $args['metadata'] ?? [], [ 'blocked_by' => 'wcpay-destination-charges-for-hivepress' ] );
	return $args;
}, 10, 2 );

/** ───────────────── WooPayments: set destination charges ───────────────── **/

/**
 * Inject on_behalf_of + transfer_data[destination] for WooPayments PaymentIntent creation.
 *
 * We hook the two common WooPayments filters. Only the correct one will run on your version.
 * If requirements aren’t met (no HP classes, multiple vendors, no stripe_id), we do nothing
 * and leave a clear order note.
 */
function wdfh_apply_destination_charges( $args, $order_in, $hook_id ) {
	$order = wdfh_get_order( $order_in );
	if ( ! $order instanceof WC_Order ) { return $args; }

	$resolved = wdfh_resolve_order_vendor( $order );

	if ( $resolved['error'] ) {
		wdfh_add_note( $order, 'Destination Charges: ' . $resolved['error'] . ' No changes applied.' );
		return $args;
	}

	$vendor = $resolved['vendor'];

	// Ensure we can read the connected account id safely.
	if ( ! $vendor || ! method_exists( $vendor, 'get_stripe_id' ) ) {
		wdfh_add_note( $order, 'Destination Charges: vendor model/stripe_id accessor not available. No changes applied.' );
		return $args;
	}

	$acct = $vendor->get_stripe_id();
	if ( ! $acct ) {
		wdfh_add_note( $order, 'Destination Charges: vendor has no Stripe account (stripe_id missing). No changes applied.' );
		return $args;
	}

	// Core: set vendor as settlement merchant and route funds directly to vendor account.
	$args['on_behalf_of']  = $acct;
	$args['transfer_data'] = [ 'destination' => $acct ];

	// Ensure 0% commission (remove any fee arg if present).
	if ( isset( $args['application_fee_amount'] ) ) {
		unset( $args['application_fee_amount'] );
	}

	// Optional but helpful for reconciliation.
	$args['transfer_group'] = 'order_' . $order->get_id();

	// Audit trail you can see in the Woo order screen.
	wdfh_add_note(
		$order,
		sprintf(
			'Destination Charges: set on_behalf_of=%s and transfer_data[destination]=%s via %s. 0%% fee.',
			esc_html( $acct ),
			esc_html( $acct ),
			esc_html( $hook_id )
		)
	);

	return $args;
}

// WooPayments (modern).
add_filter( 'woocommerce_payments_payment_intent_args', function( $args, $order ) {
	return wdfh_apply_destination_charges( $args, $order, 'woocommerce_payments_payment_intent_args' );
}, 10, 2 );

// WooPayments (variant used on some versions).
add_filter( 'woocommerce_payments_generate_create_intent_request', function( $args, $order ) {
	return wdfh_apply_destination_charges( $args, $order, 'woocommerce_payments_generate_create_intent_request' );
}, 10, 2 );

/** ───────────────── Optional: friendly pre/post breadcrumbs ───────────────── **/

// Document the intended vendor at order creation (helps if payment step fails later).
add_action( 'woocommerce_checkout_order_created', function( $order ) {
	if ( ! $order instanceof WC_Order ) return;
	$resolved = wdfh_resolve_order_vendor( $order );
	if ( $resolved['error'] ) {
		wdfh_add_note( $order, 'Destination Charges (pre-payment): ' . $resolved['error'] );
		return;
	}
	$vendor = $resolved['vendor'];
	if ( $vendor && method_exists( $vendor, 'get_stripe_id' ) ) {
		$acct = $vendor->get_stripe_id();
		if ( $acct ) {
			wdfh_add_note( $order, 'Destination Charges (pre-payment): target vendor account ' . esc_html( $acct ) . '.' );
		} else {
			wdfh_add_note( $order, 'Destination Charges (pre-payment): vendor missing stripe_id.' );
		}
	}
}, 10 );

// Post-payment breadcrumb for your records (doesn't call Stripe; just documents intent).
add_action( 'woocommerce_payment_complete', function( $order_id ) {
	$order = wc_get_order( $order_id );
	if ( ! $order ) return;
	wdfh_add_note( $order, 'Destination Charges (post-payment): payment completed. Funds should route to vendor account set at intent creation.' );
}, 10 );

I know you don’t usually fix custom code, but would you be able to at least confirm if this should in theory work/talk to HivePress as intended?

I would also highly recommend considering implementing something like this as a setting to choose from in the backend, that will switch the method used, or even make this the default.

Obviously, I can’t speak on behalf of other site owners, but I don’t feel the majority of users, including myself, will fully grasp the legal responsibilities they are signing up to, likely without knowing it, using the current method.

I’ve no idea what your schedule is like at the moment, but if I am required to hire a developer to help achieve this, I’d naturally feel most comfortable if you were the one able to provide the snippet/helper plugin.

Cheers,
Chris :victory_hand:

Hi,

Yes, with Stripe Connect all the payments are initially sent to the site owner’s Stripe Connect balance, and for completed orders there are further transfers from this balance to the vendor’s Stripe Express balances (vendors set up these Express accounts during the registration).

Unfortunately there’s no way to completely split payments in the current version, so they initially land on the vendor’s Stripe balance, this would require a custom implementation and potentially cause fraud issues — by default, the marketplace acts as an intermediary, keeping the funds until the order (booking) is completed, also ensuring the disputes/refunds policy.

If you want to implement splitting payments on checkout, please check the available Stripe hooks by searching “hivepress/v1/components/stripe” on the forum, via these hooks you can alter the Stripe API parameters in requests sent by HivePress.

To prevent payouts for specific payment methods, please check the “hivepress/v1/models/order/payout” hook in the Marketplace codebase, if you prevent scheduling this hook for specific orders, this may do the trick. We also plan to fix this for the Cash payment method in the next update.

Hope this helps

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.