fix(2meet-data-optimizer-woocommerce-addon): recompute commission amounts on every tracked status change and refund

- the commission upsert replaces subtotal, commission, vendor_payout and
  commission_rate on conflict (it kept the first amounts, so a partial refund
  before completion never reached the row)
- woocommerce_order_refunded recomputes a tracked or already recorded order;
  a fully refunded order keeps its row at zero, untracked orders get none
- Verify: WCOrdersInterceptorTest (5 new), integration 27, unit 11, phpcs;
  dev36 flow_wc partial refund -> completed
- PR: PR-092
This commit is contained in:
2026-09-27 16:29:28 +08:00
parent 6294ee2374
commit 50989396a8
2 changed files with 108 additions and 4 deletions
+46 -3
View File
@@ -49,6 +49,7 @@ class TMDO_WC_Orders_Interceptor extends TMDO_Interceptor_Base {
*/
public function register_hooks(): void {
add_action( 'woocommerce_order_status_changed', array( $this, 'action_order_status_changed' ), 10, 3 );
add_action( 'woocommerce_order_refunded', array( $this, 'action_order_refunded' ), 10, 2 );
}
/**
@@ -74,6 +75,48 @@ class TMDO_WC_Orders_Interceptor extends TMDO_Interceptor_Base {
}
}
/**
* Recompute the amounts after a refund, which need not change the status.
* Orders never tracked (no row, untracked status) are left alone; a
* recorded order that is now fully refunded keeps its row at zero.
*
* @param int $order_id Order ID.
* @param int $refund_id Refund ID (unused).
* @return void
*/
public function action_order_refunded( int $order_id, int $refund_id ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
if ( ! $this->is_active() || ! function_exists( 'wc_get_order' ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$status = (string) $order->get_status();
if ( ! in_array( $status, self::TRACKED_STATUSES, true ) && ! $this->has_commission( $order_id ) ) {
return;
}
try {
$this->sync_commission( $order_id, $status );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'woocommerce_order_refunded', $e->getMessage() );
}
}
/**
* Whether a commission row exists for the order.
*
* @param int $wc_order_id WooCommerce order ID.
* @return bool
*/
private function has_commission( int $wc_order_id ): bool {
global $wpdb;
$table = TMDO_DB::table( 'wpdo_wc_commissions' );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table, name from TMDO_DB::table().
return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM `{$table}` WHERE wc_order_id = %d LIMIT 1", $wc_order_id ) );
}
/**
* INSERT/UPDATE wp_wpdo_wc_commissions for the given order.
*
@@ -123,8 +166,8 @@ class TMDO_WC_Orders_Interceptor extends TMDO_Interceptor_Base {
// v2.1.2 race-condition fix: atomic upsert via UNIQUE KEY ui_order_vendor.
// Replaces SELECT-then-INSERT/UPDATE which could race two concurrent
// status-change events (e.g. gateway IPN + admin click) and trigger
// duplicate-key DB errors. Single round-trip; on conflict only updates
// the mutable columns (status / updated_at), preserves financials.
// duplicate-key DB errors. Single round-trip; on conflict the amounts are
// replaced too, so a later event reflects refunds (2.3 PR-092).
TMDO_DB::upsert(
$table,
array(
@@ -140,7 +183,7 @@ class TMDO_WC_Orders_Interceptor extends TMDO_Interceptor_Base {
'created_at' => $now,
'updated_at' => $now,
),
array( 'status', 'updated_at' ), // columns to update on conflict.
array( 'subtotal', 'commission', 'vendor_payout', 'commission_rate', 'status', 'updated_at' ), // columns to update on conflict.
array( 'wc_order_id', 'vendor_id' ), // composite unique key.
array( '%d', '%d', '%d', '%f', '%f', '%f', '%f', '%s', '%d', '%s', '%s' )
);