PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.2
WooCommerce v10.9.2
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / data-stores / abstract-wc-order-data-store-cpt.php
woocommerce / includes / data-stores Last commit date
abstract-wc-order-data-store-cpt.php 2 months ago abstract-wc-order-item-type-data-store.php 7 months ago class-wc-coupon-data-store-cpt.php 2 months ago class-wc-customer-data-store-session.php 9 months ago class-wc-customer-data-store.php 3 months ago class-wc-customer-download-data-store.php 3 years ago class-wc-customer-download-log-data-store.php 1 year ago class-wc-data-store-wp.php 9 months ago class-wc-order-data-store-cpt.php 2 months ago class-wc-order-item-coupon-data-store.php 8 years ago class-wc-order-item-data-store.php 2 months ago class-wc-order-item-fee-data-store.php 8 years ago class-wc-order-item-product-data-store.php 4 years ago class-wc-order-item-shipping-data-store.php 1 year ago class-wc-order-item-tax-data-store.php 7 years ago class-wc-order-refund-data-store-cpt.php 2 months ago class-wc-payment-token-data-store.php 4 months ago class-wc-product-data-store-cpt.php 2 months ago class-wc-product-grouped-data-store-cpt.php 3 months ago class-wc-product-variable-data-store-cpt.php 1 month ago class-wc-product-variation-data-store-cpt.php 9 months ago class-wc-shipping-zone-data-store.php 6 months ago class-wc-webhook-data-store.php 2 years ago
abstract-wc-order-data-store-cpt.php
1178 lines
1 <?php
2 /**
3 * Abstract_WC_Order_Data_Store_CPT class file.
4 *
5 * @package WooCommerce\Classes
6 */
7
8 use Automattic\Jetpack\Constants;
9 use Automattic\WooCommerce\Caches\OrderCache;
10 use Automattic\WooCommerce\Enums\OrderStatus;
11 use Automattic\WooCommerce\Proxies\LegacyProxy;
12 use Automattic\WooCommerce\Utilities\OrderUtil;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 // phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Backward compatibility.
19 /**
20 * Abstract Order Data Store: Stored in CPT.
21 *
22 * @version 3.0.0
23 */
24 abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Abstract_Order_Data_Store_Interface, WC_Object_Data_Store_Interface {
25
26 /**
27 * Internal meta type used to store order data.
28 *
29 * @var string
30 */
31 protected $meta_type = 'post';
32
33 /**
34 * Data stored in meta keys, but not considered "meta" for an order.
35 *
36 * @since 3.0.0
37 * @var array
38 */
39 protected $internal_meta_keys = array(
40 '_order_currency',
41 '_cart_discount',
42 '_cart_discount_tax',
43 '_order_shipping',
44 '_order_shipping_tax',
45 '_order_tax',
46 '_order_total',
47 '_order_version',
48 '_prices_include_tax',
49 '_payment_tokens',
50 );
51
52 /**
53 * Custom setters for props. Add key here if it has corresponding set_ and get_ method present.
54 *
55 * @var string[]
56 */
57 protected $internal_data_store_key_getters = array();
58
59 /**
60 * Return internal key getters name.
61 *
62 * @return string[]
63 */
64 public function get_internal_data_store_key_getters() {
65 return $this->internal_data_store_key_getters;
66 }
67
68 /*
69 |--------------------------------------------------------------------------
70 | CRUD Methods
71 |--------------------------------------------------------------------------
72 */
73
74 /**
75 * Method to create a new order in the database.
76 *
77 * @param WC_Order $order Order object.
78 */
79 public function create( &$order ) {
80 $order->set_version( Constants::get_constant( 'WC_VERSION' ) );
81 $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
82 if ( ! $order->get_date_created( 'edit' ) ) {
83 $order->set_date_created( time() );
84 }
85
86 $id = wp_insert_post(
87 /**
88 * Filters the data for a new order before it is inserted into the database.
89 *
90 * @param array $data Array of data for the new order.
91 *
92 * @since 3.3.0
93 */
94 apply_filters(
95 'woocommerce_new_order_data',
96 array(
97 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
98 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
99 'post_type' => $order->get_type( 'edit' ),
100 'post_status' => $this->get_post_status( $order ),
101 'ping_status' => 'closed',
102 'post_author' => 1,
103 'post_title' => $this->get_post_title(),
104 'post_password' => $this->get_order_key( $order ),
105 'post_parent' => $order->get_parent_id( 'edit' ),
106 'post_excerpt' => $this->get_post_excerpt( $order ),
107 )
108 ),
109 true
110 );
111
112 if ( $id && ! is_wp_error( $id ) ) {
113 $order->set_id( $id );
114 $this->update_post_meta( $order );
115 $order->save_meta_data();
116 $order->apply_changes();
117 $this->clear_caches( $order );
118 }
119 }
120
121 /**
122 * Check if an order exists by id.
123 *
124 * @since 8.0.0
125 *
126 * @param int $order_id The order id to check.
127 * @return bool True if an order exists with the given name.
128 */
129 public function order_exists( $order_id ): bool {
130 if ( ! $order_id ) {
131 return false;
132 }
133
134 $post_object = get_post( $order_id );
135 return ! is_null( $post_object ) && in_array( $post_object->post_type, wc_get_order_types(), true );
136 }
137
138 /**
139 * Method to read an order from the database.
140 *
141 * @param WC_Order $order Order object.
142 *
143 * @throws Exception If passed order is invalid.
144 */
145 public function read( &$order ) {
146 $order->set_defaults();
147 $post_object = get_post( $order->get_id() );
148 if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
149 throw new Exception( esc_html__( 'Invalid order.', 'woocommerce' ) );
150 }
151
152 $this->set_order_props(
153 $order,
154 array(
155 'parent_id' => $post_object->post_parent,
156 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
157 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
158 'status' => $post_object->post_status,
159 )
160 );
161
162 $this->read_order_data( $order, $post_object );
163 $order->read_meta_data();
164 $order->set_object_read( true );
165
166 /**
167 * In older versions, discounts may have been stored differently.
168 * Update them now so if the object is saved, the correct values are
169 * stored.
170 */
171 if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) {
172 $order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) );
173 }
174 }
175
176 /**
177 * Set the properties of an object and log the first error found while doing so.
178 *
179 * @param \WC_Order $order Order object.
180 * @param array $props The properties to set.
181 */
182 private function set_order_props( &$order, array $props ) {
183 $errors = $order->set_props( $props );
184
185 if ( ! $errors instanceof WP_Error ) {
186 return;
187 }
188
189 $order_id = $order->get_id();
190 /** @var WC_Logger_Interface $logger */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
191 $logger = WC()->call_function( 'wc_get_logger' );
192
193 foreach ( $errors->get_error_codes() as $error_code ) {
194 $property_name = $errors->get_error_data( $error_code )['property_name'] ?? '';
195 $error_message = $errors->get_error_message( $error_code );
196 $logger->warning(
197 sprintf(
198 /* translators: %1$s = order ID, %2$s = order id, %3$s = error message. */
199 __( 'Error when setting property \'%1$s\' for order %2$d: %3$s', 'woocommerce' ),
200 $property_name,
201 $order_id,
202 $error_message
203 ),
204 array(
205 'error_code' => $error_code,
206 'error_message' => $error_message,
207 'order_id' => $order_id,
208 'property_name' => $property_name,
209 )
210 );
211 }
212 }
213
214 /**
215 * Method to update an order in the database.
216 *
217 * @param WC_Order $order Order object.
218 */
219 public function update( &$order ) {
220 $order->save_meta_data();
221 $order->set_version( Constants::get_constant( 'WC_VERSION' ) );
222
223 if ( null === $order->get_date_created( 'edit' ) ) {
224 $order->set_date_created( time() );
225 }
226
227 $changes = $order->get_changes();
228
229 // Only update the post when the post data changes.
230 if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) {
231 $post_data = array(
232 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
233 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
234 'post_status' => $this->get_post_status( $order ),
235 'post_parent' => $order->get_parent_id(),
236 'post_excerpt' => $this->get_post_excerpt( $order ),
237 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
238 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
239 );
240
241 /**
242 * When updating this object, to prevent infinite loops, use $wpdb
243 * to update data, since wp_update_post spawns more calls to the
244 * save_post action.
245 *
246 * This ensures hooks are fired by either WP itself (admin screen save),
247 * or an update purely from CRUD.
248 */
249 if ( doing_action( 'save_post' ) ) {
250 $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) );
251 clean_post_cache( $order->get_id() );
252 } else {
253 wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) );
254 }
255 $order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
256 }
257 $this->update_post_meta( $order );
258 $order->apply_changes();
259 $this->clear_caches( $order );
260 }
261
262 /**
263 * Method to delete an order from the database.
264 *
265 * @param WC_Order $order Order object.
266 * @param array $args Array of args to pass to the delete method.
267 *
268 * @return void
269 */
270 public function delete( &$order, $args = array() ) {
271 $id = $order->get_id();
272 $args = wp_parse_args(
273 $args,
274 array(
275 'force_delete' => false,
276 'suppress_filters' => false,
277 )
278 );
279
280 if ( ! $id ) {
281 return;
282 }
283
284 $do_filters = ! $args['suppress_filters'];
285
286 if ( $args['force_delete'] ) {
287 if ( $do_filters ) {
288 /**
289 * Fires immediately before an order is deleted from the database.
290 *
291 * @since 8.0.0
292 *
293 * @param int $order_id ID of the order about to be deleted.
294 * @param WC_Order $order Instance of the order that is about to be deleted.
295 */
296 do_action( 'woocommerce_before_delete_order', $id, $order );
297 }
298
299 wp_delete_post( $id );
300 $order->set_id( 0 );
301
302 if ( $do_filters ) {
303 /**
304 * Fires immediately after an order is deleted.
305 *
306 * @since 2.7.0
307 *
308 * @param int $order_id ID of the order that has been deleted.
309 */
310 do_action( 'woocommerce_delete_order', $id );
311 }
312 } else {
313 if ( $do_filters ) {
314 /**
315 * Fires immediately before an order is trashed.
316 *
317 * @since 8.0.0
318 *
319 * @param int $order_id ID of the order about to be trashed.
320 * @param WC_Order $order Instance of the order that is about to be trashed.
321 */
322 do_action( 'woocommerce_before_trash_order', $id, $order );
323 }
324
325 wp_trash_post( $id );
326 $order->set_status( OrderStatus::TRASH );
327
328 if ( $do_filters ) {
329 /**
330 * Fires immediately after an order is trashed.
331 *
332 * @since 2.7.0
333 *
334 * @param int $order_id ID of the order that has been trashed.
335 */
336 do_action( 'woocommerce_trash_order', $id );
337 }
338 }
339 }
340
341 /*
342 |--------------------------------------------------------------------------
343 | Additional Methods
344 |--------------------------------------------------------------------------
345 */
346
347 /**
348 * Get the status to save to the post object.
349 *
350 * Plugins extending the order classes can override this to change the stored status/add prefixes etc.
351 *
352 * @since 3.6.0
353 * @param WC_order $order Order object.
354 * @return string
355 */
356 protected function get_post_status( $order ) {
357 $order_status = $order->get_status( 'edit' );
358
359 if ( ! $order_status ) {
360 /**
361 * Filters the default order status to use when creating a new order.
362 *
363 * @param string $order_status Default order status.
364 *
365 * @since 3.7.0
366 */
367 $order_status = apply_filters( 'woocommerce_default_order_status', OrderStatus::PENDING );
368 }
369
370 $post_status = $order_status;
371 $valid_statuses = get_post_stati();
372
373 // Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed.
374 // In the future this should only happen based on `wc_is_order_status`, but in order to
375 // preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong
376 // Notice will be needed here, followed by future removal.
377 if ( ! in_array( $post_status, array( OrderStatus::AUTO_DRAFT, OrderStatus::DRAFT, OrderStatus::TRASH ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) {
378 $post_status = 'wc-' . $post_status;
379 }
380
381 return $post_status;
382 }
383
384 /**
385 * Excerpt for post.
386 *
387 * @param WC_order $order Order object.
388 * @return string
389 */
390 protected function get_post_excerpt( $order ) {
391 return '';
392 }
393
394 /**
395 * Get a title for the new post type.
396 *
397 * @return string
398 */
399 protected function get_post_title() {
400 // @codingStandardsIgnoreStart
401 /* translators: %s: Order date */
402 return sprintf( __( 'Order &ndash; %s', 'woocommerce' ), ( new DateTime( 'now' ) )->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) );
403 // @codingStandardsIgnoreEnd
404 }
405
406 /**
407 * Get order key.
408 *
409 * @since 4.3.0
410 * @param WC_order $order Order object.
411 * @return string
412 */
413 protected function get_order_key( $order ) {
414 return wc_generate_order_key();
415 }
416
417 /**
418 * Read order data. Can be overridden by child classes to load other props.
419 *
420 * @param WC_Order $order Order object.
421 * @param object $post_object Post object.
422 * @since 3.0.0
423 */
424 protected function read_order_data( &$order, $post_object ) {
425 $id = $order->get_id();
426
427 $meta_data = get_post_meta( $id );
428
429 $prices_include_tax = $meta_data['_prices_include_tax'][0] ?? '';
430
431 $this->set_order_props(
432 $order,
433 array(
434 'currency' => $meta_data['_order_currency'][0] ?? '',
435 'discount_total' => $meta_data['_cart_discount'][0] ?? '',
436 'discount_tax' => $meta_data['_cart_discount_tax'][0] ?? '',
437 'shipping_total' => $meta_data['_order_shipping'][0] ?? '',
438 'shipping_tax' => $meta_data['_order_shipping_tax'][0] ?? '',
439 'cart_tax' => $meta_data['_order_tax'][0] ?? '',
440 'total' => $meta_data['_order_total'][0] ?? '',
441 'version' => $meta_data['_order_version'][0] ?? '',
442 'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === $prices_include_tax : 'yes' === get_option( 'woocommerce_prices_include_tax' ),
443 )
444 );
445
446 // Gets extra data associated with the order if needed.
447 foreach ( $order->get_extra_data_keys() as $key ) {
448 $function = 'set_' . $key;
449 if ( is_callable( array( $order, $function ) ) ) {
450 $order->{$function}( $meta_data[ '_' . $key ][0] ?? '' );
451 }
452 }
453 }
454
455 /**
456 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
457 *
458 * @param WC_Order $order Order object.
459 * @since 3.0.0
460 */
461 protected function update_post_meta( &$order ) {
462 $updated_props = array();
463 $meta_key_to_props = array(
464 '_order_currency' => 'currency',
465 '_cart_discount' => 'discount_total',
466 '_cart_discount_tax' => 'discount_tax',
467 '_order_shipping' => 'shipping_total',
468 '_order_shipping_tax' => 'shipping_tax',
469 '_order_tax' => 'cart_tax',
470 '_order_total' => 'total',
471 '_order_version' => 'version',
472 '_prices_include_tax' => 'prices_include_tax',
473 );
474
475 $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );
476
477 foreach ( $props_to_update as $meta_key => $prop ) {
478 $value = $order->{"get_$prop"}( 'edit' );
479 $value = is_string( $value ) ? wp_slash( $value ) : $value;
480
481 if ( 'prices_include_tax' === $prop ) {
482 $value = $value ? 'yes' : 'no';
483 }
484
485 $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );
486
487 if ( $updated ) {
488 $updated_props[] = $prop;
489 }
490 }
491
492 /**
493 * Action fired after updating order properties.
494 *
495 * @param WC_Abstract_Order $order Order object.
496 * @param string[] $updated_props Array of updated properties.
497 *
498 * @since 2.7.0
499 */
500 do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
501 }
502
503 /**
504 * Clear any caches.
505 *
506 * @param WC_Order $order Order object.
507 * @since 3.0.0
508 */
509 protected function clear_caches( &$order ) {
510 clean_post_cache( $order->get_id() );
511 wc_delete_shop_order_transients( $order );
512 wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' );
513 if ( OrderUtil::orders_cache_usage_is_enabled() ) {
514 $order_cache = wc_get_container()->get( OrderCache::class );
515 $order_cache->remove( $order->get_id() );
516 }
517 }
518
519 /**
520 * Read order items of a specific type from the database for this order.
521 *
522 * @param WC_Order $order Order object.
523 * @param string $type Order item type.
524 * @return array
525 */
526 public function read_items( $order, $type ) {
527 global $wpdb;
528
529 // When the order is not yet saved, we cannot get the items from DB. Trying to do so will risk reading items of different orders that were saved incorrectly.
530 if ( 0 === $order->get_id() ) {
531 return array();
532 }
533
534 // Get from cache if available.
535 $items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false;
536
537 if ( false === $items ) {
538 $items = $wpdb->get_results(
539 $wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() )
540 );
541 foreach ( $items as $item ) {
542 wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
543 }
544 if ( 0 < $order->get_id() ) {
545 wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' );
546 }
547 }
548
549 $items = wp_list_filter( $items, array( 'order_item_type' => $type ) );
550
551 if ( ! empty( $items ) ) {
552 $items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) );
553 } else {
554 $items = array();
555 }
556
557 return $items;
558 }
559
560 /**
561 * Return the order type of a given item which belongs to WC_Order.
562 *
563 * @since 3.2.0
564 * @param WC_Order $order Order Object.
565 * @param int $order_item_id Order item id.
566 * @return string Order Item type
567 */
568 public function get_order_item_type( $order, $order_item_id ) {
569 global $wpdb;
570 return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) );
571 }
572
573 /**
574 * Prime following caches:
575 * 1. item-$order_item_id For individual items.
576 * 2. order-items-$order-id For fetching items associated with an order.
577 * 3. order-item meta.
578 *
579 * @param array $order_ids Order Ids to prime cache for.
580 * @param array $query_vars Query vars for the query.
581 */
582 protected function prime_order_item_caches_for_orders( $order_ids, $query_vars ) {
583 global $wpdb;
584 if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
585 $line_items = array(
586 'line_items',
587 'shipping_lines',
588 'fee_lines',
589 'coupon_lines',
590 );
591
592 if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) {
593 return;
594 }
595 }
596 $cache_keys = array_map( static fn( $order_id ) => 'order-items-' . $order_id, $order_ids );
597 $cache_values = wc_cache_get_multiple( $cache_keys, 'orders' );
598 $non_cached_ids = array();
599 foreach ( $order_ids as $order_id ) {
600 if ( false === $cache_values[ 'order-items-' . $order_id ] ) {
601 $non_cached_ids[] = $order_id;
602 }
603 }
604 if ( empty( $non_cached_ids ) ) {
605 return;
606 }
607
608 $non_cached_ids = esc_sql( $non_cached_ids );
609 $non_cached_ids_string = implode( ',', $non_cached_ids );
610 $order_items = $wpdb->get_results(
611 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
612 "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;"
613 );
614 if ( empty( $order_items ) ) {
615 return;
616 }
617
618 $order_items_for_all_orders = array_reduce(
619 $order_items,
620 function ( $order_items_collection, $order_item ) {
621 if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) {
622 $order_items_collection[ $order_item->order_id ] = array();
623 }
624 $order_items_collection[ $order_item->order_id ][] = $order_item;
625 return $order_items_collection;
626 }
627 );
628 foreach ( $order_items_for_all_orders as $order_id => $items ) {
629 wp_cache_set( 'order-items-' . $order_id, $items, 'orders' );
630 }
631 foreach ( $order_items as $item ) {
632 wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
633 }
634 $order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
635 update_meta_cache( 'order_item', $order_item_ids );
636
637 // Prime WC_Data meta cache (includes meta_id required by read_meta_data).
638 $id_placeholders = implode( ', ', array_fill( 0, count( $order_item_ids ), '%d' ) );
639 $raw_meta_data_array = $wpdb->get_results(
640 $wpdb->prepare(
641 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $id_placeholders is generated above.
642 "SELECT order_item_id as object_id, meta_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id IN ({$id_placeholders}) ORDER BY meta_id",
643 ...$order_item_ids
644 )
645 );
646
647 if ( ! empty( $raw_meta_data_array ) ) {
648 $raw_meta_data_collection = array();
649 foreach ( $raw_meta_data_array as $raw_meta_data ) {
650 if ( ! isset( $raw_meta_data_collection[ $raw_meta_data->object_id ] ) ) {
651 $raw_meta_data_collection[ $raw_meta_data->object_id ] = array();
652 }
653 $raw_meta_data_collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
654 }
655 \WC_Order_Item::prime_raw_meta_data_cache( $raw_meta_data_collection, 'order-items' );
656
657 $this->prime_product_post_caches_for_order_items( $order_items, $raw_meta_data_collection );
658 }
659 }
660
661 /**
662 * Primes post caches for products which are referenced in line items with 'line_item' type.
663 *
664 * Although the product data store can be replaced, maintaining the posts table connection, as with HPOS, is necessary
665 * for products to function properly. We can therefore prime the post cache directly without compromising store isolation.
666 *
667 * @since 10.8.0
668 *
669 * @param array<int,object{order_item_id:int, order_item_type:string}> $line_items_all Line item entries.
670 * @param array<int,array<int,object{meta_key:string, meta_value:mixed}>> $raw_meta_data_collection Meta-entries grouped by line item id.
671 * @return void
672 */
673 private function prime_product_post_caches_for_order_items( array $line_items_all, array $raw_meta_data_collection ): void {
674 $product_ids = array();
675 foreach ( $line_items_all as $line_item ) {
676 if ( 'line_item' === $line_item->order_item_type ) {
677 foreach ( $raw_meta_data_collection[ $line_item->order_item_id ] ?? array() as $meta ) {
678 if ( ( '_variation_id' === $meta->meta_key || '_product_id' === $meta->meta_key ) && $meta->meta_value > 0 ) {
679 $product_ids[] = (int) $meta->meta_value;
680 }
681 }
682 }
683 }
684 _prime_post_caches( array_unique( $product_ids ) );
685 }
686
687 /**
688 * Prime refund cache for a batch of orders.
689 *
690 * WC_Order::get_refunds() checks wp_cache before querying. By fetching
691 * all refunds for the batch in a single query and populating the cache,
692 * we eliminate one query per order.
693 *
694 * @param array $order_ids Order IDs to prime cache for.
695 * @param array $query_vars Query vars for the query.
696 * @return void
697 * @since 10.7.0
698 */
699 protected function prime_refund_caches_for_orders( $order_ids, $query_vars ) {
700 if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
701 if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'], true ) ) {
702 return;
703 }
704 }
705
706 $cache_keys_mapping = array();
707 foreach ( $order_ids as $order_id ) {
708 $cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refund_ids' . $order_id;
709 }
710
711 $non_cached_ids = array();
712 $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
713
714 if ( ! is_array( $cache_values ) ) {
715 $non_cached_ids = $order_ids;
716 } else {
717 foreach ( $order_ids as $order_id ) {
718 if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
719 $non_cached_ids[] = $order_id;
720 }
721 }
722 }
723
724 if ( empty( $non_cached_ids ) ) {
725 return;
726 }
727
728 /**
729 * Fetch all refunds for the given order IDs.
730 *
731 * @var WC_Order_Refund[] $refunds
732 */
733 $refunds = wc_get_orders(
734 array(
735 'type' => 'shop_order_refund',
736 'post_parent__in' => $non_cached_ids,
737 'limit' => -1,
738 )
739 );
740
741 $order_refund_ids = array_fill_keys( $non_cached_ids, array() );
742 foreach ( $refunds as $refund ) {
743 if ( $refund instanceof \WC_Order_Refund && isset( $order_refund_ids[ $refund->get_parent_id() ] ) ) {
744 $order_refund_ids[ $refund->get_parent_id() ][] = $refund->get_id();
745 }
746 }
747
748 foreach ( $non_cached_ids as $order_id ) {
749 wp_cache_set( $cache_keys_mapping[ $order_id ], $order_refund_ids[ $order_id ], 'orders' );
750 }
751 }
752
753 /**
754 * Prime the needs_processing transient cache for a batch of orders.
755 *
756 * WC_Order::needs_processing() calls get_transient() per order, which
757 * triggers an individual wp_options query each time. By priming the
758 * object cache for all transient option names in a single query, we
759 * eliminate the N+1.
760 *
761 * @since 10.7.0
762 * @deprecated 10.8.0 `\WC_Order::needs_processing` method no longer uses transients.
763 *
764 * @param array $order_ids Order IDs to prime cache for.
765 * @param array $query_vars Query vars for the query.
766 * @return void
767 */
768 protected function prime_needs_processing_transients( $order_ids, $query_vars ) {
769 }
770
771 /**
772 * Remove all line items (products, coupons, shipping, taxes) from the order.
773 *
774 * @param WC_Order $order Order object.
775 * @param string $type Order item type. Default null.
776 */
777 public function delete_items( $order, $type = null ) {
778 global $wpdb;
779
780 $order_id = $order->get_id();
781
782 if ( ! $order_id ) {
783 return;
784 }
785
786 if ( ! empty( $type ) ) {
787 $wpdb->query( $wpdb->prepare( "DELETE itemmeta FROM {$wpdb->prefix}woocommerce_order_itemmeta as itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items as items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order_id, $type ) );
788 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order_id, $type ) );
789 } else {
790 $wpdb->query( $wpdb->prepare( "DELETE itemmeta FROM {$wpdb->prefix}woocommerce_order_itemmeta as itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items as items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order_id ) );
791 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order_id ) );
792 }
793
794 $this->clear_caches( $order );
795 }
796
797 /**
798 * Get token ids for an order.
799 *
800 * @param WC_Order $order Order object.
801 * @return array
802 */
803 public function get_payment_token_ids( $order ) {
804 $token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) );
805 return $token_ids;
806 }
807
808 /**
809 * Update token ids for an order.
810 *
811 * @param WC_Order $order Order object.
812 * @param array $token_ids Payment token ids.
813 */
814 public function update_payment_token_ids( $order, $token_ids ) {
815 update_post_meta( $order->get_id(), '_payment_tokens', $token_ids );
816 }
817
818 /**
819 * Get the order's title.
820 *
821 * @param WC_Order $order Order object.
822 *
823 * @return string Order title.
824 */
825 public function get_title( WC_Order $order ) {
826 return get_the_title( $order->get_id() );
827 }
828
829 /**
830 * Given an initialized order object, update the post/postmeta records.
831 *
832 * @param WC_Abstract_Order $order Order object.
833 *
834 * @return bool Whether the order was updated.
835 */
836 public function update_order_from_object( $order ) {
837 if ( ! $order->get_id() ) {
838 return false;
839 }
840 $this->update_order_meta_from_object( $order );
841
842 // Add hook to update post_modified date so that it's the same as order. Without this hook, WP will set the modified date to current date, and we will think that posts and orders are out of sync again.
843 add_filter( 'wp_insert_post_data', array( $this, 'update_post_modified_data' ), 10, 2 );
844 $post_data = array(
845 'ID' => $order->get_id(),
846 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
847 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
848 'post_status' => $this->get_post_status( $order ),
849 'post_parent' => $order->get_parent_id(),
850 'edit_date' => true,
851 'post_excerpt' => method_exists( $order, 'get_customer_note' ) ? $order->get_customer_note() : '',
852 'post_type' => $order->get_type(),
853 'order_modified' => ! is_null( $order->get_date_modified() ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : '',
854 'order_modified_gmt' => ! is_null( $order->get_date_modified() ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : '',
855 );
856 $updated = wp_update_post( $post_data );
857 remove_filter( 'wp_insert_post_data', array( $this, 'update_post_modified_data' ) );
858 return $updated;
859 }
860
861 /**
862 * Change the modified date of the post to match the order's modified date if passed.
863 *
864 * @hooked wp_insert_post_data See function update_order_from_object.
865 *
866 * @param array $data An array of slashed, sanitized, and processed post data.
867 * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
868 *
869 * @return array Data with updated modified date.
870 */
871 public function update_post_modified_data( $data, $postarr ) {
872 if ( ! isset( $postarr['order_modified'] ) || ! isset( $postarr['order_modified_gmt'] ) ) {
873 return $data;
874 }
875
876 $data['post_modified'] = $postarr['order_modified'];
877 $data['post_modified_gmt'] = $postarr['order_modified_gmt'];
878 return $data;
879 }
880
881 /**
882 * Helper method to update order metadata from initialized order object.
883 *
884 * @param WC_Abstract_Order $order Order object.
885 */
886 protected function update_order_meta_from_object( $order ) {
887 global $wpdb;
888
889 if ( is_null( $order->get_meta() ) ) {
890 return;
891 }
892
893 $existing_meta_data = get_post_meta( $order->get_id() );
894
895 foreach ( $order->get_meta_data() as $meta_data ) {
896 if ( isset( $existing_meta_data[ $meta_data->key ] ) ) {
897 // We don't know if the meta is single or array, so we assume it to be an array.
898 $meta_value = is_array( $meta_data->value ) ? $meta_data->value : array( $meta_data->value );
899
900 if ( $existing_meta_data[ $meta_data->key ] === $meta_value ) {
901 unset( $existing_meta_data[ $meta_data->key ] );
902 continue;
903 }
904
905 if ( is_array( $existing_meta_data[ $meta_data->key ] ) ) {
906 $value_index = array_search( maybe_serialize( $meta_data->value ), $existing_meta_data[ $meta_data->key ], true );
907 if ( false !== $value_index ) {
908 unset( $existing_meta_data[ $meta_data->key ][ $value_index ] );
909 if ( 0 === count( $existing_meta_data[ $meta_data->key ] ) ) {
910 unset( $existing_meta_data[ $meta_data->key ] );
911 }
912 continue;
913 }
914 }
915 }
916 if ( is_object( $meta_data->value ) && '__PHP_Incomplete_Class' === get_class( $meta_data->value ) ) {
917 $meta_value = maybe_serialize( $meta_data->value );
918 $result = $wpdb->insert(
919 _get_meta_table( 'post' ),
920 array(
921 'post_id' => $order->get_id(),
922 'meta_key' => $meta_data->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
923 'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
924 ),
925 array( '%d', '%s', '%s' )
926 );
927 wp_cache_delete( $order->get_id(), 'post_meta' );
928 /** @var WC_Logger_Interface $logger */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
929 $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
930 $logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `update_order_meta_from_object` in order with ID %d: "%s"', $order->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
931 } else {
932 add_post_meta( $order->get_id(), $meta_data->key, $meta_data->value, false );
933 }
934 }
935
936 // Find remaining meta that was deleted from the order but still present in the associated post.
937 // Post meta corresponding to order props is excluded (as it shouldn't be deleted).
938 $keys_to_delete = array_diff(
939 array_keys( $existing_meta_data ),
940 $this->internal_meta_keys,
941 array_keys( $this->get_internal_data_store_key_getters() )
942 );
943
944 foreach ( $keys_to_delete as $meta_key ) {
945 if ( isset( $existing_meta_data[ $meta_key ] ) ) {
946 foreach ( $existing_meta_data[ $meta_key ] as $meta_value ) {
947 delete_post_meta( $order->get_id(), $meta_key, maybe_unserialize( $meta_value ) );
948 }
949 }
950 }
951
952 $this->update_post_meta( $order );
953 }
954
955 /**
956 * Returns a prepared SQL JOIN clause for finding refund orders belonging to a given parent order.
957 *
958 * The clause aliases the refund table as `refunds`. Subclasses should override this
959 * to use a different table (e.g. the HPOS orders table).
960 *
961 * @since 10.7.0
962 * @param int $order_id Parent order ID.
963 * @return string Prepared SQL JOIN fragment.
964 */
965 protected function get_refund_orders_join_clause( int $order_id ): string {
966 global $wpdb;
967 return $wpdb->prepare( '%i AS refunds ON ( refunds.post_type = %s AND refunds.post_parent = %d )', $wpdb->posts, 'shop_order_refund', $order_id );
968 }
969
970 /**
971 * Returns a prepared SQL JOIN clause for finding refund orders belonging to multiple parent orders.
972 *
973 * The clause aliases the refund table as `refunds`. Subclasses should override this
974 * to use a different table (e.g. the HPOS orders table).
975 *
976 * @since 10.7.0
977 * @param array $order_ids List of order IDs.
978 * @return string Prepared SQL JOIN fragment.
979 */
980 protected function get_refund_orders_batch_join_clause( array $order_ids ): string {
981 global $wpdb;
982 $id_list = implode( ', ', array_map( 'absint', $order_ids ) );
983 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
984 return $wpdb->prepare( "%i AS refunds ON ( refunds.post_type = %s AND refunds.post_parent IN ( $id_list ) )", $wpdb->posts, 'shop_order_refund' );
985 }
986
987 /**
988 * Returns the column name on the refund table alias (`refunds`) that holds the parent order ID.
989 *
990 * @since 10.7.0
991 * @return string Column reference, e.g. 'refunds.post_parent'.
992 */
993 protected function get_refund_parent_column(): string {
994 return 'refunds.post_parent';
995 }
996
997 /**
998 * Query total refunded amounts per order in a batch. Returns an associative array
999 * of order_id => total (positive float).
1000 *
1001 * Subclasses should override this when the refund total is stored differently
1002 * (e.g. HPOS stores it directly in the orders table rather than postmeta).
1003 *
1004 * @since 10.7.0
1005 * @param array $order_ids List of order IDs.
1006 * @return array<int, float> Map of order_id => refund total.
1007 */
1008 protected function get_batch_refund_totals( array $order_ids ): array {
1009 global $wpdb;
1010
1011 $id_list = implode( ', ', array_map( 'absint', $order_ids ) );
1012
1013 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
1014 $refund_totals = $wpdb->get_results(
1015 $wpdb->prepare(
1016 "SELECT posts.post_parent AS order_id, SUM( postmeta.meta_value ) AS total
1017 FROM %i AS postmeta
1018 INNER JOIN %i AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent IN ( $id_list ) )
1019 WHERE postmeta.meta_key = '_refund_amount'
1020 AND postmeta.post_id = posts.ID
1021 GROUP BY posts.post_parent",
1022 $wpdb->postmeta,
1023 $wpdb->posts
1024 )
1025 );
1026 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1027
1028 $totals_by_order = array();
1029 foreach ( $refund_totals as $row ) {
1030 $totals_by_order[ $row->order_id ] = floatval( $row->total );
1031 }
1032
1033 return $totals_by_order;
1034 }
1035
1036 /**
1037 * Get the summed refund item meta value for a given order, item type, and meta keys.
1038 *
1039 * @since 10.7.0
1040 * @param WC_Order $order Order object.
1041 * @param string $item_type Order item type (e.g. 'tax', 'shipping').
1042 * @param array $meta_keys Meta keys to sum.
1043 * @return float Absolute total.
1044 */
1045 protected function get_refunded_item_meta_total( $order, string $item_type, array $meta_keys ): float {
1046 global $wpdb;
1047
1048 $refund_join = $this->get_refund_orders_join_clause( $order->get_id() );
1049 $meta_placeholder = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );
1050
1051 $total = $wpdb->get_var(
1052 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $refund_join is already prepared.
1053 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $meta_keys is splatted.
1054 $wpdb->prepare(
1055 "SELECT SUM( order_itemmeta.meta_value )
1056 FROM %i AS order_itemmeta
1057 INNER JOIN $refund_join
1058 INNER JOIN %i AS order_items ON ( order_items.order_id = refunds.id AND order_items.order_item_type = %s )
1059 WHERE order_itemmeta.order_item_id = order_items.order_item_id
1060 AND order_itemmeta.meta_key IN ( $meta_placeholder )",
1061 $wpdb->prefix . 'woocommerce_order_itemmeta',
1062 $wpdb->prefix . 'woocommerce_order_items',
1063 $item_type,
1064 ...$meta_keys,
1065 )
1066 // phpcs:enable
1067 ) ?? 0;
1068
1069 return abs( $total );
1070 }
1071
1072 /**
1073 * Get the total tax refunded.
1074 *
1075 * @param WC_Order $order Order object.
1076 * @return float
1077 */
1078 public function get_total_tax_refunded( $order ) {
1079 return $this->get_refunded_item_meta_total( $order, 'tax', array( 'tax_amount', 'shipping_tax_amount' ) );
1080 }
1081
1082 /**
1083 * Get the total shipping tax refunded.
1084 *
1085 * @param WC_Order $order Order object.
1086 *
1087 * @since 10.2.0
1088 * @return float
1089 */
1090 public function get_total_shipping_tax_refunded( $order ) {
1091 return $this->get_refunded_item_meta_total( $order, 'tax', array( 'shipping_tax_amount' ) );
1092 }
1093
1094 /**
1095 * Get the total shipping refunded.
1096 *
1097 * @param WC_Order $order Order object.
1098 * @return float
1099 */
1100 public function get_total_shipping_refunded( $order ) {
1101 return $this->get_refunded_item_meta_total( $order, 'shipping', array( 'cost' ) );
1102 }
1103
1104 /**
1105 * Prime the refund total and refund tax total caches for a batch of orders.
1106 *
1107 * @since 10.7.0
1108 * @param array $order_ids Order IDs to prime cache for.
1109 * @param array $query_vars Query vars for the query.
1110 * @return void
1111 */
1112 protected function prime_refund_total_caches_for_orders( $order_ids, $query_vars ): void {
1113 global $wpdb;
1114
1115 $cache_prefix = \WC_Cache_Helper::get_cache_prefix( 'orders' );
1116
1117 // Find which orders need priming (check both total_refunded and total_tax_refunded).
1118 $total_keys = array();
1119 $tax_keys = array();
1120 $non_cached_ids = array();
1121 foreach ( $order_ids as $order_id ) {
1122 $total_keys[ $order_id ] = $cache_prefix . 'total_refunded' . $order_id;
1123 $tax_keys[ $order_id ] = $cache_prefix . 'total_tax_refunded' . $order_id;
1124 }
1125
1126 $all_keys = array_merge( array_values( $total_keys ), array_values( $tax_keys ) );
1127 $cache_values = wc_cache_get_multiple( $all_keys, 'orders' );
1128
1129 if ( ! is_array( $cache_values ) ) {
1130 $non_cached_ids = $order_ids;
1131 } else {
1132 foreach ( $order_ids as $order_id ) {
1133 if ( false === $cache_values[ $total_keys[ $order_id ] ] || false === $cache_values[ $tax_keys[ $order_id ] ] ) {
1134 $non_cached_ids[] = $order_id;
1135 }
1136 }
1137 }
1138
1139 if ( empty( $non_cached_ids ) ) {
1140 return;
1141 }
1142
1143 // Batch query: total refunded per order.
1144 $totals_by_order = $this->get_batch_refund_totals( $non_cached_ids );
1145 foreach ( $non_cached_ids as $order_id ) {
1146 wp_cache_set( $total_keys[ $order_id ], $totals_by_order[ $order_id ] ?? 0.0, 'orders' );
1147 }
1148
1149 // Batch query: total tax refunded per order.
1150 $refund_join = $this->get_refund_orders_batch_join_clause( $non_cached_ids );
1151 $parent_col = $this->get_refund_parent_column();
1152
1153 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $refund_join is already prepared, $parent_col is hardcoded.
1154 $tax_totals = $wpdb->get_results(
1155 $wpdb->prepare(
1156 "SELECT $parent_col AS order_id, SUM( order_itemmeta.meta_value ) AS total
1157 FROM %i AS order_itemmeta
1158 INNER JOIN $refund_join
1159 INNER JOIN %i AS order_items ON ( order_items.order_id = refunds.id AND order_items.order_item_type = 'tax' )
1160 WHERE order_itemmeta.order_item_id = order_items.order_item_id
1161 AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')
1162 GROUP BY $parent_col",
1163 $wpdb->prefix . 'woocommerce_order_itemmeta',
1164 $wpdb->prefix . 'woocommerce_order_items'
1165 )
1166 );
1167 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1168
1169 $tax_by_order = array();
1170 foreach ( $tax_totals as $row ) {
1171 $tax_by_order[ $row->order_id ] = abs( floatval( $row->total ) );
1172 }
1173 foreach ( $non_cached_ids as $order_id ) {
1174 wp_cache_set( $tax_keys[ $order_id ], $tax_by_order[ $order_id ] ?? 0.0, 'orders' );
1175 }
1176 }
1177 }
1178