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 