Automattic / woocommerce-subscriptions-core

Subscriptions core package for WooCommerce
Other
80 stars 29 forks source link

Infinite order.updated webhooks being sent caused by `OrdersTableDataStore::migrate_post_record()` not able to sync subscription dates from `postmeta` to `orders_meta` #547

Closed mattallan closed 6 months ago

mattallan commented 7 months ago

Describe the bug

We discovered a problem with HPOS + data syncing where subscription date fields are not being synced properly from WP Posts -> Orders tables, and for stores with active order.updated webhooks, this causes an infinite loop of webhooks being sent.

A lot of investigation has been done internally (pelri8-lc-p2) but I'll give a quick run-down of the problem here as well.

When the subscription metadata stored in postmeta becomes out of sync with the data in wc_orders_meta and the modified date on the post is equal or most recent, WooCommerce attempts to sync the post data over to the orders table by calling migrate_post_record( $order, $post_order ).

Because our subscription date fields (i.e. start, trial_end, next payment etc) are set up as internal meta keys, these are skipped over by the migrate_meta_data_from_post_order() function and instead, WooCommerce then calls the set_order_prop() function to set these values:

private function set_order_prop( \WC_Abstract_Order $order, string $prop_name, $prop_value ) {
    $prop_setter_function_name = "set_{$prop_name}";
    if ( is_callable( array( $order, $prop_setter_function_name ) ) ) {
        return $order->{$prop_setter_function_name}( $prop_value );
    } elseif ( is_callable( array( $this, $prop_setter_function_name ) ) ) {
        return $this->{$prop_setter_function_name}( $order, $prop_value, false );
    }
    return false;
}

Here's where the problem lies... we do not have any public setters for our date props therefore these internal meta keys remain out of sync.

From here on out, every time the subscription is read into memory, it continuously tries to sync the data and is always unsuccessful. When you have an order.updated webhook active, this results in a new order.updated webhook payload being scheduled each time the sync is attempted.

To Reproduce

  1. Install the latest Woo & Subscriptions with HPOS as the data source & syncing/compatibility mode enabled.
  2. Create/activate an order.updated webhook
  3. Purchase a subscription product
  4. Inside the wc_orders_meta table, search for the subscription ID and delete the value stored next to _schedule_next_payment (can be any other date value)
  5. Trigger the order.updated event for the parent order for this subscription by running something like:
$parent_order = wc_get_order( 123 );
$parent_order->save();
  1. Visit Tools > Action Scheduler and manually run the woocommerce_deliver_webhook_async action for the parent order.
  2. Notice that after running the action, another woocommerce_deliver_webhook_async for the same parent order is scheduled.
  3. Keep running the webhook action and notice it's infinitely creating a new one from itself.